Initial Commit
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
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" };
|
||||
}
|
||||
Reference in New Issue
Block a user