import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { gardenAudioConfig } from './garden-audio-config'; import type { GardenAudioGraph } from './garden-audio-graph'; import type { PianoSampler } from './piano-sampler'; const calls = { bufferSourcesStarted: 0, }; const sampleCount = 30; class FakeAudioParam { public value = 0; public setTargetAtTime = vi.fn(); public setValueAtTime = vi.fn(); public exponentialRampToValueAtTime = vi.fn(); public cancelScheduledValues = vi.fn(); } class FakeAudioNode { public readonly gain = new FakeAudioParam(); public readonly frequency = new FakeAudioParam(); public readonly playbackRate = new FakeAudioParam(); public readonly Q = new FakeAudioParam(); public readonly pan = new FakeAudioParam(); public buffer: AudioBuffer | null = null; public type = ''; public addEventListener = vi.fn(); public connect = vi.fn(); public disconnect = vi.fn(); public start = vi.fn(); public stop = vi.fn(); } class FakeAudioContext { public readonly currentTime = 1; public readonly decodeAudioData = vi.fn(async () => ({}) as AudioBuffer); public createGain(): GainNode { return new FakeAudioNode() as unknown as GainNode; } public createBiquadFilter(): BiquadFilterNode { return new FakeAudioNode() as unknown as BiquadFilterNode; } public createStereoPanner(): StereoPannerNode { return new FakeAudioNode() as unknown as StereoPannerNode; } public createBufferSource(): AudioBufferSourceNode { const node = new FakeAudioNode() as unknown as AudioBufferSourceNode & { start: () => void; stop: () => void; }; node.start = vi.fn(() => { calls.bufferSourcesStarted += 1; }); node.stop = vi.fn(); return node; } } const makeSampler = async (context: AudioContext): Promise => { const { PianoSampler } = await import('./piano-sampler'); const eventBus = new FakeAudioNode() as unknown as GainNode; const graph = { context, delayInput: null, eventBus, getPianoBus: vi.fn(() => eventBus), } as unknown as GardenAudioGraph; return new PianoSampler(gardenAudioConfig, graph); }; describe('PianoSampler', () => { beforeEach(() => { calls.bufferSourcesStarted = 0; vi.resetModules(); }); afterEach(() => { vi.unstubAllGlobals(); }); it('loads every piano sample before playback', async () => { const context = new FakeAudioContext() as unknown as AudioContext; const sampler = await makeSampler(context); const fetch = vi.fn(async () => { return { arrayBuffer: async () => new ArrayBuffer(8), ok: true, } as Response; }); vi.stubGlobal('fetch', fetch); await sampler.load(context); sampler.play({ durationSeconds: 0.2, midi: 60, pan: 0, startTime: context.currentTime, velocity: 0.5, }); expect(fetch).toHaveBeenCalledTimes(sampleCount); expect(context.decodeAudioData).toHaveBeenCalledTimes(sampleCount); expect(calls.bufferSourcesStarted).toBe(1); }); it('stays silent when no decoded sample is available', () => { const context = new FakeAudioContext() as unknown as AudioContext; return makeSampler(context).then((sampler) => { sampler.play({ durationSeconds: 0.2, midi: 60, pan: 0, startTime: context.currentTime, velocity: 0.5, }); expect(calls.bufferSourcesStarted).toBe(0); }); }); });