import factory from 'ggwave' const PORT = 8888 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) // SSE clients waiting for results const clients = new Set() // Half-duplex: don't process mic while playing let playing = false function broadcast(data: object) { const msg = `data: ${JSON.stringify(data)}\n\n` for (const controller of clients) { try { controller.enqueue(new TextEncoder().encode(msg)) } catch { clients.delete(controller) } } } function evaluate(expr: string): string { if (!/^[\d+\-*/.() ]+$/.test(expr)) return 'ERR' try { return String(new Function(`return (${expr})`)()) } catch { return 'ERR' } } function decodeBytes(data: Int8Array): string { return Array.from(data).map(b => String.fromCharCode(b & 0xff)).join('') } async function playResult(text: string) { playing = true broadcast({ type: 'playing', result: text }) 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 broadcast({ type: 'ready' }) console.log('Played result:', text) } function startMicListener() { // Uses default device (-d). Explicit CoreAudio device names produce all-zero // data due to macOS TCC permission scoping. const sox = Bun.spawn( ['sox', '-d', '-t', 'raw', '-r', String(SAMPLE_RATE), '-c', '1', '-b', '32', '-e', 'floating-point', '-'], { stdout: 'pipe', stderr: 'pipe' } ) new Response(sox.stderr).text().then(err => { const deviceLine = err.split('\n').find(l => l.includes('Input File')) if (deviceLine) console.log('Mic:', deviceLine.trim()) }) 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) const result = evaluate(text) console.log(`${text} = ${result}`) broadcast({ type: 'received', expression: text, result }) await playResult(result) } } } } processAudio().catch(err => console.error('Mic error:', err)) } Bun.serve({ port: PORT, idleTimeout: 255, async fetch(req) { const url = new URL(req.url) if (url.pathname === '/ok') return new Response('ok') if (url.pathname === '/') { return new Response(Bun.file(import.meta.dir + '/index.html'), { headers: { 'Content-Type': 'text/html' }, }) } if (url.pathname === '/ggwave.js') { return new Response(Bun.file(import.meta.dir + '/node_modules/ggwave/ggwave.js'), { headers: { 'Content-Type': 'application/javascript' }, }) } if (url.pathname === '/ggwave.wasm') { return new Response(Bun.file(import.meta.dir + '/node_modules/ggwave/ggwave.wasm'), { headers: { 'Content-Type': 'application/wasm' }, }) } if (url.pathname === '/events') { const stream = new ReadableStream({ start(controller) { clients.add(controller) controller.enqueue(new TextEncoder().encode(`data: ${JSON.stringify({ type: 'ready' })}\n\n`)) }, cancel(controller) { clients.delete(controller) }, }) return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' }, }) } return new Response('not found', { status: 404 }) }, }) console.log(`Listening on http://localhost:${PORT}`) startMicListener()