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 => ({ ...cloneMix(DEFAULT_MIX), ...over }); const enabled = (...on: VoiceName[]): Record => ({ 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>; 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 }; 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/); }); });