const std = @import("std"); const plugin = @import("plugin.zig"); const custom = @import("custom.zig"); pub const ParseError = error{ParseFail}; pub const CommandType = enum { /// "LV2 Commands" are commands that receive split, index, and then receive /// any f64 arguments. lv2_command, custom_command, }; fn LV2Command( comptime tag: Command.Tag, comptime plugin_url: []const u8, comptime LV2Parameters: type, ) type { return struct { pub const base_tag = tag; pub const command_type = CommandType.lv2_command; pub const lv2_url = plugin_url; base: Command, split: usize, index: usize, parameters: LV2Parameters, }; } fn CustomCommand( comptime tag: Command.Tag, comptime Plugin: type, comptime PluginParameters: type, ) type { return struct { pub const base_tag = tag; pub const command_type = CommandType.custom_command; pub const plugin_type = Plugin; base: Command, split: usize, index: usize, parameters: PluginParameters, }; } pub const Command = struct { tag: Tag, pub const Tag = enum { noop, load, quicksave, runqs, amp, rflanger, eq, phaser, mbeq, chorus, pitchscaler, reverb, highpass, delay, vinyl, revdelay, gate, detune, overdrive, degrade, repsycho, talkbox, dyncomp, thruzero, foverdrive, gverb, invert, tapedelay, moddelay, multichorus, saturator, vintagedelay, noise, wildnoise, write, embed, rotate, }; pub fn tagToType(tag: Tag) type { return switch (tag) { .noop => Noop, .load => Load, .quicksave => Quicksave, .runqs => RunQS, .amp => Amp, .rflanger => RFlanger, .eq => Eq, .phaser => Phaser, .mbeq => Mbeq, .chorus => Chorus, .pitchscaler => Pitchscaler, .reverb => Reverb, .highpass => Highpass, .delay => Delay, .vinyl => Vinyl, .revdelay => Revdelay, .gate => Gate, .detune => Detune, .overdrive => Overdrive, .degrade => Degrade, .repsycho => Repsycho, .talkbox => Talkbox, .dyncomp => Dyncomp, .thruzero => Thruzero, .foverdrive => Foverdrive, .gverb => Gverb, .invert => Invert, .tapedelay => Tapedelay, .moddelay => Moddelay, .multichorus => Multichorus, .saturator => Saturator, .vintagedelay => Vintagedelay, .noise => Noise, .wildnoise => Wildnoise, .write => Write, .embed => Embed, .rotate => Rotate, }; } pub fn cast(base: *const @This(), comptime T: type) ?*const T { if (base.tag != T.base_tag) return null; return @fieldParentPtr(T, "base", base); } pub fn print(base: *const @This()) void { std.debug.warn("tag: {s}\n", .{base.tag}); } pub const Noop = struct { pub const base_tag = Tag.noop; base: Command, }; pub const Load = struct { pub const base_tag = Tag.load; base: Command, path: []const u8, }; pub const Quicksave = struct { pub const base_tag = Tag.quicksave; base: Command, }; pub const RunQS = struct { pub const base_tag = Tag.runqs; base: Command, program: []const u8, }; pub const Noise = CustomCommand(Tag.noise, custom.RandomNoise, struct { seed: u64, fill_bytes: usize, }); pub const Wildnoise = CustomCommand(Tag.wildnoise, custom.WildNoise, struct { seed: u64, fill_bytes: usize, }); pub const Write = CustomCommand(Tag.write, custom.Write, struct { data: f32, }); pub const Embed = CustomCommand(Tag.write, custom.Embed, struct { path: []const u8, }); pub const Rotate = struct { pub const base_tag = Tag.rotate; base: Command, deg: f32, bgfill: []const u8, }; pub const Amp = LV2Command( .amp, "http://lv2plug.in/plugins/eg-amp", struct { gain: f32 }, ); pub const RFlanger = LV2Command( .rflanger, "http://plugin.org.uk/swh-plugins/retroFlange", struct { delay_depth_avg: f32, law_freq: f32 }, ); pub const Eq = LV2Command( .rflanger, "http://plugin.org.uk/swh-plugins/dj_eq_mono", struct { lo: f32, mid: f32, hi: f32 }, ); pub const Phaser = LV2Command( .phaser, "http://plugin.org.uk/swh-plugins/lfoPhaser", struct { lfo_rate: f32, lfo_depth: f32, fb: f32, spread: f32 }, ); pub const Mbeq = LV2Command( .mbeq, "http://plugin.org.uk/swh-plugins/mbeq", struct { band_1: f32, band_2: f32, band_3: f32, band_4: f32, band_5: f32, band_6: f32, band_7: f32, band_8: f32, band_9: f32, band_10: f32, band_11: f32, band_12: f32, band_13: f32, band_14: f32, band_15: f32, }, ); pub const Chorus = LV2Command( .chorus, "http://plugin.org.uk/swh-plugins/multivoiceChorus", struct { voices: f32, delay_base: f32, voice_spread: f32, detune: f32, law_freq: f32, attendb: f32, }, ); pub const Pitchscaler = LV2Command( .pitchscaler, "http://plugin.org.uk/swh-plugins/pitchScaleHQ", struct { mult: f32 }, ); pub const Reverb = LV2Command( .reverb, "http://invadarecords.com/plugins/lv2/erreverb/mono", struct { roomLength: f32, roomWidth: f32, roomHeight: f32, sourceLR: f32, sourceFB: f32, listLR: f32, listFB: f32, hpf: f32, warmth: f32, diffusion: f32, }, ); pub const Highpass = LV2Command(.highpass, "http://invadarecords.com/plugins/lv2/filter/hpf/mono", struct { freq: f32, gain: f32, noClip: f32, }); pub const Delay = LV2Command(.delay, "http://plugin.org.uk/swh-plugins/delayorama", struct { seed: f32, gain: f32, feedback_pc: f32, tap_count: f32, first_delay: f32, delay_range: f32, delay_scale: f32, delay_rand_pc: f32, gain_scale: f32, wet: f32, }); pub const Vinyl = LV2Command(.vinyl, "http://plugin.org.uk/swh-plugins/vynil", struct { year: f32, rpm: f32, warp: f32, click: f32, wear: f32, }); pub const Revdelay = LV2Command(.revdelay, "http://plugin.org.uk/swh-plugins/revdelay", struct { delay_time: f32, dry_level: f32, wet_level: f32, feedback: f32, xfade_samp: f32, }); // pub const Noise= LV2Command(.,,struct{}); pub const Gate = LV2Command(.gate, "http://hippie.lt/lv2/gate", struct { @"switch": f32, threshold: f32, attack: f32, hold: f32, decay: f32, gaterange: f32, }); pub const Detune = LV2Command(.detune, "http://drobilla.net/plugins/mda/Detune", struct { detune: f32, mix: f32, output: f32, latency: f32, }); pub const Overdrive = LV2Command(.overdrive, "http://drobilla.net/plugins/mda/Overdrive", struct { drive: f32, muffle: f32, output: f32, }); pub const Degrade = LV2Command(.degrade, "http://drobilla.net/plugins/mda/Degrade", struct { headroom: f32, quant: f32, rate: f32, post_filt: f32, non_lin: f32, output: f32, }); pub const Repsycho = LV2Command(.repsycho, "http://drobilla.net/plugins/mda/RePsycho", struct { tune: f32, fine: f32, decay: f32, thresh: f32, hold: f32, mix: f32, quality: f32, }); pub const Talkbox = LV2Command(.talkbox, "http://drobilla.net/plugins/mda/TalkBox", struct { wet: f32, dry: f32, carrier: f32, quality: f32, }); pub const Dyncomp = LV2Command(.dyncomp, "http://gareus.org/oss/lv2/darc#mono", struct { enable: f32, hold: f32, inputgain: f32, threshold: f32, Ratio: f32, attack: f32, release: f32, gain_min: f32, gain_max: f32, rms: f32, }); pub const Foverdrive = LV2Command(.foverdrive, "http://plugin.org.uk/swh-plugins/foverdrive", struct { drive: f32, }); pub const Thruzero = LV2Command(.thruzero, "http://drobilla.net/plugins/mda/ThruZero", struct { rate: f32, mix: f32, feedback: f32, depth_mod: f32 }); pub const Gverb = LV2Command(.gverb, "http://plugin.org.uk/swh-plugins/gverb", struct { roomsize: f32, revtime: f32, damping: f32, inputbandwidth: f32, drylevel: f32, earlylevel: f32, taillevel: f32, }); pub const Invert = LV2Command(.invert, "http://plugin.org.uk/swh-plugins/inv", struct {}); pub const Tapedelay = LV2Command(.tapedelay, "http://plugin.org.uk/swh-plugins/tapeDelay", struct { speed: f32, da_db: f32, t1d: f32, t1a_db: f32, t2d: f32, t2a_db: f32, t3d: f32, t3a_db: f32, t4d: f32, t4a_db: f32, }); pub const Moddelay = LV2Command( .moddelay, "http://plugin.org.uk/swh-plugins/modDelay", struct { base: f32, }, ); pub const Multichorus = LV2Command(.multichorus, "http://calf.sourceforge.net/plugins/MultiChorus", struct { min_delay: f32, mod_depth: f32, mod_rate: f32, stereo: f32, voices: f32, vphase: f32, amount: f32, dry: f32, freq: f32, freq2: f32, q: f32, overlap: f32, level_in: f32, level_out: f32, lfo: f32, }); pub const Saturator = LV2Command(.saturator, "http://calf.sourceforge.net/plugins/Saturator", struct { bypass: f32, level_in: f32, level_out: f32, mix: f32, drive: f32, blend: f32, lp_pre_freq: f32, hp_pre_freq: f32, lp_post_freq: f32, hp_post_freq: f32, p_freq: f32, p_level: f32, p_q: f32, pre: f32, post: f32, }); pub const Vintagedelay = LV2Command(.vintagedelay, "http://calf.sourceforge.net/plugins/VintageDelay", struct { level_in: f32, level_out: f32, subdiv: f32, time_l: f32, time_r: f32, feedback: f32, amount: f32, mix_mode: f32, medium: f32, dry: f32, width: f32, fragmentation: f32, pbeats: f32, pfrag: f32, timing: f32, bpm: f32, ms: f32, hz: f32, bpm_host: f32, }); }; const CmdArrayList = std.ArrayList(*Command); pub const CommandList = struct { list: CmdArrayList, const Self = @This(); pub fn init(allocator: *std.mem.Allocator) Self { return .{ .list = CmdArrayList.init(allocator), }; } pub fn deinit(self: *Self) void { for (self.list.items) |cmd_ptr| { self.list.allocator.destroy(cmd_ptr); } self.list.deinit(); } pub fn append(self: *Self, cmd: *Command) !void { return try self.list.append(cmd); } }; /// A parser. pub const Lang = struct { allocator: *std.mem.Allocator, has_error: bool = false, line: usize = 0, pub fn init(allocator: *std.mem.Allocator) Lang { return Lang{ .allocator = allocator, }; } pub fn deinit(self: *Lang) void {} pub fn reset(self: *Lang) void { self.has_error = false; self.line = 0; } fn doError(self: *Lang, comptime fmt: []const u8, args: anytype) void { std.debug.warn("ERROR! at line {}: ", .{self.line}); std.debug.warn(fmt, args); std.debug.warn("\n", .{}); self.has_error = true; } fn parseCommandArguments( self: *@This(), comptime command_struct: type, tok_it: *std.mem.TokenIterator, commands: *CommandList, ) !void { // Based on the command struct fields, we can parse the arguments. var cmd = try self.allocator.create(command_struct); // we already add the command to the list to prevent memory leaks // by commands that error out try commands.append(&cmd.base); const is_lv2_command = switch (command_struct.base_tag) { .noop, .load, .quicksave, .runqs, .rotate => false, else => true, }; if (is_lv2_command) { const split = tok_it.next(); if (split == null) { self.doError("Expected split parameter, got EOL", .{}); return; } const index = tok_it.next(); if (index == null) { self.doError("Expected index parameter, got EOL", .{}); return; } cmd.split = try std.fmt.parseInt(usize, split.?, 10); cmd.index = try std.fmt.parseInt(usize, index.?, 10); inline for (@typeInfo(@TypeOf(cmd.parameters)).Struct.fields) |cmd_field| { const maybe_arg = tok_it.next(); if (maybe_arg == null) { self.doError("Expected parameter for {s}, got nothing", .{cmd_field.name}); return; } const arg = maybe_arg.?; const arg_value = switch (cmd_field.field_type) { f32 => try std.fmt.parseFloat(f32, arg), u64 => try std.fmt.parseInt(u64, arg, 10), usize => try std.fmt.parseInt(usize, arg, 10), []const u8 => arg, else => @compileError("parameter struct has unsupported type " ++ @typeName(cmd_field.field_type)), }; @field(cmd.parameters, cmd_field.name) = arg_value; } } else { inline for (@typeInfo(command_struct).Struct.fields) |cmd_field| { comptime { if (std.mem.eql(u8, cmd_field.name, "base")) { continue; } } const arg_opt = tok_it.next(); if (arg_opt == null) { self.doError("Expected parameter for {s}, got nothing", .{cmd_field.name}); return; } const arg = arg_opt.?; const argument_value = switch (cmd_field.field_type) { usize => try std.fmt.parseInt(usize, arg, 10), i32 => try std.fmt.parseInt(i32, arg, 10), f32 => try std.fmt.parseFloat(f32, arg), []const u8 => arg, else => @compileError("Invalid parameter type (" ++ @typeName(cmd_field.field_type) ++ ") left on command struct " ++ @typeName(command_struct) ++ "."), }; std.debug.warn("parsing {s}, arg of type {s} => {any}\n", .{ @typeName(command_struct), @typeName(@TypeOf(argument_value)), argument_value, }); @field(cmd, cmd_field.name) = argument_value; } } cmd.base.tag = command_struct.base_tag; const command = cmd.base.cast(command_struct).?; std.debug.warn("cmd: {s}\n", .{command}); } pub fn parse(self: *Lang, data: []const u8) !CommandList { var splitted_it = std.mem.split(data, ";"); var cmds = CommandList.init(self.allocator); errdefer cmds.deinit(); while (splitted_it.next()) |stmt_orig| { self.line += 1; var stmt = std.mem.trimRight(u8, stmt_orig, "\n"); stmt = std.mem.trimLeft(u8, stmt, "\n"); if (stmt.len == 0) continue; if (std.mem.startsWith(u8, stmt, "#")) continue; // TODO better tokenizer instead of just tokenize(" ")...maybe???? var tok_it = std.mem.tokenize(stmt, " "); var cmd_opt = tok_it.next(); if (cmd_opt == null) { self.doError("No command given", .{}); continue; } const command_string = cmd_opt.?; var found: bool = false; inline for (@typeInfo(Command).Struct.decls) |cmd_struct_decl| { switch (cmd_struct_decl.data) { .Type => |typ| switch (@typeInfo(typ)) { .Struct => {}, else => continue, }, else => continue, } const struct_name = cmd_struct_decl.name; comptime var lowered_command_name = [_]u8{0} ** struct_name.len; comptime { for (struct_name) |c, i| { lowered_command_name[i] = std.ascii.toLower(c); } } // if we have a match, we know the proper struct type // to use. this actually works compared to storing command_struct // in a variable because then that variable must be comptime. // the drawback of this approach is that our emitted code is basically linear // because we don't use the hashmap anymore. // // Attempting to use ComptimeHashMap hits compiler bugs and I'm // not sure if we can map strings to *types* in it. if (std.mem.eql(u8, &lowered_command_name, command_string)) { found = true; const cmd_struct_type = cmd_struct_decl.data.Type; try self.parseCommandArguments(cmd_struct_type, &tok_it, &cmds); } } if (!found) { self.doError("Unknown command '{s}' ({d} bytes)", .{ command_string, command_string.len }); continue; } } if (self.has_error) return ParseError.ParseFail; return cmds; } }; test "noop" { var lang = Lang.init(std.testing.allocator); defer lang.deinit(); var cmds = try lang.parse("noop;"); defer cmds.deinit(); std.testing.expectEqual(cmds.list.items.len, 1); std.testing.expectEqual(cmds.list.items[0].tag, .noop); } test "load, phaser, quicksave" { var lang = Lang.init(std.testing.allocator); defer lang.deinit(); const prog = \\load :0; \\phaser 3 1 25 0.25 0 1; \\quicksave; ; var cmds = try lang.parse(prog); defer cmds.deinit(); std.testing.expectEqual(cmds.list.items.len, 3); std.testing.expectEqual(cmds.list.items[0].tag, .load); std.testing.expectEqual(cmds.list.items[1].tag, .phaser); std.testing.expectEqual(cmds.list.items[2].tag, .quicksave); } test "load, phaser with errors, quicksave" { var lang = Lang.init(std.testing.allocator); defer lang.deinit(); const prog = \\load :0; \\phaser 3 1 25; \\quicksave; ; var cmds = lang.parse(prog) catch |err| { return; }; unreachable; }