/** * AudioWorklet integration for pitch detection * Provides low-latency audio processing in a separate thread */ export interface WorkletFrameEvent { type: 'frame'; frame: ArrayBuffer; timestamp: number; } export interface WorkletConfigMessage { type: 'config'; frameSize?: number; hopSize?: number; } /** * Manages AudioWorklet for frame extraction */ export class PitchWorklet { private workletNode: AudioWorkletNode | null = null; private frameCallbacks: Set<(frame: Float32Array, timestamp: number) => void> = new Set(); constructor( private context: AudioContext, private frameSize: number = 2048, private hopSize: number = 1024 ) {} /** * Load and initialize the AudioWorklet */ async initialize(workletPath: string = '/src/pitch-worklet.js'): Promise { // Load the worklet module await this.context.audioWorklet.addModule(workletPath); // Create worklet node this.workletNode = new AudioWorkletNode(this.context, 'pitch-worklet-processor', { processorOptions: { frameSize: this.frameSize, hopSize: this.hopSize, }, }); // Setup message handling this.workletNode.port.onmessage = (event: MessageEvent) => { if (event.data.type === 'frame') { const frame = new Float32Array(event.data.frame); const timestamp = event.data.timestamp; // Call all registered callbacks this.frameCallbacks.forEach(callback => { callback(frame, timestamp); }); } }; } /** * Connect an audio source to the worklet */ connect(source: AudioNode): void { if (!this.workletNode) { throw new Error('Worklet not initialized. Call initialize() first.'); } source.connect(this.workletNode); } /** * Disconnect the worklet */ disconnect(): void { if (this.workletNode) { this.workletNode.disconnect(); } } /** * Register a callback for frame events */ onFrame(callback: (frame: Float32Array, timestamp: number) => void): () => void { this.frameCallbacks.add(callback); // Return unsubscribe function return () => { this.frameCallbacks.delete(callback); }; } /** * Update worklet configuration */ updateConfig(frameSize?: number, hopSize?: number): void { if (!this.workletNode) { throw new Error('Worklet not initialized.'); } const message: WorkletConfigMessage = { type: 'config', frameSize, hopSize, }; this.workletNode.port.postMessage(message); if (frameSize !== undefined) { this.frameSize = frameSize; } if (hopSize !== undefined) { this.hopSize = hopSize; } } /** * Get the worklet node */ getNode(): AudioWorkletNode | null { return this.workletNode; } /** * Check if worklet is initialized */ isInitialized(): boolean { return this.workletNode !== null; } /** * Dispose resources */ dispose(): void { this.disconnect(); this.frameCallbacks.clear(); this.workletNode = null; } } /** * Helper: Create and initialize a PitchWorklet */ export async function createPitchWorklet( context: AudioContext, options: { frameSize?: number; hopSize?: number; workletPath?: string; } = {} ): Promise { const { frameSize = 2048, hopSize = 1024, workletPath } = options; const worklet = new PitchWorklet(context, frameSize, hopSize); await worklet.initialize(workletPath); return worklet; }