Keep the output switch working, and split game from chat - #2
Merged
Merged
Conversation
The panel labelled low values CHAT and high values GAME, so dragging toward GAME moved the mix toward chat. Raw 0x00 is full game and 0x14 full chat, which the captures never established -- they record the range and the clamps, not which end is which -- so it went in backwards and stayed that way until it was heard. active_end_label needs no change: it returns a positional index into the label array, so value 0 still highlights the left label, now GAME. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Holding the power button is what turns the headset off, and there is no power-button event in the protocol to hang this on -- so the trigger is parameter 0x20, the link state the dongle already pushes. The link carries no reason, so an auto-sleep or walking out of range is indistinguishable from a deliberate power-off; the two-second debounce is what stops a momentary dropout bouncing the whole machine's audio to the speakers and straight back. The way back is a persisted debt rather than a remembered device. The endpoint the sound was moved *from* is written to the registry, and its presence is the state: it means a move back is owed. Holding it in memory would strand somebody on the speakers if the tray were closed, updated, or killed while the headset was off. It is recorded only after a switch actually succeeds, so a failed switch cannot cause a later unearned one. Remembering the endpoint rather than "the headset" also matters because the dongle presents two render endpoints, Game and Chat, and only the user knows which one they were on. Setting the default output has no documented Windows API at all -- IMMDeviceEnumerator reads the default and has never had a setter. This is the workspace's one undocumented interface, confined to its own module and recorded in docs/undocumented-apis.md with the reasoning for why it is a different category from the HID rules in CONTRIBUTING.md: a local Windows setting the user can change back by hand, not a guessed byte sent to firmware nobody here can inspect. The vtable is declared only as far as the one method called, with the ten preceding slots deliberately unnamed and untyped; a unit test pins the offset, because a stray field there would silently call a different method with these arguments. The picker is an in-panel view rather than a native dropdown: the panel is a layered window that hides on losing activation, so a menu owned by another window would close the panel the menu belongs to. Verified on the machine: the undocumented interface accepts a no-op SetDefaultEndpoint against the live audio stack (an ignored test, one role only so nothing moves), and the picker renders real device names. A layout bug was caught by rendering rather than by a test -- the settings title wrapped and collided with its own description, the exact hazard the neighbouring comment warns about. Not verified: the full power-off and power-on round trip, which needs the button pressed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtGiZeMkxkrztrcVeWqy79
Building this workspace needs MinGW-w64 on PATH and a GNU *default host*, and neither is discoverable from the error it produces. `rustup target add x86_64-pc-windows-gnu` is not enough on its own: build scripts and proc macros compile for the host triple, so a stock MSVC-host rustup dies with `linker 'link.exe' not found` before reaching any of this project's code. That error names Visual Studio, which is the wrong fix for a workspace that pins the GNU target deliberately. CI never hits either problem, which is why neither was written down: GitHub's Windows runners ship MinGW-w64 already, and the workflow passes --target explicitly while running on a host that has both linkers. Found by setting the project up from scratch on a machine that had only rustup installed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtGiZeMkxkrztrcVeWqy79
The workspace manifest is the single source of truth for the version: Inno takes the same value on its command line via build-installer.ps1, and Cargo.lock follows from the build. Also records the game/chat slider fix (5938736) under Fixed, which that commit never added to the changelog. It is the user-visible reason this build exists, so release notes that omitted it would be a poor record. docs/history/ still names 0.1.0-alpha.1 in places and is deliberately left alone -- it is a record of what was known at the time, not documentation that tracks the current version. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UtGiZeMkxkrztrcVeWqy79
Two changes to the same feature, which is why they arrive together: the switch had stopped working at all, and what it does when it works was wrong for a headset that presents two channels. The switch died silently. The record of where the sound came from doubles as the "a move back is owed" flag, and its mere presence was read as "already switched". Endpoint ids are durable but not permanent -- a reinstalled driver or a re-enumerated dongle retires one -- so the moment that record named an endpoint the machine no longer had, it could never be discharged (the endpoint was gone) and never be replaced (its presence blocked the switch). Every later power-off and power-on did nothing, for good, until somebody edited the registry. A debt now counts only while the machine still has the endpoint it names, and one that never reappears is given up on rather than kept forever. The rule moved into its own module, handed facts and answering with an Action, because the alternative is a rule whose only test is powering a headset off and watching a machine's audio move. Restoring the sound was actively destructive on this hardware. Windows keeps three defaults -- console, multimedia, communications -- and the headset presents two endpoints, which is the entire reason a chat channel exists. A restore can only name the one endpoint it moved away from, and writing that into all three roles overwrote a communications default the user had pointed at the chat channel, so the voices came out of the game channel. Hence "Split game and chat": pick a Game channel and a Chat channel, and a headset coming back gets ordinary playback on the first and calls on the second. It is opt-in and off by default. A headset with two channels is not a reason to assume somebody wants their calls moved, and the setting is independent of the move-when-off switch because wanting calls on their own channel and wanting your sound moved when the headset dies are different wishes. When it is on it beats a plain restore, deliberately: restoring is the thing that breaks it, and the sound lands on the headset either way, so the debt is discharged rather than left to drag the sound back off. It also overrides "put it back where it came from" -- a power-on always lands on the chosen game channel, even if the user had moved to speakers by hand. That is the trade for roles that are deterministic rather than inferred. Both rules share one retry budget, because a wireless link comes up seconds before its audio endpoints do: "the endpoint I want is not there" is the ordinary first answer, not a failure. The three device choices are one Slot enum rather than three parallel settings and three picker views -- they ask the same question and differ only in what the answer is for -- so there is one store, one picker, and one place a fourth would go. The two channel rows appear only once the split is on, unlike "Play through", because the toggle directly above them is what reveals them and three device rows with two inert is a settings page that looks like it is asking for more than it is. Failures are surfaced rather than logged into a subscriber nobody enabled: a balloon the first time, and the problem persisted so the settings row can still explain itself hours later. Stored by name rather than by its wording, so the panel can put a complaint on the row of the feature that earned it and re-phrasing a message cannot move it. A record written by an older build reads as no complaint. Acting on a settings change immediately is held back in exactly one case: picking the game channel and being told off for not having picked the chat channel is a complaint about the step the user is visibly in the middle of. Also adds --explain-output, which prints what would happen for a powered-off and a powered-on headset, and why, without moving anything. The switch acts on a device event nobody can stage on demand and on facts the user cannot see; before this, telling those candidates apart meant reading the registry by hand. Verified: 125 tests pass, clippy clean at -D warnings, the settings panel renders without the title collision its neighbouring comment warns about, and a release build installs and runs. Not verified: the power-off and power-on round trip with the split on, which needs the button held. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AVDi4cWtmLALKXVmy1pxo5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Five commits, all on the tray. The last one is the substantial change; the four
before it are the alpha.2 work this branch already carried.
The output switch had stopped working
The record of where the sound came from doubles as the "a move back is owed"
flag, and its mere presence was read as "already switched". Endpoint ids are
durable but not permanent — a reinstalled driver or a re-enumerated dongle
retires one — so the moment that record named an endpoint the machine no longer
had, it could never be discharged (the endpoint was gone) and never be replaced
(its presence blocked the switch). Every later power-off and power-on did
nothing at all, for good, until somebody edited the registry.
A debt now counts only while the machine still has the endpoint it names, and
one that never reappears is given up on after about ten seconds rather than kept
forever. The rule moved into
headset-tray/src/output.rs, which touchesnothing: it is handed facts and answers with an
Action. The alternative is arule whose only test is powering a headset off and watching a machine's audio
move.
Split game and chat
Windows keeps three defaults — console, multimedia, communications — and the
headset presents two endpoints, which is the entire reason a chat channel
exists. A restore can only name the one endpoint it moved away from, and writing
that into all three roles overwrote a communications default pointed at the chat
channel, so the voices came out of the game channel.
So: a new Split game and chat toggle, off by default, with Game channel
and Chat channel pickers beneath it. Turn it on, pick both, and a headset
coming back gets ordinary playback on the first and calls on the second.
wants their calls moved.
channel and wanting your sound moved when the headset dies are different
wishes.
it, and the sound lands on the headset either way, so the debt is discharged
rather than left to drag the sound back off. It also overrides "put it back
where it came from": a power-on always lands on the chosen game channel, even
if you had moved to speakers by hand. That is the trade for roles that are
deterministic rather than inferred.
before its audio endpoints do. "The endpoint I want is not there" is the
ordinary first answer, not a failure.
The three device choices are one
Slotenum rather than three parallel settingsand three picker views — they ask the same question and differ only in what the
answer is for — so there is one store, one picker, and one place a fourth would
go. The two channel rows appear only once the split is on, unlike "Play
through", because the toggle directly above them is what reveals them.
Failures say so
A balloon the first time a problem appears, and the problem persisted so the
settings row can still explain itself hours later. Stored by name rather than by
its wording, so a complaint lands on the row of the feature that earned it and
re-phrasing a message cannot move it; a record written by an older build reads
as no complaint. Acting on a settings change immediately is held back in exactly
one case — picking the game channel and being told off for not having picked the
chat channel is a complaint about the step you are visibly in the middle of.
headset-tray.exe --explain-outputprints what would happen for a powered-offand a powered-on headset, and why, without moving anything.
Verification
125 tests pass, clippy is clean at
-D warnings, the settings panel renderswithout the title collision its neighbouring comment warns about, and a release
build installs and runs.
Not verified: the power-off and power-on round trip with the split on, which
needs the power button held. That is being tested by hand now.
🤖 Generated with Claude Code
https://claude.ai/code/session_01AVDi4cWtmLALKXVmy1pxo5