Today I Learned...

Today I Learned...

About Zig structs and using @ptrCast for getting Zig data in C callbacks

In the previous post, I went about my experiments with Zig and the FluidSynth Sequencer API.

I wasn’t fully happy with my Zig code, because I was resorting to global variables and didn’t quite know how to organize it.

So I went on and learned how to make abstractions in Zig, and it is actually quite simple: just use a struct! Much like a C typedef struct, that allows for state and behavior encapsulation – almost like a class, except no inheritance. But inheritance kinda stinks anyway, so… we’re good! :D

I started to refactor my code into a struct I called Metronome much like in the Python version from the previous post, adding functions that receive the reference to the struct as first argument, “cool, this is a lot like Python actually”.

Then I hit a problem: how will I get a pointer to self inside my callback function that needs to be called from C, which I cannot change its signature?

At first, I left one global variable that pointed to the struct, which I used in the callback. Then, after asking for help on the internet, someone gave me the solution: use the data parameter of the callback function to pass along the reference, do some pointer casting et voilà !

That allowed me to put the callback inside the Metronome and get rid of the final global variable!

The solution looks obvious now, but I was too deep in my mess to see it, plus I didn’t know about pointer casting.

So yeah, that’s how I learned about using structs in Zig, and @ptrCast to convert pointers from different types, and also to combine it with @alignCast to change the pointer alignment.

I am now quite pleased with the code, here it is:

// this is a Zig port of the C example at:
// https://www.fluidsynth.org/api/fluidsynth_metronome_8c-example.html
const std = @import("std");
const fluid = @cImport(@cInclude("fluidsynth.h"));

const Metronome = struct {
    // metronome parameters
    tempo: u32 = 120,
    pattern_size: u32 = 4,

    // sequencer instance
    seq: ?*fluid.fluid_sequencer_t = null,

    // will mark the begin time of a measure, updated every time a new measure is scheduled
    time_marker: c_uint = 0,

    // sequencer destination ports
    synth_port: fluid.fluid_seq_id_t = 0,
    client_callback_port: fluid.fluid_seq_id_t = 0,

    pub fn init(
        self: *Metronome,
        seq: ?*fluid.fluid_sequencer_t,
        synth: ?*fluid.fluid_synth_t,
    ) void {
        self.seq = seq;

        // we'll send note events to the synth port
        self.synth_port = fluid.fluid_sequencer_register_fluidsynth(seq, synth);

        self.client_callback_port = fluid.fluid_sequencer_register_client(
            seq,
            "zig-fluid-metronome",
            sequencer_callback,
            @ptrCast(self),
        );

        // get the current sequencer time
        self.time_marker = fluid.fluid_sequencer_get_tick(seq);
    }

    // this callback will be called every time a sequencer event is received
    fn sequencer_callback(
        time: c_uint,
        event: ?*fluid.fluid_event_t,
        seq: ?*fluid.fluid_sequencer_t,
        data: ?*anyopaque,
    ) callconv(.C) void {
        _ = time;
        _ = event;
        _ = seq;
        const self: *Metronome = @ptrCast(@alignCast(data.?));
        self.schedule_timer_event();
        self.schedule_metronome_pattern();
    }

    fn schedule_timer_event(self: *Metronome) void {
        const event = fluid.new_fluid_event();
        defer fluid.delete_fluid_event(event);

        fluid.fluid_event_set_source(event, -1);
        fluid.fluid_event_set_dest(event, self.client_callback_port);
        fluid.fluid_event_timer(event, null);
        _ = fluid.fluid_sequencer_send_at(self.seq, event, self.time_marker, 1);
    }

    fn schedule_note_on(
        self: *Metronome,
        midi_chan: i16,
        time: c_uint,
        note: i16,
        velocity: i16,
    ) void {
        const event = fluid.new_fluid_event();
        defer fluid.delete_fluid_event(event);

        fluid.fluid_event_set_source(event, -1);
        fluid.fluid_event_set_dest(event, self.synth_port);
        fluid.fluid_event_noteon(event, midi_chan, note, velocity);
        _ = fluid.fluid_sequencer_send_at(self.seq, event, time, 1);
    }

    fn schedule_metronome_pattern(self: *Metronome) void {
        const beat_duration_ms: u32 = 60000 / self.tempo;

        const midi_chan = 9; // channel 10 is the drum channel

        const strong_note = 76; // woodblock high
        const weak_note = 77; // woodblock low

        var i: u32 = 0;
        while (i < self.pattern_size) : (i += 1) {
            const note_to_play: i16 = if (i == 0) strong_note else weak_note;
            const velocity: i16 = if (i == 0) 127 else 90;
            const play_at: c_uint = self.time_marker + (i * beat_duration_ms);
            self.schedule_note_on(midi_chan, play_at, note_to_play, velocity);
        }
        self.time_marker += beat_duration_ms * self.pattern_size;
    }

    pub fn start(self: *Metronome) void {
        self.schedule_metronome_pattern();
        self.schedule_timer_event();
        self.schedule_metronome_pattern();
    }
};

pub fn main() void {
    const settings = fluid.new_fluid_settings();
    defer fluid.delete_fluid_settings(settings);
    const synth = fluid.new_fluid_synth(settings);
    defer fluid.delete_fluid_synth(synth);

    // here we load the soundfont file
    _ = fluid.fluid_synth_sfload(synth, "soundfonts/GeneralUser_GS_v1.471.sf2", 1);

    // set up the audio driver to play sounds from the synth
    const audio_driver = fluid.new_fluid_audio_driver(settings, synth);
    defer fluid.delete_fluid_audio_driver(audio_driver);

    // initialize the sequencer
    const seq = fluid.new_fluid_sequencer2(0);
    defer fluid.delete_fluid_sequencer(seq);

    var metro = Metronome{ .tempo = 120, .pattern_size = 4 };
    metro.init(seq, synth);
    metro.start();

    // run the sequencer for 1 minute
    std.time.sleep(std.time.ns_per_min * 1);
    std.debug.print("Practice time is over, stopping now\n", .{});
}