ChiptuneAudioStream
Extends AudioStreamWAV. Read from the plugin's source on every build.
A .sfx.tres or a .music.tres in any slot that takes an AudioStream:
an AnimationPlayer’s audio track, an AudioStreamRandomizer, Godot’s own
AudioStreamPlayer3D.
The resource under the nodes, the way PixelSpriteTexture and
PixelSpriteFrames sit under PixelSprite2D. A song in it renders whole: it
plays, but without ChiptuneSongPlayer’s beat signals or the calls that
steer it.
This is the positive marker for “this audio is ours”. Everything else in the editor has to guess from a file name — is there a .sfx.tres beside this .wav? — which cannot tell our export from somebody else’s audio that happens to share a stem. A node holding one of these is holding a document, and a node holding anything else is holding somebody’s else’s audio and is left alone. That matters in the project this addon is actually for: one with a sound pack in it, and chiptune as one part of it.
It also removes the drift. The .wav export is a file that goes stale the moment the document is edited, silently, in every scene playing it. This renders the document instead, so there is no second file to be out of date with the first.
An AudioStreamWAV, with no playback of its own: the
bytes are all the engine wants, ChiptuneWav already produces them, and a
custom AudioStreamPlayback would buy nothing for a stream that is
rendered whole. A song that streams is ChiptuneSongPlayer’s, which
fills an AudioStreamGenerator as it plays.
Properties
document
@export_custom(PROPERTY_HINT_RESOURCE_TYPE, "ChiptuneSfxSpec,ChiptuneSong") var document: ResourceThe .sfx.tres or .music.tres to render.
Typed as Resource because the contract is a method, whatever the class, and hinted to the two documents this addon ships so the picker and its Quick Load search offer those two alone. The hint is an editor filter and not the contract: a resource of your own answering render() plays when it is assigned from code or written into a scene by hand.
Anything not answering render() is silence, with no error, since a half assigned property in an editor is a normal intermediate state.
loop
var loop := falseWhether the render is a loop or a one-shot.
A loop is not the same audio with a flag on it. The document is asked for a
buffer that loops on itself — the song played round until the audio stops
changing, where it can say so — and the loop fields are set to the music
alone, so the trailing row a held note gets is
never between the last bar and the first. See ChiptuneSongPlayer, which
is the node this is for.
Not exported, and set before the document: for a
document that answers render_loop it changes what is rendered, so a
change to it re-renders. For any other the audio is the same either way,
and a change moves only the loop points.
Methods
rebuild
func rebuild() -> voidRenders the document into this stream’s own bytes.
Asked of the document, whatever its class, so this plays a sound, a song, or anything else answering the two methods below — which is the whole extension point the addon has. A third document type needs a panel to edit it and an entry in the plugin’s _handles, but it needs nothing here.
render(sample_rate: int, stereo: bool) -> PackedFloat32Array, and optionally prefers_stereo() -> bool, where absent means mono: a single voice has nothing to pan against, and mono is half the memory, and render_loop(sample_rate: int, stereo: bool) -> PackedFloat32Array, where absent means a loop runs the whole buffer.
Anything not answering render() is silence, with no error. A half assigned property in an editor is a normal intermediate state.
Rendered now, in this frame. A change to the document renders on its own on the next idle frame, once however many fields moved; this is for the caller that wants the audio before then, and it answers the queued render too, so the idle frame renders nothing after it. A node playing this stream is started again where it had got to.
prerender
func prerender() -> voidRenders the document on another thread, and emits prerendered
when this stream holds the result.
The opt-in to a render that does not stall: a loading screen asks for it
and waits, and nothing about a stream that is never asked changes. Until
it lands the stream keeps whatever it held before, which for one made by
prerendered_for is silence.
The document is read on the other thread until the signal, so a script
editing it in that time is racing the render. A render asked for in the
meantime, by assigning a document or by rebuild, wins
over this one, and the signal still comes.
is_prerendering
func is_prerendering() -> boolWhether a prerender has yet to land.
prerendered_for
static func prerendered_for(value: Resource, loop := false) -> ChiptuneAudioStreamA stream for value that renders on another thread, off the way
in: what a loading screen builds, then awaits
prerendered on, then hands to a node’s stream.
loop is loop, set before anything renders so the one
render is the right one.
for_document
static func for_document(value: Resource, existing: AudioStream = null, loop := false) -> ChiptuneAudioStreamThe stream a node should be playing for this document, reusing the one it already has, so an assignment builds no new resource.
Reuse matters: a node’s stream is handed out to anything already playing it, and swapping the resource under a running player is a different sound coming from the same node mid-note. Re-rendering in place is not.
reload_document
func reload_document() -> voidReads the document off disk again and renders that.
The object held is the one loaded before the save — ResourceSaver writes the panel’s own copy and does not reach into anything already holding the old one — so re-rendering what is in hand would render what was already there.
document_saved
static func document_saved(path: String) -> intRe-renders every loaded stream whose document is the file just saved, and answers how many there were.
This is the nudge that makes “edit the sound, save, hear it in the scene” true. Without it a save leaves every node playing the render from before the edit, which is the drift this resource exists to remove.
The file is loaded once and rendered once however many streams are holding it, and the rest copy the audio. Same file, same renderer, same rate: the bytes cannot differ, so rendering them again per stream was work with no result — five nodes on examples/audio/song.music.tres measured 1442 ms of it, all producing identical audio, on the main thread, on every Ctrl+S.
document_saved_in_background
static func document_saved_in_background(path: String) -> intdocument_saved off the main thread, which is what the live path
does with a song: there a render on the main thread is the running game
frozen for seconds.
The file is read once and rendered once per loop setting on another thread, and every stream holding it takes the audio when it lands. A node playing one of them is started again where it had got to, wrapped round the new length when the stream loops and clamped to it when it does not, so the music carries on from where it was. Answers how many streams will change.
remix
static func remix(songs: bool, sounds: bool) -> intRenders again, now, every live stream holding a song when songs
and a sound when sounds, and answers how many: what a save of the
style that moved its mix calls, ChiptuneGameMix, so a node already in a
scene plays at the new level, from where it had got to. Once per document
and loop setting, the rest copying the audio, as document_saved
does: five nodes on one song are one render.
Signals
document_changed
signal document_changedEmitted after new audio lands in this stream, whatever asked for it: the
document changing, rebuild, a save, a prerender. Once
per landing, so ten fields set in one frame are one of these.
prerendered
signal prerenderedEmitted on the main thread once a prerender has put its audio in
this stream, and once only however many calls were made before it did.
Always a frame or more after the call, never inside it, so
stream.prerender() then await stream.prerendered
cannot miss it.
Constants
BEATS_PER_BAR
const BEATS_PER_BAR := 4How many beats make a bar, for the restart a saved song makes under a
node playing it. A song has no time signature; four is the grid the
tracker draws, rows_per_beat rows to the beat.
JamChip, by GlitterSnack. MIT licence. Built from the source on every merge. Its artwork and screenshots are made with JamChip.