Skip to content

Dynamic voice channel#73

Open
GrantAbell wants to merge 8 commits intoImDevinC:mainfrom
GrantAbell:dynamic_voice_channel
Open

Dynamic voice channel#73
GrantAbell wants to merge 8 commits intoImDevinC:mainfrom
GrantAbell:dynamic_voice_channel

Conversation

@GrantAbell
Copy link
Copy Markdown

@GrantAbell GrantAbell commented Apr 2, 2026

feat(voice): Live voice channel display, avatars, speaking rings & user volume controls

Summary

Enhances the Change Voice Channel and User Volume actions with live visual feedback — overlapping avatar stacks, speaking indicators, mute overlays, and real-time user tracking — similar to native Discord Stream Deck integrations.


Change Voice Channel

Server thumbnail / name

  • The button shows the Discord server's guild icon fetched via GET_GUILD.
  • If no guild icon is set, the server name is rendered as a label in a configurable position (top / center / bottom).

Connected mode

  • When you join the configured channel, avatars are overlapped with the currently speaking user moved to the centre front.
  • A green speaking ring animates around each avatar in real time using SPEAKING_START / SPEAKING_STOP events.
  • A "Show my own avatar" toggle controls whether your avatar appears in the stack. You are always included in the user count regardless of this setting.

Observer mode — user count badge

  • When not connected, a pill badge showing the number of users currently in the channel is drawn over the guild icon (or fallback icon).
  • The badge corner is configurable: top-left, top-right, bottom-left, bottom-right.

Startup detection

  • Actions now detect the current voice channel on startup via GET_SELECTED_VOICE_CHANNEL, so the display reflects state correctly when StreamController launches mid-session.

User Volume

Overlapping avatar display

  • The selected user's avatar is rendered with the same overlapping stack style, with the currently controlled user moved to the centre front.
  • Placeholder avatars with colour-coded initials are shown for users without profile pictures.

Tap-to-mute

  • Tapping the touchbar toggles mute on the displayed user.
  • When viewing yourself, tap toggles your own microphone mute.
  • When viewing another user, tap toggles local per-user mute.

Mute overlay

  • Muted users display a dimmed avatar with a red diagonal slash indicator.
  • Self-mute state stays in sync with Discord voice settings events.

Self-volume control

  • Optional "Control my mic volume" toggle adds yourself as the first entry in the user cycle, allowing the dial to adjust your microphone input volume.

Shared avatar utilities (avatar_utils.py)

Rendering logic extracted into a shared module used by both actions:

  • Circle-clipped avatars, speaking rings, mute overlays, placeholder generation
  • compose_overlapping_avatars() — stacks avatars with configurable front-user positioning

Correctness

  • Cross-button contamination: VOICE_STATE_CREATE/DELETE event data from Discord contains no channel_id, so naively updating _users on every event caused all buttons to show the wrong count. Events now trigger a GET_CHANNEL re-fetch for each button's own watched channel; _on_get_channel fully reconciles the user list from the authoritative voice_states snapshot.
  • Post-leave subscription loss: Discord silently drops VOICE_STATE_* subscriptions when the local user leaves a channel. The button now resets _watching_channel_id on leave and re-subscribes immediately, keeping the observer-mode count live.
  • Startup channel detection: GET_SELECTED_VOICE_CHANNEL response data is normalised to match VOICE_CHANNEL_SELECT event format, so actions initialise correctly on launch.

New backend / event infrastructure

File Change
discordrpc/asyncdiscord.py Added get_guild(guild_id)
backend.py get_guild(), subscribe_speaking() / unsubscribe_speaking(), set_user_mute(), current_user_avatar property, dispatch routing for GET_GUILD, SPEAKING_START/STOP, VOICE_STATE_*
main.py EventHolders for GET_GUILD, SPEAKING_START, SPEAKING_STOP, VOICE_STATE_CREATE, VOICE_STATE_DELETE

Configuration options

Setting Action Type Default
Channel ID ChangeVoiceChannel Entry
Server name label position ChangeVoiceChannel Combo (top / center / bottom) bottom
Show my own avatar ChangeVoiceChannel Switch on
User count badge corner ChangeVoiceChannel Combo (top-left / top-right / bottom-left / bottom-right) bottom-right
Control my mic volume UserVolume Switch off

… and user count

- Show Discord server thumbnail (guild icon) on voice channel buttons;
  fall back to server name label when no icon is available. Label position
  (top / center / bottom) is user-configurable.

- When connected to the configured channel, render a composited grid of up
  to 4 circular user avatars. A green speaking ring animates around each
  avatar in real time as those users speak (SPEAKING_START/STOP events).

- 'Show my own avatar' toggle: include or exclude your own avatar from the
  grid while still counting yourself toward the user total.

- Observer mode (not joined): display a user-count badge in a configurable
  corner (top-left / top-right / bottom-left / bottom-right) over the guild
  icon or fallback voice-channel icon so you can see channel occupancy at a
  glance without being connected.

- Live updates: VOICE_STATE_CREATE/DELETE events trigger a GET_CHANNEL
  re-fetch rather than directly mutating per-button state, preventing
  cross-button counter contamination when multiple ChangeVoiceChannel
  buttons are on the same deck.

- Re-subscribe to voice state events after leaving a channel, since Discord
  silently drops those subscriptions on leave.

- Guild info fetch is decoupled from voice-state subscription so the server
  thumbnail populates immediately on launch even if the subscription is not
  yet active.
Copy link
Copy Markdown
Owner

@ImDevinC ImDevinC left a comment

Choose a reason for hiding this comment

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

Thanks for the PR. To help keep things organized, I think it would be best to move most of the logic in these actions to a helper file of some kind as it really muddies up the logic of "the action" vs everything else

- Move HTTP requests out of the action and into backend.py.
  New fetch_avatar() and fetch_guild_icon() methods on Backend perform
  the requests.get calls and return raw bytes; the action's existing
  thread-pool tasks call these methods and decode the bytes into PIL
  Images locally.  This keeps blocking I/O out of the action layer
  without adding new events or infrastructure.

- Scope label clearing to the configured position only.
  _render_button() previously blanked all three label slots on every
  render, which would erase any labels the user placed in the other
  positions.  Now only the slot managed by this action (the configured
  label_position) is cleared.
@GrantAbell
Copy link
Copy Markdown
Author

Thanks for the PR. To help keep things organized, I think it would be best to move most of the logic in these actions to a helper file of some kind as it really muddies up the logic of "the action" vs everything else

Done
Let me know if this is up to snuff

…isplay

- Tap touchbar to toggle mute on the displayed user (self-mute uses
  mic mute; other users use local per-user mute)
- Mute state shown as dimmed overlay with red slash on avatar
- Both UserVolume and ChangeVoiceChannel now render avatars in an
  overlapping stack instead of a grid, with the active user (selected
  or speaking) moved to the centre front
- Detect current voice channel on startup so actions reflect state
  when StreamController launches mid-session
- Expose current user avatar hash from backend auth response so self
  avatars render correctly for users with profile pictures
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a “dynamic voice channel” button experience by rendering live Discord voice channel state (guild thumbnail/name, user count, avatars, and speaking indicators) directly onto the Stream Deck key face.

Changes:

  • Introduces new RPC/event plumbing for guild fetches and speaking + voice-state events.
  • Enhances ChangeVoiceChannel to render guild icon/name, live user-count badge (observer mode), and avatar/speaking overlays (connected mode).
  • Updates UserVolume to show avatar stacks + speaking state and adds new configuration/UI options.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
settings.py Guards settings UI when backend is not yet initialized/authenticated.
main.py Adds new EventHolders for GET_GUILD, speaking, and voice-state events; hardens backend setup.
discordrpc/asyncdiscord.py Adds get_guild(guild_id) RPC command helper.
backend.py Adds guild fetch, speaking subscribe/unsubscribe, current-user avatar tracking, and CDN fetch helpers.
actions/UserVolume.py Adds avatar/speaking rendering and a new “control self mic volume” toggle (plus mute toggle).
actions/ChangeVoiceChannel.py Major UI/rendering upgrade for live channel display, guild icon/name, speaking rings, and user-count badge.
actions/avatar_utils.py New shared image/placeholder/ring/badge composition utilities for avatar rendering.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

GrantAbell and others added 3 commits April 4, 2026 00:42
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Remove dead image_bytes branch and use backend.fetch_avatar() in
  ChangeVoiceChannel._fetch_avatar
- Clean up _fetching_avatars on all early-return paths
- Unsubscribe voice states and speaking before clearing state in
  _on_channel_id_changed
- Remove unused _current_channel and show_self assignments
- Implement Backend.set_input_volume for self mic volume control
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.

3 participants