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);
}
}