fleeting-garden/src/audio/piano-sampler.test.ts
2026-05-16 16:15:54 +01:00

125 lines
3.4 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { gardenAudioConfig } from './garden-audio-config';
import type { GardenAudioGraph } from './garden-audio-graph';
import { PianoSampler } from './piano-sampler';
import { pianoSampleDefinitions, resetPianoSampleCacheForTest } from './piano-samples';
const calls = {
bufferSourcesStarted: 0,
};
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 = (context: AudioContext): PianoSampler => {
const eventBus = new FakeAudioNode() as unknown as GainNode;
const graph = {
context,
delayInput: null,
eventBus,
} as unknown as GardenAudioGraph;
return new PianoSampler(gardenAudioConfig, graph);
};
describe('PianoSampler', () => {
beforeEach(() => {
calls.bufferSourcesStarted = 0;
resetPianoSampleCacheForTest();
});
afterEach(() => {
vi.unstubAllGlobals();
resetPianoSampleCacheForTest();
});
it('loads every piano sample before playback', async () => {
const context = new FakeAudioContext() as unknown as AudioContext;
const sampler = 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(pianoSampleDefinitions.length);
expect(context.decodeAudioData).toHaveBeenCalledTimes(pianoSampleDefinitions.length);
expect(calls.bufferSourcesStarted).toBe(1);
});
it('stays silent when no decoded sample is available', () => {
const context = new FakeAudioContext() as unknown as AudioContext;
const sampler = makeSampler(context);
sampler.play({
durationSeconds: 0.2,
midi: 60,
pan: 0,
startTime: context.currentTime,
velocity: 0.5,
});
expect(calls.bufferSourcesStarted).toBe(0);
});
});