[object Object]

← back to Butlr

butlr: live-listen now broadcasts BOTH call tracks (inbound + outbound) so the owner hears the full conversation, not just the called party

5ed99e35beec15388de9738026e4f42f27aac02d · 2026-05-26 10:05:23 -0700 · SteveStudio2

Fixes "can't hear anything on live-listen". The bridge only called
broadcastPcm for the inbound track, so the AI (outbound) and on-hold/silent
business produced no audio. Now every decoded 20ms frame is broadcast as it
arrives; the browser's nextPlayTime queue interleaves them in order. Phone
calls are mostly half-duplex so this sounds clean with no per-sample summing,
no clipping, no buffering latency, and zero client change (still mono 8kHz
Int16). Stereo MP3 archive (archiveAppend both tracks) untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 5ed99e35beec15388de9738026e4f42f27aac02d
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Tue May 26 10:05:23 2026 -0700

    butlr: live-listen now broadcasts BOTH call tracks (inbound + outbound) so the owner hears the full conversation, not just the called party
    
    Fixes "can't hear anything on live-listen". The bridge only called
    broadcastPcm for the inbound track, so the AI (outbound) and on-hold/silent
    business produced no audio. Now every decoded 20ms frame is broadcast as it
    arrives; the browser's nextPlayTime queue interleaves them in order. Phone
    calls are mostly half-duplex so this sounds clean with no per-sample summing,
    no clipping, no buffering latency, and zero client change (still mono 8kHz
    Int16). Stereo MP3 archive (archiveAppend both tracks) untouched.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 lib/listen-bridge.js | 14 +++++++++++---
 1 file changed, 11 insertions(+), 3 deletions(-)

diff --git a/lib/listen-bridge.js b/lib/listen-bridge.js
index c893d79..561155b 100644
--- a/lib/listen-bridge.js
+++ b/lib/listen-bridge.js
@@ -8,8 +8,10 @@
 // SEPARATE frames distinguished by media.track — they are NOT pre-mixed.
 // We archive each track into its own .pcm file and ffmpeg-merge to a
 // STEREO MP3 at call end (inbound=left, outbound=right). Live broadcast
-// to /listen-ws is inbound-only for now (single-channel, matches the
-// browser's existing playback path; no client changes needed).
+// to /listen-ws sends BOTH tracks (each 20ms frame as it arrives) so a
+// browser listener hears the full conversation; the half-duplex nature
+// of a phone call makes arrival-order interleave sound clean (no client
+// change needed — still mono 8kHz Int16 frames).
 //
 // Two WebSocket "lanes" on the same upgrade handler — Twilio Streams on
 // /stream/:call_id, browser listeners on /listen-ws/:call_id.
@@ -241,7 +243,13 @@ function attachListenBridge(httpServer) {
         const mulaw = Buffer.from(msg.media.payload, 'base64');
         const pcm = mulawDecodeBuffer(mulaw);
         archiveAppend(callId, track, pcm);                        // both tracks → separate .pcm files
-        if (track === 'inbound') broadcastPcm(callId, pcm);       // live listen = inbound only (mic)
+        // Live listen broadcasts BOTH tracks (not just inbound) so Steve hears the full
+        // conversation — the AI speaks on outbound, the business on inbound. We send each
+        // 20ms frame as it arrives rather than per-sample summing: a phone call is mostly
+        // half-duplex (one party at a time), so arrival-order interleave in the browser's
+        // nextPlayTime queue reproduces the conversation cleanly with no clipping/buffering
+        // latency, and needs zero client change (it already expects mono 8kHz Int16 frames).
+        broadcastPcm(callId, pcm);
       });
       ws.on('close', () => { finalizeArchive(callId); });
       ws.on('error', () => { finalizeArchive(callId); });

← 4ef812b live page: chime on every new call (two-tone Web Audio beep,  ·  back to Butlr  ·  butlr: add voicemail-IVR language guard to auto-hangup when 40322db →