Chiptune
An in-editor chiptune tracker and retro SFX generator for Godot 4.7, in pure GDScript. One synth engine drives both the SFX and Tracker tabs.
Output is standard Godot audio. A sound or a song exports to .wav, and
nothing in a shipped game depends on this plugin.
Or skip the export. Easiest is the node: Add Child Node and type “chiptune”. There are two kinds.
- A sound.
ChiptunePlayer, orChiptunePlayer2DandChiptunePlayer3Dfor audio positioned in a scene. One property to set,document, whose picker lists your.sfx.tresfiles. - Music.
ChiptuneMusicPlayertakes a.music.tresand has aloop, on by default: the last bar runs into the first with no pause, because what is rendered is the song played round until the audio stops changing, so every bar carries the tail of the bar before it. Turnloopoff for a one-shot — a jingle or a stinger — and it plays the song once, last note and all, then emitsfinished. Music is not positional, so there is no 2D or 3D version.
Either way the node builds and keeps the stream for you. A song on a sound player still plays, once through, and says in the scene tree where the loop lives.
What it costs. A node renders its document when the scene loads, on the
main thread. A sound is milliseconds. A song is a stall of some hundreds of
milliseconds, and a looping song is rendered until the loop settles, which
for a song whose last note dies inside one pass is twice that. For long
music in a shipped game, export a .wav: Godot’s importer compresses it
and it loads instead of rendering.
The resource underneath is ChiptuneAudioStream. It holds a .sfx.tres
or .music.tres and renders it, so editing the document updates every node
playing it. It is an ordinary AudioStream and drops into any node with a
stream, ours or not. Four ways to get one:
- From the document. With a sound or song open and saved, Add a
ChiptunePlayer to the scene in the File menu drops a
ChiptunePlayer, or aChiptuneMusicPlayerfor a song, into the open scene. - From a node playing an export. Select a player whose stream is a
.wavthis addon exported and press Sync to the document in the inspector. - Back out again. Unsync on that node returns it to the
.wav, so it stops needing this addon at runtime. The.wavhas to exist. - By hand. The node’s
streamproperty, New ›ChiptuneAudioStream, then set itsdocument.
Each is one editor undo step.
Changing a sound or a song while the game runs
A document is an ordinary resource, so game code can change one. Nothing
watches it, so ask the node for its audio again with rerender(). It is on
ChiptunePlayer, ChiptunePlayer2D, ChiptunePlayer3D and
ChiptuneMusicPlayer, renders into the stream the node already holds, and
keeps the music node’s loop.
Calling it on a node that is already playing is allowed. The node is stopped, rendered, and started again where it had got to, or at the end of the new audio if that is sooner. There is a seam where the sound changes, because the sound changed.
Vary a sound:
var laser := $Laser as ChiptunePlayervar spec := laser.document as ChiptuneSfxSpecspec.pitch += 3laser.rerender()laser.play()ChiptuneSfx.generate(action, seed) makes a new sound for an action, Tick,
Confirm, Deny, Pickup, Jump, Hit, Boom, Swell, Warn or Drop, the picker on
the SFX tab: the action sets the shape and the seed picks the rest.
ChiptuneSfx.generate_feel(feel, seed) makes one with a feel, Bright, Calm,
Tense, Sad, Fast or Big, the way the tracker’s Generated instruments are
made; a feel’s name given to generate still answers.
ChiptuneSfx.mutate(spec, seed, amount) hands back a copy of a sound with
its parameters nudged, and ChiptuneSfx.generate_free(seed) makes one from
anywhere in the range. All of them answer a new ChiptuneSfxSpec and leave
the one they were given alone, so handing one to a node is an assignment
and needs no rerender(): setting document renders it.
laser.document = ChiptuneSfx.mutate(spec, randi(), 0.2)rerender() is for the other way round — a field changed on the document
the node already holds, as above. That document is the loaded resource, so
every node holding the same .sfx.tres is holding the same object and hears
the change on its own next rerender(). Give one node a sound of its own
with spec.duplicate().
What that costs. One render of a short sound is most of a frame. So for
variety on every shot, set pitch_scale on the node instead, which is
free. A re-render is for a sound whose parameters have actually changed.
Change the music:
var music := $Music as ChiptuneMusicPlayervar song := music.document as ChiptuneSongsong.bpm = 90.0song.mixer.set_muted(2, true)music.rerender()What that costs. A song is hundreds of milliseconds per few seconds of audio, and a looping render pays that again for each pass the loop takes to settle. That is a stall rather than a hitch: re-render music where the game can afford one, at a level change or a pause, and never mid-bar. Changing music that is already playing without a gap wants streaming playback, which this addon does not do yet.
A save reaches the running game. Run the game from the editor, edit a
sound in the SFX tab, Ctrl+S: every node whose stream holds that
.sfx.tres re-renders it, and the next shot plays the new sound. A
.sfx.tres that changes on disk some other way reaches the game once the
editor has noticed the file. Songs do not yet: a .music.tres saved while
the game runs is played at the next run, and the game’s log says so. It
rides the debugger connection the editor already has to the game, so an
exported build carries the listener and nothing arrives. To turn it off,
the project setting Jamchip ▸ Live ▸ Push To Game.
Where a sound or a song lives
Each tab holds one document. Its name is the chip on the shell strip; click
it for the File menu. A document never saved reads Untitled, and unsaved
changes show a seed in the warning colour on the chip. The .sfx.tres or
.music.tres suffix is left off; the full path is in the tooltip. Help,
last on either strip, lists the tab’s keys, opens this README, and shows
About JamChip with the version installed. In the
FileSystem dock a sound shows the shape of its first two seconds and a
song shows its order as a row of blocks, one colour per pattern.
New and Open ask before replacing unsaved work. Closing the dock only hides the tabs. Quitting the editor names each unsaved sound and song in Godot’s save-before-quit dialog, and Save All writes them. A document never saved anywhere is named but left alone.
Unsaved work is also written to a recovery file every thirty seconds, or
as often as the Saving page of Preferences says, under
user://jamchip/autosave/: sfx.sfx.res for the SFX tab,
tracker.music.res for the Tracker, beside the Pixel tab’s. If the editor
is killed, the next start lists every tab with work waiting in one dialog,
by name and time, a row per tab. Recover puts that document back as
unsaved work on the file it belongs to, or as Untitled when that file is
gone; Discard removes that file; Later leaves the rest for next time, and
that tab’s autosave waits until it has been answered. Saving, or
discarding a document by choice, removes the file. Nothing under res://
is touched.
Editing in the tracker
A new song opens on an empty grid, and two menus give it a start. Generate loop, under the Song menu, writes a four-channel loop in the feel you pick: Bright, Calm, Tense, Sad, Fast or Big. You get two patterns played A A B A, with a lead, a bass, a chord played one note at a time, and drums on six instruments it adds. It sets the song’s key, scale and tempo, plays once through and lands selected, so change what you like. Generate kit, under the Instruments section’s menu, sets the instruments, the key and the tempo and leaves the notes to you. Both list the same six feels. A loop or a kit is one undo step, and pressing again replaces.
Shift with the arrows, Home, End, PageUp and PageDown selects whole cells from where the cursor was; Ctrl+A takes the whole pattern and Esc drops it. The Edit menu holds cut, copy, paste, delete and transpose, each named with its key. Paste lands with its top left at the cursor, writes every cell of the clip including the gaps, and drops whatever falls past the last row or channel; the pasted block is left selected. Ctrl+Up and Ctrl+Down move the selected notes a semitone, and with Shift an octave; a note that would leave the range stays put. The clipboard is the editor’s own, not the system one, and each operation is one undo step. Entries grey out when they cannot apply.
Grid and Roll in the bar are two views of the pattern on screen. Roll is
a piano roll: time runs across, pitch runs down, and it shows one channel
at a time. Click a channel’s name in the header to switch. Click an empty
spot to place a note there, one row long, with the instrument selected in
the rack; drag right and it grows, the note-off (===) that ends it moving with the
pointer, up to the next note. Click a note to select it: the bar shows its
Ins and Vol to edit, where Oct and Step sit for the grid, and Delete
removes it. With nothing selected, Ins and Vol are what the next note
takes. Grid after Roll shows what you just wrote, and each edit is
one undo step.
The rack’s Key section, folded to start, names the key the song is in and a scale over it. With a key set, every note in the grid outside that scale is drawn in the warning colour on a band of it, so a stray reads before it plays; the note is still entered, played, named and transposed the same, and a transpose tints against the same key. In the roll, the pitch rows outside the scale are shaded darker across the lane, the key’s own rows are lined and named in the brighter ink, and a note off the key is a block in the warning colour. Off by default: a song with no key, which is every song until you pick one, looks as it always has. Seven scales, the modes of the major scale from Major to Locrian; the key is saved with the song and setting it is one undo step.
Playing your own resources
A stream asks its document to render rather than asking what class it is,
so any Resource with these methods plays through one:
func render(sample_rate: int, stereo: bool) -> PackedFloat32Arrayfunc prefers_stereo() -> bool # optional; absent means monofunc render_loop(sample_rate: int, stereo: bool) -> PackedFloat32Array # optional; absent means a loop runs the # whole bufferInstall
Chiptune is one module of JamChip and does not install alone. Copy
addons/jamchip/ into your project and enable JamChip under Project ›
Project Settings › Plugins.
Layout
model/: the spec, the song and its patterns, the two stores, the recovery file, the instrument, its palette, the envelope, the presets and the mixer. What a document is made of and what saves it. No editor dependencies.synth/: oscillators, voice, note events, the three renderers and the stream feed. The maths over a document.format/: the WAV writer.tracker/: the tracker panel, its strip, bar, grid, piano roll, arrangement lane, rack and mixer.sfx/: the SFX panel and its waveform stage.host/: what both panels share: the instrument editor, the document chip, the scene link, the shortcuts dialog, the FileSystem previews, and the inspector button the plugin adds for both.runtime/:ChiptuneAudioStream, the three sound players and the music player. What a game opens; an export carries the whole ofaddons/jamchip/.icons/: the node and resource icons.tests/: the headless suite. Not shipped.
JamChip 0.1.0, by GlitterSnack. MIT licence. Built from the source on every merge, artwork and screenshots included, with JamChip.