feat: add Wave Link integration plus volume/mute rule actions#26
Merged
Conversation
Mutators (Upsert, Delete, Reorder) previously mutated the underlying List<RoutingRule>
in place. Concurrent readers iterating Rules from the routing applier or UI hit
InvalidOperationException ("Collection was modified") when a rule changed mid-iteration.
The new Wave Link apply path widened that race, surfacing it as a hard crash on delete.
Mutators now build a fresh list, mutate the copy, and atomically swap the volatile
reference. Readers see whichever list was current when they took the reference and
that list is never modified again, so iteration is lock-free and safe even while
mutators run.
Wave Link integration - Connect to Wave Link's local WebSocket via discovered port from ws-info.json (fallback range 1884-1893), authenticate via Origin: streamdeck:// header - WaveLinkService is a long-lived singleton with a 5s polling loop and immediate Closed-event detection; exposes IsEnabled, State, LastSnapshot and StateChanged/SnapshotChanged events - Settings toggle "Enable Wave Link integration" with a colored status dot (green Connected / red Unavailable / gray Off) and tooltip - Probe-WaveLink.ps1 script under scripts/ for ad-hoc protocol testing Wave Link rule actions - AddWaveLinkMixOutput: ensure matched device is on matched mix (idempotent) - RemoveWaveLinkMixOutput: ensure matched device is not on matched mix - SetWaveLinkMixOutput: exact-match mode that keeps only matched devices on the matched mix; non-matching outputs are removed in a second pass so multiple Set actions on the same mix compose correctly - New MixPattern field on RuleAction; Get-before-Set on the wire avoids redundant audio glitches Device volume/mute rule actions - SetDeviceVolume (per-action Volume in [0,1]) - MuteDevice / UnmuteDevice - IAudioEndpointService grows GetVolume/GetMuted/SetVolume/SetMuted, all idempotent (set is skipped if delta < 0.5% for volume, or already at target for mute) - Per-device first-match-wins semantics: rule list order is the priority, volume and mute are independent dimensions Editor improvements - AutoSuggestBox replaces TextBox for device and mix patterns; suggestions populated from live endpoints / Wave Link snapshot, filtered as you type, inserted as the literal name on selection - PatternMatcher.Matches adds an exact-string shortcut so literal names with regex metacharacters (parentheses, dots) just work without escaping; regex compile is a fallback - Per-action diagnostic banner extended to cover invalid regex, unmatched patterns, and Wave Link unavailability; messages echo the user pattern and list available mixes / devices for context - Volume slider visible only when the action is SetDeviceVolume Chrome layout - Pane toggle button moved into TitleBar.LeftHeader; the icon, "Earmark" title and subtitle shift right; NavigationView's built-in toggle is hidden via IsPaneToggleButtonVisible="False"
hoobio
pushed a commit
that referenced
this pull request
Apr 30, 2026
This PR was generated automatically by [release-please](https://github.com/googleapis/release-please). --- ## [0.1.6](v0.1.5...v0.1.6) (2026-04-30) ### Features * add Wave Link integration plus volume/mute rule actions ([#26](#26)) ([f526d7e](f526d7e)) ### Bug Fixes * **rules:** atomic copy-on-write store to prevent crash when deleting a ([f526d7e](f526d7e)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: release-please-hoobi[bot] <279189756+release-please-hoobi[bot]@users.noreply.github.com>
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.
Summary
Adds Wave Link 3.x integration so rules can route Wave Link mix outputs, plus three new rule actions for Windows audio endpoint volume and mute. Also a clutch of editor + chrome polish.
Changes
ws-info.json(fallback range 1884-1893), authenticates viaOrigin: streamdeck://. Long-lived service with a 5s polling loop and immediate disconnect detection via the WSClosedevent. Settings toggle with a status dot (green Connected / red Unavailable / gray Off).AddWaveLinkMixOutput,RemoveWaveLinkMixOutput,SetWaveLinkMixOutput. Apply is two-pass for Set so multiple Set actions on the same mix compose without removing each other's claims. All idempotent (Get-before-Set).SetDeviceVolume,MuteDevice,UnmuteDevice. Per-device first-match-wins; volume and mute are independent dimensions.IAudioEndpointServicegrowsSetVolume/SetMutedthat skip writes when already at target.TitleBar.LeftHeader; the icon, title and subtitle shift right (MainWindow.xaml).RulesServicenow does atomic copy-on-write so deleting a rule while the applier is iterating no longer crashes.Testing
%LocalAppData%\Earmark\logs\shows the expectedApplied rule .../Skip Set .../Skip re-apply ...lines after the changetests/Earmark.Core.Testsdotnet test -p:Platform=x64)Live-tested: rule-driven Wave Link Add/Remove cycles audio cut/return, volume slider applies and skips on second apply, Mute/Unmute rule precedence verified per-device, app didn't crash on mid-apply rule delete.
Checklist
feat:,fix:,chore:, etc.) - enforced by CIdotnet build src/Earmark.App/Earmark.App.csproj -c Debug -p:Platform=x64)Earmark.Core, Windows audio interop inEarmark.Audio, UI inEarmark.Appfix(rules): atomic copy-on-write store to prevent crash when deleting a rule mid-apply