527 lines
18 KiB
Zig
527 lines
18 KiB
Zig
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_sample_rate = 44_100; // Hz
|
|
|
|
var lib: Lib = undefined;
|
|
const Lib = struct {
|
|
handle: std.DynLib,
|
|
|
|
pw_init: *const fn ([*c]c_int, [*c][*c][*c]u8) callconv(.C) void,
|
|
pw_deinit: *const fn () callconv(.C) void,
|
|
pw_thread_loop_new: *const fn ([*c]const u8, [*c]const c.spa_dict) callconv(.C) ?*c.pw_thread_loop,
|
|
pw_thread_loop_destroy: *const fn (?*c.pw_thread_loop) callconv(.C) void,
|
|
pw_thread_loop_start: *const fn (?*c.pw_thread_loop) callconv(.C) c_int,
|
|
pw_thread_loop_stop: *const fn (?*c.pw_thread_loop) callconv(.C) void,
|
|
pw_thread_loop_signal: *const fn (?*c.pw_thread_loop, bool) callconv(.C) void,
|
|
pw_thread_loop_wait: *const fn (?*c.pw_thread_loop) callconv(.C) void,
|
|
pw_thread_loop_lock: *const fn (?*c.pw_thread_loop) callconv(.C) void,
|
|
pw_thread_loop_unlock: *const fn (?*c.pw_thread_loop) callconv(.C) void,
|
|
pw_thread_loop_get_loop: *const fn (?*c.pw_thread_loop) callconv(.C) [*c]c.pw_loop,
|
|
pw_properties_new: *const fn ([*c]const u8, ...) callconv(.C) [*c]c.pw_properties,
|
|
pw_stream_new_simple: *const fn ([*c]c.pw_loop, [*c]const u8, [*c]c.pw_properties, [*c]const c.pw_stream_events, ?*anyopaque) callconv(.C) ?*c.pw_stream,
|
|
pw_stream_destroy: *const fn (?*c.pw_stream) callconv(.C) void,
|
|
pw_stream_connect: *const fn (?*c.pw_stream, c.spa_direction, u32, c.pw_stream_flags, [*c][*c]const c.spa_pod, u32) callconv(.C) c_int,
|
|
pw_stream_queue_buffer: *const fn (?*c.pw_stream, [*c]c.pw_buffer) callconv(.C) c_int,
|
|
pw_stream_dequeue_buffer: *const fn (?*c.pw_stream) callconv(.C) [*c]c.pw_buffer,
|
|
pw_stream_get_state: *const fn (?*c.pw_stream, [*c][*c]const u8) callconv(.C) c.pw_stream_state,
|
|
|
|
pub fn load() !void {
|
|
lib.handle = std.DynLib.open("libpipewire-0.3.so") catch return error.LibraryNotFound;
|
|
inline for (@typeInfo(Lib).@"struct".fields[1..]) |field| {
|
|
const name = std.fmt.comptimePrint("{s}\x00", .{field.name});
|
|
const name_z: [:0]const u8 = @ptrCast(name[0 .. name.len - 1]);
|
|
@field(lib, field.name) = lib.handle.lookup(field.type, name_z) orelse return error.SymbolLookup;
|
|
}
|
|
}
|
|
};
|
|
|
|
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.Context.DeviceChangeFn,
|
|
user_data: ?*anyopaque,
|
|
thread: *c.pw_thread_loop,
|
|
aborted: std.atomic.Value(bool),
|
|
};
|
|
|
|
pub fn init(allocator: std.mem.Allocator, options: main.Context.Options) !backends.Context {
|
|
try Lib.load();
|
|
|
|
lib.pw_init(null, null);
|
|
|
|
const ctx = try allocator.create(Context);
|
|
errdefer allocator.destroy(ctx);
|
|
ctx.* = .{
|
|
.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 = .{ .raw = false },
|
|
// };
|
|
// } else break :blk null;
|
|
// },
|
|
};
|
|
|
|
return .{ .pipewire = ctx };
|
|
}
|
|
|
|
pub fn deinit(ctx: *Context) void {
|
|
for (ctx.devices_info.list.items) |d|
|
|
freeDevice(ctx.allocator, d);
|
|
ctx.devices_info.list.deinit(ctx.allocator);
|
|
lib.pw_deinit();
|
|
ctx.allocator.destroy(ctx);
|
|
lib.handle.close();
|
|
}
|
|
|
|
pub fn refresh(ctx: *Context) !void {
|
|
for (ctx.devices_info.list.items) |d|
|
|
freeDevice(ctx.allocator, d);
|
|
ctx.devices_info.clear();
|
|
|
|
try ctx.devices_info.list.append(ctx.allocator, default_playback);
|
|
try ctx.devices_info.list.append(ctx.allocator, default_capture);
|
|
|
|
ctx.devices_info.setDefault(.playback, 0);
|
|
ctx.devices_info.setDefault(.capture, 1);
|
|
|
|
ctx.devices_info.list.items[0].channels = try ctx.allocator.alloc(main.ChannelPosition, 2);
|
|
ctx.devices_info.list.items[1].channels = try ctx.allocator.alloc(main.ChannelPosition, 2);
|
|
|
|
ctx.devices_info.list.items[0].channels[0] = .front_right;
|
|
ctx.devices_info.list.items[0].channels[1] = .front_left;
|
|
ctx.devices_info.list.items[1].channels[0] = .front_right;
|
|
ctx.devices_info.list.items[1].channels[1] = .front_left;
|
|
}
|
|
|
|
pub fn devices(ctx: Context) []const main.Device {
|
|
return ctx.devices_info.list.items;
|
|
}
|
|
|
|
pub fn defaultDevice(ctx: Context, mode: main.Device.Mode) ?main.Device {
|
|
return ctx.devices_info.default(mode);
|
|
}
|
|
|
|
pub fn createPlayer(ctx: *Context, device: main.Device, writeFn: main.WriteFn, options: main.StreamOptions) !backends.Player {
|
|
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 orelse default_sample_rate}) catch unreachable;
|
|
|
|
const props = lib.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,
|
|
ctx.app_name.ptr,
|
|
|
|
c.PW_KEY_AUDIO_RATE,
|
|
audio_rate.ptr,
|
|
|
|
@as(*allowzero u0, @ptrFromInt(0)),
|
|
);
|
|
|
|
const stream_events = c.pw_stream_events{
|
|
.version = c.PW_VERSION_STREAM_EVENTS,
|
|
.process = Player.processCb,
|
|
.destroy = null,
|
|
.state_changed = stateChangedCb,
|
|
.control_info = null,
|
|
.io_changed = null,
|
|
.param_changed = null,
|
|
.add_buffer = null,
|
|
.remove_buffer = null,
|
|
.drained = null,
|
|
.command = null,
|
|
.trigger_done = null,
|
|
};
|
|
|
|
const player = try ctx.allocator.create(Player);
|
|
errdefer ctx.allocator.destroy(player);
|
|
|
|
const thread = lib.pw_thread_loop_new(device.id, null) orelse return error.SystemResources;
|
|
const stream = lib.pw_stream_new_simple(
|
|
lib.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 = @as(u32, @intCast(device.channels.len)),
|
|
.rate = options.sample_rate orelse default_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 (lib.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 = ctx.allocator,
|
|
.thread = thread,
|
|
.stream = stream,
|
|
.is_paused = .{ .raw = false },
|
|
.vol = 1.0,
|
|
.writeFn = writeFn,
|
|
.user_data = options.user_data,
|
|
.channels = device.channels,
|
|
.format = .f32,
|
|
.sample_rate = options.sample_rate orelse default_sample_rate,
|
|
};
|
|
return .{ .pipewire = player };
|
|
}
|
|
|
|
pub fn createRecorder(ctx: *Context, device: main.Device, readFn: main.ReadFn, options: main.StreamOptions) !backends.Recorder {
|
|
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 orelse default_sample_rate}) catch unreachable;
|
|
|
|
const props = lib.pw_properties_new(
|
|
c.PW_KEY_MEDIA_TYPE,
|
|
"Audio",
|
|
|
|
c.PW_KEY_MEDIA_CATEGORY,
|
|
"Capture",
|
|
|
|
c.PW_KEY_MEDIA_ROLE,
|
|
media_role.ptr,
|
|
|
|
c.PW_KEY_MEDIA_NAME,
|
|
ctx.app_name.ptr,
|
|
|
|
c.PW_KEY_AUDIO_RATE,
|
|
audio_rate.ptr,
|
|
|
|
@as(*allowzero u0, @ptrFromInt(0)),
|
|
);
|
|
|
|
const stream_events = c.pw_stream_events{
|
|
.version = c.PW_VERSION_STREAM_EVENTS,
|
|
.process = Recorder.processCb,
|
|
.destroy = null,
|
|
.state_changed = stateChangedCb,
|
|
.control_info = null,
|
|
.io_changed = null,
|
|
.param_changed = null,
|
|
.add_buffer = null,
|
|
.remove_buffer = null,
|
|
.drained = null,
|
|
.command = null,
|
|
.trigger_done = null,
|
|
};
|
|
|
|
const recorder = try ctx.allocator.create(Recorder);
|
|
errdefer ctx.allocator.destroy(recorder);
|
|
|
|
const thread = lib.pw_thread_loop_new(device.id, null) orelse return error.SystemResources;
|
|
const stream = lib.pw_stream_new_simple(
|
|
lib.pw_thread_loop_get_loop(thread),
|
|
"audio-capture",
|
|
props,
|
|
&stream_events,
|
|
recorder,
|
|
) 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 = @as(u32, @intCast(device.channels.len)),
|
|
.rate = options.sample_rate orelse default_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 (lib.pw_stream_connect(
|
|
stream,
|
|
c.PW_DIRECTION_INPUT,
|
|
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;
|
|
|
|
recorder.* = .{
|
|
.allocator = ctx.allocator,
|
|
.thread = thread,
|
|
.stream = stream,
|
|
.is_paused = .{ .raw = false },
|
|
.vol = 1.0,
|
|
.readFn = readFn,
|
|
.user_data = options.user_data,
|
|
.channels = device.channels,
|
|
.format = .f32,
|
|
.sample_rate = options.sample_rate orelse default_sample_rate,
|
|
};
|
|
return .{ .pipewire = recorder };
|
|
}
|
|
};
|
|
|
|
fn stateChangedCb(player_opaque: ?*anyopaque, old_state: c.pw_stream_state, state: c.pw_stream_state, err: [*c]const u8) callconv(.C) void {
|
|
_ = old_state;
|
|
_ = err;
|
|
|
|
const player = @as(*Player, @ptrCast(@alignCast(player_opaque.?)));
|
|
|
|
if (state == c.PW_STREAM_STATE_STREAMING or state == c.PW_STREAM_STATE_ERROR) {
|
|
lib.pw_thread_loop_signal(player.thread, false);
|
|
}
|
|
}
|
|
|
|
pub const Player = struct {
|
|
allocator: std.mem.Allocator,
|
|
thread: *c.pw_thread_loop,
|
|
stream: *c.pw_stream,
|
|
is_paused: std.atomic.Value(bool),
|
|
vol: f32,
|
|
writeFn: main.WriteFn,
|
|
user_data: ?*anyopaque,
|
|
|
|
channels: []main.ChannelPosition,
|
|
format: main.Format,
|
|
sample_rate: u24,
|
|
|
|
pub fn processCb(player_opaque: ?*anyopaque) callconv(.C) void {
|
|
var player = @as(*Player, @ptrCast(@alignCast(player_opaque.?)));
|
|
|
|
const buf = lib.pw_stream_dequeue_buffer(player.stream) orelse return;
|
|
if (buf.*.buffer.*.datas[0].data == null) return;
|
|
defer _ = lib.pw_stream_queue_buffer(player.stream, buf);
|
|
|
|
buf.*.buffer.*.datas[0].chunk.*.offset = 0;
|
|
if (player.is_paused.load(.unordered)) {
|
|
buf.*.buffer.*.datas[0].chunk.*.stride = 0;
|
|
buf.*.buffer.*.datas[0].chunk.*.size = 0;
|
|
return;
|
|
}
|
|
|
|
const stride = player.format.frameSize(@intCast(player.channels.len));
|
|
const frames = @min(buf.*.requested * stride, buf.*.buffer.*.datas[0].maxsize);
|
|
buf.*.buffer.*.datas[0].chunk.*.stride = stride;
|
|
buf.*.buffer.*.datas[0].chunk.*.size = @intCast(frames);
|
|
|
|
player.writeFn(player.user_data, @as([*]u8, @ptrCast(buf.*.buffer.*.datas[0].data.?))[0..frames]);
|
|
}
|
|
|
|
pub fn deinit(player: *Player) void {
|
|
lib.pw_thread_loop_stop(player.thread);
|
|
lib.pw_thread_loop_destroy(player.thread);
|
|
lib.pw_stream_destroy(player.stream);
|
|
player.allocator.destroy(player);
|
|
}
|
|
|
|
pub fn start(player: *Player) !void {
|
|
if (lib.pw_thread_loop_start(player.thread) < 0) return error.SystemResources;
|
|
|
|
lib.pw_thread_loop_lock(player.thread);
|
|
lib.pw_thread_loop_wait(player.thread);
|
|
lib.pw_thread_loop_unlock(player.thread);
|
|
|
|
if (lib.pw_stream_get_state(player.stream, null) == c.PW_STREAM_STATE_ERROR) {
|
|
return error.CannotPlay;
|
|
}
|
|
}
|
|
|
|
pub fn play(player: *Player) !void {
|
|
player.is_paused.store(false, .unordered);
|
|
}
|
|
|
|
pub fn pause(player: *Player) !void {
|
|
player.is_paused.store(true, .unordered);
|
|
}
|
|
|
|
pub fn paused(player: *Player) bool {
|
|
return player.is_paused.load(.unordered);
|
|
}
|
|
|
|
pub fn setVolume(player: *Player, vol: f32) !void {
|
|
player.vol = vol;
|
|
}
|
|
|
|
pub fn volume(player: *Player) !f32 {
|
|
return player.vol;
|
|
}
|
|
};
|
|
|
|
pub const Recorder = struct {
|
|
allocator: std.mem.Allocator,
|
|
thread: *c.pw_thread_loop,
|
|
stream: *c.pw_stream,
|
|
is_paused: std.atomic.Value(bool),
|
|
vol: f32,
|
|
readFn: main.ReadFn,
|
|
user_data: ?*anyopaque,
|
|
|
|
channels: []main.ChannelPosition,
|
|
format: main.Format,
|
|
sample_rate: u24,
|
|
|
|
pub fn processCb(recorder_opaque: ?*anyopaque) callconv(.C) void {
|
|
var recorder = @as(*Recorder, @ptrCast(@alignCast(recorder_opaque.?)));
|
|
|
|
const buf = lib.pw_stream_dequeue_buffer(recorder.stream) orelse return;
|
|
if (buf.*.buffer.*.datas[0].data == null) return;
|
|
defer _ = lib.pw_stream_queue_buffer(recorder.stream, buf);
|
|
|
|
buf.*.buffer.*.datas[0].chunk.*.offset = 0;
|
|
if (recorder.is_paused.load(.unordered)) {
|
|
buf.*.buffer.*.datas[0].chunk.*.stride = 0;
|
|
buf.*.buffer.*.datas[0].chunk.*.size = 0;
|
|
return;
|
|
}
|
|
|
|
const frames = buf.*.buffer.*.datas[0].chunk.*.size;
|
|
recorder.readFn(recorder.user_data, @as([*]u8, @ptrCast(buf.*.buffer.*.datas[0].data.?))[0..frames]);
|
|
}
|
|
|
|
pub fn deinit(recorder: *Recorder) void {
|
|
lib.pw_thread_loop_stop(recorder.thread);
|
|
lib.pw_thread_loop_destroy(recorder.thread);
|
|
lib.pw_stream_destroy(recorder.stream);
|
|
recorder.allocator.destroy(recorder);
|
|
}
|
|
|
|
pub fn start(recorder: *Recorder) !void {
|
|
if (lib.pw_thread_loop_start(recorder.thread) < 0) return error.SystemResources;
|
|
|
|
lib.pw_thread_loop_lock(recorder.thread);
|
|
lib.pw_thread_loop_wait(recorder.thread);
|
|
lib.pw_thread_loop_unlock(recorder.thread);
|
|
|
|
if (lib.pw_stream_get_state(recorder.stream, null) == c.PW_STREAM_STATE_ERROR) {
|
|
return error.CannotRecord;
|
|
}
|
|
}
|
|
|
|
pub fn record(recorder: *Recorder) !void {
|
|
recorder.is_paused.store(false, .unordered);
|
|
}
|
|
|
|
pub fn pause(recorder: *Recorder) !void {
|
|
recorder.is_paused.store(true, .unordered);
|
|
}
|
|
|
|
pub fn paused(recorder: *Recorder) bool {
|
|
return recorder.is_paused.load(.unordered);
|
|
}
|
|
|
|
pub fn setVolume(recorder: *Recorder, vol: f32) !void {
|
|
recorder.vol = vol;
|
|
}
|
|
|
|
pub fn volume(recorder: *Recorder) !f32 {
|
|
return recorder.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;
|