Skip to content

ChiptuneAudioStream

Extends AudioStreamWAV. Read from the plugin's source on every build.

A sound or a song as something an AudioStreamPlayer can hold directly.

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 among it rather than instead 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 rather than an AudioStream with its own playback: the bytes are all the engine wants, ChiptuneWav already produces them, and a custom AudioStreamPlayback would buy nothing until playback is streamed, which is a separate piece of work with its own entry on the roadmap.

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 rather than a class, and hinted to the two documents this addon ships so the picker and its Quick Load search offer those rather than every resource in the project. 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 rather than an 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 rather than to the whole buffer, so the trailing row a held note gets is never between the last bar and the first. See ChiptuneMusicPlayer, which is the node this is for.

Not exported, and set before the document rather than after: it changes what is rendered, so a change to it re-renders.

Methods

render_document

func render_document() -> void

Renders the document into this stream’s own bytes.

Asked of the document rather than switched on 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 rather than an error. A half assigned property in an editor is a normal intermediate state.

rerender_playing

static func rerender_playing(player: Node, current: ChiptuneAudioStream) -> void

Renders stream again for a node that may be playing it, and leaves a playing node playing.

The four player classes all call this, because all four ship a rerender() a game is told to call. A render replaces the bytes the mixer is reading: done under a live playback head it is a click at best, and where the new render is shorter than the old position it is the head past the end of the buffer. So the node is stopped first, rendered, and started again at where it had reached, clamped to what the new audio is long.

The seam is audible — the audio genuinely changed — and it is a seam the caller asked for. What it is not is a read past the end.

player is any of the four: each has is_playing, get_playback_position, stop and play, which is all this needs.

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 rather than building a new resource on every assignment.

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.

Constants

HIDDEN_PROPERTIES

const HIDDEN_PROPERTIES: Array[StringName] = [ &"format", &"mix_rate", &"stereo", &"loop_mode", &"loop_begin", &"loop_end", ]

What render_document writes, hidden from the inspector.

Every one of them is an output of the render: the format and rate are the renderer’s, stereo is what the document asked for, and the loop fields are _apply_loop reading loop. Offering them is offering an edit the next render overwrites — and a mix_rate typed in by hand is a sound that plays at the wrong speed until something happens to re-render it. document is the one thing to set.

Stored as AudioStreamWAV stores them, apart from data, which is the exception below.

DERIVED_PROPERTIES

const DERIVED_PROPERTIES: Array[StringName] = [&"data"]

The rendered bytes are never written to disk.

They are the document rendered, which makes them derived: storing them puts the same audio in two files, one of which is the one that goes stale, and for a song of any length it is megabytes of base64 in a resource whose actual content is a single path. AudioStreamWAV exports data, so the storage flag has to be taken off it rather than simply not set.

JamChip 0.1.0, by GlitterSnack. MIT licence. Built from the source on every merge, artwork and screenshots included, with JamChip.