using System.Runtime.InteropServices; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// L27-2: 시스템 볼륨 제어 핸들러. "vol" 프리픽스로 사용합니다. /// /// 예: vol → 현재 볼륨 표시 /// vol 50 → 볼륨 50% 설정 /// vol up / down → ±10% 조절 /// vol mute → 음소거 토글 /// Enter → 해당 명령 실행. /// Windows Core Audio API (IAudioEndpointVolume) COM 인터페이스 사용. /// public class VolHandler : IActionHandler { public string? Prefix => "vol"; public PluginMetadata Metadata => new( "볼륨 제어", "시스템 볼륨 조절 — 설정·증감·음소거", "1.0", "AX"); public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim().ToLowerInvariant(); var items = new List(); // 현재 볼륨 읽기 float curLevel; bool curMuted; try { using var audio = AudioEndpoint.GetDefault(); curLevel = audio.GetVolume(); curMuted = audio.GetMute(); } catch { items.Add(new LauncherItem( "오디오 장치를 찾을 수 없습니다", "기본 재생 장치가 연결되어 있는지 확인하세요", null, null, Symbol: Symbols.Warning)); return Task.FromResult>(items); } int pct = (int)Math.Round(curLevel * 100); var bar = VolumeBar(pct); var muteLabel = curMuted ? " 🔇 음소거" : ""; var symbol = curMuted ? Symbols.VolumeMute : pct == 0 ? Symbols.VolumeMute : pct < 50 ? Symbols.VolumeDown : Symbols.VolumeUp; if (string.IsNullOrWhiteSpace(q)) { items.Add(new LauncherItem( $"현재 볼륨: {pct}%{muteLabel}", $"{bar} · vol 50 / vol up / vol down / vol mute", null, null, Symbol: symbol)); items.Add(new LauncherItem("vol up", "볼륨 +10%", null, ("set", Math.Min(pct + 10, 100)), Symbol: Symbols.VolumeUp)); items.Add(new LauncherItem("vol down", "볼륨 −10%", null, ("set", Math.Max(pct - 10, 0)), Symbol: Symbols.VolumeDown)); items.Add(new LauncherItem("vol mute", curMuted ? "음소거 해제" : "음소거", null, ("mute", !curMuted), Symbol: Symbols.VolumeMute)); items.Add(new LauncherItem("vol 0", "볼륨 0% (무음)", null, ("set", 0), Symbol: Symbols.VolumeMute)); items.Add(new LauncherItem("vol 50", "볼륨 50%", null, ("set", 50), Symbol: Symbols.VolumeDown)); items.Add(new LauncherItem("vol 100", "볼륨 100%", null, ("set", 100), Symbol: Symbols.VolumeUp)); return Task.FromResult>(items); } // 명령 파싱 if (q is "up" or "올려" or "+") { var target = Math.Min(pct + 10, 100); items.Add(new LauncherItem($"볼륨 +10% → {target}%", $"{VolumeBar(target)}", null, ("set", target), Symbol: Symbols.VolumeUp)); } else if (q is "down" or "내려" or "-") { var target = Math.Max(pct - 10, 0); items.Add(new LauncherItem($"볼륨 −10% → {target}%", $"{VolumeBar(target)}", null, ("set", target), Symbol: Symbols.VolumeDown)); } else if (q is "mute" or "음소거" or "m") { items.Add(new LauncherItem( curMuted ? "음소거 해제" : "음소거 설정", $"현재: {pct}%{muteLabel}", null, ("mute", !curMuted), Symbol: Symbols.VolumeMute)); } else if (int.TryParse(q, out int val) && val is >= 0 and <= 100) { items.Add(new LauncherItem( $"볼륨 {val}% 설정", $"{VolumeBar(val)} (현재 {pct}%)", null, ("set", val), Symbol: val > pct ? Symbols.VolumeUp : Symbols.VolumeDown)); } else { items.Add(new LauncherItem( $"'{query}' — 알 수 없는 명령", "사용법: vol 50 / vol up / vol down / vol mute", null, null, Symbol: Symbols.Warning)); } return Task.FromResult>(items); } public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { try { using var audio = AudioEndpoint.GetDefault(); if (item.Data is ("set", int level)) { audio.SetVolume(level / 100f); if (audio.GetMute()) audio.SetMute(false); NotificationService.Notify("vol", $"볼륨 {level}%"); } else if (item.Data is ("mute", bool mute)) { audio.SetMute(mute); NotificationService.Notify("vol", mute ? "음소거" : "음소거 해제"); } } catch (Exception ex) { NotificationService.Notify("vol", $"오류: {ex.Message}"); } return Task.CompletedTask; } private static string VolumeBar(int pct) { int filled = pct / 5; // 0~20 return "[" + new string('█', filled) + new string('░', 20 - filled) + "]"; } // ─── Core Audio API COM 래퍼 ───────────────────────────────────────────── private sealed class AudioEndpoint : IDisposable { private readonly IAudioEndpointVolume _vol; private AudioEndpoint(IAudioEndpointVolume vol) => _vol = vol; public static AudioEndpoint GetDefault() { var enumerator = (IMMDeviceEnumerator)new MMDeviceEnumeratorClass(); enumerator.GetDefaultAudioEndpoint(0 /*eRender*/, 1 /*eMultimedia*/, out var device); var iid = typeof(IAudioEndpointVolume).GUID; device.Activate(ref iid, 1 /*CLSCTX_ALL*/, IntPtr.Zero, out var obj); return new AudioEndpoint((IAudioEndpointVolume)obj); } public float GetVolume() { _vol.GetMasterVolumeLevelScalar(out float l); return l; } public bool GetMute() { _vol.GetMute(out bool m); return m; } public void SetVolume(float level) { var g = Guid.Empty; _vol.SetMasterVolumeLevelScalar(Math.Clamp(level, 0f, 1f), ref g); } public void SetMute(bool mute) { var g = Guid.Empty; _vol.SetMute(mute, ref g); } public void Dispose() { if (_vol is IDisposable d) d.Dispose(); if (_vol != null) Marshal.ReleaseComObject(_vol); } } // ─── COM 인터페이스 정의 (Windows Core Audio API) ────────────────────── [ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] private class MMDeviceEnumeratorClass { } [Guid("A95664D2-9614-4F35-A746-DE8DB63617E6")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IMMDeviceEnumerator { int EnumAudioEndpoints(int dataFlow, uint stateMask, out IntPtr devices); int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice device); } [Guid("D666063F-1587-4E43-81F1-B948E807363F")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IMMDevice { int Activate(ref Guid iid, uint clsCtx, IntPtr activationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface); } [Guid("5CDF2C82-841E-4546-9722-0CF74078229A")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] private interface IAudioEndpointVolume { int RegisterControlChangeNotify(IntPtr pNotify); int UnregisterControlChangeNotify(IntPtr pNotify); int GetChannelCount(out uint pnChannelCount); int SetMasterVolumeLevel(float fLevelDB, ref Guid pguidEventContext); int SetMasterVolumeLevelScalar(float fLevel, ref Guid pguidEventContext); int GetMasterVolumeLevel(out float pfLevelDB); int GetMasterVolumeLevelScalar(out float pfLevel); int SetChannelVolumeLevel(uint nChannel, float fLevelDB, ref Guid pguidEventContext); int SetChannelVolumeLevelScalar(uint nChannel, float fLevel, ref Guid pguidEventContext); int GetChannelVolumeLevel(uint nChannel, out float pfLevelDB); int GetChannelVolumeLevelScalar(uint nChannel, out float pfLevel); int SetMute([MarshalAs(UnmanagedType.Bool)] bool bMute, ref Guid pguidEventContext); int GetMute([MarshalAs(UnmanagedType.Bool)] out bool pbMute); } }