125 lines
3.4 KiB
TypeScript
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);
|
|
});
|
|
});
|