Let a behavior hear a song, pattern or sound come to its end (#373)
A game should be able to start the next piece of music when one is over, or wait for a sound before it goes on. Until now a behavior could start and stop music and sounds, but never learn that they had ended. - Two new events: On music finished with the fields Song and Pattern and the input playback, and On sound finished with the field Sound and the input voice. Both give out what ended and stopped. - Two new data nodes, Music playing and Sound playing, ask whether something is still heard. - Over means nothing is heard any more: no tail of the effects, and a fade-out has run down. - stopped is on for Stop song, pattern or sound, when the behavior that owns it ends and on a scene change. For a sound also when a newer voice needs its place or the file does not load. - Only what the Play nodes of this run started is heard, across scene changes too. Previews in the tabs, compositions and notes of instruments do not count. - Switch song ends nothing. The number goes on, and the end is told under the song heard last. - Playbacks and the SoundLibrary tell each end once to a new PlayedEnds per run. The AudioSwitch passes them on from both sinks. Found and fixed: a pattern whose last bar ends in silence counted as done up to one planning window before its end. In e2e 65 the next pattern started after 1.962 s instead of 2 s. Scheduler.done now also wants the playhead to have reached the end. Cleaning up done playbacks therefore comes a few tenths of a second later. One test that expected the early end is adjusted. Tests: - Unit: 23 new (played-ends 12, sound-library 5, node catalog 5, audio-switch 1), all green. - Counter-probes: 19 bends, all red. One was green at first for want of a case, which is added now. - e2e 65 on atx: ALL PASSED, 78 checks. New 65k6: the event came once with stopped off, and the second pattern started 2.051 s after the stinger. Stop song gave exactly one end with stopped on, and the switch from 65k3 gave none. - Gate on atx: CHECK EXIT 0, 337 files, 5222 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2333f66ba2
commit
e53121a0bb
22 files changed
+932
-29
No files matched your search
+67
-2
@@ -1,4 +1,4 @@
|
||||
import { edge, node } from '../lib/builders.mjs';
|
||||
import { edge, node, port as readPort, variable, written } from '../lib/builders.mjs';
|
||||
import { LANES, pickAssetType, scaffoldBehavior } from './lib/scaffold.mjs';
|
||||
import { sampleZoom } from './lib/zoomTransition.mjs';
|
||||
|
||||
@@ -1009,6 +1009,18 @@ export async function song(t) {
|
||||
node('sg65-r', 'KeyIsDown', 0, 2000, { key: 'r' }),
|
||||
node('sg65-rif', 'Branch', 1300, 1800),
|
||||
node('sg65-let', 'ReleaseSongLane', 1560, 1800, { track: tracks[0], lane: 'volume', fade: 0.5 }),
|
||||
// 65k6 (#373): once the stinger is over, the pattern once more, at its own tempo. The
|
||||
// stinger's number is kept in a variable, as no event reads an output another one left; heard
|
||||
// by it, the one started after is not heard. And the end of the song, logged.
|
||||
node('sg65-keep', 'Variables', 1300, 1600, {}, [written('sting', 'number')]),
|
||||
node('sg65-kept', 'Variables', 1040, 1800, {}, [readPort('sting', 'number')]),
|
||||
node('sg65-fin', 'OnMusicFinished', 1300, 1400),
|
||||
node('sg65-again', 'PlayPattern', 1560, 1400, { pattern: PATTERN, loop: false }),
|
||||
node('sg65-jf', 'Join', 1560, 1600, { a: 'finished stinger ' }),
|
||||
node('sg65-logf', 'Log', 1820, 1400),
|
||||
node('sg65-fins', 'OnMusicFinished', 0, 2200, { song: id }),
|
||||
node('sg65-js', 'Join', 260, 2200, { a: 'finished song ' }),
|
||||
node('sg65-logs', 'Log', 520, 2200),
|
||||
],
|
||||
edges: [
|
||||
edge('sg65-e3', 'sg65-start', 'exec', 'sg65-play', 'exec'),
|
||||
@@ -1048,9 +1060,24 @@ export async function song(t) {
|
||||
edge('sg65-c18', 'sg65-r', 'down', 'sg65-rif', 'condition'),
|
||||
edge('sg65-c19', 'sg65-rif', 'then', 'sg65-let', 'exec'),
|
||||
edge('sg65-c20', 'sg65-cue', 'playback', 'sg65-let', 'playback'),
|
||||
edge('sg65-f0', 'sg65-sting', 'exec', 'sg65-keep', 'exec'),
|
||||
edge('sg65-f00', 'sg65-sting', 'playback', 'sg65-keep', 'sting'),
|
||||
edge('sg65-f1', 'sg65-kept', 'sting', 'sg65-fin', 'playback'),
|
||||
edge('sg65-f2', 'sg65-fin', 'exec', 'sg65-again', 'exec'),
|
||||
edge('sg65-f3', 'sg65-again', 'exec', 'sg65-logf', 'exec'),
|
||||
edge('sg65-f4', 'sg65-fin', 'stopped', 'sg65-jf', 'b'),
|
||||
edge('sg65-f5', 'sg65-jf', 'text', 'sg65-logf', 'message'),
|
||||
edge('sg65-f6', 'sg65-fins', 'exec', 'sg65-logs', 'exec'),
|
||||
edge('sg65-f7', 'sg65-fins', 'stopped', 'sg65-js', 'b'),
|
||||
edge('sg65-f8', 'sg65-js', 'text', 'sg65-logs', 'message'),
|
||||
],
|
||||
},
|
||||
});
|
||||
await mutate(
|
||||
`__gm.state.project.assets.find((a) => a.id === 'sg65-logic').variables = ${JSON.stringify([
|
||||
variable('sting', 'number', 0),
|
||||
])};`,
|
||||
);
|
||||
const playbacks = () => json('__gm.sound.playbacks()');
|
||||
const heardFrom = t.consoleLog.length;
|
||||
await play();
|
||||
@@ -1233,7 +1260,18 @@ export async function song(t) {
|
||||
const song = stung.find((p) => p.name === `song:${id}`);
|
||||
const stinger = stung.find((p) => p.name === `pattern:${PATTERN}`);
|
||||
const beatsIn = stinger && song ? (stinger.startedAt - song.startedAt) / 0.5 : NaN;
|
||||
const alone = await waitFor(playbacks, (list) => list.length === 1, 60);
|
||||
// Every pattern playing seen on the way, the one 65k6 starts after the stinger among them.
|
||||
const patterns = new Map();
|
||||
const alone = await waitFor(
|
||||
async () => {
|
||||
const list = await playbacks();
|
||||
for (const p of list)
|
||||
if (p.name === `pattern:${PATTERN}` && p.startedAt !== null) patterns.set(p.id, p);
|
||||
return list;
|
||||
},
|
||||
(list) => list.length === 1 && patterns.size >= 2,
|
||||
80,
|
||||
);
|
||||
check(
|
||||
'a pattern synced to the song plays once on its next beat, to the sample, at the tempo of the song',
|
||||
Math.abs(beatsIn - Math.round(beatsIn)) < 1e-6 &&
|
||||
@@ -1244,6 +1282,28 @@ export async function song(t) {
|
||||
alone[0].name === `song:${id}`,
|
||||
JSON.stringify({ pressedAt, song: song?.startedAt, stinger, beatsIn, alone: alone.map((p) => p.name) }),
|
||||
);
|
||||
// 65k6. The end of a playing (#373): On music finished, hearing the stinger by its number, runs
|
||||
// once it is over, its "stopped" off, and Play pattern starts the pattern once more – after
|
||||
// the stinger's 2 s, within a frame and the lead of the scheduler. That one it does not hear.
|
||||
const finishedLines = (what) =>
|
||||
t.consoleLog.slice(heardFrom).filter((text) => text.includes(`finished ${what}`));
|
||||
// P held over two beats plays two stingers; the listener hears the last, by the number it left.
|
||||
const lastStinger = [...patterns.values()].filter((p) => p.startedAt < stinger.startedAt + 1.5).at(-1);
|
||||
const chained = [...patterns.values()].filter((p) => p.startedAt > lastStinger.startedAt + 1);
|
||||
const chainedAfter = chained[0] ? chained[0].startedAt - lastStinger.startedAt : NaN;
|
||||
check(
|
||||
'On music finished runs once the stinger is over, not stopped, and starts the pattern again right after it',
|
||||
finishedLines('stinger').length === 1 &&
|
||||
finishedLines('stinger')[0].endsWith('finished stinger false') &&
|
||||
chained.length === 1 &&
|
||||
chainedAfter >= 2 &&
|
||||
chainedAfter < 2.4,
|
||||
JSON.stringify({
|
||||
lines: finishedLines('stinger'),
|
||||
patterns: [...patterns.values()].map((p) => ({ id: p.id, startedAt: p.startedAt })),
|
||||
chainedAfter,
|
||||
}),
|
||||
);
|
||||
// 65k5. A game takes a lane over (#338): L held over a beat holds the volume lane of the sound's
|
||||
// track at -40 dB from the next bar line on, its curve (-24 to 0 dB each round) no longer
|
||||
// heard; R held lets it go, back to the curve over half a second. Read from the level of the
|
||||
@@ -1318,6 +1378,11 @@ export async function song(t) {
|
||||
leftBuses.length === 0,
|
||||
JSON.stringify({ fading: fading.map((g) => g.fading), fadedOut, leftBuses }),
|
||||
);
|
||||
check(
|
||||
'On music finished runs once for the song after its fade out, "stopped" on – not for the switch 65k3 made',
|
||||
finishedLines('song').length === 1 && finishedLines('song')[0].endsWith('finished song true'),
|
||||
JSON.stringify(finishedLines('song')),
|
||||
);
|
||||
await mutate(
|
||||
`delete __gm.state.project.assets.find((a) => a.id === ${JSON.stringify(PATTERN)}).props.tempo;`,
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user