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