#886·fonoster

[FEATURE]: Asynchronous playback verb (sPlay) for background/ambient audio in voice apps and Autopilot

Author: psandersCreated Sep 8, 2026Updated Sep 8, 2026
Labelsenhancementvoice

Is your feature request related to a problem?

Play is synchronous — it holds the voice session until PlaybackFinished. There is no way to have audio playing underneath an ongoing interaction. We want a background/ambient audio bed (music or room ambience) to play for the duration of a call, mixed under the agent's speech at a low volume, for both @fonoster/voice applications and AUTOPILOT applications. Nothing supports this today: Play on the caller channel blocks the flow and is interrupted by Say/Gather, and Autopilot exposes no config for it.

Describe the solution you'd like

A new asynchronous playback verb (working name sPlay) — the async sibling of Play, the way sGather is the async sibling of Gather. Desired contract:

  • Fire-and-forget: returns immediately with a playback handle; never blocks the session.
  • Plays into the call mix, so it sits under Say / Gather / streamed TTS and survives them — not a transient prompt that gets torn down.
  • Looping: a repeat option taking a count or "infinite", handled server-side, so "loop this bed for the whole call" needs no client loop.
  • Volume: a volume parameter so the bed stays well under the agent's voice.
  • Lifecycle events on the session stream (playback started / finished, per iteration) so an app can drive its own looping or sequencing if it wants.
  • stopPlay(handle), plus automatic stop on hangup / session end (no leaked channels or playbacks).
  • Available to any voice app, and exposed to AUTOPILOT via an application-config field (e.g. conversationSettings.backgroundAudio { url, volume, repeat }) that the Autopilot turns into an sPlay call at answer — so AI agents get it declaratively.

What we want to be able to do:

  1. Start a looping audio bed at answer, from a hosted audio file, without blocking later verbs.
  2. Keep it audible but clearly beneath the agent.
  3. Have it stop cleanly at hangup.
  4. Use the same mechanism from a plain @fonoster/voice app and from an AUTOPILOT app.
  5. Optionally react to each loop boundary via an event.

Describe alternatives you've considered

  • Re-triggering Play on each PlaybackFinishedPlay is synchronous and runs on the caller channel, so it blocks the flow and fights Say/Gather. Not usable as a bed.
  • Pre-mixing music into prompt audio — only covers a single pre-recorded prompt, breaks across gather gaps, impossible for live TTS / Autopilot.
  • Music-on-hold on the bridge — loops, but MoH is semantically "hold" and stock Asterisk MoH has no volume control.

Additional context

Non-prescriptive implementation notes: every call already has a mixing bridge (caller + external-media channels). A natural home for the bed is a looping playback on that bridge (ari.bridges.play(...) re-armed on PlaybackFinished, or a dedicated low-gain source channel joined to the bridge for runtime-adjustable volume via func_volume / VOLUME(TX)). We can supply the asset as 8 kHz mono 16-bit PCM WAV at an HTTPS URL (same style Play's media: "sound:<url>" accepts). Happy to contribute this as a PR.

Open questions: should the bed auto-pause during Gather/sGather? should it follow the caller across Dial into the second bridge, or stop? confirm a bridge-level playback won't leak into the STT feed.