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>
This commit is contained in:
1 parent
a6a41b6b69
commit
53ec1094f4
23 files changed
+1259
-101
No files matched your search
+112
-2
@@ -937,7 +937,10 @@ export async function song(t) {
|
||||
{ ...base, id: 'sg65-i2', track: song.tracks[1].id, kind: 'pattern', asset: ${JSON.stringify(PATTERN)}, tick: ${BAR}, length: ${BAR}, offset: 0 },
|
||||
];
|
||||
song.loop = { start: ${BAR}, end: ${2 * BAR} };
|
||||
song.markers = [{ id: 'sg65-mark', name: 'Round', tick: ${BAR} }];`);
|
||||
song.markers = [
|
||||
{ id: 'sg65-mark', name: 'Round', tick: ${BAR} },
|
||||
{ id: 'sg65-half', name: 'Half', tick: ${BAR + BAR / 2} },
|
||||
];`);
|
||||
await showTab('scene');
|
||||
// 65k2 hears it through these (#338): each beat logs bar, beat, how late it is told and where
|
||||
// Song position stands then, each marker its name.
|
||||
@@ -969,6 +972,20 @@ export async function song(t) {
|
||||
node('sg65-marker', 'OnSongMarker', 0, 800, { song: id }),
|
||||
node('sg65-jm', 'Join', 200, 800, { a: 'marker ' }),
|
||||
node('sg65-logm', 'Log', 400, 800),
|
||||
// 65k3 (#338): on a beat with W held, Switch song to "Half" on the next bar; with V held, the
|
||||
// track of the pattern turned down to silence there.
|
||||
node('sg65-cue', 'OnBeat', 0, 1000, { song: id }),
|
||||
node('sg65-w', 'KeyIsDown', 0, 1200, { key: 'w' }),
|
||||
node('sg65-wif', 'Branch', 260, 1000),
|
||||
node('sg65-switch', 'SwitchSong', 520, 1000, { marker: 'sg65-half', quantize: 'bar', fade: 0 }),
|
||||
node('sg65-v', 'KeyIsDown', 0, 1400, { key: 'v' }),
|
||||
node('sg65-vif', 'Branch', 520, 1200),
|
||||
node('sg65-turn', 'SetSongTrack', 780, 1200, {
|
||||
track: tracks[1],
|
||||
volume: -60,
|
||||
fade: 0,
|
||||
quantize: 'bar',
|
||||
}),
|
||||
],
|
||||
edges: [
|
||||
edge('sg65-e3', 'sg65-start', 'exec', 'sg65-play', 'exec'),
|
||||
@@ -988,6 +1005,14 @@ export async function song(t) {
|
||||
edge('sg65-m1', 'sg65-marker', 'name', 'sg65-jm', 'b'),
|
||||
edge('sg65-m2', 'sg65-marker', 'exec', 'sg65-logm', 'exec'),
|
||||
edge('sg65-m3', 'sg65-jm', 'text', 'sg65-logm', 'message'),
|
||||
edge('sg65-c1', 'sg65-cue', 'exec', 'sg65-wif', 'exec'),
|
||||
edge('sg65-c2', 'sg65-w', 'down', 'sg65-wif', 'condition'),
|
||||
edge('sg65-c3', 'sg65-wif', 'then', 'sg65-switch', 'exec'),
|
||||
edge('sg65-c4', 'sg65-cue', 'playback', 'sg65-switch', 'playback'),
|
||||
edge('sg65-c5', 'sg65-wif', 'else', 'sg65-vif', 'exec'),
|
||||
edge('sg65-c6', 'sg65-v', 'down', 'sg65-vif', 'condition'),
|
||||
edge('sg65-c7', 'sg65-vif', 'then', 'sg65-turn', 'exec'),
|
||||
edge('sg65-c8', 'sg65-cue', 'playback', 'sg65-turn', 'playback'),
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -1066,11 +1091,96 @@ export async function song(t) {
|
||||
beats.every((b) => b.late >= 0 && b.late <= 0.15 && Math.abs(b.fraction * 0.5 - b.late) < 0.02),
|
||||
JSON.stringify({ beats, markersHeard: markersHeard.length }),
|
||||
);
|
||||
// 65k3. Music follows the game (#338): W held over a beat asks Switch song for "Half", bar 2 beat 3,
|
||||
// on the next bar. The playing keeps its number on buses of its own, heard from that bar line
|
||||
// to the sample; On beat starts over from the marker. V held turns the pattern's track down to
|
||||
// silence on the bar after, on its input, its level as the song has it.
|
||||
const clockNow = () => json('__gm.sound.clock()');
|
||||
const firstPlaying = (await playbacks())[0];
|
||||
const linesBefore = (await heardLines()).length;
|
||||
await t.holdKey('w', 600);
|
||||
const pair = await waitFor(playbacks, (list) => list.length === 2, 20);
|
||||
const landed = await waitFor(
|
||||
playbacks,
|
||||
(list) => list.length === 1 && list[0].startedAt !== firstPlaying?.startedAt,
|
||||
40,
|
||||
);
|
||||
const switchedTo = landed[0];
|
||||
const beatsSince = (lines) =>
|
||||
lines.slice(linesBefore).flatMap((text) => {
|
||||
const m = /\bbeat (\d+)\.(\d+) /.exec(text);
|
||||
return m ? [`${m[1]}.${m[2]}`] : [];
|
||||
});
|
||||
// Only the jump to the marker hears 2.3 right after 2.4: the loop goes from 2.4 to 2.1.
|
||||
const jumpIn = (beats) => beats.findIndex((b, i) => b === '2.4' && beats[i + 1] === '2.3') + 1;
|
||||
const sinceSwitch = beatsSince(
|
||||
await waitFor(
|
||||
heardLines,
|
||||
(lines) => {
|
||||
const beats = beatsSince(lines);
|
||||
const at = jumpIn(beats);
|
||||
return at > 0 && beats.length > at + 1;
|
||||
},
|
||||
40,
|
||||
),
|
||||
);
|
||||
const fromHalf = jumpIn(sinceSwitch) || -1;
|
||||
const bars = switchedTo && firstPlaying ? (switchedTo.startedAt - firstPlaying.startedAt) / 2 : NaN;
|
||||
check(
|
||||
'Switch song plays the song from its marker on the next bar line, to the sample, under the same number; On beat goes on from the marker',
|
||||
pair.length === 2 &&
|
||||
pair.every((p) => p.id === firstPlaying?.id) &&
|
||||
switchedTo?.id === firstPlaying?.id &&
|
||||
Math.abs(bars - Math.round(bars)) < 1e-6 &&
|
||||
Math.round(bars) >= 1 &&
|
||||
fromHalf >= 0 &&
|
||||
sinceSwitch[fromHalf + 1] === '2.4',
|
||||
JSON.stringify({
|
||||
firstPlaying,
|
||||
pair: pair.map((p) => [p.id, p.startedAt]),
|
||||
switchedTo,
|
||||
bars,
|
||||
sinceSwitch,
|
||||
}),
|
||||
);
|
||||
const newMaster = (await json('__gm.song.buses()')).find((b) => /^play:\d+$/.test(b.name))?.name;
|
||||
const pattern = `${newMaster}:${busOf(tracks[1])}`;
|
||||
const turnedOf = async () => {
|
||||
const at = await clockNow();
|
||||
const bus = (await json('__gm.song.buses()')).find((b) => b.name === pattern);
|
||||
return { at, turned: bus?.turned ?? 1, gain: bus?.gain };
|
||||
};
|
||||
// Read from before V on until it is silent: a cut, so 1 up to a bar line and 0 from it, nothing
|
||||
// between. The switched song started on beat 3 of bar 2, so its bar lines lie 1 s after it, then
|
||||
// every 2 s.
|
||||
const turns = [await turnedOf()];
|
||||
await t.keyDown('v');
|
||||
const silent = await waitFor(
|
||||
async () => (turns.push(await turnedOf()), turns.at(-1)),
|
||||
(r) => r.turned === 0,
|
||||
40,
|
||||
);
|
||||
await t.keyUp('v');
|
||||
const lastOn = turns.findLast((r) => r.turned === 1);
|
||||
const firstBar = (switchedTo?.startedAt ?? 0) + 1;
|
||||
const barLine = lastOn && firstBar + Math.max(0, Math.ceil((lastOn.at - 0.05 - firstBar) / 2)) * 2;
|
||||
check(
|
||||
'Set song track turns the pattern’s track to silence on the next bar line, its own level left as the song has it',
|
||||
newMaster !== `play:${firstPlaying?.id}` &&
|
||||
silent.turned === 0 &&
|
||||
turns.every((r) => r.turned === 1 || r.turned === 0) &&
|
||||
barLine !== undefined &&
|
||||
silent.at >= barLine - 0.05 &&
|
||||
silent.at < barLine + 0.5 &&
|
||||
Math.abs(silent.gain - 10 ** (-6 / 20)) < 1e-6,
|
||||
JSON.stringify({ newMaster, barLine, turns }),
|
||||
);
|
||||
await press('s');
|
||||
const fading = await waitFor(playbacks, (list) => list[0]?.fading !== null);
|
||||
const fadedOut = await waitFor(playbacks, (list) => list.length === 0, 40);
|
||||
await sleep(100);
|
||||
const leftBuses = (await json('__gm.song.buses()')).filter((b) => b.name.startsWith(master));
|
||||
// Every playing's, the one 65k3 switched to among them.
|
||||
const leftBuses = (await json('__gm.song.buses()')).filter((b) => /^play:/.test(b.name));
|
||||
await stop();
|
||||
check(
|
||||
'Stop song fades it out over its second, playing on meanwhile, then it and its buses are gone',
|
||||
|
||||
Reference in new issue
Block a user