Files
feuerwerk/src/core/graph/nodes/action/songSync.ts
T
adminandClaude Opus 5.5 53ec1094f4 Let a game switch songs and turn their tracks on the beat (#338)
A game could start and stop a song, but not steer one while it plays.
The battle theme could not take over on the next bar, a song could not
jump to a marker, and a track could not fall away when the player
hides. This is stage 3 of 5 in #338.

- Quantize: now, next beat, next bar (the default) or next marker. The
  spot is the next one as the song is heard, and the change is planned
  on the audio clock to the sample, round the loop as the ruler counts.
- Set song track turns one track in dB on top of the song's level, over
  a fade, on the bus input before its FX and sends.
- Switch song plays another song, or the same one from a marker, in
  place of the playing one. It keeps the playback number, so Stop song,
  Set song track and On beat go on reaching it.
- A playing now has a key for its buses apart from its public number.
  During a switch the old and the new song each have buses of their own.
- Play song has a "live tracks" switch. With it off a baked song plays
  its bake, and Set song track on it writes a line to the log.
- A Switch song to a marker plays the notes, as a bake only plays from
  its start.

Found on the way:

- A cut on a bar line faded over seconds in Chrome. cancelAndHoldAtTime
  holds nothing where nothing is planned later, so the ramp ran from the
  bus's last change. The turn now sets its start value on the spot. It
  had been measured at 0.52 of full level 0.2 s past the bar line.

Tests:
- Unit: 23 new cases in beats, song-play, sound-library and the node
  catalog. Counter-checks: 17 bends, all compile, all red.
- e2e 65 (atx): ALL PASSED, 73 checks. 65k3 is new. The switch landed
  three bars after the start under the same number, and the track cut
  on the next bar line with nothing in between. With the old ramp 65k3
  is red (0.53 and 0.51 between).
- Gate (atx): CHECK EXIT 0, 334 files, 5144 tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 21:17:52 +01:00

111 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { heldQuantize, QUANTIZE_OPTIONS } from '../../../music/beats';
import { TRACK_MAX_DB, TRACK_MIN_DB } from '../../../music/level';
import type { NodeDef, PortDef } from '../../NodeRegistry';
import { asNumber, asString } from '../../values';
import { exec, num, withDefault } from '../shared';
import { songPort } from './PlaySong';
/** Where a change to the music lands (#338): at once, or on the next beat, bar or marker of the playing. */
const quantizePort = (): PortDef => ({
key: 'quantize',
label: 'quantize',
type: 'string',
default: 'bar',
field: { key: 'quantize', label: 'Quantize', kind: 'enum', default: 'bar', options: [...QUANTIZE_OPTIONS] },
});
/** The playing a node changes: not typed in, as it is a number Play song hands out. */
const playbackPort = (): PortDef => ({ ...num('playback'), inline: false });
const trackPort = (): PortDef => ({
key: 'track',
label: 'track',
type: 'string',
field: { key: 'track', label: 'Track', kind: 'asset', default: '', assetTypes: ['song'], tracks: true },
});
const markerPort = (): PortDef => ({
key: 'marker',
label: 'marker',
type: 'string',
field: { key: 'marker', label: 'Marker', kind: 'asset', default: '', assetTypes: ['song'], markers: true },
});
const QUANTIZE =
'"quantize" says where it lands: Now, or on the next beat, bar or marker of the song as it is ' +
'heard, planned on the audio clock to the sample; Next marker in a song without markers is the ' +
'next bar.';
export const SetSongTrack: NodeDef = {
type: 'SetSongTrack',
label: 'Set song track',
category: 'action',
description:
'Turns one track of a playing song up or down, by the "playback" number Play song handed out: ' +
'"volume" in decibels on top of the level the song gives it, 0 as the song has it, -60 silent – ' +
'what goes into the track, its sound FX and its sends alike. It gets there over "fade" seconds. ' +
`${QUANTIZE} The track is picked as "Song / Track"; a track the playing song does not have is ` +
'left alone. A song played from its bake has no tracks to turn: switch on "live tracks" in Play ' +
'song, else the log says so. A Switch song to the same song keeps what was turned.',
inputs: [
exec(),
playbackPort(),
trackPort(),
withDefault(
{
...num('volume', 'volume (dB)'),
field: {
key: 'volume',
label: 'Volume (dB)',
kind: 'number',
default: 0,
min: TRACK_MIN_DB,
max: TRACK_MAX_DB,
},
},
0,
),
withDefault(num('fade'), 1),
quantizePort(),
],
outputs: [exec()],
params: [],
run: (ctx, inputs) => {
ctx.setSongTrack(
asNumber(inputs.playback),
asString(inputs.track),
asNumber(inputs.volume),
asNumber(inputs.fade),
heldQuantize(inputs.quantize),
);
return { next: 'exec' };
},
};
export const SwitchSong: NodeDef = {
type: 'SwitchSong',
label: 'Switch song',
category: 'action',
description:
'Plays another song, or the same one from a marker, in place of a playing song, by the ' +
'"playback" number Play song handed out; the playing keeps its number, so Stop song and Set ' +
'song track go on reaching it. "song" left empty is the same song, "marker" left empty its ' +
`start. ${QUANTIZE} With "fade" 0 it is a cut on that spot; else the old song falls silent over ` +
'that many seconds as the new one rises from it. The new song plays with the volume, loop, ' +
'bus, sound FX and "live tracks" of its Play song. A marker the song does not have is logged, ' +
'and nothing changes.',
inputs: [exec(), playbackPort(), songPort(), markerPort(), quantizePort(), withDefault(num('fade'), 0)],
outputs: [exec()],
params: [],
run: (ctx, inputs) => {
ctx.switchSong(
asNumber(inputs.playback),
asString(inputs.song),
asString(inputs.marker),
heldQuantize(inputs.quantize),
asNumber(inputs.fade),
);
return { next: 'exec' };
},
};