/** * Audio transcoding bridge — uses smartrust to communicate with the Rust * opus-codec binary, which handles Opus ↔ G.722 ↔ PCMU/PCMA transcoding. * * All codec conversion happens in Rust (libopus + SpanDSP G.722 port). * The TypeScript side just passes raw payloads back and forth. */ import path from 'node:path'; import { RustBridge } from '@push.rocks/smartrust'; // --------------------------------------------------------------------------- // Command type map for smartrust // --------------------------------------------------------------------------- type TCodecCommands = { init: { params: Record; result: Record; }; create_session: { params: { session_id: string }; result: Record; }; destroy_session: { params: { session_id: string }; result: Record; }; transcode: { params: { data_b64: string; from_pt: number; to_pt: number; session_id?: string; direction?: string }; result: { data_b64: string }; }; encode_pcm: { params: { data_b64: string; sample_rate: number; to_pt: number; session_id?: string }; result: { data_b64: string }; }; }; // --------------------------------------------------------------------------- // Bridge singleton // --------------------------------------------------------------------------- let bridge: RustBridge | null = null; let initialized = false; function buildLocalPaths(): string[] { const root = process.cwd(); return [ path.join(root, 'dist_rust', 'opus-codec'), path.join(root, 'rust', 'target', 'release', 'opus-codec'), path.join(root, 'rust', 'target', 'debug', 'opus-codec'), ]; } let logFn: ((msg: string) => void) | undefined; /** * Initialize the audio transcoding bridge. Spawns the Rust binary. */ export async function initCodecBridge(log?: (msg: string) => void): Promise { if (initialized && bridge) return true; logFn = log; try { bridge = new RustBridge({ binaryName: 'opus-codec', localPaths: buildLocalPaths(), }); const spawned = await bridge.spawn(); if (!spawned) { log?.('[codec] failed to spawn opus-codec binary'); bridge = null; return false; } // Auto-restart: reset state when the Rust process exits so the next // transcode attempt triggers re-initialization instead of silent failure. bridge.on('exit', () => { logFn?.('[codec] Rust audio transcoder process exited — will re-init on next use'); bridge = null; initialized = false; }); await bridge.sendCommand('init', {} as any); initialized = true; log?.('[codec] Rust audio transcoder initialized (Opus + G.722 + PCMU/PCMA)'); return true; } catch (e: any) { log?.(`[codec] init error: ${e.message}`); bridge = null; return false; } } // --------------------------------------------------------------------------- // Session management — per-call codec isolation // --------------------------------------------------------------------------- /** * Create an isolated codec session. Each session gets its own Opus/G.722 * encoder/decoder state, preventing concurrent calls from corrupting each * other's stateful codec predictions. */ export async function createSession(sessionId: string): Promise { if (!bridge || !initialized) { // Attempt auto-reinit if bridge died. const ok = await initCodecBridge(logFn); if (!ok) return false; } try { await bridge!.sendCommand('create_session', { session_id: sessionId }); return true; } catch (e: any) { logFn?.(`[codec] create_session error: ${e?.message || e}`); return false; } } /** * Destroy a codec session, freeing its encoder/decoder state. */ export async function destroySession(sessionId: string): Promise { if (!bridge || !initialized) return; try { await bridge.sendCommand('destroy_session', { session_id: sessionId }); } catch { // Best-effort cleanup. } } // --------------------------------------------------------------------------- // Transcoding // --------------------------------------------------------------------------- /** * Transcode an RTP payload between two codecs. * All codec work (Opus, G.722, PCMU, PCMA) + resampling happens in Rust. * * @param data - raw RTP payload (no header) * @param fromPT - source payload type (0=PCMU, 8=PCMA, 9=G.722, 111=Opus) * @param toPT - target payload type * @param sessionId - optional session for isolated codec state * @returns transcoded payload, or null on failure */ export async function transcode(data: Buffer, fromPT: number, toPT: number, sessionId?: string, direction?: string): Promise { if (!bridge || !initialized) return null; try { const params: any = { data_b64: data.toString('base64'), from_pt: fromPT, to_pt: toPT, }; if (sessionId) params.session_id = sessionId; if (direction) params.direction = direction; const result = await bridge.sendCommand('transcode', params); return Buffer.from(result.data_b64, 'base64'); } catch { return null; } } /** * Encode raw 16-bit PCM to a target codec. * @param pcmData - raw 16-bit LE PCM bytes * @param sampleRate - input sample rate (e.g. 22050 for Piper TTS) * @param toPT - target payload type (9=G.722, 111=Opus, 0=PCMU, 8=PCMA) * @param sessionId - optional session for isolated codec state */ export async function encodePcm(pcmData: Buffer, sampleRate: number, toPT: number, sessionId?: string): Promise { if (!bridge || !initialized) return null; try { const params: any = { data_b64: pcmData.toString('base64'), sample_rate: sampleRate, to_pt: toPT, }; if (sessionId) params.session_id = sessionId; const result = await bridge.sendCommand('encode_pcm', params); return Buffer.from(result.data_b64, 'base64'); } catch (e: any) { console.error('[encodePcm] error:', e?.message || e); return null; } } /** Check if the codec bridge is ready. */ export function isCodecReady(): boolean { return initialized && bridge !== null; } /** Shut down the codec bridge. */ export function shutdownCodecBridge(): void { if (bridge) { try { bridge.kill(); } catch { /* ignore */ } bridge = null; initialized = false; } }