Skip to content

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: Resource

The .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 := false

Whether 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() -> void

Renders 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() -> void

Renders 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() -> bool

Whether a prerender has yet to land.

prerendered_for

static func prerendered_for(value: Resource, loop := false) -> ChiptuneAudioStream

A 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) -> ChiptuneAudioStream

The 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() -> void

Reads 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) -> int

Re-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) -> int

document_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) -> int

Renders 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_changed

Emitted 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 prerendered

Emitted 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 := 4

How 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.