203 lines
5.7 KiB
TypeScript
203 lines
5.7 KiB
TypeScript
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<number, number>;
|
|
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<Uint8Array>;
|
|
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;
|
|
}
|