|
188 | 188 |
|
189 | 189 | :transform async-in-transform}))) |
190 | 190 |
|
191 | | -(def realtime-speakers-out-describe |
192 | | - {:ins {:in "Channel for audio output frames " |
193 | | - :sys-in "Channel for system messages"} |
194 | | - :outs {:out "Channel for bot speech status frames"} |
195 | | - :params {:audio.out/sample-rate "Sample rate of the output audio" |
196 | | - :audio.out/sample-size-bits "Size in bits for each sample" |
197 | | - :audio.out/channels "Number of channels. 1 or 2 (mono or stereo audio)" |
198 | | - :audio.out/duration-ms "Duration in ms of each chunk that will be streamed to output"}}) |
199 | | - |
200 | | -(defn realtime-speakers-out-transition |
201 | | - [{::flow/keys [in-ports out-ports] :as state} transition] |
202 | | - (when (= transition ::flow/stop) |
203 | | - (when-let [line (::speaker-line state)] |
204 | | - (sound/stop! line) |
205 | | - (sampled/flush! line) |
206 | | - (close! line)) |
207 | | - (doseq [port (concat (vals in-ports) (vals out-ports))] |
208 | | - (a/close! port)))) |
209 | | - |
210 | | -(defn realtime-speakers-out-init! |
211 | | - [{:audio.out/keys [duration-ms sample-rate sample-size-bits channels] |
212 | | - :or {sample-rate 16000 |
213 | | - channels 1 |
214 | | - sample-size-bits 16}}] |
215 | | - |
216 | | - (let [;; send every 10ms to account for network |
217 | | - duration (or duration-ms 20) |
218 | | - sending-interval (/ duration 2) |
219 | | - next-send-time (atom (u/mono-time)) |
220 | | - |
221 | | - ;; Track bot speaking state |
222 | | - speaking? (atom false) |
223 | | - last-audio-time (atom 0) |
224 | | - |
225 | | - line (open-line! :source (sampled/audio-format sample-rate sample-size-bits channels)) |
226 | | - audio-write-c (a/chan 1024) |
227 | | - events-chan (a/chan 1024) |
228 | | - |
229 | | - realtime-loop #(loop [] |
230 | | - (when-let [msg (a/<!! audio-write-c)] |
231 | | - (assert (frame/audio-output-raw? msg) "Only audio-output-raw frames can be played to speakers.") |
232 | | - (let [now (u/mono-time)] |
233 | | - (reset! last-audio-time now) |
234 | | - |
235 | | - ;; Check if we need to emit bot started speaking |
236 | | - (when (not @speaking?) |
237 | | - (reset! speaking? true) |
238 | | - (a/>!! events-chan |
239 | | - (frame/bot-speech-start true))) |
240 | | - |
241 | | - (a/<!! (a/timeout (- @next-send-time now))) |
242 | | - (sound/write! (:frame/data msg) line 0) |
243 | | - (reset! next-send-time (+ now sending-interval))) |
244 | | - (recur))) |
245 | | - ;; Monitor for end of speech |
246 | | - speech-monitor #(loop [] |
247 | | - (a/<!! (a/timeout 1000)) ;; Check every second |
248 | | - (let [now (u/mono-time) |
249 | | - silence-duration (- now @last-audio-time)] |
250 | | - ;; If we've been silent for 2x chunk duration and were speaking |
251 | | - (when (and @speaking? |
252 | | - (> silence-duration (* 4 duration))) |
253 | | - (reset! speaking? false) |
254 | | - (a/>!! events-chan (frame/bot-speech-stop true))) |
255 | | - (recur)))] |
256 | | - ((flow/futurize realtime-loop :exec :io)) |
257 | | - ((flow/futurize speech-monitor :exec :io)) |
258 | | - {::flow/out-ports {:audio-write audio-write-c} |
259 | | - ::flow/in-ports {:events events-chan} |
260 | | - :bot/speaking? speaking? |
261 | | - :bot/last-audio-time last-audio-time |
262 | | - ::speaker-line line})) |
263 | | - |
264 | 191 | (defn realtime-out-transform |
265 | 192 | [{:transport/keys [serializer] :as state} in msg] |
266 | 193 | (if (= in :events) |
|
422 | 349 |
|
423 | 350 | (def microphone-transport-in (flow/process mic-transport-in-fn)) |
424 | 351 |
|
425 | | -(def realtime-speakers-out-processor |
426 | | - "Processor that streams audio out in real time so we can account for |
427 | | - interruptions." |
428 | | - (flow/process (fn ([] realtime-speakers-out-describe) |
429 | | - ([params] |
430 | | - (realtime-speakers-out-init! params)) |
431 | | - ([state transition] |
432 | | - (realtime-speakers-out-transition state transition)) |
433 | | - ([state in msg] |
434 | | - (realtime-out-transform state in msg))))) |
435 | | - |
436 | 352 | ;; ============================================================================= |
437 | 353 | ;; Realtime Speakers Out V2 - Activity Monitor Pattern |
438 | 354 | ;; Moves business logic from init! to transform for better testability |
439 | 355 | ;; ============================================================================= |
440 | 356 |
|
441 | | -(defn process-audio-frame-v2 |
| 357 | +(defn process-realtime-out-audio-frame |
442 | 358 | "Pure function to process an audio frame and determine state changes. |
443 | 359 | Returns [updated-state output-map] for the given state, frame, and current time." |
444 | 360 | [state frame serializer current-time] |
|
464 | 380 | [updated-state {:out events |
465 | 381 | :audio-write [audio-command]}])) |
466 | 382 |
|
467 | | -(defn realtime-speakers-out-describe-v2 |
468 | | - [] |
| 383 | +(def realtime-speakers-out-describe |
469 | 384 | {:ins {:in "Channel for audio output frames" |
470 | 385 | :sys-in "Channel for system messages"} |
471 | 386 | :outs {:out "Channel for bot speech status frames"} |
|
474 | 389 | :audio.out/channels "Number of channels. 1 or 2 (mono or stereo audio)" |
475 | 390 | :audio.out/duration-ms "Duration in ms of each chunk that will be streamed to output"}}) |
476 | 391 |
|
477 | | -(defn realtime-speakers-out-init-v2! |
| 392 | +(defn realtime-speakers-out-init! |
478 | 393 | [{:audio.out/keys [duration-ms sample-rate sample-size-bits channels] |
479 | 394 | :or {sample-rate 16000 |
480 | 395 | channels 1 |
|
524 | 439 | ::silence-threshold silence-threshold |
525 | 440 | ::audio-line line})) |
526 | 441 |
|
527 | | -(defn realtime-speakers-out-transition-v2 |
| 442 | +(defn realtime-speakers-out-transition |
528 | 443 | [{::flow/keys [in-ports out-ports] :as state} transition] |
529 | 444 | (when (= transition ::flow/stop) |
530 | 445 | (when-let [line (::audio-line state)] |
|
535 | 450 | (a/close! port))) |
536 | 451 | state) |
537 | 452 |
|
538 | | -(defn realtime-speakers-out-transform-v2 |
| 453 | +(defn realtime-speakers-out-transform |
539 | 454 | [{:transport/keys [serializer] :as state} input-port frame] |
540 | 455 | (let [current-time (u/mono-time)] |
541 | 456 | (cond |
542 | 457 | ;; Handle incoming audio frames - core business logic moved here |
543 | 458 | ;; Handle incoming audio frames - use pure function for business logic |
544 | 459 | (and (= input-port :in) |
545 | 460 | (frame/audio-output-raw? frame)) |
546 | | - (process-audio-frame-v2 state frame serializer current-time) |
| 461 | + (process-realtime-out-audio-frame state frame serializer current-time) |
547 | 462 |
|
548 | 463 | ;; Handle timer ticks for speech monitoring - moved from background loop |
549 | 464 | (and (= input-port :timer-out) |
|
576 | 491 | ;; Default case |
577 | 492 | :else [state {}]))) |
578 | 493 |
|
579 | | -(defn realtime-speakers-out-fn-v2 |
| 494 | +(defn realtime-speakers-out-fn |
580 | 495 | "Refactored realtime speakers out following activity monitor pattern. |
581 | 496 | Moves business logic from init! to transform for better testability." |
582 | | - ([] (realtime-speakers-out-describe-v2)) |
583 | | - ([params] (realtime-speakers-out-init-v2! params)) |
584 | | - ([state transition] (realtime-speakers-out-transition-v2 state transition)) |
585 | | - ([state input-port frame] (realtime-speakers-out-transform-v2 state input-port frame))) |
| 497 | + ([] realtime-speakers-out-describe) |
| 498 | + ([params] (realtime-speakers-out-init! params)) |
| 499 | + ([state transition] (realtime-speakers-out-transition state transition)) |
| 500 | + ([state input-port frame] (realtime-speakers-out-transform state input-port frame))) |
586 | 501 |
|
587 | | -(def realtime-speakers-out-processor-v2 |
| 502 | +(def realtime-speakers-out-processor |
588 | 503 | "V2 processor that moves timing and speech detection logic to transform. |
589 | 504 | Follows activity monitor pattern for better testability and reasoning." |
590 | | - (flow/process realtime-speakers-out-fn-v2)) |
| 505 | + (flow/process realtime-speakers-out-fn)) |
0 commit comments