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: ResourceThe .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 := trueWhether 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 := falseWhether 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 := falseWhether 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.0The 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 := 0Semitones 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) -> intHow 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() -> voidTakes 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) -> voidMoves 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) -> voidMoves 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() -> intThe row being heard, counted from the start of the song through its order.
pattern_row
func pattern_row() -> intThe 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() -> intThe entry of the song’s order being heard, the number
jump_to_pattern takes; -1 with no song.
position_bar
func position_bar() -> intThe 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) -> voidSilences 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) -> boolWhether 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) -> voidSets channel’s volume on this node, 0 to 1, over the song’s.
set_channel_solo
func set_channel_solo(channel: int, solo: bool) -> voidWhile any channel is soloed, only the soloed ones sound.
set_send
func set_send(channel: int, effect: StringName, amount: float, seconds := 0.0) -> boolEases 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) -> voidPlays 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) -> voidGives channel back the instruments the song names.
Signals
document_changed
signal document_changedEmitted 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 loopedEmitted 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_BARBeats 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_SECONDSHow 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.