const std = @import("std"); const clap = @import("./clap.zig"); const shared = @import("./shared.zig"); const GmSynth = @import("./gmsynth/plugin.zig"); const Logger = GmSynth.Logger; const desc = clap.clap_plugin_descriptor_t{ .clap_version = shared.build_clap_version, .id = "io.jengamon.gm-synth", .name = "GM Synth", .vendor = "Jengamon / µü", .url = "https://jengamon.neocities.org/gm-synth", .manual_url = null, .support_url = null, .version = "0.1.0-alpha", .description = "A General MIDI synth player", .features = &[_:null]?[*:0]const u8{ clap.CLAP_PLUGIN_FEATURE_INSTRUMENT, null, }, }; pub const entry = .{ .desc = &desc, .create = create, }; // Used for Zig-side code. Plugin code should retrieve // self using get_self const PlugData = union(enum) { ready, uninited: *const clap.clap_host_t, inited: GmSynth, }; pub var plugin_state: PlugData = PlugData{ .ready = void{}, }; pub fn logFn( self: *GmSynth, comptime message_level: std.log.Level, comptime scope: @Type(.EnumLiteral), comptime format: []const u8, args: anytype, ) !void { const clap_level = switch (message_level) { .debug => clap.CLAP_LOG_DEBUG, .info => clap.CLAP_LOG_INFO, .warn => clap.CLAP_LOG_WARNING, .err => clap.CLAP_LOG_ERROR, }; const level_txt = comptime message_level.asText(); const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; var text = std.ArrayList(u8).init(shared.allocator); defer text.deinit(); const writer = text.writer(); std.fmt.format(writer, "plugin " ++ level_txt ++ prefix2 ++ format, args) catch return; const out = try text.toOwnedSliceSentinel(0); defer shared.allocator.free(out); if (self.host_data.host_log) |host_log| { host_log.log.?(self.host_data.host, clap_level, out); } else { return error.NoHostLogExtension; } } const clap_plugin = clap.clap_plugin_t{ .desc = &desc, .plugin_data = &plugin_state, .init = gmsynth_init, .destroy = gmsynth_destroy, .activate = gmsynth_activate, .deactivate = gmsynth_deactivate, .start_processing = gmsynth_start_processing, .stop_processing = gmsynth_stop_processing, .reset = gmsynth_reset, .process = gmsynth_process, .get_extension = gmsynth_get_extension, .on_main_thread = gmsynth_on_main_thread, }; fn create(host: *const clap.clap_host_t) !*const clap.clap_plugin_t { switch (plugin_state) { .ready, .uninited => { plugin_state = .{ .uninited = host }; }, .inited => |plug| { Logger.err("attempted to create inited plugin", .{}); return plug.plugin; }, } return &clap_plugin; } fn get_host_extension(comptime T: type, host: *const clap.clap_host, ext: [*:0]const u8) ?*const T { // we expect host to have get_extension be not-null return @ptrCast(?*const T, @alignCast(@alignOf(T), host.get_extension.?(host, ext))); } pub fn get_state(plugin: *const clap.clap_plugin) PlugData { return @ptrCast(*PlugData, @alignCast(@alignOf(PlugData), plugin.plugin_data)).*; } fn gmsynth_init(plugin: ?*const clap.clap_plugin) callconv(.C) bool { if (plugin) |cplug| { // var plug = get_self(cplug); switch (get_state(cplug)) { .ready, .inited => { return false; }, .uninited => |host| { // Fetch host extensions const host_data = GmSynth.HostData{ .host = host, .host_log = get_host_extension(clap.clap_host_log, host, &clap.CLAP_EXT_LOG), .host_thread_check = get_host_extension(clap.clap_host_thread_check, host, &clap.CLAP_EXT_THREAD_CHECK), .host_latency = get_host_extension(clap.clap_host_latency, host, &clap.CLAP_EXT_LATENCY), .host_state = get_host_extension(clap.clap_host_state, host, &clap.CLAP_EXT_STATE), .host_gui = get_host_extension(clap.clap_host_gui, host, &clap.CLAP_EXT_GUI), }; host_data.assert_main_thread() catch return false; plugin_state = .{ .inited = GmSynth.init(shared.allocator, host_data, cplug) catch return false, }; Logger.debug("Initialized GM Synth version {s}", .{desc.version}); return true; }, } } return false; } fn gmsynth_destroy(plugin: ?*const clap.clap_plugin) callconv(.C) void { if (plugin) |cplug| { var state = get_state(cplug); switch (state) { .ready, .uninited => {}, .inited => |*plug| { Logger.debug("Destroying GM Synth", .{}); plug.deinit(); // Right now, we force calling init again, but theoretically // we could move back to .uninited with the previous host plugin_state = .{ .ready = void{} }; }, } } } fn gmsynth_get_extension(plugin: ?*const clap.clap_plugin, maybe_id: ?[*:0]const u8) callconv(.C) ?*const anyopaque { if (maybe_id) |id| { if (plugin) |cplug| { var state = get_state(cplug); switch (state) { .uninited, .ready => { return null; }, .inited => |plug| { return plug.extension(std.mem.span(id)); }, } } } return null; } fn gmsynth_activate( plugin: ?*const clap.clap_plugin, sample_rate: f64, min_frames_count: u32, max_frames_count: u32, ) callconv(.C) bool { if (plugin) |cplug| { var state = get_state(cplug); switch (state) { .uninited, .ready => { return false; }, .inited => |*plug| { plug.host_data.assert_main_thread() catch return false; plug.activate(sample_rate, min_frames_count, max_frames_count) catch |err| { Logger.err("Failed to activate: {}", .{err}); return false; }; return true; }, } } return false; } fn gmsynth_deactivate(plugin: ?*const clap.clap_plugin) callconv(.C) void { if (plugin) |cplug| { var state = get_state(cplug); switch (state) { .uninited, .ready => {}, .inited => |*plug| { plug.host_data.assert_main_thread() catch return; plug.deactivate() catch |err| { Logger.err("Failed to deactivate: {}", .{err}); }; }, } } } fn gmsynth_start_processing(plugin: ?*const clap.clap_plugin) callconv(.C) bool { if (plugin) |cplug| { var state = get_state(cplug); switch (state) { .uninited, .ready => { return false; }, .inited => |*plug| { // TODO Maybe be more noisy? plug.host_data.assert_audio_thread() catch return false; return plug.start_processing(); }, } } return false; } fn gmsynth_stop_processing(plugin: ?*const clap.clap_plugin) callconv(.C) void { if (plugin) |cplug| { var state = get_state(cplug); switch (state) { .uninited, .ready => {}, .inited => |*plug| { // TODO Maybe be more noisy? plug.host_data.assert_audio_thread() catch return; plug.stop_processing(); }, } } } fn gmsynth_reset(plugin: ?*const clap.clap_plugin) callconv(.C) void { if (plugin) |cplug| { var state = get_state(cplug); switch (state) { .uninited, .ready => {}, .inited => |*plug| { // TODO Maybe be more noisy? plug.host_data.assert_audio_thread() catch return; plug.reset(); }, } } } fn gmsynth_process(plugin: ?*const clap.clap_plugin, maybe_process: ?*const clap.clap_process_t) callconv(.C) clap.clap_process_status { if (plugin) |cplug| { var state = get_state(cplug); switch (state) { .ready, .uninited => { // Misbehavior: calling process on non-inited return clap.CLAP_PROCESS_ERROR; }, .inited => |*plug| { if (maybe_process) |process| { const res = plug.plugin_process(process) catch |err| { Logger.err("Failed to process: {}", .{err}); return clap.CLAP_PROCESS_ERROR; }; return switch (res) { .Continue => clap.CLAP_PROCESS_CONTINUE, .ContinueIfNotQuiet => clap.CLAP_PROCESS_CONTINUE_IF_NOT_QUIET, .Tail => clap.CLAP_PROCESS_TAIL, .Sleep => clap.CLAP_PROCESS_SLEEP, }; } }, } } return clap.CLAP_PROCESS_ERROR; } fn gmsynth_on_main_thread(plugin: ?*const clap.clap_plugin) callconv(.C) void { if (plugin) |cplug| { switch (get_state(cplug)) { .ready, .uninited => {}, .inited => |*plug| { plug.host_data.assert_main_thread() catch return; plug.on_main_thread(); }, } } }