import type { SoundBankLike, SpessaSynthModuleLike, SpessaSynthProcessorLike, } from "../src/loader/soundfont-loader.js"; interface ActiveStubNote { channel: number; note: number; velocity: number; phase: number; } export interface StubProcessorObserver { notes: ActiveStubNote[]; programs: Map; cc: Array<{ channel: number; controller: number; value: number }>; pitchBend: Array<{ channel: number; value: number }>; channelPressure: Array<{ channel: number; pressure: number }>; polyPressure: Array<{ channel: number; note: number; pressure: number; }>; sysex: Array; destroyed: boolean; resetCount: number; stopAllCount: number; } export interface StubBank extends SoundBankLike { readonly stub: true; } class StubProcessor implements SpessaSynthProcessorLike { public readonly sampleRate: number; public readonly observer: StubProcessorObserver = { notes: [], programs: new Map(), cc: [], pitchBend: [], channelPressure: [], polyPressure: [], sysex: [], destroyed: false, resetCount: 0, stopAllCount: 0, }; public readonly soundBankManager = { addSoundBank: (_font: SoundBankLike, _id: string) => { this.bankAdded = true; }, }; private bankAdded = false; public constructor(sampleRate: number) { this.sampleRate = sampleRate; } public get voiceCount(): number { return this.observer.notes.length; } public process( left: Float32Array, right: Float32Array, _startIndex?: number, _sampleCount?: number ): void { const len = left.length; for (let i = 0; i < len; i++) { let sum = 0; for (const v of this.observer.notes) { const freq = 440 * 2 ** ((v.note - 69) / 12); const phaseInc = (2 * Math.PI * freq) / this.sampleRate; v.phase += phaseInc; sum += Math.sin(v.phase) * (v.velocity / 127); } left[i] = sum; right[i] = sum; } } public noteOn(channel: number, midiNote: number, velocity: number): void { if (velocity === 0) { this.noteOff(channel, midiNote); return; } this.observer.notes.push({ channel, note: midiNote, velocity, phase: 0, }); } public noteOff(channel: number, midiNote: number): void { for (let i = this.observer.notes.length - 1; i >= 0; i--) { const v = this.observer.notes[i]!; if (v.channel === channel && v.note === midiNote) { this.observer.notes.splice(i, 1); return; } } } public programChange(channel: number, programNumber: number): void { this.observer.programs.set(channel, programNumber); } public controllerChange( channel: number, controller: number, value: number ): void { this.observer.cc.push({ channel, controller, value }); if (controller === 123) { for (let i = this.observer.notes.length - 1; i >= 0; i--) { if (this.observer.notes[i]!.channel === channel) { this.observer.notes.splice(i, 1); } } } } public pitchWheel(channel: number, pitch: number): void { this.observer.pitchBend.push({ channel, value: pitch }); } public polyPressure( channel: number, midiNote: number, pressure: number ): void { this.observer.polyPressure.push({ channel, note: midiNote, pressure }); } public channelPressure(channel: number, pressure: number): void { this.observer.channelPressure.push({ channel, pressure }); } public systemExclusive(syx: Uint8Array | number[]): void { this.observer.sysex.push( syx instanceof Uint8Array ? syx : new Uint8Array(syx) ); } public stopAllChannels(_force?: boolean): void { this.observer.stopAllCount++; this.observer.notes.length = 0; } public reset(): void { this.observer.resetCount++; } public destroySynthProcessor(): void { this.observer.destroyed = true; } public wasBankAdded(): boolean { return this.bankAdded; } } export function makeStubModule(): SpessaSynthModuleLike & { lastProcessor?: StubProcessor; } { const mod = { SoundBankLoader: { fromArrayBuffer(buffer: ArrayBuffer): StubBank { if (buffer.byteLength < 12) { throw new Error("stub-parse-fail"); } return { stub: true }; }, }, SpessaSynthProcessor: class extends StubProcessor { constructor(sampleRate: number) { super(sampleRate); (mod as { lastProcessor?: StubProcessor }).lastProcessor = this; } } as unknown as SpessaSynthModuleLike["SpessaSynthProcessor"], } as SpessaSynthModuleLike & { lastProcessor?: StubProcessor }; return mod; } /** Build a minimal valid RIFF/sfbk header so the magic sniff passes. */ export function buildFakeSf2Bytes(payloadSize = 32): Uint8Array { const total = 12 + payloadSize; const bytes = new Uint8Array(total); bytes[0] = 0x52; // R bytes[1] = 0x49; // I bytes[2] = 0x46; // F bytes[3] = 0x46; // F // size field at offset 4..7 — ignored by our magic check bytes[8] = 0x73; // s bytes[9] = 0x66; // f bytes[10] = 0x62; // b bytes[11] = 0x6b; // k return bytes; }