diff --git a/build.zig b/build.zig index d03d67a..a411208 100644 --- a/build.zig +++ b/build.zig @@ -15,19 +15,6 @@ pub fn build(b: *std.Build) void { // set a preferred release mode, allowing the user to decide how to optimize. const optimize = b.standardOptimizeOption(.{}); - // This creates a "module", which represents a collection of source files alongside - // some compilation options, such as optimization mode and linked system libraries. - // Every executable or library we compile will be based on one or more modules. - const lib_mod = b.createModule(.{ - // `root_source_file` is the Zig "entry point" of the module. If a module - // only contains e.g. external object files, you can make this `null`. - // In this case the main source file is merely a path, however, in more - // complicated build scripts, this could be a generated file. - .root_source_file = b.path("src/root.zig"), - .target = target, - .optimize = optimize, - }); - // We will also create a module for our other entry point, 'main.zig'. const exe_mod = b.createModule(.{ // `root_source_file` is the Zig "entry point" of the module. If a module @@ -37,31 +24,17 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, + .link_libc = true, }); - // Modules can depend on one another using the `std.Build.Module.addImport` function. - // This is what allows Zig source code to use `@import("foo")` where 'foo' is not a - // file path. In this case, we set up `exe_mod` to import `lib_mod`. - exe_mod.addImport("zig_lib", lib_mod); - - // Now, we will create a static library based on the module we created above. - // This creates a `std.Build.Step.Compile`, which is the build step responsible - // for actually invoking the compiler. - const lib = b.addLibrary(.{ - .linkage = .static, - .name = "zig", - .root_module = lib_mod, + exe_mod.linkSystemLibrary("libpipewire-0.3", .{ + .needed = true, }); - // This declares intent for the library to be installed into the standard - // location when the user invokes the "install" step (the default step when - // running `zig build`). - b.installArtifact(lib); - // This creates another `std.Build.Step.Compile`, but this one builds an executable // rather than a static library. const exe = b.addExecutable(.{ - .name = "zig", + .name = "recorder", .root_module = exe_mod, }); @@ -93,14 +66,6 @@ pub fn build(b: *std.Build) void { const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); - // Creates a step for unit testing. This only builds the test executable - // but does not run it. - const lib_unit_tests = b.addTest(.{ - .root_module = lib_mod, - }); - - const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); - const exe_unit_tests = b.addTest(.{ .root_module = exe_mod, }); @@ -111,6 +76,5 @@ pub fn build(b: *std.Build) void { // the `zig build --help` menu, providing a way for the user to request // running the unit tests. const test_step = b.step("test", "Run unit tests"); - test_step.dependOn(&run_lib_unit_tests.step); test_step.dependOn(&run_exe_unit_tests.step); } diff --git a/src/main.zig b/src/main.zig index 0ac2719..9dc1aa4 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,46 +1,180 @@ -//! By convention, main.zig is where your main function lives in the case that -//! you are building an executable. If you are making a library, the convention -//! is to delete this file and start with root.zig instead. +const data_struct = struct { + loop: ?*pipewire.pw_main_loop, + stream: ?*pipewire.pw_stream, + format: pipewire.spa_audio_info, + move: u1, +}; + +/// our data processing function is in general: +/// +/// struct pw_buffer *b; +/// b = pw_stream_dequeue_buffer(stream); +/// +/// .. consume stuff in the buffer ... +/// +/// pw_stream_queue_buffer(stream, b); +export fn on_process(userdata: ?*anyopaque) void { + const data: *data_struct = @ptrCast(@alignCast(userdata)); + const b_optional: ?*pipewire.pw_buffer = pipewire.pw_stream_dequeue_buffer(data.stream); + var buf: *pipewire.spa_buffer = undefined; + + var samples_optional: ?[*]f32 = undefined; + var max: f32 = undefined; + + var c: u32 = 0; + var n: u32 = undefined; + var n_channels: u32 = undefined; + var n_samples: u32 = undefined; + var peak: u32 = undefined; + + if (b_optional) |b| { + buf = b.buffer; + samples_optional = @ptrCast(@alignCast(buf.datas[0].data)); + if (samples_optional) |samples| { + n_channels = data.format.info.raw.channels; + n_samples = buf.datas[0].chunk.*.size / @sizeOf(f32); + + if (data.move != @as(u1, 0)) + _ = pipewire.fprintf(pipewire.stdout, "%c[%dA", @as(u8, 0x1b), n_channels + 1); + + _ = pipewire.fprintf(pipewire.stdout, "captured %d samples\n", n_samples / n_channels); + while (c < data.format.info.raw.channels) : (c += 0) { + max = 0.0; + n = c; + while (n < n_samples) : (n += n_channels) + max = @max(max, @abs(samples[n])); + + peak = @bitCast(@min(@max(max * 30.0, 0.0), 39.0)); + + _ = pipewire.fprintf( + pipewire.stdout, + "channel %d: |%*s%*s| peak:%f\n", + c, + peak + 1, + "*", + 40 - peak, + "", + max, + ); + } + data.move = @intFromBool(true); + _ = pipewire.fflush(pipewire.stdout); + + _ = pipewire.pw_stream_queue_buffer(data.stream, b); + } + } else { + pw_log_warn("out of buffers: %m"); + } +} + +/// Be notified when the stream param changes. We're only looking at the format changes. +export fn on_stream_param_changed(_data: ?*anyopaque, id: u32, param_optional: ?*const pipewire.spa_pod) void { + const data: *data_struct = @ptrCast(@alignCast(_data)); + + // NULL means to clear the format + if (param_optional) |param| { + if (id != pipewire.SPA_PARAM_Format) { + if (pipewire.spa_format_parse(param, &data.format.media_type, &data.format.media_subtype) >= 0) { + if (data.format.media_type == pipewire.SPA_MEDIA_TYPE_audio and data.format.media_subtype == pipewire.SPA_MEDIA_SUBTYPE_raw) { + _ = pipewire.spa_format_audio_raw_parse(param, &data.format.info.raw); + _ = pipewire.fprintf(pipewire.stdout, "capturing rate:%d channels:%d\n", data.format.info.raw.rate, data.format.info.raw.channels); + } + } + } + } +} + +const stream_events = pipewire.pw_stream_events{ + .version = pipewire.PW_VERSION_STREAM_EVENTS, + .param_changed = on_stream_param_changed, + .process = on_process, +}; + +export fn do_quit(userdata: ?*anyopaque, signalnumber: c_int) void { + const data: *data_struct = @ptrCast(@alignCast(userdata)); + _ = pipewire.pw_main_loop_quit(data.loop); + _ = signalnumber; +} pub fn main() !void { - // Prints to stderr (it's a shortcut based on `std.io.getStdErr()`) - std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); - - // stdout is for the actual output of your application, for example if you - // are implementing gzip, then only the compressed bytes should be sent to - // stdout, not any debugging messages. - const stdout_file = std.io.getStdOut().writer(); - var bw = std.io.bufferedWriter(stdout_file); - const stdout = bw.writer(); - - try stdout.print("Run `zig build test` to run the tests.\n", .{}); - - try bw.flush(); // Don't forget to flush! -} - -test "simple test" { - var list = std.ArrayList(i32).init(std.testing.allocator); - defer list.deinit(); // Try commenting this out and see if zig detects the memory leak! - try list.append(42); - try std.testing.expectEqual(@as(i32, 42), list.pop()); -} - -test "use other module" { - try std.testing.expectEqual(@as(i32, 150), lib.add(100, 50)); -} - -test "fuzz example" { - const Context = struct { - fn testOne(context: @This(), input: []const u8) anyerror!void { - _ = context; - // Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case! - try std.testing.expect(!std.mem.eql(u8, "canyoufindme", input)); - } + var data: data_struct = undefined; + var params: [1][*c]const pipewire.spa_pod = undefined; + var buffer: [1024]u8 = undefined; + var props: *pipewire.pw_properties = undefined; + var b: pipewire.spa_pod_builder = pipewire.spa_pod_builder{ + .data = &buffer, + .size = @sizeOf([1024]u8), + ._padding = 0, + .state = .{ + .flags = 0, + .offset = 0, + .frame = null, + }, + .callbacks = .{ + .data = null, + .funcs = null, + }, }; - try std.testing.fuzz(Context{}, Context.testOne, .{}); + + const argv = std.os.argv; + var c_ptr: [*c][*c]u8 = @ptrCast(argv.ptr); + var argc: c_int = @intCast(std.os.argv.len); + + pipewire.pw_init(&argc, &c_ptr); + + std.log.info( + \\ Compiled with libpipewire {s} + \\ Linked with libpipewire {s}, + , .{ + pipewire.pw_get_headers_version(), + pipewire.pw_get_library_version(), + }); + + data.loop = pipewire.pw_main_loop_new(null); + + _ = pipewire.pw_loop_add_signal(pipewire.pw_main_loop_get_loop(data.loop), pipewire.SIGINT, do_quit, &data); + _ = pipewire.pw_loop_add_signal(pipewire.pw_main_loop_get_loop(data.loop), pipewire.SIGTERM, do_quit, &data); + + props = pipewire.pw_properties_new( + pipewire.PW_KEY_MEDIA_TYPE, + "Audio", + pipewire.PW_KEY_MEDIA_CATEGORY, + "Capture", + pipewire.PW_KEY_MEDIA_ROLE, + "Music", + @as(?*anyopaque, @ptrFromInt(0)), + ); + + if (argc > 1) _ = pipewire.pw_properties_set(props, pipewire.PW_KEY_TARGET_OBJECT, argv[1]); + + data.stream = pipewire.pw_stream_new_simple( + pipewire.pw_main_loop_get_loop(data.loop), + "audio-capture", + props, + &stream_events, + &data, + ); + + params[0] = pipewire.spa_format_audio_raw_build( + &b, + pipewire.SPA_PARAM_EnumFormat, + &pipewire.spa_audio_info_raw{ .format = pipewire.SPA_AUDIO_FORMAT_F32 }, + ); + + _ = pipewire.pw_stream_connect(data.stream, pipewire.PW_DIRECTION_INPUT, pipewire.PW_ID_ANY, pipewire.PW_STREAM_FLAG_AUTOCONNECT | pipewire.PW_STREAM_FLAG_MAP_BUFFERS | pipewire.PW_STREAM_FLAG_RT_PROCESS, params[0..].ptr, @as(u32, 1)); + + _ = pipewire.pw_main_loop_run(data.loop); + + pipewire.pw_stream_destroy(data.stream); + pipewire.pw_main_loop_destroy(data.loop); + pipewire.pw_deinit(); } +extern "c" fn pw_log_warn(...) void; + const std = @import("std"); -/// This imports the separate module containing `root.zig`. Take a look in `build.zig` for details. -const lib = @import("zig_lib"); +const pipewire = @cImport({ + @cInclude("pipewire/pipewire.h"); + @cInclude("spa/param/audio/format-utils.h"); +});