Skip to content

ChiptuneSongPlayer2D

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

Music the game steers, from a place on screen, panned and quieter with distance. An AudioStreamPlayer2D that plays a .music.tres, a song from the Tracker.

ChiptuneSongPlayer placed in 2D: a radio in a room, a band on a stage, a boss whose theme follows it. What a game uses, by need, is the same:

Loop or stinger. loop, and AudioStreamPlayer2D.finished when it is off.

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.

A class of its own, since Godot’s players descend from Node and Node2D and share no ancestor that has a stream; both hold the same engine, so a song streams, lands its changes and says its beats the same way on each, and at a AudioStreamPlayer2D.volume_db of -80 or below renders nothing and keeps its place; one only out of the listener’s range still renders. Music heard the same wherever the camera is belongs on ChiptuneSongPlayer. For 3D, put a ChiptuneAudioStream on Godot’s own AudioStreamPlayer3D: it plays the song whole, with no beat signals and nothing to steer it.

AudioStreamPlayer2D.seek and the position play takes do not move the music, since a generator cannot seek: jump_to_pattern and jump_to_row do.

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. The picker offers songs alone; a sound assigned from code still plays, and warns.

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, own_loop, as on ChiptuneSongPlayer; on with no song. A loop comes round at the end of the last row; a one-shot plays the last note’s tail out and stops. Turned over while the music plays, it takes effect at the end of the pass under way. Declared after document, as on ChiptuneSongPlayer, so a stored loop = false 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, so a script’s edit to it is heard here alone; the scene still stores the file, and a save while the game runs copies it again. 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, for a change the same frame has to hear; a change on its own is heard on the next idle frame. It builds the notes and renders nothing, as on ChiptuneSongPlayer.

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.