not-tonight/tests/mix.test.ts
type-two 1a66c602fd LANE-JUICE2: ear-pass mix desk, ambience beds, night arc in the engine
The Phase-1 debt was 'verified by ear: NOTHING'. I still can't hear, so this
lane builds the instrument that lets someone who can tell me what's wrong, and
makes their answer a diff instead of a conversation.

- TechnoEngine drives every voice from the live TrackMix and the arc, instead of
  module consts. Behaviour-identical at DEFAULT_MIX (verified constant by
  constant). Voices whose effective gain rounds to silence are skipped, not
  clamped — at 9PM the bass builds zero nodes rather than ~8/sec of silence.
  setMixValue quantises integer paths, so what formatMix dumps is what played.
- MixDeskScene: solo/mute per voice with a per-voice listening prompt, every
  mix param on log-mapped sliders, an arc scrubber (audition 1AM at 9PM), and
  DUMP MIX -> a pasteable TrackMix literal. Restores the arc pin and solo state
  it found on close, rather than forcing a default — the demo deliberately pins
  PEAK and the desk was silently un-tuning it.
- Ambience: rain outside, crowd murmur inside, kebab-shop fluoro hum. On the SFX
  bus, not the music lowpass — that filter models the PA being behind a wall,
  and rain is not behind anything. Headcount comes off door:verdict admits and
  floor:ejection, NOT door:clicker: the clicker is the player's claimed count and
  its divergence from reality is the mechanic, so the room would sound like the
  lie. Verified live: 120 + 2 admits - 1 ejection = 121, clicker ignored.
- JuiceDemoScene pins the arc at PEAK on entry. GameClock runs 360 game-min in
  13 real min, so an unpinned demo opens on kick+hat and anyone judging the bass
  would conclude it's broken.
- Phone thread scrolls with visible-window culling and a binary-searched range,
  a history cap, and a 'new below' cue when a text lands off-screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 21:47:53 +10:00

272 lines
9.2 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
clampTo,
cloneMix,
DEFAULT_MIX,
formatMix,
MIX_RANGES,
soloedVoice,
voiceGain,
VOICE_NAMES,
type TrackMix,
type VoiceName,
} from '../src/audio/mix';
const mix = (over: Partial<TrackMix> = {}): TrackMix => ({ ...cloneMix(DEFAULT_MIX), ...over });
const enabled = (...on: VoiceName[]): Record<VoiceName, boolean> => ({
kick: on.includes('kick'),
hat: on.includes('hat'),
bass: on.includes('bass'),
pad: on.includes('pad'),
});
describe('DEFAULT_MIX', () => {
// Tripwire. A retune is a deliberate, reviewed diff — if this goes red because
// someone pasted an ear-pass dump, update the literals in the same commit.
it('holds the Phase-1 hardcoded values exactly', () => {
expect(DEFAULT_MIX).toEqual({
master: 0.8,
levels: { kick: 0.9, hat: 0.25, bass: 0.11, pad: 0.05 },
enabled: { kick: true, hat: true, bass: true, pad: true },
kick: { startHz: 150, endHz: 50, dropS: 0.06, attackS: 0.008, decayS: 0.34 },
hat: { hpHz: 7000, decayS: 0.04 },
bass: { rootHz: 55, filterLoHz: 180, filterHiHz: 900, q: 8, decayS: 0.12, detuneCents: 8 },
pad: { lpHz: 600, bars: 8 },
doorQ: 6,
floorQ: 0.7,
});
});
it('ships every voice unmuted and every level inside its range', () => {
for (const v of VOICE_NAMES) {
expect(DEFAULT_MIX.enabled[v]).toBe(true);
expect(DEFAULT_MIX.levels[v]).toBe(clampTo(DEFAULT_MIX.levels[v], MIX_RANGES.level));
}
expect(DEFAULT_MIX.master).toBe(clampTo(DEFAULT_MIX.master, MIX_RANGES.master));
});
});
describe('cloneMix', () => {
it('deep-copies every nested branch', () => {
const src = cloneMix(DEFAULT_MIX);
const copy = cloneMix(src);
expect(copy).toEqual(src);
expect(copy.levels).not.toBe(src.levels);
expect(copy.enabled).not.toBe(src.enabled);
expect(copy.kick).not.toBe(src.kick);
expect(copy.hat).not.toBe(src.hat);
expect(copy.bass).not.toBe(src.bass);
expect(copy.pad).not.toBe(src.pad);
});
it('leaves the source untouched when every branch of the clone is mutated', () => {
const src = cloneMix(DEFAULT_MIX);
const copy = cloneMix(src);
copy.master = 0.1;
copy.levels.kick = 0.01;
copy.levels.hat = 0.02;
copy.levels.bass = 0.03;
copy.levels.pad = 0.04;
copy.enabled.kick = false;
copy.enabled.hat = false;
copy.enabled.bass = false;
copy.enabled.pad = false;
copy.kick.startHz = 999;
copy.hat.hpHz = 999;
copy.bass.rootHz = 999;
copy.pad.lpHz = 999;
copy.doorQ = 99;
copy.floorQ = 99;
expect(src.master).toBe(0.8);
expect(src.levels).toEqual({ kick: 0.9, hat: 0.25, bass: 0.11, pad: 0.05 });
expect(src.enabled).toEqual({ kick: true, hat: true, bass: true, pad: true });
expect(src.kick.startHz).toBe(150);
expect(src.hat.hpHz).toBe(7000);
expect(src.bass.rootHz).toBe(55);
expect(src.pad.lpHz).toBe(600);
expect(src.doorQ).toBe(6);
expect(src.floorQ).toBe(0.7);
});
it('does not let a clone of DEFAULT_MIX mutate the module-level default', () => {
const c = cloneMix(DEFAULT_MIX);
c.levels.kick = 0;
c.enabled.pad = false;
expect(DEFAULT_MIX.levels.kick).toBe(0.9);
expect(DEFAULT_MIX.enabled.pad).toBe(true);
});
});
describe('clampTo', () => {
const range: readonly [number, number] = [0.5, 20];
it('passes values inside the range through unchanged', () => {
expect(clampTo(0.5, range)).toBe(0.5);
expect(clampTo(6, range)).toBe(6);
expect(clampTo(20, range)).toBe(20);
});
it('clamps both ends', () => {
expect(clampTo(-100, range)).toBe(0.5);
expect(clampTo(1e9, range)).toBe(20);
expect(clampTo(20.0001, range)).toBe(20);
expect(clampTo(0.4999, range)).toBe(0.5);
});
// A NaN gain reaching WebAudio silently kills a voice for the rest of the
// night, so a bad input must land on the minimum, never propagate. Infinity
// takes the same branch: non-finite reads as a broken value, not a loud one.
it('maps every non-finite input to the range minimum, not NaN and not the max', () => {
for (const bad of [NaN, Infinity, -Infinity]) {
expect(clampTo(bad, range)).toBe(0.5);
expect(clampTo(bad, MIX_RANGES.master)).toBe(0);
expect(Number.isNaN(clampTo(bad, range))).toBe(false);
}
});
it('keeps every declared range within its own bounds', () => {
for (const [lo, hi] of Object.values(MIX_RANGES)) {
expect(lo).toBeLessThan(hi);
expect(clampTo(lo - 1, [lo, hi])).toBe(lo);
expect(clampTo(hi + 1, [lo, hi])).toBe(hi);
}
});
});
describe('voiceGain', () => {
it('multiplies level by the arc scale', () => {
const m = mix();
expect(voiceGain(m, 'kick', 1)).toBeCloseTo(0.9, 10);
expect(voiceGain(m, 'kick', 0.5)).toBeCloseTo(0.45, 10);
expect(voiceGain(m, 'bass', 0.5)).toBeCloseTo(0.055, 10);
});
it('defaults the arc scale to 1', () => {
expect(voiceGain(mix(), 'hat')).toBe(mix().levels.hat);
});
it('returns 0 for a disabled voice regardless of level or arc scale', () => {
const m = mix({ enabled: enabled('hat'), levels: { kick: 1.5, hat: 0.25, bass: 1, pad: 1 } });
expect(voiceGain(m, 'kick', 1)).toBe(0);
expect(voiceGain(m, 'kick', 10)).toBe(0);
expect(voiceGain(m, 'bass', 1)).toBe(0);
expect(voiceGain(m, 'pad', 0.5)).toBe(0);
expect(voiceGain(m, 'hat', 1)).toBe(0.25);
});
it('yields exactly 0 at a 0 arc scale', () => {
for (const v of VOICE_NAMES) expect(voiceGain(mix(), v, 0)).toBe(0);
});
it('never returns a negative gain', () => {
const m = mix();
for (const v of VOICE_NAMES) {
expect(voiceGain(m, v, -1)).toBe(0);
expect(voiceGain(m, v, -1e6)).toBe(0);
expect(voiceGain(m, v, 2)).toBeGreaterThanOrEqual(0);
}
expect(voiceGain(mix({ levels: { kick: -1, hat: 0, bass: 0, pad: 0 } }), 'kick')).toBe(0);
});
});
describe('soloedVoice', () => {
it('is null when every voice is on', () => {
expect(soloedVoice(mix())).toBeNull();
});
it('is null when no voice is on', () => {
expect(soloedVoice(mix({ enabled: enabled() }))).toBeNull();
});
it('is null with two voices on', () => {
expect(soloedVoice(mix({ enabled: enabled('kick', 'bass') }))).toBeNull();
expect(soloedVoice(mix({ enabled: enabled('hat', 'pad') }))).toBeNull();
});
it('is null with three voices on', () => {
expect(soloedVoice(mix({ enabled: enabled('kick', 'hat', 'bass') }))).toBeNull();
});
it('names the voice when exactly one is on', () => {
for (const v of VOICE_NAMES) {
expect(soloedVoice(mix({ enabled: enabled(v) }))).toBe(v);
}
});
});
describe('formatMix', () => {
// The dump is how a tuning session becomes a diff; parse it back and compare
// field by field so a silently dropped knob goes red.
const parseDump = (s: string): unknown =>
JSON.parse(
s
.replace(/([{,]\s*)([A-Za-z][A-Za-z0-9]*)\s*:/g, '$1"$2":')
.replace(/,(\s*})/g, '$1'),
);
const tuned: TrackMix = {
master: 0.95,
levels: { kick: 1.05, hat: 0.3125, bass: 0.123456789, pad: 0.07 },
enabled: enabled('bass'),
kick: { startHz: 165, endHz: 44, dropS: 0.075, attackS: 0.003, decayS: 0.41 },
hat: { hpHz: 8200, decayS: 0.055 },
bass: { rootHz: 41, filterLoHz: 220, filterHiHz: 1400, q: 11.5, decayS: 0.19, detuneCents: 14 },
pad: { lpHz: 780, bars: 4 },
doorQ: 7.25,
floorQ: 0.85,
};
it('round-trips every numeric field, rounded to 4 decimal places', () => {
expect(parseDump(formatMix(tuned))).toEqual({
master: 0.95,
levels: { kick: 1.05, hat: 0.3125, bass: 0.1235, pad: 0.07 },
enabled: { kick: true, hat: true, bass: true, pad: true },
kick: { startHz: 165, endHz: 44, dropS: 0.075, attackS: 0.003, decayS: 0.41 },
hat: { hpHz: 8200, decayS: 0.055 },
bass: {
rootHz: 41,
filterLoHz: 220,
filterHiHz: 1400,
q: 11.5,
decayS: 0.19,
detuneCents: 14,
},
pad: { lpHz: 780, bars: 4 },
doorQ: 7.25,
floorQ: 0.85,
});
});
it('round-trips DEFAULT_MIX back to itself', () => {
expect(parseDump(formatMix(DEFAULT_MIX))).toEqual(DEFAULT_MIX);
});
it('emits every key of every branch', () => {
const out = parseDump(formatMix(tuned)) as Record<string, Record<string, unknown>>;
expect(Object.keys(out).sort()).toEqual(Object.keys(tuned).sort());
for (const branch of ['levels', 'enabled', 'kick', 'hat', 'bass', 'pad'] as const) {
expect(Object.keys(out[branch] ?? {}).sort()).toEqual(Object.keys(tuned[branch]).sort());
}
});
// Mutes are an ear-pass tool, not a shipped state: the dump deliberately
// un-mutes everything so a solo pass cannot ship as silence.
it('always emits enabled: all true, even from a soloed mix', () => {
expect(soloedVoice(tuned)).toBe('bass');
const out = parseDump(formatMix(tuned)) as { enabled: Record<string, boolean> };
expect(out.enabled).toEqual({ kick: true, hat: true, bass: true, pad: true });
});
it('emits integers without a decimal point', () => {
const dump = formatMix(DEFAULT_MIX);
expect(dump).toContain('startHz: 150');
expect(dump).toContain('hpHz: 7000');
expect(dump).toContain('doorQ: 6');
expect(dump).not.toMatch(/\d\.0000/);
});
});