Pulsar: Zig-only PulseAudio Library

Link to the repository.

Pulsar

A pulsar is a highly magnetized rotating neutron star… [they] are very dense and have short, regular rotational periods. This produces… pulses that range from milliseconds to seconds… exceeding the accuracy of [certain] atomic clocks in keeping time. — Wikipedia page on pulsars

Pulsar is a small library for audio playback on linux, with no dependency on libc or other shared libraries. Pulsar does depend on the presence of a PulseAudio server on the target system. This can be PulseAudio itself or something like pipewire-pulse. Pulsar’s target audience is game developers, so it exposes only the subset of PulseAudio meant for realtime audio playback.

Example Code

This code is also available in examples/sine.zig, it can be run using zig build run -Dexample=sine.

const std = @import("std");
const pulsar = @import("pulsar");

var sample_offset: usize = 0;

fn audio_callback(info: pulsar.Stream.Info, samples: []i16) usize {
    const sample_rate: f32 = @floatFromInt(info.sample_rate);
    for (0..samples.len) |i| {
        const amp = @cos(880 * @as(f32, @floatFromInt(sample_offset + i)) / sample_rate);
        samples[i] = @intFromFloat(amp * 32000.0);
    }
    sample_offset += samples.len;
    return samples.len;
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    var ctx = try pulsar.Context.create(allocator, .{});
    defer ctx.destroy(allocator);

    var stream = try pulsar.Stream.create(ctx, &audio_callback, .{});
    ctx.addStream(&stream);

    sample_offset = 0;

    const start_time = std.time.timestamp();
    while (std.time.timestamp() - start_time < 10) {
        try ctx.run();
    }
}

What

Pulsar is inspired mostly by Shimizu - The Wayland Protocol, in Zig. I plan to use it as a component in Seizer: Wayland application framework. The basic idea is to allow for audio applications on Linux with no shared library dependencies - only system calls. PulseAudio (or pipewire exposing a PulseAudio API) is still a run-time dependency with this approach, but by avoiding shared libraries Pulsar-powered applications won’t break when glibc or other libraries make a breaking change.

19 Likes

Very cool!

Here is the libpulse API surface area in my music player project. Do you think Pulsar could be a good fit?

2 Likes

In the future I think it will be. Pulsar currently doesn’t have most features implemented, so it would be too soon to try and port it though :sweat_smile: My own use case is games which can use a little less of the API, but I’d be happy to add the features necessary for GrooveBasin to make use of it too. I’ve made a tracking issue for it.

2 Likes

Would be pretty sweet to combine this, seizer, and dvui :thinking:

I was thinking of using this and create a GeigerAllocator. Could be a fun WE project.

2 Likes

I’ve made a new release of Pulsar!

This release greatly improves Pulsar’s API from version 0.0.0. It removes the need to use an allocator, gives the user greater control over the event loop, and handles the PulseAudio protocol more correctly in general. A few more examples have been added to make Pulsar easier to figure out.

Read the changelog for more details.

3 Likes

I’ve made a patch release, Pulsar 0.1.1. The only change is that I’ve marked the dependencies in build.zig.zon as lazy. They is a non-breaking change, and the only effect is to prevent extra downloads when using Pulsar as a dependency. The dependencies are only there for some of the examples to use, as otherwise Pulsar depends only on the zig standard library for compilation.

3 Likes