import factory from 'ggwave' export const SAMPLE_RATE = 48000 const ggwave = await factory() const params = ggwave.getDefaultParameters() params.sampleRateInp = SAMPLE_RATE params.sampleRateOut = SAMPLE_RATE params.sampleRate = SAMPLE_RATE const instance = ggwave.init(params) let playing = false function decodeBytes(data: Int8Array): string { return Array.from(data).map(b => String.fromCharCode(b & 0xff)).join('') } export async function playAudio(text: string) { playing = true const waveform = ggwave.encode( instance, text, ggwave.ProtocolId.GGWAVE_PROTOCOL_AUDIBLE_FAST, 50 ) const rawBytes = new Uint8Array(waveform.length) rawBytes.set(new Uint8Array(waveform.buffer, waveform.byteOffset, waveform.length)) const play = Bun.spawn( ['sox', '-t', 'raw', '-r', String(SAMPLE_RATE), '-c', '1', '-b', '32', '-e', 'floating-point', '-', '-d'], { stdin: 'pipe', stdout: 'ignore', stderr: 'ignore' } ) play.stdin.write(rawBytes) play.stdin.end() await play.exited await new Promise(r => setTimeout(r, 300)) playing = false } export async function loopbackTest(): Promise { const testMessage = 'TEST' const mic = Bun.spawn( ['sox', '-d', '-t', 'raw', '-r', String(SAMPLE_RATE), '-c', '1', '-b', '32', '-e', 'floating-point', '-'], { stdout: 'pipe', stderr: 'ignore' } ) await new Promise(r => setTimeout(r, 200)) const waveform = ggwave.encode( instance, testMessage, ggwave.ProtocolId.GGWAVE_PROTOCOL_AUDIBLE_FAST, 50 ) const rawBytes = new Uint8Array(waveform.length) rawBytes.set(new Uint8Array(waveform.buffer, waveform.byteOffset, waveform.length)) const play = Bun.spawn( ['sox', '-t', 'raw', '-r', String(SAMPLE_RATE), '-c', '1', '-b', '32', '-e', 'floating-point', '-', '-d'], { stdin: 'pipe', stdout: 'ignore', stderr: 'ignore' } ) play.stdin.write(rawBytes) play.stdin.end() await play.exited const reader = mic.stdout.getReader() const bytesPerFrame = params.samplesPerFrame * 4 let buffer = new Uint8Array(0) let decoded = false const timeout = setTimeout(() => { mic.kill() }, 5000) try { while (true) { const { done, value } = await reader.read() if (done) break const newBuf = new Uint8Array(buffer.length + value.length) newBuf.set(buffer) newBuf.set(value, buffer.length) buffer = newBuf while (buffer.length >= bytesPerFrame) { const frame = buffer.slice(0, bytesPerFrame) buffer = buffer.slice(bytesPerFrame) const result = ggwave.decode(instance, frame) if (result && result.length > 0) { const text = decodeBytes(result) if (text === testMessage) { decoded = true mic.kill() break } } } if (decoded) break } } catch { // reader closed from kill } clearTimeout(timeout) return decoded } export function startMicListener(onMessage: (text: string) => void) { const sox = Bun.spawn( ['sox', '-d', '-t', 'raw', '-r', String(SAMPLE_RATE), '-c', '1', '-b', '32', '-e', 'floating-point', '-'], { stdout: 'pipe', stderr: 'ignore' } ) const reader = sox.stdout.getReader() const bytesPerFrame = params.samplesPerFrame * 4 let buffer = new Uint8Array(0) async function processAudio() { while (true) { const { done, value } = await reader.read() if (done) break if (playing) continue const newBuf = new Uint8Array(buffer.length + value.length) newBuf.set(buffer) newBuf.set(value, buffer.length) buffer = newBuf while (buffer.length >= bytesPerFrame) { const frame = buffer.slice(0, bytesPerFrame) buffer = buffer.slice(bytesPerFrame) const decoded = ggwave.decode(instance, frame) if (decoded && decoded.length > 0) { const text = decodeBytes(decoded) onMessage(text) } } } } processAudio().catch(err => console.error('Mic error:', err)) }