Today I Learned...

Today I Learned...

Using libsndfile in Zig

Continuing on my journey learning Zig and audio programming, and also making a good follow-up to the time I wrote Zig to manually generate a WAVE file, today I decided to generate a WAVE file using libsndfile.

If you don’t know what libsndfile is, it’s a library for reading and writing audio files. It supports many formats and is quite popular. Learning its basics is a good preparation step for me to write more ambitious programs.

Let’s just go straight to the code – I’ve added comments to the relevant bits:

const std = @import("std");
const c = @cImport({
    @cInclude("sndfile.h");
});
const print = std.debug.print;
const math = std.math;

pub fn main() !u8 {
    // Global params
    const volume = 0.3;
    const samplerate = 44100;
    const nchannels = 2;
    const duration = 3; // seconds
    const outfile = "out_sndfile_test.wav";

    // Here we build a sine table that we'll use to generate the audio samples later
    const TABLE_SIZE = 200;
    var sine_table: [TABLE_SIZE]f32 = undefined;
    const table_size_float: f32 = @floatFromInt(TABLE_SIZE);
    for (&sine_table, 0..) |*item, i| {
        item.* = math.sin((@as(f32, @floatFromInt(i)) / table_size_float) * 2.0 * math.pi);
    }

    // Here we build the SF_INFO struct, which configures the output WAV file
    var sf_info: c.SF_INFO = undefined;
    sf_info.samplerate = samplerate;
    sf_info.channels = nchannels;
    sf_info.format = c.SF_FORMAT_WAV | c.SF_FORMAT_PCM_32;

    const sf: ?*c.SNDFILE = c.sf_open(outfile, c.SFM_WRITE, &sf_info);
    if (sf == null) {
        var errstr: [256]u8 = undefined;
        _ = c.sf_error_str(sf, &errstr, errstr.len - 1);
        print("Cannot open sndfile {s} for writing: {s}\n", .{ "test.wav", errstr });
        return 1;
    }

    const nframes = samplerate * duration;

    const buffer_size = 1024;
    var buffer: [buffer_size]f32 = undefined;
    var left_phase: usize = 0;
    var right_phase: usize = 0;

    // Now, we'll proceed by generating the audio samples and writing them to the WAV file
    const needed_iterations = nframes / (buffer_size / nchannels);
    for (0..needed_iterations) |_| {
        // First, we fill the buffer with interleaved left and right channel samples...
        var buf_index: usize = 0;
        while (buf_index < buffer_size - 1) : (buf_index += 2) {
            left_phase += 1;
            if (left_phase >= TABLE_SIZE) {
                left_phase -= TABLE_SIZE;
            }
            // we use a different phase for the right channel, to have a different note:
            right_phase += 2;
            if (right_phase >= TABLE_SIZE) {
                right_phase -= TABLE_SIZE;
            }
            buffer[buf_index] = volume * sine_table[left_phase];
            buffer[buf_index + 1] = volume * sine_table[right_phase];
        }

        // Then, we write the buffer to the output file -- note that we divide
        // buffer_size by nchannels to get the correct number of frames
        const frames_to_write = buffer_size / nchannels;
        _ = c.sf_writef_float(sf, &buffer, frames_to_write);
    }
    _ = c.sf_close(sf);
    print("Wrote {d} frames to {s}\n", .{ nframes, outfile });

    return 0;
}