const std = @import("std"); const plugin = @import("plugin.zig"); pub const ParseError = error{ OutOfMemory, ArgRequired, ParseFail, }; pub const CommandType = 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, Noise, WildNoise, Write, Embed, Rotate, }; pub const Command = struct { command: CommandType, args: ArgList, cur_idx: usize = 0, pub fn print(self: Command) void { std.debug.warn("cmd:{}\n", .{self.command}); } pub fn argAt(self: Command, idx: usize) ![]const u8 { std.debug.warn("{} {}", .{ idx, self.args.len }); if (idx > (self.args.len - 1)) { std.debug.warn("Expected argument at index {}\n", .{idx}); return ParseError.ArgRequired; } const args = self.args.toSliceConst(); return args[idx]; } pub fn usizeArgAt(self: Command, idx: usize) !usize { var arg = try self.argAt(idx); return try std.fmt.parseInt(usize, arg, 10); } pub fn consumePosition(self: *Command) !plugin.Position { self.cur_idx = 2; return plugin.Position{ .split = try self.usizeArgAt(0), .index = try self.usizeArgAt(1), }; } pub fn intArgAt(self: Command, idx: usize) !i32 { var arg = try self.argAt(idx); return try std.fmt.parseInt(i32, arg, 10); } pub fn floatArgAt(self: Command, idx: usize) !f32 { var arg = try self.argAt(idx); return try std.fmt.parseFloat(f32, arg); } pub fn floatArgMany( self: Command, allocator: *std.mem.Allocator, start_index: usize, elements: usize, default: f32, ) ![]const f32 { var i: usize = start_index; var arr = std.ArrayList(f32).init(allocator); while (i < elements) : (i += 1) { var value: f32 = self.floatArgAt(i) catch |err| blk: { std.debug.warn("\tdoing default on arg {}\n", .{i}); break :blk default; }; try arr.append(value); } return arr.toSliceConst(); } pub fn appendParam( self: *Command, params: *plugin.ParamList, symbol: []const u8, ) !void { var val = try self.floatArgAt(self.cur_idx); self.cur_idx += 1; try params.append(plugin.Param{ .sym = symbol, .value = val, }); } pub fn appendParamMap( self: *Command, map: *plugin.ParamMap, symbol: []const u8, ) !void { var val = try self.floatArgAt(self.cur_idx); self.cur_idx += 1; _ = try map.put(symbol, val); } pub fn copy(self: Command, allocator: *std.mem.Allocator) !*Command { var cmd = try allocator.create(Command); cmd.* = Command{ .command = self.command, .args = self.args, .cur_idx = self.cur_idx, }; return cmd; } }; pub const CommandList = std.ArrayList(Command); pub const ArgList = std.ArrayList([]const u8); pub const KeywordMap = std.StringHashMap(CommandType); /// A parser. pub const Lang = struct { allocator: *std.mem.Allocator, keywords: KeywordMap, has_error: bool = false, line: usize = 0, pub fn init(allocator: *std.mem.Allocator) Lang { return Lang{ .allocator = allocator, .keywords = KeywordMap.init(allocator), }; } pub fn deinit(self: *Lang) void { self.keywords.deinit(); } pub fn reset(self: *Lang) void { self.has_error = false; self.line = 0; } fn fillKeywords(self: *Lang) !void { _ = try self.keywords.put("noop", .Noop); _ = try self.keywords.put("load", .Load); _ = try self.keywords.put("quicksave", .Quicksave); _ = try self.keywords.put("runqs", .RunQS); _ = try self.keywords.put("amp", .Amp); _ = try self.keywords.put("rflanger", .RFlanger); _ = try self.keywords.put("eq", .Eq); _ = try self.keywords.put("mbeq", .Mbeq); _ = try self.keywords.put("phaser", .Phaser); _ = try self.keywords.put("chorus", .Chorus); _ = try self.keywords.put("pitchscaler", .PitchScaler); _ = try self.keywords.put("reverb", .Reverb); _ = try self.keywords.put("highpass", .Highpass); _ = try self.keywords.put("delay", .Delay); _ = try self.keywords.put("vinyl", .Vinyl); _ = try self.keywords.put("revdelay", .RevDelay); _ = try self.keywords.put("gate", .Gate); _ = try self.keywords.put("detune", .Detune); _ = try self.keywords.put("overdrive", .Overdrive); _ = try self.keywords.put("talkbox", .TalkBox); _ = try self.keywords.put("thruzero", .ThruZero); _ = try self.keywords.put("foverdrive", .Foverdrive); _ = try self.keywords.put("gverb", .Gverb); _ = try self.keywords.put("invert", .Invert); // custom implementations (not lv2) _ = try self.keywords.put("noise", .Noise); _ = try self.keywords.put("wildnoise", .WildNoise); _ = try self.keywords.put("write", .Write); _ = try self.keywords.put("embed", .Embed); _ = try self.keywords.put("degrade", .Degrade); _ = try self.keywords.put("repsycho", .RePsycho); _ = try self.keywords.put("dyncomp", .RePsycho); // even more custom _ = try self.keywords.put("rotate", .Rotate); } pub fn getCommand(self: *Lang, stmt: []const u8) ?CommandType { var kv_opt = self.keywords.get(stmt); if (kv_opt) |kv| { return kv.value; } else { return null; } } fn expectAny(self: *Lang, count: usize, args: ArgList) !void { if (args.len != count) { self.doError("expected {} arguments, found {}", .{ count, args.len }); return error.ArgRequired; } } fn expectSingle(self: *Lang, args: ArgList) !void { return try self.expectAny(1, args); } fn expectFloat(self: *Lang, count: usize, args: ArgList) !void { var i: usize = 0; if (args.len != count) { self.doError("expected {} arguments, found {}", .{ count, args.len }); return error.ArgRequired; } while (i < count) : (i += 1) { var arg = args.at(i); _ = std.fmt.parseFloat(f32, arg) catch |err| { std.debug.warn("failed to parse f32: {}\n", .{err}); return error.FloatParseFail; }; } } fn validateCommand(self: *Lang, cmd: Command) !void { switch (cmd.command) { .Quicksave, .Noop => {}, .Load, .RunQS => try self.expectSingle(cmd.args), .Amp => try self.expectFloat(3, cmd.args), .RFlanger => try self.expectFloat(4, cmd.args), .Eq => try self.expectFloat(5, cmd.args), .Phaser => try self.expectFloat(6, cmd.args), .Mbeq => try self.expectFloat(17, cmd.args), .Chorus => try self.expectFloat(8, cmd.args), .PitchScaler => try self.expectFloat(3, cmd.args), .Reverb => try self.expectFloat(12, cmd.args), .Highpass => try self.expectFloat(5, cmd.args), .Delay => try self.expectFloat(12, cmd.args), .Vinyl => try self.expectFloat(7, cmd.args), .RevDelay => try self.expectFloat(7, cmd.args), .Noise => try self.expectFloat(4, cmd.args), .WildNoise => try self.expectFloat(4, cmd.args), .Write => try self.expectFloat(3, cmd.args), .Rotate => try self.expectAny(2, cmd.args), .Embed => try self.expectAny(3, cmd.args), else => std.debug.warn("WARN unchecked command {}\n", .{cmd.command}), } } fn doError(self: *Lang, comptime fmt: []const u8, args: var) void { std.debug.warn("error at line {}: ", .{self.line}); std.debug.warn(fmt, args); std.debug.warn("\n", .{}); self.has_error = true; } pub fn parse(self: *Lang, data: []const u8) ParseError!CommandList { var splitted_it = std.mem.separate(data, ";"); try self.fillKeywords(); var cmds = CommandList.init(self.allocator); 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(" "); var tok_it = std.mem.tokenize(stmt, " "); var cmd_opt = tok_it.next(); if (cmd_opt == null) { self.doError("No command given", .{}); continue; } var command = cmd_opt.?; var ctype_opt = self.getCommand(command); var ctype: CommandType = undefined; if (ctype_opt) |ctype_val| { ctype = ctype_val; } else { self.doError("Unknown command '{}' ({})", .{ command, command.len }); continue; } var args = ArgList.init(self.allocator); errdefer args.deinit(); while (tok_it.next()) |arg| { try args.append(arg); } // construct final Command based on command var cmd = Command{ .command = ctype, .args = args }; self.validateCommand(cmd) catch |err| { //self.doError("error validating command '{}': {}", command, err); continue; }; try cmds.append(cmd); } if (self.has_error) return ParseError.ParseFail; return cmds; } }; test "noop" { var lang = Lang.init(std.heap.direct_allocator); defer lang.deinit(); var cmds = try lang.parse("noop;"); defer cmds.deinit(); std.testing.expectEqual(cmds.len, 1); std.testing.expectEqual(cmds.at(0).command, .Noop); } test "load, phaser, quicksave" { var lang = Lang.init(std.heap.direct_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.len, 3); std.testing.expectEqual(cmds.at(0).command, .Load); std.testing.expectEqual(cmds.at(1).command, .Phaser); std.testing.expectEqual(cmds.at(2).command, .Quicksave); }