97 lines
2.9 KiB
TypeScript
97 lines
2.9 KiB
TypeScript
import type {
|
|
AudioFxPlugin,
|
|
ChainContext,
|
|
} from "@suaveplan/audio-fx-plugin-base";
|
|
import type { AudioWorkletManager } from "../worklet/worklet-manager.js";
|
|
|
|
export const FX_BLOCK_SIZE = 128;
|
|
|
|
interface AudioNodeLike {
|
|
connect: (
|
|
target: unknown,
|
|
outputIndex?: number,
|
|
inputIndex?: number
|
|
) => unknown;
|
|
disconnect: (target?: unknown) => void;
|
|
}
|
|
|
|
interface FxContextLike {
|
|
readonly audioWorklet?: unknown;
|
|
readonly sampleRate: number;
|
|
createScriptProcessor(
|
|
bufferSize: number,
|
|
numberOfInputChannels: number,
|
|
numberOfOutputChannels: number
|
|
): AudioNodeLike & {
|
|
onaudioprocess: ((e: ScriptProcessingEvent) => void) | null;
|
|
};
|
|
}
|
|
|
|
interface ScriptProcessingEvent {
|
|
readonly inputBuffer: {
|
|
getChannelData(channel: number): Float32Array;
|
|
};
|
|
readonly outputBuffer: {
|
|
getChannelData(channel: number): Float32Array;
|
|
};
|
|
readonly playbackTime: number;
|
|
}
|
|
|
|
export type FxRenderPath = "audio-node" | "worklet" | "block-processor";
|
|
|
|
export interface FxHostResult {
|
|
readonly node: AudioNodeLike;
|
|
readonly path: FxRenderPath;
|
|
}
|
|
|
|
/**
|
|
* Select and instantiate the best available render path for a plugin:
|
|
*
|
|
* 1. `capabilities.hasAudioNode` and `plugin.createAudioNode` → native
|
|
* `AudioNode` chain.
|
|
* 2. `capabilities.hasWorklet`, `plugin.getWorkletProcessor`, and
|
|
* `context.audioWorklet` → `AudioWorkletNode`.
|
|
* 3. Fallback → `ScriptProcessorNode` wrapping `plugin.process()`.
|
|
*/
|
|
export async function createFxNode(
|
|
plugin: AudioFxPlugin,
|
|
context: FxContextLike,
|
|
worklets: AudioWorkletManager,
|
|
chainCtx: ChainContext
|
|
): Promise<FxHostResult> {
|
|
if (plugin.capabilities.hasAudioNode && plugin.createAudioNode) {
|
|
const node = plugin.createAudioNode(context, chainCtx) as AudioNodeLike;
|
|
return { node, path: "audio-node" };
|
|
}
|
|
if (
|
|
plugin.capabilities.hasWorklet &&
|
|
plugin.getWorkletProcessor &&
|
|
context.audioWorklet
|
|
) {
|
|
const moduleUrl = plugin.getWorkletProcessor();
|
|
await worklets.addModule(moduleUrl);
|
|
const node = worklets.createNode(
|
|
plugin.metadata.id
|
|
) as unknown as AudioNodeLike;
|
|
return { node, path: "worklet" };
|
|
}
|
|
const sp = context.createScriptProcessor(FX_BLOCK_SIZE, 2, 2);
|
|
sp.onaudioprocess = (e: ScriptProcessingEvent): void => {
|
|
const input = [
|
|
e.inputBuffer.getChannelData(0),
|
|
e.inputBuffer.getChannelData(1),
|
|
];
|
|
const output = [
|
|
e.outputBuffer.getChannelData(0),
|
|
e.outputBuffer.getChannelData(1),
|
|
];
|
|
plugin.process(input, output, {
|
|
sampleRate: context.sampleRate,
|
|
channels: 2,
|
|
blockOffsetSamples: 0,
|
|
opts: { transportTimeSec: e.playbackTime },
|
|
});
|
|
};
|
|
return { node: sp, path: "block-processor" };
|
|
}
|