Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
dotnet-version: '8.0.x'

- name: Restore
run: dotnet restore AudioMonitorRouter.sln
run: dotnet restore ScreenSound.sln

- name: Build
run: dotnet build AudioMonitorRouter.sln -c Release --no-restore
run: dotnet build ScreenSound.sln -c Release --no-restore
14 changes: 7 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
Write-Host "Building version $version (event=${{ github.event_name }})"

- name: Restore
run: dotnet restore AudioMonitorRouter.sln
run: dotnet restore ScreenSound.sln

- name: Publish framework-dependent (win-x64)
# Framework-dependent: ~6 MB output vs ~85 MB self-contained. The installer
Expand All @@ -74,7 +74,7 @@ jobs:
# DirectWrite factory init — see https://github.com/dotnet/wpf/issues/6792
shell: pwsh
run: |
dotnet publish AudioMonitorRouter\AudioMonitorRouter.csproj `
dotnet publish ScreenSound\ScreenSound.csproj `
-c Release `
-r win-x64 `
--self-contained false `
Expand All @@ -86,7 +86,7 @@ jobs:
- name: Package portable zip
shell: pwsh
run: |
$zipName = "AudioMonitorRouter-${{ steps.version.outputs.VERSION }}-portable.zip"
$zipName = "ScreenSound-${{ steps.version.outputs.VERSION }}-portable.zip"
Compress-Archive -Path "publish\*" -DestinationPath "publish\$zipName"
Write-Host "Created $zipName"

Expand All @@ -100,13 +100,13 @@ jobs:
- name: Upload portable build artifact
uses: actions/upload-artifact@v4
with:
name: AudioMonitorRouter-portable
path: publish\AudioMonitorRouter-${{ steps.version.outputs.VERSION }}-portable.zip
name: ScreenSound-portable
path: publish\ScreenSound-${{ steps.version.outputs.VERSION }}-portable.zip

- name: Upload installer artifact
uses: actions/upload-artifact@v4
with:
name: AudioMonitorRouter-installer
name: ScreenSound-installer
path: installer\Output\*.exe

- name: Create GitHub Release
Expand Down Expand Up @@ -134,5 +134,5 @@ jobs:
prerelease: false
generate_release_notes: true
files: |
publish/AudioMonitorRouter-${{ steps.version.outputs.VERSION }}-portable.zip
publish/ScreenSound-${{ steps.version.outputs.VERSION }}-portable.zip
installer/Output/*.exe
2 changes: 1 addition & 1 deletion AudioMonitorRouter.sln → ScreenSound.sln
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AudioMonitorRouter", "AudioMonitorRouter\AudioMonitorRouter.csproj", "{1EC589BF-B74B-4DA1-B37F-FD128C16763D}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScreenSound", "ScreenSound\ScreenSound.csproj", "{1EC589BF-B74B-4DA1-B37F-FD128C16763D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
4 changes: 2 additions & 2 deletions AudioMonitorRouter/App.xaml → ScreenSound/App.xaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Application x:Class="AudioMonitorRouter.App"
<Application x:Class="ScreenSound.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpfui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:local="clr-namespace:AudioMonitorRouter"
xmlns:local="clr-namespace:ScreenSound"
>
<Application.Resources>
<ResourceDictionary>
Expand Down
63 changes: 60 additions & 3 deletions AudioMonitorRouter/App.xaml.cs → ScreenSound/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,56 @@
using AudioMonitorRouter.Interop;
using AudioMonitorRouter.Views;
using ScreenSound.Interop;
using ScreenSound.Views;
using System.Globalization;
using System.Threading;
using System.Windows;
using System.Windows.Data;

namespace AudioMonitorRouter;
namespace ScreenSound;

public partial class App : Application
{
// Single-instance guard. Two ScreenSound processes would race on the
// same HKCU Run-at-startup state, both create tray icons, and both
// fight over the per-session audio-policy calls for the same sessions.
// The "Local\" prefix scopes the mutex to the current Windows login
// session, so fast-user-switching still lets each user have their own
// copy while a double-click on the shortcut won't spawn a second one.
private const string SingleInstanceMutexName = @"Local\ScreenSound-SingleInstance";
private static Mutex? _singleInstanceMutex;

protected override void OnStartup(StartupEventArgs e)
{
// Acquire the mutex FIRST, before any side effects (DPI config,
// window creation, tray icon). If another copy is already running
// we want to bail out cleanly with zero partial initialisation.
_singleInstanceMutex = new Mutex(initiallyOwned: false, name: SingleInstanceMutexName);
bool acquired;
try
{
acquired = _singleInstanceMutex.WaitOne(millisecondsTimeout: 0);
}
catch (AbandonedMutexException)
{
// Previous instance crashed without releasing. WaitOne still
// hands us ownership in that case, so treat as success — the
// mutex is now ours.
acquired = true;
}

if (!acquired)
{
MessageBox.Show(
"ScreenSound is already running — check the system tray.",
"ScreenSound",
MessageBoxButton.OK,
MessageBoxImage.Information);

_singleInstanceMutex.Dispose();
_singleInstanceMutex = null;
Shutdown();
return;
}

// Force PerMonitorV2 DPI awareness before WPF creates any windows.
try
{
Expand All @@ -29,6 +70,22 @@ protected override void OnStartup(StartupEventArgs e)
}
// Otherwise, tray icon is already set up — window stays hidden
}

protected override void OnExit(ExitEventArgs e)
{
// Release + dispose so an immediate relaunch can acquire cleanly.
// Without this, the OS would still reap the mutex on process exit,
// but explicit release avoids a brief race window where a fast
// double-click sees the mutex as still held.
if (_singleInstanceMutex != null)
{
try { _singleInstanceMutex.ReleaseMutex(); }
catch (ApplicationException) { /* Not owned (e.g. after an AbandonedMutexException path) — nothing to release. */ }
_singleInstanceMutex.Dispose();
_singleInstanceMutex = null;
}
base.OnExit(e);
}
}

public class ZeroToVisibilityConverter : IValueConverter
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Runtime.InteropServices;

namespace AudioMonitorRouter.Interop;
namespace ScreenSound.Interop;

public enum EDataFlow
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Runtime.InteropServices;

namespace AudioMonitorRouter.Interop;
namespace ScreenSound.Interop;

public static class NativeMethods
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Runtime.InteropServices;

namespace AudioMonitorRouter.Interop;
namespace ScreenSound.Interop;

/// <summary>
/// Thin wrapper around SetWinEventHook for observing global window events
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace AudioMonitorRouter.Models;
namespace ScreenSound.Models;

public class AppSettings
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace AudioMonitorRouter.Models;
namespace ScreenSound.Models;

public class AudioDeviceInfo
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace AudioMonitorRouter.Models;
namespace ScreenSound.Models;

public class AudioSessionInfo
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace AudioMonitorRouter.Models;
namespace ScreenSound.Models;

public class MonitorInfo
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@
<NoWarn>WFAC010</NoWarn>

<!-- Assembly metadata - shown in file properties & installer -->
<Version>1.0.0</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<FileVersion>1.0.0.0</FileVersion>
<Product>Audio Monitor Router</Product>
<AssemblyTitle>Audio Monitor Router</AssemblyTitle>
<Version>2.0.0</Version>
<AssemblyVersion>2.0.0.0</AssemblyVersion>
<FileVersion>2.0.0.0</FileVersion>
<Product>ScreenSound</Product>
<AssemblyTitle>ScreenSound</AssemblyTitle>
<Authors>twibster</Authors>
<Company>twibster</Company>
<Description>Automatically routes per-app audio to different speakers based on monitor placement on Windows 11.</Description>
<Description>Sound follows your windows. Per-app audio routing for multi-monitor Windows 11 — each app plays through the speaker paired with the monitor it's on.</Description>
<Copyright>Copyright (c) 2026 Omar Omran</Copyright>
<RepositoryUrl>https://github.com/twibster/AudioMonitorRouter</RepositoryUrl>
<RepositoryUrl>https://github.com/twibster/ScreenSound</RepositoryUrl>
<RepositoryType>git</RepositoryType>
</PropertyGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using NAudio.CoreAudioApi;
using NAudio.CoreAudioApi.Interfaces;

namespace AudioMonitorRouter.Services;
namespace ScreenSound.Services;

/// <summary>
/// Subscribes to Core Audio endpoint notifications so the UI and routing engine
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using AudioMonitorRouter.Models;
using ScreenSound.Models;
using NAudio.CoreAudioApi;

namespace AudioMonitorRouter.Services;
namespace ScreenSound.Services;

public class AudioDeviceService : IDisposable
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using AudioMonitorRouter.Interop;
using ScreenSound.Interop;

namespace AudioMonitorRouter.Services;
namespace ScreenSound.Services;

public class AudioRouterService : IDisposable
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using NAudio.CoreAudioApi;

namespace AudioMonitorRouter.Services;
namespace ScreenSound.Services;

/// <summary>
/// Subscribes to <see cref="AudioSessionManager.OnSessionCreated"/> across all
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using AudioMonitorRouter.Models;
using ScreenSound.Models;
using NAudio.CoreAudioApi;
using System.Diagnostics;

namespace AudioMonitorRouter.Services;
namespace ScreenSound.Services;

public class AudioSessionService
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using AudioMonitorRouter.Interop;
using AudioMonitorRouter.Models;
using ScreenSound.Interop;
using ScreenSound.Models;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace AudioMonitorRouter.Services;
namespace ScreenSound.Services;

public class MonitorService
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using AudioMonitorRouter.Interop;
using AudioMonitorRouter.Models;
using ScreenSound.Interop;
using ScreenSound.Models;
using Microsoft.Win32;
using System.Collections.Concurrent;

namespace AudioMonitorRouter.Services;
namespace ScreenSound.Services;

/// <summary>
/// Drives audio routing in response to signals:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
using AudioMonitorRouter.Models;
using ScreenSound.Models;
using System.IO;
using System.Text.Json;

namespace AudioMonitorRouter.Services;
namespace ScreenSound.Services;

public class SettingsService
{
private static readonly string SettingsDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"AudioMonitorRouter");
"ScreenSound");

private static readonly string SettingsFile = Path.Combine(SettingsDir, "settings.json");
Comment thread
twibster marked this conversation as resolved.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace AudioMonitorRouter.Services;
namespace ScreenSound.Services;

/// <summary>
/// Outcome of an update probe. The About page binds its status text to one of
Expand Down Expand Up @@ -41,7 +41,7 @@ public class UpdateService
// Public, unauthenticated endpoint — rate-limited to 60 requests/hour per IP,
// which is plenty for a user-initiated "check for updates" button.
private const string LatestReleaseApi =
"https://api.github.com/repos/twibster/AudioMonitorRouter/releases/latest";
"https://api.github.com/repos/twibster/ScreenSound/releases/latest";

// GitHub requires a User-Agent on every request; the product name is also
// useful in their server logs if we ever need to correlate a rate-limit bug
Expand All @@ -53,8 +53,8 @@ public class UpdateService

private static ProductInfoHeaderValue BuildUserAgent()
{
try { return new ProductInfoHeaderValue("AudioMonitorRouter", GetInformationalVersion()); }
catch { return new ProductInfoHeaderValue("AudioMonitorRouter", "1.0"); }
try { return new ProductInfoHeaderValue("ScreenSound", GetInformationalVersion()); }
catch { return new ProductInfoHeaderValue("ScreenSound", "1.0"); }
}

/// <summary>
Expand Down Expand Up @@ -118,7 +118,7 @@ public async Task<UpdateCheckResult> CheckForUpdateAsync(CancellationToken ct =
// legally contain characters (/, #, spaces) that would otherwise
// break the path segment.
string releaseUrl = release.HtmlUrl ??
$"https://github.com/twibster/AudioMonitorRouter/releases/tag/{Uri.EscapeDataString(release.TagName)}";
$"https://github.com/twibster/ScreenSound/releases/tag/{Uri.EscapeDataString(release.TagName)}";

return CompareSemVer(latest, current) > 0
? new UpdateCheckResult.UpdateAvailable(current, latest, releaseUrl)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using AudioMonitorRouter.Models;
using AudioMonitorRouter.Services;
using ScreenSound.Models;
using ScreenSound.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Win32;
Expand All @@ -11,7 +11,7 @@
using System.Windows.Media.Imaging;
using System.Windows.Threading;

namespace AudioMonitorRouter.ViewModels;
namespace ScreenSound.ViewModels;

public partial class MonitorMappingViewModel : ObservableObject
{
Expand Down Expand Up @@ -135,7 +135,7 @@ public partial class MainWindowViewModel : ObservableObject, IDisposable
private static readonly TimeSpan DeviceRefreshDebounce = TimeSpan.FromMilliseconds(300);

private const string AutoStartRegistryKey = @"Software\Microsoft\Windows\CurrentVersion\Run";
private const string AppRegistryName = "AudioMonitorRouter";
private const string AppRegistryName = "ScreenSound";
Comment thread
twibster marked this conversation as resolved.

private readonly Dictionary<uint, ImageSource?> _iconCache = new();

Expand Down
Loading
Loading