import * as jose from "https://deno.land/x/jose@v5.9.6/index.ts"; import { getUserByEmail } from "./supabase.ts"; import { SupabaseClient } from "@supabase/supabase-js"; import crypto from "node:crypto"; import { Buffer } from "node:buffer"; import { Encoder } from "@evan/opus"; export const defaultVolume = 50; export const defaultGeminiVoice = "Sadachbia"; export const defaultOpenAIVoice = "ash"; export const defaultGrokVoice = "Ara"; // Define your audio parameters export const SAMPLE_RATE = 24000; // For example, 24000 Hz const CHANNELS = 1; // Mono (set to 2 if you have stereo) const FRAME_DURATION = 120; // Frame length in ms const BYTES_PER_SAMPLE = 2; // 16-bit PCM: 2 bytes per sample const FRAME_SIZE = (SAMPLE_RATE * FRAME_DURATION / 1000) * CHANNELS * BYTES_PER_SAMPLE; // 960 bytes for 24000 Hz mono 16-bit export function createOpusEncoder() { const enc = new Encoder({ channels: CHANNELS, sample_rate: SAMPLE_RATE, application: "voip", }); enc.expert_frame_duration = FRAME_DURATION; enc.bitrate = 24000; return enc; } export function createOpusPacketizer( sendPacket: (packet: Uint8Array) => void, ) { const enc = createOpusEncoder(); let pending = Buffer.alloc(0); let closed = false; const push = (pcm: Uint8Array) => { if (closed) return; if (!pcm || pcm.length === 0) return; pending = Buffer.concat([pending, Buffer.from(pcm)]); while (pending.length >= FRAME_SIZE) { const frame = pending.subarray(0, FRAME_SIZE); pending = pending.subarray(FRAME_SIZE); try { const packet = enc.encode(frame); sendPacket(packet); } catch (err) { console.error("Opus encode failed:", err); } } }; const flush = (padFinalFrame = false) => { if (closed) return; if (pending.length === 0) return; if (!padFinalFrame) { pending = Buffer.alloc(0); return; } const padded = Buffer.alloc(FRAME_SIZE); pending.copy(padded, 0, 0, pending.length); pending = Buffer.alloc(0); try { const packet = enc.encode(padded); sendPacket(packet); } catch (err) { console.error("Opus encode failed:", err); } }; const reset = () => { pending = Buffer.alloc(0); }; const close = () => { closed = true; pending = Buffer.alloc(0); }; const bufferedBytes = () => pending.length; return { push, flush, reset, close, bufferedBytes }; } // Legacy encoder for backwards compatibility during migration const encoder = new Encoder({ channels: CHANNELS, sample_rate: SAMPLE_RATE, application: "voip", }); encoder.expert_frame_duration = FRAME_DURATION; encoder.bitrate = 24000; export const openaiApiKey = Deno.env.get("OPENAI_API_KEY"); export const geminiApiKey = Deno.env.get("GEMINI_API_KEY"); export const elevenLabsApiKey = Deno.env.get("ELEVENLABS_API_KEY"); export const humeApiKey = Deno.env.get('HUME_API_KEY'); export const xaiApiKey = Deno.env.get('XAI_API_KEY'); export { encoder, FRAME_SIZE }; export const isDev = Deno.env.get("DEV_MODE") === "True"; export const authenticateUser = async ( supabaseClient: SupabaseClient, authToken: string, ): Promise => { try { const jwtSecret = Deno.env.get("JWT_SECRET_KEY"); if (!jwtSecret) throw new Error("JWT_SECRET_KEY not configured"); const secretBytes = new TextEncoder().encode(jwtSecret); const payload = await jose.jwtVerify(authToken, secretBytes); const { payload: { email } } = payload; const user = await getUserByEmail(supabaseClient, email as string); return user; } catch (error: any) { throw new Error(error.message || "Failed to authenticate user"); } }; /** * Decrypts an encrypted secret with the same master encryption key. * @param encryptedData - base64 string from the database * @param iv - base64 IV from the database * @param masterKey - 32-byte string or buffer * @returns the original plaintext secret */ export function decryptSecret( encryptedData: string, iv: string, masterKey: string, ) { // Decode the base64 master key const decodedKey = Buffer.from(masterKey, "base64"); if (decodedKey.length !== 32) { throw new Error( "ENCRYPTION_KEY must be 32 bytes when decoded from base64.", ); } const decipher = crypto.createDecipheriv( "aes-256-cbc", decodedKey, // Use the decoded key instead of raw masterKey Buffer.from(iv, "base64"), ); let decrypted = decipher.update(encryptedData, "base64", "utf8"); decrypted += decipher.final("utf8"); return decrypted; } export function boostLimitPCM16LEInPlace( pcmBytes: Uint8Array, // Buffer is fine (subclass of Uint8Array) gainDb = 6.0, ceiling = 0.89, // ≈ −1 dBFS ): void { const dv = new DataView(pcmBytes.buffer, pcmBytes.byteOffset, pcmBytes.byteLength); const g = Math.pow(10, gainDb / 20); // Pass 1: measure post-gain peak let peak = 0; for (let i = 0; i < dv.byteLength; i += 2) { const s = dv.getInt16(i, true) / 32768; // 16-bit LE → [-1,1] const y = s * g; const a = Math.abs(y); if (a > peak) peak = a; } const scale = peak > ceiling && peak > 0 ? (ceiling / peak) : 1; // Pass 2: apply gain + scale + gentle soft-clip for (let i = 0; i < dv.byteLength; i += 2) { let y = (dv.getInt16(i, true) / 32768) * g * scale; // cubic soft-clip (tanh-ish) for nicer peaks const y2 = y * y; y = 0.5 * y * (3 - y2); if (y > 0.999) y = 0.999; if (y < -0.999) y = -0.999; dv.setInt16(i, (y * 32767) | 0, true); } } // Function to downsample PCM audio from 48kHz to 24kHz export function downsamplePcm(pcmBuffer: Buffer, fromRate: number, toRate: number): Buffer { if (fromRate === toRate) { return pcmBuffer; } const ratio = fromRate / toRate; const inputSamples = pcmBuffer.length / 2; // 16-bit = 2 bytes per sample const outputSamples = Math.floor(inputSamples / ratio); const outputBuffer = Buffer.alloc(outputSamples * 2); for (let i = 0; i < outputSamples; i++) { const sourceIndex = Math.floor(i * ratio) * 2; const sample = pcmBuffer.readInt16LE(sourceIndex); outputBuffer.writeInt16LE(sample, i * 2); } return outputBuffer; } // Function to extract PCM data from WAV file export function extractPcmFromWav(wavBuffer: Buffer): Buffer | null { try { // Check minimum WAV header size if (wavBuffer.length < 44) { console.error('WAV file too small'); return null; } // Verify RIFF header const riffHeader = wavBuffer.subarray(0, 4).toString('ascii'); if (riffHeader !== 'RIFF') { console.error('Not a RIFF file'); return null; } // Verify WAVE format const waveHeader = wavBuffer.subarray(8, 12).toString('ascii'); if (waveHeader !== 'WAVE') { console.error('Not a WAVE file'); return null; } // Find the data chunk let offset = 12; while (offset < wavBuffer.length - 8) { const chunkId = wavBuffer.subarray(offset, offset + 4).toString('ascii'); const chunkSize = wavBuffer.readUInt32LE(offset + 4); if (chunkId === 'data') { // Found data chunk, extract PCM data const pcmData = wavBuffer.subarray(offset + 8, offset + 8 + chunkSize); return pcmData; } // Move to next chunk offset += 8 + chunkSize; } console.error('No data chunk found in WAV file'); return null; } catch (error) { console.error('Error extracting PCM from WAV:', error); return null; } }