From bb6a654c90748a8f23e176b3c68dacb9ffbc62e3 Mon Sep 17 00:00:00 2001 From: Ali Chraghi Date: Thu, 19 Jan 2023 17:32:46 +0330 Subject: [PATCH] sysaudio: pipewire backend missing features:\n - volume adjustment\n - device watcher\n - device listing (default device) --- libs/sysaudio/sdk.zig | 2 + libs/sysaudio/src/backends.zig | 10 +- libs/sysaudio/src/pipewire.zig | 326 ++++++++++++++++++++++++++ libs/sysaudio/src/pipewire/sysaudio.c | 11 + 4 files changed, 347 insertions(+), 2 deletions(-) create mode 100644 libs/sysaudio/src/pipewire.zig create mode 100644 libs/sysaudio/src/pipewire/sysaudio.c diff --git a/libs/sysaudio/sdk.zig b/libs/sysaudio/sdk.zig index fe6ec747..1ea3effc 100644 --- a/libs/sysaudio/sdk.zig +++ b/libs/sysaudio/sdk.zig @@ -33,6 +33,8 @@ pub fn Sdk(comptime deps: anytype) type { step.linkFramework("CoreFoundation"); step.linkFramework("CoreAudio"); } else if (step.target.toTarget().os.tag == .linux) { + step.addCSourceFile(sdkPath("/src/pipewire/sysaudio.c"), &.{"-std=gnu99"}); + step.linkSystemLibrary("pipewire-0.3"); step.linkSystemLibrary("asound"); step.linkSystemLibrary("pulse"); step.linkSystemLibrary("jack"); diff --git a/libs/sysaudio/src/backends.zig b/libs/sysaudio/src/backends.zig index 87035bec..689fd12f 100644 --- a/libs/sysaudio/src/backends.zig +++ b/libs/sysaudio/src/backends.zig @@ -5,12 +5,15 @@ pub const Backend = std.meta.Tag(BackendContext); pub const BackendContext = switch (builtin.os.tag) { .linux => union(enum) { pulseaudio: *@import("pulseaudio.zig").Context, - alsa: *@import("alsa.zig").Context, jack: *@import("jack.zig").Context, + pipewire: *@import("pipewire.zig").Context, + alsa: *@import("alsa.zig").Context, dummy: *@import("dummy.zig").Context, }, .freebsd, .netbsd, .openbsd, .solaris => union(enum) { pulseaudio: *@import("pulseaudio.zig").Context, + jack: *@import("jack.zig").Context, + pipewire: *@import("pipewire.zig").Context, dummy: *@import("dummy.zig").Context, }, .macos, .ios, .watchos, .tvos => union(enum) { @@ -36,12 +39,15 @@ pub const BackendContext = switch (builtin.os.tag) { pub const BackendPlayer = switch (builtin.os.tag) { .linux => union(enum) { pulseaudio: *@import("pulseaudio.zig").Player, - alsa: *@import("alsa.zig").Player, jack: *@import("jack.zig").Player, + pipewire: *@import("pipewire.zig").Player, + alsa: *@import("alsa.zig").Player, dummy: *@import("dummy.zig").Player, }, .freebsd, .netbsd, .openbsd, .solaris => union(enum) { pulseaudio: *@import("pulseaudio.zig").Player, + jack: *@import("jack.zig").Player, + pipewire: *@import("pipewire.zig").Player, dummy: *@import("dummy.zig").Player, }, .macos, .ios, .watchos, .tvos => union(enum) { diff --git a/libs/sysaudio/src/pipewire.zig b/libs/sysaudio/src/pipewire.zig new file mode 100644 index 00000000..d43cbc44 --- /dev/null +++ b/libs/sysaudio/src/pipewire.zig @@ -0,0 +1,326 @@ +const std = @import("std"); +const c = @cImport({ + @cInclude("pipewire/pipewire.h"); + @cInclude("spa/param/audio/format-utils.h"); +}); +const main = @import("main.zig"); +const backends = @import("backends.zig"); +const util = @import("util.zig"); + +const default_playback = main.Device{ + .id = "default-playback", + .name = "Default Device", + .mode = .playback, + .channels = undefined, + .formats = std.meta.tags(main.Format), + .sample_rate = .{ + .min = main.min_sample_rate, + .max = main.max_sample_rate, + }, +}; + +const default_capture = main.Device{ + .id = "default-capture", + .name = "Default Device", + .mode = .capture, + .channels = undefined, + .formats = std.meta.tags(main.Format), + .sample_rate = .{ + .min = main.min_sample_rate, + .max = main.max_sample_rate, + }, +}; + +pub const Context = struct { + allocator: std.mem.Allocator, + devices_info: util.DevicesInfo, + app_name: [:0]const u8, + // watcher: ?Watcher, + + const Watcher = struct { + deviceChangeFn: main.DeviceChangeFn, + user_data: ?*anyopaque, + thread: *c.pw_thread_loop, + aborted: std.atomic.Atomic(bool), + }; + + pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.BackendContext { + c.pw_init(null, null); + + var self = try allocator.create(Context); + errdefer allocator.destroy(self); + self.* = .{ + .allocator = allocator, + .devices_info = util.DevicesInfo.init(), + .app_name = options.app_name, + // TODO: device change watcher + // .watcher = blk: { + // if (options.deviceChangeFn != null) { + // const thread = c.pw_thread_loop_new("device-change-watcher", null) orelse return error.SystemResources; + // const context = c.pw_context_new(c.pw_thread_loop_get_loop(thread), null, 0); + // const core = c.pw_context_connect(context, null, 0); + // const registry = c.pw_core_get_registry(core, c.PW_VERSION_REGISTRY, 0); + // _ = c.spa_zero(registry); + + // var registry_listener: c.spa_hook = undefined; + // _ = c.pw_registry_add_listener(registry, registry_listener); + + // break :blk .{ + // .deviceChangeFn = options.deviceChangeFn.?, + // .user_data = options.user_data, + // .thread = thread, + // .aborted = .{ .value = false }, + // }; + // } else break :blk null; + // }, + }; + + return .{ .pipewire = self }; + } + + pub fn deinit(self: *Context) void { + for (self.devices_info.list.items) |d| + freeDevice(self.allocator, d); + self.devices_info.list.deinit(self.allocator); + c.pw_deinit(); + self.allocator.destroy(self); + } + + pub fn refresh(self: *Context) !void { + for (self.devices_info.list.items) |d| + freeDevice(self.allocator, d); + self.devices_info.clear(self.allocator); + + try self.devices_info.list.append(self.allocator, default_playback); + try self.devices_info.list.append(self.allocator, default_capture); + + self.devices_info.setDefault(.playback, 0); + self.devices_info.setDefault(.capture, 1); + + self.devices_info.list.items[0].channels = try self.allocator.alloc(main.Channel, 2); + self.devices_info.list.items[1].channels = try self.allocator.alloc(main.Channel, 2); + + self.devices_info.list.items[0].channels[0] = .{ .id = .front_right }; + self.devices_info.list.items[0].channels[1] = .{ .id = .front_left }; + self.devices_info.list.items[1].channels[0] = .{ .id = .front_right }; + self.devices_info.list.items[1].channels[1] = .{ .id = .front_left }; + } + + pub fn devices(self: Context) []const main.Device { + return self.devices_info.list.items; + } + + pub fn defaultDevice(self: Context, mode: main.Device.Mode) ?main.Device { + return self.devices_info.default(mode); + } + + const stream_events = c.pw_stream_events{ + .version = c.PW_VERSION_STREAM_EVENTS, + .process = Player.processCb, + .destroy = null, + .state_changed = Player.stateChangedCb, + .control_info = null, + .io_changed = null, + .param_changed = null, + .add_buffer = null, + .remove_buffer = null, + .drained = null, + .command = null, + .trigger_done = null, + }; + + pub fn createPlayer(self: *Context, device: main.Device, writeFn: main.WriteFn, options: main.Player.Options) !backends.BackendPlayer { + const thread = c.pw_thread_loop_new(device.id, null) orelse return error.SystemResources; + + const media_role = switch (options.media_role) { + .default => "Screen", + .game => "Game", + .music => "Music", + .movie => "Movie", + .communication => "Communication", + }; + + var buf: [8]u8 = undefined; + const audio_rate = std.fmt.bufPrintZ(&buf, "{d}", .{options.sample_rate}) catch unreachable; + + const props = c.pw_properties_new( + c.PW_KEY_MEDIA_TYPE, + "Audio", + + c.PW_KEY_MEDIA_CATEGORY, + "Playback", + + c.PW_KEY_MEDIA_ROLE, + media_role.ptr, + + c.PW_KEY_MEDIA_NAME, + self.app_name.ptr, + + c.PW_KEY_AUDIO_RATE, + audio_rate.ptr, + + @intToPtr(*allowzero u0, 0), + ); + + var player = try self.allocator.create(Player); + errdefer self.allocator.destroy(player); + + const stream = c.pw_stream_new_simple( + c.pw_thread_loop_get_loop(thread), + "audio-src", + props, + &stream_events, + player, + ) orelse return error.OpeningDevice; + + var builder_buf: [256]u8 = undefined; + var pod_builder = c.spa_pod_builder{ + .data = &builder_buf, + .size = builder_buf.len, + ._padding = 0, + .state = .{ + .offset = 0, + .flags = 0, + .frame = null, + }, + .callbacks = .{ .funcs = null, .data = null }, + }; + var info = c.spa_audio_info_raw{ + .format = c.SPA_AUDIO_FORMAT_F32, + .channels = @intCast(u32, device.channels.len), + .rate = options.sample_rate, + .flags = 0, + .position = undefined, + }; + var params = [1][*c]c.spa_pod{ + sysaudio_spa_format_audio_raw_build(&pod_builder, c.SPA_PARAM_EnumFormat, &info), + }; + + if (c.pw_stream_connect( + stream, + c.PW_DIRECTION_OUTPUT, + c.PW_ID_ANY, + c.PW_STREAM_FLAG_AUTOCONNECT | c.PW_STREAM_FLAG_MAP_BUFFERS | c.PW_STREAM_FLAG_RT_PROCESS, + ¶ms, + params.len, + ) < 0) return error.OpeningDevice; + + player.* = .{ + .allocator = self.allocator, + .thread = thread, + .stream = stream, + .is_paused = .{ .value = false }, + .vol = 1.0, + .writeFn = writeFn, + .user_data = options.user_data, + .channels = device.channels, + .format = .f32, + .sample_rate = options.sample_rate, + .write_step = main.Format.frameSize(.f32, 2), + }; + return .{ .pipewire = player }; + } +}; + +pub const Player = struct { + allocator: std.mem.Allocator, + thread: *c.pw_thread_loop, + stream: *c.pw_stream, + is_paused: std.atomic.Atomic(bool), + vol: f32, + writeFn: main.WriteFn, + user_data: ?*anyopaque, + + channels: []main.Channel, + format: main.Format, + sample_rate: u24, + write_step: u8, + + pub fn stateChangedCb(self_opaque: ?*anyopaque, old_state: c.pw_stream_state, state: c.pw_stream_state, err: [*c]const u8) callconv(.C) void { + _ = old_state; + _ = err; + + var self = @ptrCast(*Player, @alignCast(@alignOf(*Player), self_opaque.?)); + + if (state == c.PW_STREAM_STATE_STREAMING or state == c.PW_STREAM_STATE_ERROR) { + c.pw_thread_loop_signal(self.thread, false); + } + } + + pub fn processCb(self_opaque: ?*anyopaque) callconv(.C) void { + var self = @ptrCast(*Player, @alignCast(@alignOf(*Player), self_opaque.?)); + + const buf = c.pw_stream_dequeue_buffer(self.stream) orelse unreachable; + if (buf.*.buffer.*.datas[0].data == null) return; + defer _ = c.pw_stream_queue_buffer(self.stream, buf); + + const stride = self.format.frameSize(self.channels.len); + const n_frames = std.math.min( + buf.*.requested, + buf.*.buffer.*.datas[0].maxsize / stride, + ); + + for (self.channels) |*ch, i| { + ch.*.ptr = @ptrCast([*]u8, buf.*.buffer.*.datas[0].data.?) + self.format.frameSize(i); + } + + buf.*.buffer.*.datas[0].chunk.*.offset = 0; + if (!self.is_paused.load(.Unordered)) { + buf.*.buffer.*.datas[0].chunk.*.stride = stride; + buf.*.buffer.*.datas[0].chunk.*.size = n_frames * stride; + self.writeFn(self.user_data, n_frames); + } else { + buf.*.buffer.*.datas[0].chunk.*.stride = 0; + buf.*.buffer.*.datas[0].chunk.*.size = 0; + } + } + + pub fn deinit(self: *Player) void { + c.pw_thread_loop_stop(self.thread); + c.pw_thread_loop_destroy(self.thread); + c.pw_stream_destroy(self.stream); + self.allocator.destroy(self); + } + + pub fn start(self: *Player) !void { + if (c.pw_thread_loop_start(self.thread) < 0) return error.SystemResources; + + c.pw_thread_loop_lock(self.thread); + c.pw_thread_loop_wait(self.thread); + c.pw_thread_loop_unlock(self.thread); + + if (c.pw_stream_get_state(self.stream, null) == c.PW_STREAM_STATE_ERROR) { + return error.CannotPlay; + } + } + + pub fn play(self: *Player) !void { + self.is_paused.store(false, .Unordered); + } + + pub fn pause(self: *Player) !void { + self.is_paused.store(true, .Unordered); + } + + pub fn paused(self: Player) bool { + return self.is_paused.load(.Unordered); + } + + pub fn setVolume(self: *Player, vol: f32) !void { + self.vol = vol; + } + + pub fn volume(self: Player) !f32 { + return self.vol; + } +}; + +fn freeDevice(allocator: std.mem.Allocator, device: main.Device) void { + allocator.free(device.channels); +} + +extern fn sysaudio_spa_format_audio_raw_build(builder: [*c]c.spa_pod_builder, id: u32, info: [*c]c.spa_audio_info_raw) callconv(.C) [*c]c.spa_pod; + +test { + std.testing.refAllDeclsRecursive(@This()); +} diff --git a/libs/sysaudio/src/pipewire/sysaudio.c b/libs/sysaudio/src/pipewire/sysaudio.c new file mode 100644 index 00000000..069d4a7e --- /dev/null +++ b/libs/sysaudio/src/pipewire/sysaudio.c @@ -0,0 +1,11 @@ +#include +#include + +struct spa_pod *sysaudio_spa_format_audio_raw_build(struct spa_pod_builder *builder, uint32_t id, struct spa_audio_info_raw *info) +{ + return spa_format_audio_raw_build(builder, id, info); +} + +void sysaudio_pw_registry_add_listener(struct pw_registry *reg, struct spa_hook *reg_listener, struct pw_registry_events *events) { + pw_registry_add_listener(reg, reg_listener, events, NULL); +} \ No newline at end of file