Add announcement role (announcement@v1) - #126
Conversation
Doesn't this defeat the purpose? The nice thing about MA doing the announcement is that all speakers in my living room mute at the same time, play the announcement, and then continue playing. If only 1 player ducks the music, I might still miss it? Also, this proposal made me initially think that announcements would be possible to a second connected client, that is not the one currently streaming the music (as Sonos allows), but that doesn't seem to be the case. All-in-all, I think this proposal could use some more thought about the use cases we want to unlock and if the current proposal covers these. |
Requirements 1, 2, 3 come from experience with this after having this feature in MA for a few years.
Yes that is exactly how it should work - is that not clearly described enough ? EDIT: tightened the wording a bit as I can see where the confusion came from. |
|
But if it's per speaker, it won't work with stereo pairs, or any other set up where a user has 2 speakers set up in the same room. Or if a user targets an area in Home Assistant, it would end up hearing the same announcement in 1 room slightly out of sync. Or you think that HA could be smart to see it's targeting 2 Sendspin players, and get a sync lock before playing the announcement without too much delay? |
It has coordinated start, which is perfect for this usecase |
This proposes a new optional role family,
announcement@v1, giving Sendspin a native way to play short client-specific audio clips — voice-assistant responses, doorbell chimes, alerts — alongside or independent of media playback, with ducking of the media handled locally on the client.Why
Announcements are currently outside the protocol by design: TTS/alerts are delegated to higher-level applications. In practice that means the server-side workaround is ugly — Music Assistant today fakes an announcement by snapshotting player state, ungrouping, stopping the stream, playing the clip as regular media, and restoring everything afterwards. For ESPHome devices this is a regression: their firmware has a first-class dual-pipeline announcement model (media keeps playing, announcement mixes over it, media ducks), but it is only reachable through the Home Assistant API, not through Sendspin. MA currently relays announcements for ESPHome-backed Sendspin players via HA (music-assistant/server#4916) — that stopgap works only for ESPHome devices and only when HA is present.
The headline capability this role adds: announce on any speaker(s) without touching a playing group's timeline. The media stream keeps flowing untouched to every member; each targeted client ducks its own output locally and mixes the announcement over it.
Use cases
Design in one paragraph
A client advertises the role plus an
announcement@v1_supportobject (its announcement formats, priority-ordered, plus a dedicatedbuffer_capacity). The server starts a per-client announcement stream withstream/start {announcement: {codec params, media_duck_db, duck_ramp_ms, volume?}}and pushes audio as a new binary type 24 (role 6, the next free 4-slot block; 12–15 belong to source) with the usual[type][int64 BE µs timestamp]framing. Timestamps are contiguous (the server pads silence when TTS is slow); late chunks are not dropped — there is no cross-device sync to protect, so playback starts at the first chunk's timestamp or as soon as possible after. The existing role-scopedstream/clear/stream/endhandle replace and completion; ducking is bound to the local stream lifetime, so a dropped transport can never leave media ducked. An optionalclient/state.announcement {state, required_lead_time_ms}object keeps the server informed, but completion is always server-derivable from the timeline, so minimal clients can omit it entirely.Announcements are addressed per client; a multi-speaker announcement is one stream per targeted client with a shared scheduled start time. Only sample-accurate cross-client lock of the announcement audio itself is out of scope.
Why a new role family (and not player@v2 or a vendor role)
announcement@v1never receives an announcement-scoped message. Capability detection is justactive_roles.player@v2would fork the most critical role's wire for an orthogonal optional feature — and since announcement support would still be optional inside v2, version wouldn't even equal capability. It would also forbid announcement-only devices (a chime-only notification box is a legitimate client)._vendorrole works as a private experiment but fragments the ecosystem as an end state; the goal is a role ESPHome ships natively.stream/clear/stream/endvalidators reject unknown role names — negotiation gating is what makes the addition safe.Ducking and volume model
Mirrors the ESPHome dual-pipeline + mixer model (the main embedded client, where all the pieces — announcement pipeline, mixer,
apply_ducking(decibel_reduction, duration)— already ship):media_duck_db(0–50 dB, ESPHome's scale) reduces the client's own media signal pre-mix;duck_ramp_msramps it in and out. Clients that can't do fractional gain MAY duck to silence. Starting an announcement MUST NOT pause, stop, mute, or shift the media timeline.volume(optional, Sonosplay_audio_clipprecedent): render the announcement at the loudness that master volumevolumewould produce, regardless of the current master volume. It's a stateless per-pipeline gain — nothing to restore if the connection dies mid-clip, and media loudness is never touched.stream/start {announcement}while active updates config without clearing buffers (change the duck level mid-clip, ramping from the current gain); replacing the audio itself requires an explicitstream/clear.Edge cases the draft pins down
Idle announce (duck is a no-op, role works without media or even without a player role) · group churn and media stream lifecycle never tear down an announcement · ephemeral semantics (no resume/catch-up after transport loss) · stuck-duck prevention (duck released on end, abort, or transport loss) · a 5 s underrun guard for stalled streams · announcement audio MUST NOT feed visualizer/color/metadata · one announcement per client at a time, replace-requires-clear ·
stream/endwith omitted roles ends announcements too, whilestream/clearwith omitted roles (a media seek) explicitly does not.Implementation sketches
Client (sendspin-cpp + ESPHome): the role slots into sendspin-cpp's documented add-a-role pattern; type 24 routes through the existing
get_binary_role()helper unchanged. The decode path is a dedicated lightweight task (own ring buffer + decoder instance, no Kalman machinery — the sink's blocking writes pace it). On the ESPHome side thespeaker_sourceorchestrator already implements the dual pipeline and mixer ducking that Voice PE ships; the net-new work is a Sendspin announcement source feeding the announcement pipeline plus plumbing the per-stream duck parameters intoapply_ducking. A companion draft PR with this skeleton is linked below. Note the release ordering: ESPHome pins sendspin-cpp as a managed component, so the role must land and release there first.Server (aiosendspin + Music Assistant): a per-client announcement stream engine outside the group
PushStream, riding the existing per-role-family send queues, epochs, and buffer tracking; public API shaped likeclient.play_announcement(pcm_source, fmt, *, media_duck_db, duck_ramp_ms, volume) -> awaitable. MA's side is small: a negotiated role togglesPLAY_ANNOUNCEMENTon the player, andplay_announcementfeeds the existing announcement PCM pipeline (chime + TTS handling included) into the role stream. The fallback ladder stays intact: native role → HA relay (music-assistant/server#4916) → today's stop-and-restore path (which also remains the answer for bridged clients).Server-side mixing as a universal fallback — considered, not proposed. We looked hard at having the server mix announcements into the outgoing audio for clients without the role (per-client channels make per-client content on the aligned timeline possible, and the mix itself is trivial). It founders on send-ahead buffering: making the mix audible now on one grouped client requires flushing and re-feeding that client's buffered window with identical timestamps while leaving siblings untouched — a per-client clear/re-feed primitive that doesn't exist and would live in the riskiest timeline/catch-up code, and without it the onset latency equals the buffer depth (up to ~30 s). A solo-client duck-mix via the existing seek path remains a possible MA-side enhancement, but the protocol role is the right primary mechanism.
Open questions
announcement@v1directly (pre-1.0 spec), or stage asannouncement@_draft_r1per the visualizer precedent? The wire is identical either way.media_duck_dbdefault 0 (off) andduck_ramp_msdefault 100 ms — input from the ESPHome side especially welcome.connection.md's admission model (an incoming announcement-scoped connection that does not displace the playback connection). Do we want that as part of v1 or as a follow-up spec change?Companion implementation draft (client skeleton, compiles + tested): Sendspin/sendspin-cpp#101.