Skip to content

Broadcast microphone muted event on every mute state change#4

Merged
omaramin-2000 merged 5 commits into
omaramin-2000:leds-and-buttons-eventsfrom
genericJE:fix/broadcast-volume-muted
Jul 3, 2026
Merged

Broadcast microphone muted event on every mute state change#4
omaramin-2000 merged 5 commits into
omaramin-2000:leds-and-buttons-eventsfrom
genericJE:fix/broadcast-volume-muted

Conversation

@genericJE

@genericJE genericJE commented May 12, 2026

Copy link
Copy Markdown

Hi Omar, second small fix from the same smoke test on the Pi Zero 2 W with the ReSpeaker 2-Mic HAT.

MUTED is declared in the LVAEvent enum and documented in the peripheral_api docstring, but nothing in LVA actually emits it. Peripherals that maintain a local copy of state.muted only ever see two signals: the snapshot on connect, and the MUTED event. The MUTED event flips the cache to True, but there is no matching event when LVA unmutes (only the generic IDLE event fires, and IDLE can come from many situations, so a peripheral cannot infer mute state from it).

I noticed this while testing the centre button on the ReSpeaker. The button reads peripheral state.muted to decide whether to send mute_mic or unmute_mic. After the first successful mute, that local flag was stuck on True, so every subsequent press sent unmute_mic. From the user's side it looked like unmute kept working and mute did not. Fixing this fully on the peripheral side felt like working around the missing event, since the protocol already has muted for exactly this purpose.

The change just adds one emit in _set_muted that carries the new boolean. The peripheral's existing muted handler (already in the docs example and in my smoke test peripheral) clears or sets its cache automatically, and the LEDs follow. Verified end to end: idle press mutes, muted press unmutes, repeated for several cycles without losing sync.

Happy to put it somewhere else if you'd prefer, or to wrap it in a separate helper.

LVA never broadcast the muted bool to peripheral_api clients. The
``muted`` event signalled "we just entered the muted display state"
but no event ever signalled "we just left it" — only the generic
``idle`` did, and peripherals can't infer mute state from an idle
event since idle fires in many other situations (post-TTS, post wake
word, after errors).

As a result the ReSpeaker 2-Mic peripheral script's cached ``muted``
flag flipped to True on the first mute and stayed True forever. Its
button handler keyed off that flag (``muted → unmute_mic``,
``unmuted → mute_mic``), so after the very first mute the button
could only ever emit ``unmute_mic``: the user could unmute but never
re-mute.

The ``volume_muted`` event was already declared in the protocol and
already handled by the peripheral script — it just had no emitter.
Hook one into ``_set_muted`` so every mute transition (both
directions) ships the boolean. The peripheral's existing handler
clears its cached ``muted`` and switches the animator state back to
IDLE automatically.
@genericJE
genericJE force-pushed the fix/broadcast-volume-muted branch from 42ae3d1 to 5eacd46 Compare May 12, 2026 09:28
@omaramin-2000 omaramin-2000 self-assigned this May 14, 2026
@omaramin-2000

Copy link
Copy Markdown
Owner

@genericJE Is the purpose to emit the muted microphone event which is muted? Do you mean to implement that? Because this is another event than the one for the volume_muted

@genericJE

genericJE commented May 21, 2026

Copy link
Copy Markdown
Author

You're right that this overloads volume_muted with a different responsibility than its name suggests. My intent was to track mic mute, not media mute. The reason I reached for volume_muted is that the existing muted event has no payload and no paired unmuted event, so a peripheral that caches the mic mute state can learn when it goes True from muted, but has no signal for when it goes False. That is the symptom I hit on the ReSpeaker.

A cleaner fix in the same spirit would be either to extend muted itself to carry {"muted": bool} (a small breaking change for anyone consuming the bare event today), or to add a paired unmuted event with no data. Happy to switch this PR to whichever shape you prefer.

@omaramin-2000 omaramin-2000 changed the title Broadcast volume_muted on every mute state change Broadcast microphone muted event on every mute state change May 28, 2026
@omaramin-2000

omaramin-2000 commented May 30, 2026

Copy link
Copy Markdown
Owner

@genericJE I think this recent commit should fix your issue, just make sure that your peripheral script is up to date with the latest changes. https://github.com/omaramin-2000/linux-voice-assistant/tree/leds-and-buttons-events
So before merging this, I want you to revise the script for your ReSpeaker board without your change and tell me the result so we can decide whether to add this or not.

c523617 switched LVA to emit `muted` carrying {"muted": bool} in place
of the dedicated `volume_muted` event. The ReSpeaker `muted` handler
still hardcoded `muted=True`, so it set the flag on mute but never
cleared it on unmute. The cached state stuck on, the muted indicator
stayed latched, and the button toggle fell out of sync after the first
mute (every later press read as already muted).

Read `data["muted"]` instead, mirroring the existing `volume_muted`
handler, and default to True so a bare `muted` event still reads as
muted. Update the peripheral_api docstring, the event and command
tables, and the reference handler in peripheral_api.md to document the
new payload.
@genericJE

Copy link
Copy Markdown
Author

Pushed a small follow-up so the MUTED + {"muted": bool} change works end to end: the ReSpeaker muted handler now reads data["muted"] (defaulting to true when the event arrives without data), so the peripheral tracks unmute as well as mute. Verified across repeated mute/unmute cycles, and the docs + reference handler are updated to match the new payload.

One question on _set_muted while I was in there: on the mute path MUTED now fires twice, once via the new self._emit(LVAEvent.MUTED, {"muted": self.state.muted}) at the top and again via the bare self._emit(LVAEvent.MUTED) inside the if. Now that the top emit also carries the state, is the bare second one supposed to be there? It's harmless either way on the peripheral side, I just wanted to check before touching anything.

@omaramin-2000

omaramin-2000 commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Pushed a small follow-up so the MUTED + {"muted": bool} change works end to end: the ReSpeaker muted handler now reads data["muted"] (defaulting to true when the event arrives without data), so the peripheral tracks unmute as well as mute. Verified across repeated mute/unmute cycles, and the docs + reference handler are updated to match the new payload.

One question on _set_muted while I was in there: on the mute path MUTED now fires twice, once via the new self._emit(LVAEvent.MUTED, {"muted": self.state.muted}) at the top and again via the bare self._emit(LVAEvent.MUTED) inside the if. Now that the top emit also carries the state, is the bare second one supposed to be there? It's harmless either way on the peripheral side, I just wanted to check before touching anything.

@genericJE Do you think your change that fires the muted event twice differs with your test, which makes it worth to add?

Comment thread linux_voice_assistant/satellite.py Outdated

# Carry the boolean back to peripherals. MUTED and IDLE below
# only describe the animator state.
self._emit(LVAEvent.VOLUME_MUTED, {"muted": self.state.muted})

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This event for the silent audio when the media player volume is set to 0 which is different from the muted microphone event. So this _set_muted function is for muting the mic.

Comment on lines +411 to 425

if self.state.muted:
# voice_assistant.stop behavior
_LOGGER.debug("Muting voice assistant (voice_assistant.stop)")
self._is_streaming_audio = False
self.state.tts_player.stop()
# Stop any ongoing voice processing
self.state.stop_word.is_active = False # type: ignore[attr-defined]
self.state.tts_player.play(self.state.mute_sound)
self._emit(LVAEvent.MUTED)
else:
# voice_assistant.start_continuous behavior
_LOGGER.debug("Unmuting voice assistant (voice_assistant.start_continuous)")
self.state.tts_player.play(self.state.unmute_sound)
self._emit(LVAEvent.IDLE)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also as you can see in line 420 that it emits the LVAEvent.MUTED when muting. So, do you think it makes sense when adding another one in line 410?

@omaramin-2000
omaramin-2000 merged commit acf2b39 into omaramin-2000:leds-and-buttons-events Jul 3, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants