[FEATURE]: Asynchronous playback verb (sPlay) for background/ambient audio in voice apps and Autopilot
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
repeatoption taking a count or"infinite", handled server-side, so "loop this bed for the whole call" needs no client loop. - Volume: a
volumeparameter 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 ansPlaycall at answer — so AI agents get it declaratively.
What we want to be able to do:
- Start a looping audio bed at answer, from a hosted audio file, without blocking later verbs.
- Keep it audible but clearly beneath the agent.
- Have it stop cleanly at hangup.
- Use the same mechanism from a plain
@fonoster/voiceapp and from an AUTOPILOT app. - Optionally react to each loop boundary via an event.
Describe alternatives you've considered
- Re-triggering
Playon eachPlaybackFinished—Playis synchronous and runs on the caller channel, so it blocks the flow and fightsSay/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.
Source: fonoster/fonoster