Skip to content

ChiptuneSongPlayer

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

Music the game steers while it plays. An AudioStreamPlayer that plays a .music.tres, a song from the Tracker.

What a game uses, by need:

Loop or stinger. With loop on, the last bar runs into the first. Off, it plays once and emits AudioStreamPlayer.finished, for a jingle or a game-over theme.

On the beat. beat, bar and looped as they are heard; position_bar, pattern_index and pattern_row say where the music is.

Sections. Calm, combat, a boss’s second phase: jump_to_pattern turns at the next bar line, and jump_to_row at the next buffer.

Mood. tempo and transpose, from the next pattern: faster at low health, up a key for the last lap.

Layers. set_channel_muted, set_channel_volume and set_channel_solo, from the next buffer: the drums come in when the enemies do.

A part’s sound. set_instrument and clear_instrument, from the next pattern.

ChiptuneSfxPlayer is the node for a sound. This is the node for music, and the difference is the loop: a song has bars, so it has a place the music ends that is not the place the audio ends, and only something that knows it is holding a song can tell them apart. ChiptuneSongPlayer2D is this node placed in 2D.

The song is streamed. The node holds an AudioStreamGenerator and fills it a fifth of a second ahead of what is heard, from ChiptuneStreamFeed, so a scene holding a song loads without rendering it, and a change — tempo, transpose, an instrument, jump_to_pattern, an edit to the song — lands at the next pattern or bar line with nothing rendered twice, and jump_to_row at the next buffer. Mute, solo and volume per channel land on the next buffer. None of them writes to the song, so two nodes playing one song can differ. The rendering runs on a worker thread where the engine has threads, outside the editor, so a frame pays almost nothing for the music; a script editing an instrument the song is playing races that render, as it does ChiptuneAudioStream.prerender. At a AudioStreamPlayer.volume_db of -80 or below, a AudioStreamPlayer.volume_linear of 0 included, nothing is rendered: the music keeps its place and its beat and bar, and turned up again it goes on from there, a buffer, a fifth of a second, later, with a note held across that moment left out.

A generator cannot seek, and AudioStreamPlayer.seek and the position AudioStreamPlayer.play takes are the engine’s and cannot be overridden, so neither moves the music. play starts at the top, or at the place jump_to_pattern or jump_to_row named before it; move it in the song’s own units with those two. AudioStreamPlayer.pitch_scale still works and moves the speed with the pitch; transpose moves the pitch alone.

A document that is not a song, a sound assigned from code, is still rendered whole into a ChiptuneAudioStream as it was before streaming.

stream is hidden and not saved. It is built from document and loop, so showing it would be a property that can disagree with the two it is made of.

Properties

document

@export_custom(PROPERTY_HINT_RESOURCE_TYPE, "ChiptuneSong") var document: Resource

The .music.tres to play.

Hinted to ChiptuneSong alone: a sound belongs on a ChiptuneSfxPlayer, which is where the sound-shaped controls are. The hint is an editor filter and not the contract — anything answering render() still plays when it is assigned from code.

loop

@export var loop := true

Whether the song comes round again or plays once.

The song’s own ChiptuneSong.loop until the node sets its own, so a sting plays once with nothing set here, and the node follows when the song’s is changed; set back to the song’s, it follows again, own_loop. With no song, on by default, because music that stops after one pass is almost never what a scene holding a song wants, and the jam’s game reached the plugin for the first time by writing that loop by hand.

A loop comes round at the end of the last row, and the last note’s tail rings on over the first bar, with no gap between the two; a one-shot plays that tail out and then stops. Turned over while the music plays, it takes effect at the end of the pass under way.

Declared after document, so the file comes first in the inspector. A scene assigns exported properties in the order they are declared, so a scene storing loop = false sets it on a song already assigned, which is a flag on the feed and renders nothing.

own_loop

@export_storage var own_loop := false

Whether loop is this node’s own rather than its song’s: set by setting loop to anything but the song’s, and stored with the scene. Set it false to follow the song again.

unique

@export var unique := false

Whether this node plays its own copy of document, apart from the file every other node of it shares.

Off, a script’s edit to document is heard on every node holding the same song. On, the copy is made when the song is assigned, so a tempo or a note set on this node’s document is this node’s alone; the scene still stores the file. A save of the file while the game runs copies it again, and the node’s own edits go with it; the copy is heard from the next pattern, like any save, with nothing rendered. In the editor the node holds the file, so the inspector shows what the scene stores. The mixer, tempo and transpose on the node are its own either way.

tempo

var tempo := 0.0

The tempo this node plays the song at, in bpm; 0 is the song’s own ChiptuneSong.bpm. Lands at the next pattern. Not stored: it is what a game does to the music, not part of the scene.

transpose

var transpose := 0

Semitones added to every note as it starts; 0 plays the song as written. Lands at the next pattern.

Methods

streaming

static func streaming(path: String) -> int

How many live nodes of this class are streaming a song from the file at path, which no ChiptuneAudioStream counts when it is saved. What the live path asks before it reloads the file.

rebuild

func rebuild() -> void

Takes document as it stands now, in this frame, without waiting for the next idle frame.

A change to the song is heard without this: a script that sets ChiptuneSong.bpm or writes a note has the node rebuild once on the next idle frame however many fields moved. This is for the frame the change was made in. It builds the notes, which is cheap, and the music takes them at the next pattern; nothing is rendered.

jump_to_pattern

func jump_to_pattern(order_index: int, on_bar_end := true) -> void

Moves the music to the start of the pattern at order_index in the song’s order.

At the next bar line when on_bar_end, so the music turns on the beat; at the next buffer when not, a fifth of a second from now. Before play, it is where play starts. A second jump before the first is heard replaces it: the last one asked for is the one that lands.

jump_to_row

func jump_to_row(order_index: int, row: int) -> void

Moves the music to row row of the pattern at order_index in the song’s order, at the next buffer, a fifth of a second from now.

What steps the music note to note: jump_to_row(pattern_index(), pattern_row() + 1) is the next row. A row past the end of its pattern goes on into the entries after it, and a negative one back into those before, so the step works across a pattern’s edge; past either end of the song it stops at the first or the last row. A note that started before the row and would still be sounding is not picked up part way: what plays from the row on is what starts on it. Before play, it is where play starts. A second jump before the first is heard replaces it.

position_rows

func position_rows() -> int

The row being heard, counted from the start of the song through its order.

pattern_row

func pattern_row() -> int

The row being heard in its own pattern, from 0, the number jump_to_row takes beside pattern_index; -1 with no song.

pattern_index

func pattern_index() -> int

The entry of the song’s order being heard, the number jump_to_pattern takes; -1 with no song.

position_bar

func position_bar() -> int

The bar being heard, from 0 at the start of the song. Every pattern starts a bar, so a pattern whose rows are not a whole number of bars ends on a short one.

set_channel_muted

func set_channel_muted(channel: int, muted: bool) -> void

Silences channel on this node, from the next buffer, without a click; false lets it sound again, a channel the song itself mutes included.

is_channel_muted

func is_channel_muted(channel: int) -> bool

Whether channel is silent on this node: muted here or in the song, or left out by another channel’s solo.

set_channel_volume

func set_channel_volume(channel: int, volume: float) -> void

Sets channel’s volume on this node, 0 to 1, over the song’s.

set_channel_solo

func set_channel_solo(channel: int, solo: bool) -> void

While any channel is soloed, only the soloed ones sound.

set_send

func set_send(channel: int, effect: StringName, amount: float, seconds := 0.0) -> bool

Eases channel’s send to effect, &"echo", to amount, 0 to 1, over seconds: set_send(1, &"echo", 0.3) gives the lead alone a little echo, and set_send(1, &"echo", 0.8, 2.0) swells it for a win. Heard from the next buffer. False for an effect or a channel this version does not have.

set_instrument

func set_instrument(channel: int, instrument: ChiptuneInstrument) -> void

Plays channel’s notes on instrument in place of the one the song names, from the next pattern. An instrument the song never held works.

clear_instrument

func clear_instrument(channel: int) -> void

Gives channel back the instruments the song names.

Signals

document_changed

signal document_changed

Emitted when a change to document is heard: an edit to the song, a save pushed from the dock, or rebuild. While the music plays that is at the next pattern, which is where the change lands.

beat

signal beat(index: int)

Emitted on each beat as it is heard, index counting from 0 at the bar line. A beat is ChiptuneSong.rows_per_beat rows from the start of a pattern.

bar

signal bar(index: int)

Emitted on each bar line as it is heard, index as position_bar counts it. A pattern starts a bar.

looped

signal looped

Emitted as the loop comes round to the first bar, before that bar’s bar.

Constants

BEATS_PER_BAR

const BEATS_PER_BAR := _SongEngine.BEATS_PER_BAR

Beats in a bar, for bar and position_bar. A song has no time signature; four is what the tracker’s grid and the live path’s bar already count in.

STREAM_BUFFER_SECONDS

const STREAM_BUFFER_SECONDS := _SongEngine.STREAM_BUFFER_SECONDS

How far ahead of the ear the generator is filled. Long enough that a frame which runs long does not starve it, short enough that a mute is heard at once; the tracker holds the same.

JamChip, by GlitterSnack. MIT licence. Built from the source on every merge. Its artwork and screenshots are made with JamChip.