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
2 changes: 2 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## General Guidelines
- The display language of the application shall be English and must be documented as a design guideline.
- Provide a professional README without generic AI formulations; it should start with a clear explanation of GCB, highlight improvements since version 1.0, and include explicit screenshot placeholders.

## Projektrichtlinien
- Der Nutzer möchte ein zentrales Umsetzungsbacklog und eine Designentscheidungsdokumentation als Steuerungs- und Gedächtnisartefakte im Repository; außerdem soll die Migration auf eine aktuelle .NET-Version (net10.0-windows) bereits in einer frühen Projektphase eingeplant werden.
Expand All @@ -10,6 +11,7 @@
- Der Nutzer möchte den Migrationsplan um einen expliziten Schritt erweitern, der den Systray-/Lifecycle-Host von Form1 entkoppelt, damit Form1 später vollständig entfernt werden kann.
- Änderungen sollen sorgfältig geplant und stabil umgesetzt werden, damit nichts kaputt geht.
- Umsetzung soll in kleinen Schritten erfolgen.
- Minimal-risk Refactoring ist bevorzugt; bestehende event-getriebene Architektur strikt beibehalten; kein neues Polling/Timer-Refresh; Fokus auf zentralen Runtime-State, Entkopplung MainWindow-Programmlogik und Verkleinerung großer Dateien in kleinen stabilen Schritten.

## Architekturelles Prinzip (verbindlich)
### Event Driven
Expand Down
104 changes: 104 additions & 0 deletions AI Agent/AudioManager-Replacement-Analysis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# AudioManager-Ersatz – Funktions- und Abhängigkeitsanalyse

## Status

Diese Analyse wurde vor Änderungen an der Audio-Service-Implementierung erstellt. `AudioControl/AudioManager.cs` bleibt bis zur bestätigten Umsetzung unverändert.

## Ist-Zustand und Herkunftshinweise

`AudioManager.cs` enthält eine statische Audio-Implementierung mit eigener COM-Interop. Im produktiven Quelltext stehen derzeit folgende Herkunftsverweise:

- StackOverflow: `controling-volume-mixer`
- CodePlex: `netcoreaudio`
- Gist: `sverrirs` und `d099b34b7f72bb4fb386`

Die COM-Interfaces, `MMDeviceEnumerator`, Session-Notification-Klasse, Monitoring-Thread und Audiooperationen sind in `AudioManager.cs` zusammengefasst. Es gibt keine separate Audio-NuGet-Abhängigkeit in `AudioControl.csproj`.

## Repositoryweite öffentliche Funktionalität

| Bestehende Funktion | Aktuelle Repository-Verwendung | Ziel der neuen Implementierung | Windows-Core-Audio-Zuordnung |
|---|---|---|---|
| Audio-Sessions des Multimedia-Render-Endpunkts enumerieren | Aktiv: `GetAudioApplications` über `IAudioSessionService`; Anzeige im Tray/UI | Beibehalten | `IMMDeviceEnumerator`, `IMMDevice`, `IAudioSessionManager2`, `IAudioSessionEnumerator`, `IAudioSessionControl2` |
| Anwendungen nach Prozessnamen/PID ausgeben | Aktiv: Anzeige mit/ohne PID | Beibehalten, einschließlich des bestehenden Ausgabeformats soweit UI-Vertrag es verlangt | `IAudioSessionControl2.GetProcessId`, Prozessauflösung über `Process` |
| Lautstärke anhand eines Anwendungsnamens setzen | Aktiv: `AudioBalanceService` für Game-/Chat-Listen | Beibehalten; Matching-Verhalten vor Änderung testen und dokumentieren | `ISimpleAudioVolume.SetMasterVolume` |
| Mehrere Session-Einträge je Anwendung behandeln | Aktiv implizit durch Enumeration und Namensvergleich | Beibehalten | Jede passende Session einzeln über `ISimpleAudioVolume` |
| Neue Audio-Session melden | Aktiv: `IAudioSessionNotification` → `AudioSessionsChanged` | Beibehalten, ohne Polling | `IAudioSessionManager2.RegisterSessionNotification`, `IAudioSessionNotification` |
| Session-Monitoring starten | Aktiv: `AppHost.Start` | Beibehalten, idempotent | Eigener `WindowsAudioSessionController.Start` |
| Session-Monitoring stoppen | Aktiv: `AppHost.Stop`/Dispose | Beibehalten, mit sicherer Abmeldung und COM-Freigabe | `UnregisterSessionNotification` plus kontrollierter COM-Lifecycle |
| Master-Lautstärke lesen | Keine aktuelle Call-Site gefunden; öffentliche AudioManager-Funktion | Bis zur expliziten Paritätsentscheidung erhalten oder als geprüfte, ungenutzte Kompatibilitätsfunktion dokumentieren | `IAudioEndpointVolume.GetMasterVolumeLevelScalar` |
| Master-Lautstärke setzen/Schritt setzen | Keine aktuelle Call-Site gefunden; öffentliche AudioManager-Funktionen | Nicht stillschweigend entfernen; Ziel-API muss Entscheidung abbilden | `IAudioEndpointVolume.SetMasterVolumeLevelScalar`, `GetMasterVolumeLevelScalar` |
| Master-Mute lesen/setzen/toggeln | Keine aktuelle Call-Site gefunden; öffentliche AudioManager-Funktionen | Nicht stillschweigend entfernen; Ziel-API muss Entscheidung abbilden | `IAudioEndpointVolume.GetMute`, `SetMute` |
| Anwendungslautstärke per PID lesen/setzen | Keine aktuelle Call-Site gefunden; öffentliche AudioManager-Funktionen | Prüfen, ob öffentliche Kompatibilität benötigt wird; falls ja erhalten | Session-Enumeration + `ISimpleAudioVolume` |
| Anwendungs-Mute per PID setzen | Keine aktuelle Call-Site gefunden; öffentliche AudioManager-Funktion | Prüfen, ob öffentliche Kompatibilität benötigt wird; falls ja erhalten | `ISimpleAudioVolume.SetMute` |
| PIDs anhand eines Anwendungsnamens finden | Keine aktuelle Call-Site gefunden; öffentliche AudioManager-Funktion | Prüfen, ob öffentliche Kompatibilität benötigt wird; falls ja erhalten | `IAudioSessionControl2.GetProcessId` + Prozessauflösung |
| Diagnose-Logging übergeben | Aktiv indirekt: `AudioSessionService` übergibt `IDiagnosticsSink` | Durch Constructor-/Dependency-Injection in der Audio-Infrastruktur ersetzen; kein statischer Sink | `IDiagnosticsSink` bleibt Application-Vertrag, COM-Callbacks bleiben leichtgewichtig |

## Direkte Abhängigkeiten und Call-Graph

```text
LegacyApplicationHostFactory
-> AudioSessionService(IDiagnosticsSink)
-> IAudioSessionService
-> AudioManager.AudioManager (statischer Fremd-/Interop-Bestand)

AppHost
-> StartSessionMonitoring / StopSessionMonitoring

AudioBalanceService
-> SetApplicationVolumeByName(gameApps, gameVolume)
-> SetApplicationVolumeByName(chatApps, chatVolume)

TrayIconApplicationHost
-> AudioSessionsChanged
-> GetAudioApplications(false)
```

Die Application-Schicht kennt aktuell nur `IAudioSessionService`. COM-Typen dürfen in der Zielarchitektur ausschließlich unter `AudioControl/Infrastructure/Audio` liegen.

## Ereignis- und Lifecycle-Verhalten

| Ereignis/Zustand | Aktuelles Verhalten | Zielverhalten |
|---|---|---|
| Monitoring-Start | Hintergrundthread, MTA, COM-Initialisierung, Default-Rendergerät, Session Manager | Eigener Controller mit explizitem Start/Stop und COM-Lifetime |
| Initiale Sessions | `GetSessionEnumerator` und `GetCount` beim Start | Initiale Enumeration über Infrastructure; Application erhält weiterhin den vorhandenen Refresh-Impuls bzw. Snapshot-Vertrag |
| Neue Session | `IAudioSessionNotification.OnSessionCreated` löst `AudioSessionsChanged` aus | Callback nur als minimale Signalweitergabe; Dispatch/Business-Logik außerhalb des COM-Callbacks |
| Session entfernt/disconnected | Kein eigener `IAudioSessionEvents`- oder State-Tracker vorhanden; UI refresh erfolgt primär über neue Session | Zielimplementierung muss die tatsächlich erforderliche Entfernung/State-Semantik festlegen und event-getrieben abbilden |
| Session-Volume geändert | Nicht an Application weitergereicht | Nur ergänzen, wenn bestehende Consumer oder der neue Vertrag dies benötigen; keine Polling-Schleife |
| Device-Wechsel | Kein `IMMNotificationClient` vorhanden | Default-Device-/Invalidation-Verhalten analysieren und bei benötigtem Verhalten event-getrieben ergänzen |
| Stop/Dispose | Notification-Abmeldung und COM-Release im Worker-Finally; Fehler werden teilweise verschluckt | Deterministische Abmeldung, HRESULT-Prüfung, sichere Shutdown-Reihenfolge, keine Unregister-/Release-Operation im Callback |

## Zielgrenze

```text
Application
-> IAudioSessionService / IAudioSessionController (COM-freier Vertrag)
-> WindowsAudioSessionController
-> CoreAudio-Interop (nur Infrastructure)
-> Windows Core Audio / WASAPI
```

Vorgesehene kleine Struktur:

- `AudioControl/Infrastructure/Audio/WindowsAudioSessionController.cs`
- `AudioControl/Infrastructure/Audio/CoreAudio/` für die minimal benötigten COM-Interfaces und Callback-Adapter
- bestehender `IAudioSessionService`-Vertrag, sofern für die Feature-Parität ausreichend

`IAudioSessionEvents`, `IAudioSessionNotification`, `IMMNotificationClient`, `IAudioEndpointVolume` und weitere Interfaces werden nur umgesetzt, wenn sie durch die bestehende Funktion oder die bestätigte Zielsemantik erforderlich sind. Kein Polling und keine periodische Session-Enumeration.

## Validierungs- und Paritätscheckliste

- [ ] Bestehende Sessions werden über den Default-Multimedia-Render-Endpunkt gefunden.
- [ ] Ausgabe mit und ohne PID bleibt kompatibel.
- [ ] Game-/Chat-Anwendungen werden nach bestehendem Matching-Verhalten geregelt.
- [ ] Mehrere passende Sessions werden korrekt behandelt.
- [ ] Neue Sessions lösen weiterhin den Application-Refresh aus.
- [ ] Session-, Device- und Shutdown-Lifecycle besitzen keine offenen COM-Referenzen oder Event-Subscriptions.
- [ ] Callback-Code blockiert nicht und führt keine komplexe Business-Logik aus.
- [ ] Start, Stop, Dispose, Device-Invalidierung und Fehlerfälle sind getestet.
- [ ] Der Build und vorhandene Tests sind erfolgreich.
- [ ] Nach der Umstellung verbleiben keine produktiven Gist-/CodePlex-/StackOverflow-Referenzen.
- [ ] Erst nach Feature-Parität wird `AudioManager.cs` entfernt.

## Freigabepunkt

Die Implementierung verändert Audio-Service-Logik und COM-Interop. Daher ist vor dem nächsten Schritt eine ausdrückliche Bestätigung erforderlich. Nach Bestätigung wird die Zielarchitektur in kleinen Schritten umgesetzt und nach jedem Schritt gebaut bzw. getestet.
2 changes: 1 addition & 1 deletion AI Agent/Migration-Analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ Hinweis: .NET 7 ist außerhalb regulärem Supportfenster. Für Windows-11-Zielbi

## 6) Windows-Audio-Integration

- Eigene CoreAudio-COM-Interop in `AudioManager.cs`
- Eigene CoreAudio-COM-Interop unter `AudioControl/Infrastructure/Audio/CoreAudio/`
- Session-Enumeration über `IAudioSessionManager2`
- Prozess-basierte Volume-Steuerung per Prozessname
- Logik:
Expand Down
59 changes: 59 additions & 0 deletions AI Agent/Refactoring-Minimal-Result.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Refactoring-Ergebnis (minimal, risikoarm)

## Scope
- Zentraler Runtime-State eingeführt
- MainWindow-Konstruktorabhängigkeiten gekapselt
- TrayIconApplicationHost um Assignment-Verantwortung entlastet
- Event-driven Architektur unverändert beibehalten

## Umgesetzte Änderungen

### 1) Zentraler Runtime-State
- Neu:
- `AudioControl/GameChatState.cs`
- `AudioControl/GameChatStateChangedEventArgs.cs`
- `AudioControl/IGameChatStateStore.cs`
- `AudioControl/GameChatStateStore.cs`
- `StateChanged` wird nur bei echten Änderungen ausgelöst (Change-Detection inkl. App-Listen).

### 2) Event-Quellen angebunden
- `TrayIconApplicationHost` synchronisiert bestehende Event-/Handlerpfade in den State:
- Hardware Connected/Port
- Input/Display/Game/Chat-Volume
- InvertControl
- NoiseReduction
- Game/Chat-App-Assignments
- Keine Polling-/Timer-Erweiterung eingeführt.

### 3) MainWindow entkoppelt (inkrementell)
- Neu: `GameChatBalancer.Wpf/MainWindowViewModel.cs`
- `MainWindow` akzeptiert jetzt zusätzlich `MainWindow(MainWindowViewModel viewModel, bool showDebugOption)`.
- Bestehende Event- und UI-Logik im Code-Behind bleibt unverändert.

### 4) Func/Action-Fläche reduziert
- `TrayIconApplicationHost.CreateMainWindow` erstellt jetzt ein `MainWindowViewModel` und übergibt dieses als gebündelte Abhängigkeit.

### 5) Verantwortlichkeit aus TrayHost extrahiert
- Neu: `AudioControl/AppAssignmentManager.cs`
- verfügbare Apps bestimmen
- Assignment-Path auflösen/persistieren
- Game/Chat-CSV anwenden
- `AssignmentsChanged`-Event
- `TrayIconApplicationHost` reagiert weiterhin event-driven auf `AssignmentsChanged` (State-Sync + UI-Refresh).

### 6) Doppelte Runtime-State-Nutzung reduziert
- Mehrere Leser wurden auf `stateStore.Current` umgestellt:
- Tray-Statusprovider
- MainWindow-Statusprovider
- Start-UI-Initialisierung
- Software-Control-Step/Center-Berechnung

## Validierung
- `dotnet build` erfolgreich
- `dotnet test AudioControl.sln` ohne Testfälle (keine Tests im aktuellen Scope gefunden)

## Nicht geändert (bewusst)
- Keine neue Polling-Mechanik
- Keine Änderung an Hardware-/Audio-Protokollen
- Keine Änderung an Autostart-/Settings-System-Architektur
- Kein Rewrite der bestehenden Event-Ketten
75 changes: 75 additions & 0 deletions AudioControl/AppAssignmentManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
namespace AudioControl;

internal sealed class AppAssignmentManager
{
private readonly ISettingsStore settingsStore;
private readonly AppAssignmentPathStore appAssignmentPathStore;
private readonly Func<IAudioSessionService?> audioSessionServiceProvider;

public AppAssignmentManager(
ISettingsStore settingsStore,
AppAssignmentPathStore appAssignmentPathStore,
Func<IAudioSessionService?> audioSessionServiceProvider)
{
this.settingsStore = settingsStore;
this.appAssignmentPathStore = appAssignmentPathStore;
this.audioSessionServiceProvider = audioSessionServiceProvider;
}

public event EventHandler? AssignmentsChanged;

public IEnumerable<string> GetAvailableApps()
{
var audioSessionService = audioSessionServiceProvider();
if (audioSessionService == null)
{
return Enumerable.Empty<string>();
}

var gameApps = ParseCsv(settingsStore.Game).ToHashSet(StringComparer.OrdinalIgnoreCase);
var chatApps = ParseCsv(settingsStore.Chat).ToHashSet(StringComparer.OrdinalIgnoreCase);

return audioSessionService.GetAudioApplications(false)
.Split("\r\n", StringSplitOptions.RemoveEmptyEntries)
.Select(item => item.Trim())
.Where(item => !string.IsNullOrWhiteSpace(item))
.Where(item => !item.Equals("Idle", StringComparison.OrdinalIgnoreCase))
.Where(item => !gameApps.Contains(item) && !chatApps.Contains(item))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
}

public string? ResolveAssignedAppPath(string appName)
{
return appAssignmentPathStore.GetPathOrResolve(appName);
}

public void PersistResolvedAssignedAppPath(string appName, string exePath)
{
appAssignmentPathStore.SetPath(appName, exePath);
}

public void ApplyGameApps(string csv)
{
settingsStore.Game = csv ?? string.Empty;
settingsStore.ScheduleSave();
appAssignmentPathStore.SyncAssignedApps(ParseCsv(settingsStore.Game), ParseCsv(settingsStore.Chat));
AssignmentsChanged?.Invoke(this, EventArgs.Empty);
}

public void ApplyChatApps(string csv)
{
settingsStore.Chat = csv ?? string.Empty;
settingsStore.ScheduleSave();
appAssignmentPathStore.SyncAssignedApps(ParseCsv(settingsStore.Game), ParseCsv(settingsStore.Chat));
AssignmentsChanged?.Invoke(this, EventArgs.Empty);
}

private static IEnumerable<string> ParseCsv(string csv)
{
return (csv ?? string.Empty)
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Trim())
.Where(x => !string.IsNullOrWhiteSpace(x));
}
}
1 change: 1 addition & 0 deletions AudioControl/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public void Stop()
public void Dispose()
{
Stop();
audioSessionService.Dispose();
}

private static string MapNoiseReductionToCommand(string noiseReduction)
Expand Down
1 change: 1 addition & 0 deletions AudioControl/AudioControl.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<Version>2.0.1</Version>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
Expand Down
Loading