Chiptune
Make sound effects for a Godot 4.7 game without leaving the editor, in pure GDScript. One synth drives the SFX tab and the Tracker’s songs.
Output is standard Godot audio. Export in a tab’s File menu, the
document’s name on the strip, opens one dialog: pick WAV, then where
it goes, and a sound or a song is written as a .wav, which plays in a
game with no plugin at all. A song exported from the Tracker into the
project loops when Godot imports it, from the end of its last row back to
the top, or back to its loop point when it has one. In a browser the last row is Download as,
and Sound file or Song file sends the saved document itself.
Or skip the export. Easiest is the node: Add Child Node and type “chiptune”. There are two kinds.
- A sound.
ChiptuneSfxPlayertakes a.sfx.tres, andChiptuneSfxPlayer2Dis the same positioned in 2D. One property to set,document, whose picker lists your sounds. For 3D, put aChiptuneAudioStreamon Godot’s ownAudioStreamPlayer3D. - Music.
ChiptuneSongPlayertakes a.music.tres, a Tracker song, andChiptuneSongPlayer2Dis the same positioned in 2D. Each has aloop, which is the song’s ownloopuntil you set it on the node. On, the last bar runs into the first with no pause, and the last note rings on over the first bar. Off, for a one-shot — a jingle or a stinger — it plays the song once, last note and all, then emitsfinished. Set it back to the song’s, or revert it in the Inspector, and the node follows the song again. A song can have an intro: itsloop_fromis the entry of its order the loop comes round to,song edit FILE --loop-from 3orloop_from = 3in its text, and a looping node plays the entries before it once. A game can change the music while it plays; see below.
| Node | Document | For | What a game uses, by need |
|---|---|---|---|
ChiptuneSfxPlayer |
.sfx.tres |
A sound the game fires, like a jump, a hit or a coin. | Fire it: play, play_at(pitch). Repeats that differ: variants, pitch_variance. The same variety every run: seed. One node’s own copy: unique. |
ChiptuneSfxPlayer2D |
.sfx.tres |
The same, from a place on screen, panned and quieter with distance. | The same as ChiptuneSfxPlayer. |
ChiptuneSongPlayer, Tracker |
.music.tres |
Music the game steers while it plays. | Loop or stinger: loop, finished. On the beat: beat, bar, looped, position_bar, pattern_index, pattern_row. Sections: jump_to_pattern, jump_to_row. Mood: tempo, transpose. Layers: set_channel_muted, set_channel_volume, set_channel_solo. A part’s sound: set_instrument, clear_instrument. Echo: set_send. |
ChiptuneSongPlayer2D, Tracker |
.music.tres |
The same, from a place on screen, panned and quieter with distance. | The same as ChiptuneSongPlayer. |
ChiptuneAudioStream, a resource |
either | Any other slot: an AnimationPlayer audio track, an AudioStreamRandomizer, an AudioStreamPlayer3D. |
document. A song renders whole: it plays, with no beat signals and nothing to steer it. |
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 sound node renders its document when the scene loads,
on the main thread, in milliseconds. A music node renders nothing when the
scene loads. It plays the song a fifth of a second ahead of what you hear,
rendered on a worker thread while it plays: about 0.4 s of one core for each
second of a five-channel song, and almost none of the frame. In the editor,
and in a web export without thread support, that render is on the main
thread, about 6 ms a frame at 60 frames a second, so two songs at once there
can slow the game. A song on any other player is rendered whole
when the scene loads, a stall of some hundreds of milliseconds, and twice
that looping. For that case, export a .wav: Godot’s importer compresses it
and it loads instead of rendering.
Rendering behind a loading screen. ChiptuneAudioStream.prerendered_for(document)
makes a stream that renders on another thread instead of when it is
made. Wait for its prerendered signal, then give it to a node’s
stream. Until then it plays silence.
var music := ChiptuneAudioStream.prerendered_for(boss_song, true)await music.prerendered$Music.stream = music$Music.play()On a stream you already have, prerender() does the same and
is_prerendering() says whether it is still going. Leave the document
alone until the signal comes.
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 ChiptuneSfxPlayer to scene or Add ChiptuneSongPlayer to scene in its File menu drops that node 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
RUNTIME.md at the addon’s root has both modules’ runtime classes,
EXAMPLE-GAME.md the example game node by node; this is the sound half.
A document is an ordinary resource, so game code can change one, and the node follows it. Set a field and the node renders again on the next idle frame, once however many fields you set in that frame. A knob on the sound’s instrument counts, and so does a strip on the song’s mixer.
A node that is already playing 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.
To hear the change in the same frame, call rebuild() on the node or on
ChiptuneAudioStream; each emits document_changed once the change is in.
A sound renders in place. A song on ChiptuneSongPlayer renders nothing
and doesn’t stop: it plays the change from the next pattern; see Change the
music, below.
Vary a sound:
var laser := $Laser as ChiptuneSfxPlayervar spec := laser.document as ChiptuneSfxSpecspec.pitch += 3laser.rebuild()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.nudge(spec, seed, amount) hands back a copy of a sound with
every number moved a little and the wave kept, as the SFX tab’s Nudge does,
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:
setting document renders it.
laser.document = ChiptuneSfx.nudge(spec, randi(), 0.2)A field changed on the document the node already holds is the other way
round, 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 too.
Turn on unique to give one node a copy of its own, made when the document
is assigned: an edit to that node’s document is heard on it alone. The
scene still stores the file, and a save of the file copies it again, over
the node’s edits. Both song players have unique too.
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.
One sound, many shots. ChiptuneSfxPlayer and ChiptuneSfxPlayer2D
vary a sound without rendering it again for each shot:
play_at(pitch)plays at a MIDI note, 60 being middle C, by settingpitch_scale.$Pickup.play()or the sound ending puts it back.pitch_variancemoves each play up to that many semitones either way. A semitone is one key on a piano, and 12 is an octave.variantsrenders that manynudges of the sound, when it loads and when it changes, and each play takes the next.seedpicks which.
var pickup := $Pickup as ChiptuneSfxPlayerpickup.play_at(72 + coins_in_a_row * 2)With pitch_variance or variants set, the node’s stream is an
AudioStreamRandomizer holding the renders, so every play varies, however
it was started.
Change the music. A song player, ChiptuneSongPlayer or
ChiptuneSongPlayer2D, plays the song as it goes, so
nothing is rendered and the music does not stop. Everything below is on the
node and leaves the song alone, so two nodes playing one song can sound
different:
var music := $Music as ChiptuneSongPlayermusic.set_channel_muted(2, true) # the drums drop out, from the next buffermusic.tempo = 150.0 # from the next pattern; 0 is the song's ownmusic.transpose = 2 # semitones, from the next patternmusic.set_instrument(0, preload("res://audio/organ.tres"))music.jump_to_pattern(3) # at the next bar linemusic.jump_to_row(3, 8) # row 8 of that entry, at the next buffermusic.bar.connect(func(_index: int) -> void: $Lights.flash())- The mix.
set_channel_muted,set_channel_soloandset_channel_volume, andis_channel_mutedto ask. They land on the next buffer, a fifth of a second at most, mid-note and without a click. - The echo. A song’s echo is
mixer.echo, an instrument’s itsecho.set_send(1, &"echo", 0.8, 2.0)swells channel 1’s send over two seconds. In the song’s text, whatsong showprints, the echo is theecholine,noneor its settings in pairs,rows 3 feedback 0.3 damp 0.3 level 0.35;sendsis each channel’s send to it,0 0.8 0 0; and an instrument’s own echo is theecholine of its block.song createandsong edittake the first two as--echoand--sends. - What plays.
tempoin beats per minute,transposein semitones,set_instrument(channel, instrument)andclear_instrument(channel). They land at the start of the next pattern. The instrument can be one the song never had. - Where it is.
jump_to_pattern(index)goes to an entry of the song’s order at the next bar line, or right away withfalseas the second argument.jump_to_row(index, row)goes to a row of that entry’s pattern at the next buffer; a row past the end goes on into the next entry, sojump_to_row(pattern_index(), pattern_row() + 1)steps to the next note. Called beforeplay(), either is whereplay()starts. Jump twice before the first lands and only the second happens.position_rows(),position_bar(),pattern_index()andpattern_row()say what you are hearing now.play(),stop()andstream_pausedwork as on any player.seek()andplay(from)do not move the music; usejump_to_patternorjump_to_row.pitch_scalestill works, and moves the speed with the pitch. - The beat.
beat(index)on every beat,bar(index)on every bar line andloopedas the loop comes round, each as you hear it. A beat isrows_per_beatrows, four beats make a bar, and a pattern always starts a new bar.
Editing the song itself works too: set song.bpm or write a note and the
node picks it up at the next pattern, then emits document_changed. That
change is heard by every node holding that song.
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. A song on any other player does the same
without freezing the game: it renders on another thread, then the music
starts again on the bar
nearest where it was. A song on a ChiptuneSongPlayer renders nothing: the
music carries on and plays the saved song from the next pattern. Either way
the node’s document is the saved one as soon as the save arrives. 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/push_to_game, Push To Game under Jamchip.
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 and opens this README; About
JamChip, with the version installed, is on the project page’s ≡. 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.
Save As, under Save in the File menu or Ctrl+Shift+S, always asks where. The chip then shows the new file’s name and the first file stays as it was, so you can save the level theme as a boss theme and keep writing. A sound or a song built into a scene gets a file of its own this way, and the scene uses that file once you save it.
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.
A project’s style, the .jcstyle the project setting jamchip/style
names, sets the sound chip both tabs keep to. A song may use up to its
channel count; a new one starts on four, or fewer when the style allows
fewer. A new sound and a new song’s instruments use only its waves,
which are all either waveform picker offers. A song or sound made outside
those limits still opens and saves, with a note on the status line, and
the picker still shows the wave an instrument is on.
The style also sets how loud the music is against the sound effects:
music_db for every song and sounds_db for every sound, in the style’s
inspector. They’re in decibels: 0 plays it as made, -10 is about half as
loud, -60 is silent, and up to +12 boosts it. A boost is kept from
clipping. Both tabs play at those levels, and so do every
player in the game and every .wav you export, so a jump is as loud
against the music in the tab as it’ll be in the game. Save the style and
everything already playing changes with it. A node’s own
volume_db and bus are still yours, for a fade or a volume slider in
your options menu.
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, in the File menu, writes a four-channel loop in the feel you pick: Bright, Calm, Tense, Sad, Fast or Big. You get two or three patterns, played in one of four orders: A A B A, A B A B, A A B B, or A B A C, where C is a new tune over B’s chords. There’s 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. Under a style with a sound palette, both play each part on the palette’s instrument for it, by its role, then by its wave, and start in the style’s home key. A loop may open on one of the style’s motifs, short tunes any song can use: Add a tune from the style, in the same menu as Generate kit, writes one at the cursor in the song’s own key. Vary, in the Edit menu or Ctrl+Right, changes the selected notes a little, in key. The first and last notes, the notes on the beat, the kick and snare, and every instrument stay as they are. Vary into a new pattern, on a pattern’s menu, plays a varied copy right after it.
Space plays and stops the pattern on screen from anywhere on the tab, the grid included, and stops the song too. Shift+Space plays the song from the cursor’s row, even over a playing pattern, and stops a playing song. In a text field Space types a space, and on a button you reached with Tab it presses the button.
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. Drag a note up or down to change its
pitch, or its end across to stretch it; a one-row note follows whichever
way you drag first. 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, whatever 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 bufferfunc loop_begin_frames(sample_rate: int) -> int # optional; where that loop comes round to, # absent means its first frameJamChip, by GlitterSnack. MIT licence. Built from the source on every merge. Its artwork and screenshots are made with JamChip.