/// “脉冲”音量管理,这是基于 PulseAudio 的封装
[SupportedOSPlatform("linux")]
public partial class PulseAudioVolumeManager
private TaskCompletionSource<bool>? _initTaskCompletionSource;
private IntPtr _mainLoop;
private readonly pa_context_subscribe_cb_t _contextSubscribeCallback;
private readonly string _applicationName;
public event EventHandler<int>? VolumeChanged;
public event EventHandler<bool>? MuteChanged;
/// <param name="applicationName">应用名,可选,只是用于调用 pa_context_new 时传入,无特别含义和作用</param>
public PulseAudioVolumeManager(string? applicationName = null)
_applicationName = applicationName ?? Path.GetRandomFileName().Replace('.', '_');
_contextSubscribeCallback = ContextSubscribeCallback;
public async Task<bool> Init()
if (_initTaskCompletionSource == null)
_initTaskCompletionSource = new TaskCompletionSource<bool>();
_mainLoop = pa_threaded_mainloop_new();
if (_mainLoop != IntPtr.Zero)
var mainloopApi = pa_threaded_mainloop_get_api(_mainLoop);
if (mainloopApi != IntPtr.Zero)
var context = pa_context_new(mainloopApi, _applicationName);
if (context != IntPtr.Zero)
pa_context_set_state_callback(context, ContextStateCallback, IntPtr.Zero);
var result = pa_context_connect(context, IntPtr.Zero, 0, IntPtr.Zero);
result = pa_threaded_mainloop_start(_mainLoop);
var state = pa_context_get_state(context);
if (state == pa_context_state_t.PA_CONTEXT_READY)
if (!PA_CONTEXT_IS_GOOD(state))
pa_threaded_mainloop_wait(_mainLoop);
_initTaskCompletionSource.SetResult(isReady);
return await _initTaskCompletionSource.Task;
private void ContextStateCallback(IntPtr c, IntPtr userdata)
switch (pa_context_get_state(c))
case pa_context_state_t.PA_CONTEXT_READY:
pa_context_set_subscribe_callback(c, _contextSubscribeCallback, IntPtr.Zero);
var op = pa_context_subscribe(c, pa_subscription_mask_t.PA_SUBSCRIPTION_MASK_SINK, IntPtr.Zero, IntPtr.Zero);
pa_threaded_mainloop_signal(_mainLoop, 0);
case pa_context_state_t.PA_CONTEXT_TERMINATED:
case pa_context_state_t.PA_CONTEXT_FAILED:
pa_threaded_mainloop_signal(_mainLoop, 0);
private void ContextSubscribeCallback(IntPtr c, pa_subscription_event_type_t t, uint idx, IntPtr userdata)
if ((t & pa_subscription_event_type_t.PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == pa_subscription_event_type_t.PA_SUBSCRIPTION_EVENT_SINK)
pa_threaded_mainloop_lock(_mainLoop);
var sinkName = GetDefaultSinkName(c);
var info = GetSinkInfo(c, sinkName);
volume = GetVolumeValue(info.volume);
pa_threaded_mainloop_unlock(_mainLoop);
if (volume is int volumeVal && volumeVal != _volume)
VolumeChanged?.Invoke(this, volumeVal);
if (mute is bool muteVal && muteVal != _mute)
MuteChanged?.Invoke(this, muteVal);
private void ServerInfoCallback(IntPtr c, in pa_server_info i, IntPtr userdata)
#pragma warning disable CS8500
*(string*) userdata = Marshal.PtrToStringUTF8(i.default_sink_name);
#pragma warning restore CS8500
pa_threaded_mainloop_signal(_mainLoop, 0);
private void SinkInfoCallback(IntPtr c, in pa_sink_info i, int eol, IntPtr userdata)
pa_threaded_mainloop_signal(_mainLoop, 0);
*((pa_cvolume, bool)*) userdata = (i.volume, i.mute != 0);
public async Task<bool> GetMute()
if (_context != IntPtr.Zero)
pa_threaded_mainloop_lock(_mainLoop);
var sinkName = GetDefaultSinkName(_context);
var (volume, mute) = GetSinkInfo(_context, sinkName);
pa_threaded_mainloop_unlock(_mainLoop);
public async Task<int> GetVolume()
if (_context != IntPtr.Zero)
pa_threaded_mainloop_lock(_mainLoop);
var sinkName = GetDefaultSinkName(_context);
var (volume, mute) = GetSinkInfo(_context, sinkName);
result = GetVolumeValue(volume);
pa_threaded_mainloop_unlock(_mainLoop);
public async Task SetMute(bool mute)
if (_context != IntPtr.Zero)
pa_threaded_mainloop_lock(_mainLoop);
var sinkName = GetDefaultSinkName(_context);
pa_context_set_sink_mute_by_name(_context, sinkName, mute ? 1 : 0, IntPtr.Zero, IntPtr.Zero);
pa_threaded_mainloop_unlock(_mainLoop);
public async Task SetVolume(int volume)
if (_context != IntPtr.Zero)
pa_threaded_mainloop_lock(_mainLoop);
var sinkName = GetDefaultSinkName(_context);
var info = GetSinkInfo(_context, sinkName);
pa_cvolume_set(ref info.volume, info.volume.channels, (uint) (volume * 65536 / 100));
pa_context_set_sink_volume_by_name(_context, sinkName, info.volume, IntPtr.Zero, IntPtr.Zero);
pa_threaded_mainloop_unlock(_mainLoop);
/// 这个方法要在 pa_threaded_mainloop_lock 和 pa_threaded_mainloop_unlock 之间调用
private string? GetDefaultSinkName(IntPtr context)
// 取 sinkName 地址,相当于 ref string 用法,在 ServerInfoCallback 给 sinkName 赋值
var op = pa_context_get_server_info(context, ServerInfoCallback, (IntPtr) (&sinkName));
while (pa_operation_get_state(op) == pa_operation_state_t.PA_OPERATION_RUNNING)
pa_threaded_mainloop_wait(_mainLoop);
/// 这个方法要在 pa_threaded_mainloop_lock 和 pa_threaded_mainloop_unlock 之间调用
/// <param name="context"></param>
/// <param name="sinkName"></param>
private (pa_cvolume volume, bool mute) GetSinkInfo(IntPtr context, string sinkName)
var op = pa_context_get_sink_info_by_name(context, sinkName, SinkInfoCallback, (IntPtr) (&info));
while (pa_operation_get_state(op) == pa_operation_state_t.PA_OPERATION_RUNNING)
pa_threaded_mainloop_wait(_mainLoop);
private int GetVolumeValue(in pa_cvolume volume)
var val = pa_cvolume_avg(volume);
return (int) Math.Round((val * 100d / 65536));
public partial class PulseAudioVolumeManager
public static class PInvoke
public enum pa_context_state_t
public enum pa_subscription_event_type_t : uint
PA_SUBSCRIPTION_EVENT_SINK = 0x0000U,
PA_SUBSCRIPTION_EVENT_SOURCE = 0x0001U,
PA_SUBSCRIPTION_EVENT_SINK_INPUT = 0x0002U,
PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT = 0x0003U,
PA_SUBSCRIPTION_EVENT_MODULE = 0x0004U,
PA_SUBSCRIPTION_EVENT_CLIENT = 0x0005U,
PA_SUBSCRIPTION_EVENT_SAMPLE_CACHE = 0x0006U,
PA_SUBSCRIPTION_EVENT_SERVER = 0x0007U,
PA_SUBSCRIPTION_EVENT_AUTOLOAD = 0x0008U,
PA_SUBSCRIPTION_EVENT_CARD = 0x0009U,
PA_SUBSCRIPTION_EVENT_FACILITY_MASK = 0x000FU,
PA_SUBSCRIPTION_EVENT_NEW = 0x0000U,
PA_SUBSCRIPTION_EVENT_CHANGE = 0x0010U,
PA_SUBSCRIPTION_EVENT_REMOVE = 0x0020U,
PA_SUBSCRIPTION_EVENT_TYPE_MASK = 0x0030U,
public enum pa_subscription_mask_t : uint
PA_SUBSCRIPTION_MASK_NULL = 0x0000U,
PA_SUBSCRIPTION_MASK_SINK = 0x0001U,
PA_SUBSCRIPTION_MASK_SOURCE = 0x0002U,
PA_SUBSCRIPTION_MASK_SINK_INPUT = 0x0004U,
PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT = 0x0008U,
PA_SUBSCRIPTION_MASK_MODULE = 0x0010U,
PA_SUBSCRIPTION_MASK_CLIENT = 0x0020U,
PA_SUBSCRIPTION_MASK_SAMPLE_CACHE = 0x0040U,
PA_SUBSCRIPTION_MASK_SERVER = 0x0080U,
PA_SUBSCRIPTION_MASK_AUTOLOAD = 0x0100U,
PA_SUBSCRIPTION_MASK_CARD = 0x0200U,
PA_SUBSCRIPTION_MASK_ALL = 0x02ffU,
public enum pa_sample_format_t
public enum pa_operation_state_t
[StructLayout(LayoutKind.Sequential)]
public struct pa_server_info
public IntPtr server_version;
public IntPtr server_name;
pa_sample_spec sample_spec;
public IntPtr default_sink_name;
public IntPtr default_source_name;
pa_channel_map channel_map;
[StructLayout(LayoutKind.Sequential)]
public struct pa_sample_spec
public pa_sample_format_t format;
[StructLayout(LayoutKind.Sequential, Size = 132)]
public struct pa_channel_map
//public pa_channel_position_t map[PA_CHANNELS_MAX];
[StructLayout(LayoutKind.Sequential, Size = 132)]
//public pa_volume_t values[PA_CHANNELS_MAX];
[StructLayout(LayoutKind.Sequential)]
public struct pa_sink_info
public IntPtr description;
public pa_sample_spec sample_spec;
public pa_channel_map channel_map;
public uint owner_module;
public pa_cvolume volume;
public uint monitor_source;
public IntPtr monitor_source_name;
public ulong configured_latency;
public uint n_volume_steps;
public IntPtr active_port;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool PA_CONTEXT_IS_GOOD(pa_context_state_t x)
return x == pa_context_state_t.PA_CONTEXT_CONNECTING || x == pa_context_state_t.PA_CONTEXT_AUTHORIZING ||
x == pa_context_state_t.PA_CONTEXT_SETTING_NAME || x == pa_context_state_t.PA_CONTEXT_READY;
public delegate void pa_context_notify_cb_t(IntPtr c, IntPtr userdata);
public delegate void pa_context_subscribe_cb_t(IntPtr c, pa_subscription_event_type_t t, uint idx, IntPtr userdata);
public delegate void pa_server_info_cb_t(IntPtr c, in pa_server_info i, IntPtr userdata);
public delegate void pa_sink_info_cb_t(IntPtr c, in pa_sink_info i, int eol, IntPtr userdata);
[DllImport("libpulse.so.0")]
public static extern IntPtr pa_threaded_mainloop_new();
[DllImport("libpulse.so.0")]
public static extern IntPtr pa_threaded_mainloop_get_api(IntPtr m);
[DllImport("libpulse.so.0")]
public static extern IntPtr pa_context_new(IntPtr mainloop, [MarshalAs(UnmanagedType.LPUTF8Str)] string name);
[DllImport("libpulse.so.0")]
public static extern void pa_context_set_state_callback(IntPtr c, pa_context_notify_cb_t cb, IntPtr userdata);
[DllImport("libpulse.so.0")]
public static extern int pa_context_connect(IntPtr c, IntPtr server, uint flags, IntPtr api);
[DllImport("libpulse.so.0")]
public static extern int pa_threaded_mainloop_start(IntPtr m);
[DllImport("libpulse.so.0")]
public static extern pa_context_state_t pa_context_get_state(IntPtr c);
[DllImport("libpulse.so.0")]
public static extern void pa_threaded_mainloop_wait(IntPtr m);
[DllImport("libpulse.so.0")]
public static extern void pa_context_set_subscribe_callback(IntPtr c, pa_context_subscribe_cb_t cb, IntPtr userdata);
[DllImport("libpulse.so.0")]
public static extern IntPtr pa_context_subscribe(IntPtr c, pa_subscription_mask_t m, IntPtr cb, IntPtr userdata);
[DllImport("libpulse.so.0")]
public static extern void pa_operation_unref(IntPtr o);
[DllImport("libpulse.so.0")]
public static extern void pa_threaded_mainloop_signal(IntPtr m, int wait_for_accept);
[DllImport("libpulse.so.0")]
public static extern void pa_threaded_mainloop_lock(IntPtr m);
[DllImport("libpulse.so.0")]
public static extern void pa_threaded_mainloop_unlock(IntPtr m);
[DllImport("libpulse.so.0")]
public static extern IntPtr pa_context_get_server_info(IntPtr c, pa_server_info_cb_t cb, IntPtr userdata);
[DllImport("libpulse.so.0")]
public static extern pa_operation_state_t pa_operation_get_state(IntPtr o);
[DllImport("libpulse.so.0")]
public static extern IntPtr pa_context_get_sink_info_by_name(IntPtr c, [MarshalAs(UnmanagedType.LPUTF8Str)] string name, pa_sink_info_cb_t cb, IntPtr userdata);
[DllImport("libpulse.so.0")]
public static extern uint pa_cvolume_avg(in pa_cvolume a);
[DllImport("libpulse.so.0")]
public static extern IntPtr pa_cvolume_set(ref pa_cvolume a, uint channels, uint v);
[DllImport("libpulse.so.0")]
public static extern IntPtr pa_context_set_sink_mute_by_name(IntPtr c, [MarshalAs(UnmanagedType.LPUTF8Str)] string name, int mute, IntPtr cb, IntPtr userdata);
[DllImport("libpulse.so.0")]
public static extern IntPtr pa_context_set_sink_volume_by_name(IntPtr c, [MarshalAs(UnmanagedType.LPUTF8Str)] string name, in pa_cvolume volume, IntPtr cb, IntPtr userdata);