Skip to content

feat: add Wave Link integration plus volume/mute rule actions#26

Merged
hoobio merged 2 commits into
mainfrom
feat/wavelink-and-volume-rules
Apr 30, 2026
Merged

feat: add Wave Link integration plus volume/mute rule actions#26
hoobio merged 2 commits into
mainfrom
feat/wavelink-and-volume-rules

Conversation

@hoobio
Copy link
Copy Markdown
Owner

@hoobio hoobio commented Apr 30, 2026

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

  • Wave Link integration (WaveLinkClient.cs, WaveLinkService.cs). Connects to the local WebSocket via the port published in ws-info.json (fallback range 1884-1893), authenticates via Origin: streamdeck://. Long-lived service with a 5s polling loop and immediate disconnect detection via the WS Closed event. Settings toggle with a status dot (green Connected / red Unavailable / gray Off).
  • Wave Link rule actions: 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).
  • Volume/mute rule actions: SetDeviceVolume, MuteDevice, UnmuteDevice. Per-device first-match-wins; volume and mute are independent dimensions. IAudioEndpointService grows SetVolume/SetMuted that skip writes when already at target.
  • Exact-string match shortcut (RuleMatcher.cs:PatternMatcher). Pattern that verbatim equals a device/mix name matches by equality before regex compile, so literal names with parentheses just work. Existing regex rules unchanged.
  • AutoSuggestBox for device + mix pattern fields, populated from live endpoints / Wave Link snapshot, filtered as you type.
  • Per-action diagnostic banner: surfaces invalid regex, unmatched patterns, Wave Link unavailable, with the offending pattern echoed back and the available alternatives listed.
  • Chrome layout: pane toggle moved into TitleBar.LeftHeader; the icon, title and subtitle shift right (MainWindow.xaml).
  • Bug fix: RulesService now does atomic copy-on-write so deleting a rule while the applier is iterating no longer crashes.

Testing

  • Manual testing on a Windows 10 (19041+) or Windows 11 host
  • Tail of the latest log file under %LocalAppData%\Earmark\logs\ shows the expected Applied rule ... / Skip Set ... / Skip re-apply ... lines after the change
  • Unit tests added/updated under tests/Earmark.Core.Tests
  • Existing tests pass (dotnet 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

  • PR title follows Conventional Commits (feat:, fix:, chore:, etc.) - enforced by CI
  • Code builds without warnings (dotnet build src/Earmark.App/Earmark.App.csproj -c Debug -p:Platform=x64)
  • No emoji / gitmoji in commit messages or PR title (breaks release-please)
  • Architecture boundary respected: domain logic in Earmark.Core, Windows audio interop in Earmark.Audio, UI in Earmark.App
  • CLAUDE.md / README / CONTRIBUTING updated if behavior, build steps, or schema changed

fix(rules): atomic copy-on-write store to prevent crash when deleting a rule mid-apply

hoobio added 2 commits April 30, 2026 22:26
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 hoobio enabled auto-merge (squash) April 30, 2026 12:29
@hoobio hoobio merged commit f526d7e into main Apr 30, 2026
11 checks passed
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>
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.

1 participant