Keep both sounds full at the middle of a Mix with a curve #331
A Mix crossfaded: at 0.5 each sound was at half. Layering two sounds in an instrument now goes through a Mix (076c564), so the user asked for a second curve where each side stays full up to the middle and only fades on the other half. At 0.25 a is full and b half, at 0.5 both are full, at 0.75 a is half and b full. - The Mix has a setting Curve with Crossfade (default) and Both at center. A plan and a chain only say `curve` where it is `both`, so old projects and their compiled plans stay as they were. - A share is not a straight line of `mix` with that curve. Two ramps of their own would dip to half each when `mix` glides across the middle. So a ConstantSourceNode carries `mix` on the audio thread, and two WaveShaperNodes make the shares from it. The knob of such a Mix is that one source. - The source is started with the voice and stopped when it is cut or ends by itself, so it does not run on after the voice. - A sound FX node in an instrument keeps the curve of a Mix inside it. - At 0.5 both sounds are added in full, up to +6 dB where both are loud. That is what several edges into the Output did before. The wiki says so. Found on the way: the suite of23bdc3f(task 529) failed the Step check in e2e 34. It timed the jump from the start of the voice, but a timed change runs from when it arrives. In CI that was 80 ms later, and the pitch rightly held to 2.08 s. - stats() says `tunedAt`, the second of the audio clock the last change moved a voice. A change that moves nothing, as On update sends every frame, leaves it. - The Step check times its readings from there. Tests: - sound-library.test.ts: both shares shaped from one source, a glide moves only the source, the source stops with a cut and with an end by itself (50 tests, 2 new) - soundfx.test.ts: curve only where set, one sound at 0 (41 tests) - instrument.test.ts: the curve through a sound FX node (34 tests) - Counter-checks: dropping the curve in throughFx and not stopping the source in done() each turn their new test red - e2e 34 on melektaus-atx: ALL PASSED, 48 checks (+1). Rendered offline in Chrome, a and b come to 1/0, 1/0.5, 1/1, 0.5/1 and 0/1 at mix 0 to 1, and a glide from 0 to 1 reads the same at 0.25, 0.5 and 0.75 s. Step jumped 2.04 s after the change. - Gate on melektaus-atx: CHECK EXIT 0, 244 test files, 3793 tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
076c564415
commit
fb4d65a2ab
8 files changed
+261
-25
No files matched your search
+53
-4
@@ -578,8 +578,9 @@ export async function sound(t) {
|
||||
await play();
|
||||
const pitchNow = () =>
|
||||
json(`({ pitch: __gm.sound.levels(${JSON.stringify(blip.file)})?.[0]?.[0], clock: __gm.sound.clock() })`);
|
||||
const sinceStart = async (seen) => {
|
||||
const from = (await stats()).last.at;
|
||||
const sinceStart = async (seen, since = 'start') => {
|
||||
const now = await stats();
|
||||
const from = since === 'tune' ? now.tunedAt : now.last.at;
|
||||
return seen
|
||||
.filter((s) => typeof s.pitch === 'number')
|
||||
.map((s) => ({ t: +(s.clock - from).toFixed(3), pitch: +s.pitch.toFixed(3) }));
|
||||
@@ -596,12 +597,16 @@ export async function sound(t) {
|
||||
);
|
||||
await stop();
|
||||
// Step holds the pitch for the whole time and then jumps there: every reading before 2 s on the
|
||||
// audio clock is still 7, the first one past it is the end.
|
||||
// audio clock is still 7, the first one past it is the end. Timed from the change, not from the
|
||||
// start of the voice: task 529 saw it arrive 80 ms later and the 7 held, rightly, to 2.08 s.
|
||||
await mutate(`
|
||||
const tune = __gm.state.project.assets.find((a) => a.id === 'sn-logic').graph.nodes.find((n) => n.id === 'sn-tune');
|
||||
Object.assign(tune.params, { ease: 'step', time: 2 });`);
|
||||
await play();
|
||||
const steps = await sinceStart(await trace(pitchNow, (s) => typeof s.pitch === 'number' && s.pitch < 6));
|
||||
const steps = await sinceStart(
|
||||
await trace(pitchNow, (s) => typeof s.pitch === 'number' && s.pitch < 6),
|
||||
'tune',
|
||||
);
|
||||
const held = steps.filter(({ pitch }) => pitch === 7);
|
||||
const jumped = steps.find(({ pitch }) => pitch < 6);
|
||||
check(
|
||||
@@ -750,6 +755,50 @@ export async function sound(t) {
|
||||
[echo.late, echo.latePlain].every((late) => late.every((n, k) => n === 0 || n === k * 128)),
|
||||
JSON.stringify(echo),
|
||||
);
|
||||
// A Mix "Both at center", rendered offline: a steady 1 on a and 0.5 on b in two channels, so each
|
||||
// share reads off as it is. Then mix glides 0 → 1 over a second: at its middle both stay full.
|
||||
const atCenter = await evaluate(`
|
||||
(async () => {
|
||||
const RATE = 48000;
|
||||
const render = async (mix, glide) => {
|
||||
const context = new OfflineAudioContext(2, RATE, RATE);
|
||||
const merger = context.createChannelMerger(2);
|
||||
merger.connect(context.destination);
|
||||
const shareA = context.createGain();
|
||||
const shareB = context.createGain();
|
||||
shareA.gain.value = 0;
|
||||
shareB.gain.value = 0;
|
||||
const driver = __gm.sound.bothAtCenter(context, mix, shareA, shareB);
|
||||
for (const [share, level, side] of [[shareA, 1, 0], [shareB, 0.5, 1]]) {
|
||||
const steady = context.createConstantSource();
|
||||
steady.offset.value = level;
|
||||
steady.connect(share).connect(merger, 0, side);
|
||||
steady.start();
|
||||
}
|
||||
if (glide) driver.offset.setValueAtTime(0, 0).linearRampToValueAtTime(1, 1);
|
||||
driver.start();
|
||||
const out = await context.startRendering();
|
||||
const at = (s) => [out.getChannelData(0)[Math.round(s * RATE)], out.getChannelData(1)[Math.round(s * RATE)] * 2];
|
||||
return glide ? [0.25, 0.5, 0.75].map(at) : at(0.5);
|
||||
};
|
||||
const round = (pair) => pair.map((v) => Math.round(v * 1000) / 1000);
|
||||
const fixed = {};
|
||||
for (const mix of [0, 0.25, 0.5, 0.75, 1]) fixed[mix] = round(await render(mix, false));
|
||||
return { fixed, glide: (await render(0, true)).map(round) };
|
||||
})()`);
|
||||
const pairNear = (pair, a, b) => near(pair[0], a) && near(pair[1], b);
|
||||
check(
|
||||
'a Mix "Both at center" keeps a full up to the middle and b full from it, also while mix glides',
|
||||
pairNear(atCenter.fixed[0], 1, 0) &&
|
||||
pairNear(atCenter.fixed[0.25], 1, 0.5) &&
|
||||
pairNear(atCenter.fixed[0.5], 1, 1) &&
|
||||
pairNear(atCenter.fixed[0.75], 0.5, 1) &&
|
||||
pairNear(atCenter.fixed[1], 0, 1) &&
|
||||
pairNear(atCenter.glide[0], 1, 0.5) &&
|
||||
pairNear(atCenter.glide[1], 1, 1) &&
|
||||
pairNear(atCenter.glide[2], 0.5, 1),
|
||||
JSON.stringify(atCenter),
|
||||
);
|
||||
await mutate(`
|
||||
const logic = __gm.state.project.assets.find((a) => a.id === 'sn-logic');
|
||||
logic.variables = [];
|
||||
|
||||
Reference in new issue
Block a user