Files
suaveplan/packages/media/audio/audio-engine-browser/src/adapter/fx-host.ts
2026-05-27 23:47:53 +01:00

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" };
}