Pulsar is a no-dependency client library for PulseAudio, written in Zig.
Find a file
2025-03-13 13:36:03 -06:00
examples feat(examples): add example using chibi-xmplay 2025-03-09 15:27:36 -06:00
src feat: add support for seeking 2025-03-03 23:53:47 -07:00
.gitignore feat: successfully connect 2025-02-24 02:01:53 -07:00
build.zig release: Pulsar version 0.1.0 2025-03-09 17:17:49 -06:00
build.zig.zon docs: Clarify that dependencies are optional 2025-03-10 01:34:48 -06:00
CHANGELOG.md docs: add unreleased section to changelog, add release dates 2025-03-13 13:36:03 -06:00
LICENSE doc: add MIT license 2025-02-24 15:46:32 -07:00
README.md release: Pulsar version 0.1.0 2025-03-09 17:17:49 -06:00

Pulsar

NOTE: This code is a work in progress! Using it means participating in its development.

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 example code connects to the PulseAudio server, creates a playback stream, and plays a 880Hz sine wave for 10 seconds. It is also available in examples/sine_f32.zig, and can be run using zig build run -Dexample=sine_f32.

var sample_offset: usize = 0;

fn audio_callback(stream: *pulsar.Stream, requested_bytes: usize, _: ?*anyopaque) []const u8 {
    const samples = stream.getSlice(.Float32Ne, requested_bytes);
    const sample_rate: f32 = @floatFromInt(stream.sample_spec.sample_rate);
    for (0..samples.len) |i| {
        const amp = @cos(880 * @as(f32, @floatFromInt(sample_offset + i)) / sample_rate);
        samples[i] = amp;
    }
    sample_offset += samples.len;
    return std.mem.sliceAsBytes(samples);
}

pub fn main() !void {
    var ctx = pulsar.Context{};
    try ctx.connect(.{});
    defer ctx.disconnect();

    var stream = pulsar.Stream{
        .callback_write = &audio_callback,
        .sample_spec = .{ .channels = 2 },
        .suspended = false,
    };

    try ctx.createPlaybackStream(&stream, .{});

    sample_offset = 0;

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

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

Additional examples:

examples/chibi-xmplay.zig
Demonstrates playing back an extended mod (xm) file using chibi-xmplay
examples/client_name.zig
Shows how to set client properties, like name and application id, to be displayed by the PulseAudio server.
examples/pocketmod.zig
Demonstrates playing back a mod file using pocketmod
examples/state_change.zig
Demonstrates listening for state changes with a callback.
examples/sine.zig
Like examples/sine_f32.zig, but samples are sent to PulseAudio as 16-bit signed integers.
examples/seek.zig
Shows to seek the audio stream by seeking forward to skip already sent data.
examples/xev.zig
Shows how to drive Pulsar using an external event loop, specifically libxev

Recommendations on usage

A minimal application can use Pulsar as shown in the above example. In larger applications Pulsar should be run in its own thread, preferably with (soft) realtime priority. Memory should be pre-allocated, or handled on a seperate thread before being passed to the Pulsar thread.

Pulsar's API is very low-level, you will likely want an audio mixer or dsp library to layer on top of it.

Planning

Defintely

  • [ ] Zero-copy streaming with shm_pool or memfd pipewire-pulse doesn't support shared memory
  • Recording streams
  • Configurable sample rate, format, etc. for streams
  • More examples
    • Using a synthesis library
    • Audio thread
    • Using libxev for asynchronous reads and writes (see examples/xev.zig)

Maybe

  • PulseAudio Sample Cache
  • Non-unix domain socket transport (TCP)
  • PulseAudio introspection
  • Reference counting