using System.Diagnostics; using System.Text; using HidSharp; using NAudio.CoreAudioApi; namespace Ausiomixer.Host; internal static class Protocol { public const int Vid = 0x2E8A; public const int Pid = 0xA541; public const byte ReportIdInput = 1; public const byte ReportIdOutput = 2; public const int InputSize = 8; public const int OutputSize = 64; public const byte FlagE1Sw = 0x01; public const byte FlagE2Sw = 0x02; public const int NameChars = 12; public const int SlotSize = 1 + 1 + NameChars; // vol + peak + name public const int MaxVisible = 4; public const int HdrSize = 7; // id + count + sel + master + page + nVis + masterPeak } internal sealed class SessionInfo { public required string Name { get; init; } public required AudioSessionControl Control { get; init; } public float Volume { get => Control.SimpleAudioVolume.Volume; set => Control.SimpleAudioVolume.Volume = Math.Clamp(value, 0f, 1f); } public float Peak { get { try { return Control.AudioMeterInformation.MasterPeakValue; } catch { return 0f; } } } } internal static class Program { private static int _selected; private static HidStream? _stream; private static readonly object Gate = new(); public static async Task Main() { Console.WriteLine("Ausiomixer Host PoC"); Console.WriteLine($"Looking for HID VID=0x{Protocol.Vid:X4} PID=0x{Protocol.Pid:X4} ..."); using var cts = new CancellationTokenSource(); Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; while (!cts.IsCancellationRequested) { try { if (_stream == null) { _stream = OpenDevice(); if (_stream == null) { Console.WriteLine("Device not found, retry in 2s..."); await Task.Delay(2000, cts.Token); continue; } Console.WriteLine("Device opened."); } await RunLoop(cts.Token); } catch (OperationCanceledException) { break; } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); CloseDevice(); await Task.Delay(1500, CancellationToken.None); } } CloseDevice(); Console.WriteLine("Bye."); } private static HidStream? OpenDevice() { var list = DeviceList.Local; foreach (var dev in list.GetHidDevices(Protocol.Vid, Protocol.Pid)) { try { // Prefer interface that can do 64-byte output (our mixer report) if (dev.GetMaxOutputReportLength() < Protocol.OutputSize && dev.GetMaxFeatureReportLength() < Protocol.OutputSize) { // still try open if input matches } if (!dev.TryOpen(out var stream) || stream == null) { continue; } stream.ReadTimeout = 50; stream.WriteTimeout = 200; Console.WriteLine($"Opened: {dev.DevicePath}"); Console.WriteLine($" MaxIn={dev.GetMaxInputReportLength()} MaxOut={dev.GetMaxOutputReportLength()}"); return stream; } catch { // try next } } return null; } private static void CloseDevice() { lock (Gate) { _stream?.Dispose(); _stream = null; _lastOutPayload = null; } } private static byte[]? _lastOutPayload; private static async Task RunLoop(CancellationToken ct) { using var enumerator = new MMDeviceEnumerator(); var lastPush = DateTime.MinValue; while (!ct.IsCancellationRequested) { // Read input (non-blocking-ish) TryReadInput(enumerator); // ~20 Hz for live VU; skip identical payloads below if ((DateTime.UtcNow - lastPush).TotalMilliseconds >= 50) { PushOutput(enumerator); lastPush = DateTime.UtcNow; } await Task.Delay(15, ct); } } private static List CollectSessions(MMDeviceEnumerator enumerator) { var result = new List(); MMDevice device; try { device = enumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); } catch { return result; } var sessions = device.AudioSessionManager.Sessions; for (int i = 0; i < sessions.Count; i++) { var s = sessions[i]; try { string name = s.DisplayName; if (string.IsNullOrWhiteSpace(name)) { uint pid = s.GetProcessID; if (pid != 0) { try { name = Process.GetProcessById((int)pid).ProcessName; } catch { name = $"pid{pid}"; } } else { name = "System"; } } // Skip pure system sounds entry sometimes named empty result.Add(new SessionInfo { Name = TruncateName(name), Control = s }); } catch { // ignore inaccessible sessions } } return result; } private static string TruncateName(string name) { name = name.Trim(); if (name.Length <= Protocol.NameChars) { return name; } return name[..Protocol.NameChars]; } private static void TryReadInput(MMDeviceEnumerator enumerator) { HidStream? stream; lock (Gate) { stream = _stream; } if (stream == null) { return; } var buf = new byte[Math.Max(Protocol.InputSize, 64)]; int n; try { n = stream.Read(buf); } catch (TimeoutException) { return; } catch { CloseDevice(); throw; } if (n < Protocol.InputSize) { return; } // HidSharp usually includes report id at [0] int off = 0; if (buf[0] == Protocol.ReportIdInput) { off = 0; } else { return; } sbyte e1 = unchecked((sbyte)buf[off + 1]); sbyte e2 = unchecked((sbyte)buf[off + 2]); byte flags = buf[off + 3]; byte pot = buf[off + 4]; var sessions = CollectSessions(enumerator); if (sessions.Count == 0) { _selected = 0; } else { _selected = Math.Clamp(_selected + e1, 0, sessions.Count - 1); } if (e1 != 0 || e2 != 0 || flags != 0) { Console.WriteLine($"IN e1={e1} e2={e2} flags=0x{flags:X2} pot={pot} sel={_selected}"); } if (e2 != 0 && sessions.Count > 0 && _selected >= 0 && _selected < sessions.Count) { try { var s = sessions[_selected]; float nv = s.Volume + (e2 * 0.02f); s.Volume = nv; Console.WriteLine($" vol[{s.Name}]={(int)(s.Volume * 100)}%"); } catch (Exception ex) { Console.WriteLine($" vol fail: {ex.Message}"); } } if ((flags & Protocol.FlagE1Sw) != 0) { // optional: mute selected — for PoC just log Console.WriteLine(" E1 SW"); } if ((flags & Protocol.FlagE2Sw) != 0) { Console.WriteLine(" E2 SW"); } // Master from pot (absolute) try { var device = enumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); float target = pot / 100f; float cur = device.AudioEndpointVolume.MasterVolumeLevelScalar; if (Math.Abs(cur - target) > 0.01f) { device.AudioEndpointVolume.MasterVolumeLevelScalar = target; } } catch { // ignore } } private static void PushOutput(MMDeviceEnumerator enumerator) { HidStream? stream; lock (Gate) { stream = _stream; } if (stream == null) { return; } var sessions = CollectSessions(enumerator); if (sessions.Count > 0) { _selected = Math.Clamp(_selected, 0, sessions.Count - 1); } else { _selected = 0; } byte master = 50; byte masterPeak = 0; try { var device = enumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); master = (byte)Math.Clamp((int)(device.AudioEndpointVolume.MasterVolumeLevelScalar * 100), 0, 100); masterPeak = (byte)Math.Clamp((int)(device.AudioMeterInformation.MasterPeakValue * 100), 0, 100); } catch { // keep default } // Window of MaxVisible around selection int count = sessions.Count; int pageStart = 0; if (count > Protocol.MaxVisible) { pageStart = Math.Clamp(_selected - Protocol.MaxVisible / 2, 0, count - Protocol.MaxVisible); } int nVis = Math.Min(Protocol.MaxVisible, count); var report = new byte[Protocol.OutputSize]; report[0] = Protocol.ReportIdOutput; report[1] = (byte)Math.Min(count, 255); report[2] = (byte)_selected; report[3] = master; report[4] = (byte)pageStart; report[5] = (byte)nVis; report[6] = masterPeak; int off = Protocol.HdrSize; for (int i = 0; i < nVis; i++) { var s = sessions[pageStart + i]; byte vol = (byte)Math.Clamp((int)(s.Volume * 100), 0, 100); byte peak = (byte)Math.Clamp((int)(s.Peak * 100), 0, 100); report[off] = vol; report[off + 1] = peak; var nameBytes = Encoding.UTF8.GetBytes(s.Name); int copy = Math.Min(Protocol.NameChars, nameBytes.Length); Array.Copy(nameBytes, 0, report, off + 2, copy); off += Protocol.SlotSize; } // Skip identical payloads if (_lastOutPayload != null && _lastOutPayload.AsSpan().SequenceEqual(report)) { return; } try { stream.Write(report); _lastOutPayload = (byte[])report.Clone(); } catch { _lastOutPayload = null; CloseDevice(); throw; } } }