Thanks for your interest in contributing! This guide will help you get started.
- Windows 10 (19041+) or Windows 11 (Win11 22000+ for the modern audio policy interface)
- .NET 10 SDK (the repo pins the SDK via
global.json) - Visual Studio 2022 or VS Code with the C# Dev Kit
git clone https://github.com/hoobio/earmark.git
cd earmark
dotnet build src/Earmark.App/Earmark.App.csproj -c Debug -p:Platform=x64The csproj declares <Platforms>x64;ARM64</Platforms> and has no AnyCPU configuration, so you must always pass -p:Platform=x64 (or ARM64).
dotnet test -p:Platform=x64The app holds open file handles on its own DLLs while running, so incremental builds fail with MSB3027 if a previous instance is still alive. The standard inner loop is:
taskkill //F //IM Earmark.App.exe 2>&1 | head -2
sleep 1
dotnet build src/Earmark.App/Earmark.App.csproj -c Debug -p:Platform=x64 --no-restore
EXE="src/Earmark.App/bin/x64/Debug/net10.0-windows10.0.26100.0/win-x64/Earmark.App.exe"
nohup "$EXE" >/dev/null 2>&1 & disownLogs are at %LocalAppData%\Earmark\logs\earmark-{yyyyMMdd-HHmmss}.log, one file per launch. Tail the latest to verify behavior:
Get-ChildItem "$env:LocalAppData\Earmark\logs\*.log" |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1 |
Get-Content -Tail 50 -WaitIf you only changed Earmark.Core or Earmark.Audio, you can build those projects in isolation without killing the app, but the final Earmark.App build still needs the kill.
Open a Bug Report issue. Include:
- Steps to reproduce
- Expected vs actual behavior
- Your Windows version and Earmark version (Settings > About)
- A snippet of the latest log file (most useful) or screenshots
For a useful log, turn on Verbose logging (Settings > Diagnostics), reproduce the issue, then grab the newest file via Settings > Help & feedback > Logs > Open logs folder.
Open a Feature Request issue. Describe the problem you're trying to solve and the solution you'd like.
- Fork the repository and create a branch from
dev(notmain) - Make your changes, keep them focused and minimal
- Add or update tests where it makes sense (the test project lives at
tests/Earmark.Core.Tests) - Update documentation if your change affects behavior or adds a setting
- Open a pull request against
dev
The branch model: feature work lands on dev, dev is PR'd to main periodically, and release-please drafts a release PR from main. Don't push directly to main.
All commits must follow Conventional Commits. The repo uses release-please for automated releases, which reads conventional-commit history off main.
<type>[optional scope]: <description>
| Type | Purpose | Version Bump |
|---|---|---|
feat |
New feature | Minor |
fix |
Bug fix | Patch |
perf |
Performance improvement | Patch |
docs |
Documentation only | None |
test |
Add/update tests | None |
refactor |
Code change (no feature/fix) | None |
chore |
Maintenance, deps, tooling | None |
ci |
CI/CD changes | None |
build |
Build system changes | None |
style |
Formatting, no logic change | None |
revert |
Revert a previous commit | Depends |
- Use imperative mood: "add feature", not "added feature"
- Keep the first line under 72 characters
- Add
!after the type for breaking changes:feat!: rename rule schema - Gitmoji prefixes (e.g.
✨) are accepted but optional
PR titles must match Conventional Commits format. CI enforces this via the pr-title-check.yaml workflow. The PR title becomes the squash-merge commit message that release-please reads.
- Self-documenting code: avoid unnecessary comments. Names should do the work.
- No over-engineering: don't add features, abstractions, or config beyond what's needed.
- Fix linting before committing: the editor analyzers are configured in
.editorconfig. Don't suppress them in new code without a good reason. - Two reasons not to over-comment: well-named identifiers describe what the code does, and PR descriptions describe why it changed. A comment is only worth writing when something would surprise a future reader (a hidden constraint, a workaround, an invariant).
- Earmark.Core has no Windows-specific dependencies in its public interfaces. Models, the rule matcher, and the rule evaluator live here. New domain logic generally belongs here.
- Earmark.Audio wraps Windows audio APIs (NAudio + Core Audio + the COM interop for
IAudioPolicyConfigFactoryandIPolicyConfigVista). Touch this layer when adding routing capabilities. - Earmark.App is the WinUI 3 host: pages, view-models, settings, tray, single-instance handling. UI changes go here.
If you find yourself reaching from Earmark.Core into a Windows API, that's a sign the abstraction needs another seam.
The audio policy interfaces are undocumented and modern .NET doesn't marshal IInspectable-based WinRT interfaces declaratively. The interop layer in src/Earmark.Audio/Interop/ declares interfaces as IUnknown-based with three reserved IInspectable slots at the start of the vtable. HSTRING parameters are passed as IntPtr and built via combase!WindowsCreateString / WindowsDeleteString.
If you change anything in this folder:
- Don't use
[InterfaceType(InterfaceIsIInspectable)]. Modern .NET doesn't support it. UseIUnknownwith reserved vtable slots. - Don't use
[MarshalAs(UnmanagedType.HString)]. Same reason. UseIntPtr+ theHStringhelper. - Don't reintroduce
Process.MainModule.FileName. It blocks on protected processes (anti-cheat games). The replacement isProcessPath.TryGetinEarmark.Audio.Interop.
- Two-way
x:BindonComboBox.SelectedItemagainst a value-type property (e.g. an enum) NREs during item-template recycling. UseMode=OneWayplus aSelectionChangedhandler that updates the source. Microsoft.UI.Xaml.UnhandledExceptionEventArgsis ambiguous betweenMicrosoft.UI.XamlandSystemnamespaces. Always fully-qualify.Grid.Paddingwants aThickness. Don't define anx:Doubleresource and bind it toPadding.
After a change, rebuild and check the log. The interesting lines:
ApplyAll: N sessions, M rules(one per cycle)Applied rule X to <Process> (pid <P>, <Flow>) -> <Endpoint>(a rule applied)Skip Set ... already pinned to target(Get-before-Set short-circuited a redundant write)Skip re-apply for ... already pinned(dedupe cache hit)Applied default-device rule -> Render = <Endpoint>(a Default* rule fired)
If you don't see Skip Set / Skip re-apply lines after the first cycle, the dedupe is broken and the periodic timer will glitch audio every 10 seconds.
For UI changes, the running app must be killed before rebuilding. There's no useful hot-reload path for unpackaged WinUI 3 apps in this configuration.
Open a Discussion or check the existing issues for context.