const std = @import("std"); const lang = @import("lang.zig"); const images = @import("image.zig"); const plugin = @import("plugin.zig"); const custom = @import("custom.zig"); const magick = @import("magick.zig"); const Position = plugin.Position; const ParamList = plugin.ParamList; const ParamMap = plugin.ParamMap; const Image = images.Image; pub const RunError = error{ UnknownCommand, NoBMP, ImageRequired, }; pub const Runner = struct { allocator: *std.mem.Allocator, /// The currently opened image in the runner image: ?*Image = null, /// If the runner is in REPL mode repl: bool = false, pub fn init(allocator: *std.mem.Allocator, repl: bool) Runner { return Runner{ .allocator = allocator, .repl = repl, }; } pub fn deinit(self: *Runner) void { if (self.image) |image| { image.close(); } } pub fn clone(self: *Runner) !Runner { var cloned_image = if (self.image) |image| try image.clone() else null; return Runner{ .allocator = self.allocator, .image = cloned_image }; } fn resolveArg(self: *Runner, load_path: []const u8) ![]const u8 { if (load_path[0] == ':') { // parse the index from 1 to end var index = try std.fmt.parseInt(usize, load_path[1..], 10); // don't care about the 'repl' being prepended when we're in repl if (self.repl) index += 1; var args_it = std.process.args(); _ = args_it.skip(); var i: usize = 0; while (i <= index) : (i += 1) { _ = args_it.skip(); } const arg = try (args_it.next(self.allocator) orelse @panic("expected argument")); return arg; } else { return load_path; } } fn resolveArgPath(self: *Runner, path_or_argidx: []const u8) ![]const u8 { const path = try self.resolveArg(path_or_argidx); const resolved_path = try std.fs.path.resolve( self.allocator, &[_][]const u8{path}, ); return resolved_path; } fn loadCmd(self: *Runner, path_or_argidx: []const u8) !void { var load_path = try self.resolveArgPath(path_or_argidx); std.debug.warn("\tload path: {}\n", .{load_path}); // we could use ImageMagick to convert from X to BMP // but i can't find an easy way to do things in memory. // the upside is that this allows some pre-processing by the user // before loading the file into scritcher. for example, you can start // krita/gimp and make it export a bmp and while in the program you can // apply filters, etc. if (!std.mem.endsWith(u8, load_path, ".bmp") and !std.mem.endsWith(u8, load_path, ".ppm")) { std.debug.warn("Only BMP files are allowed to be loaded.\n", .{}); return RunError.NoBMP; } // we don't copy load_path into a temporary file because we're already // loading it under the SFM_READ mode, which won't cause any destructive // operations on the file. self.image = try Image.open(self.allocator, load_path); } fn getImage(self: *Runner) !*Image { if (self.image) |image| { return image; } else { std.debug.warn("image is required!\n", .{}); return RunError.ImageRequired; } } fn makeGlitchedPath(self: *Runner) ![]const u8 { // we want to transform basename, if it is "x.bmp" to "x_gN.bmp", where // N is the maximum non-used integer. var image = try self.getImage(); const basename = std.fs.path.basename(image.path); const dirname = std.fs.path.dirname(image.path).?; var dir = try std.fs.Dir.open(dirname); defer dir.close(); const period_idx = std.mem.lastIndexOf(u8, basename, ".").?; const extension = basename[period_idx..basename.len]; // starts_with would be "x_g", we want to find all files in the directory // that start with that name. const starts_with = try std.fmt.allocPrint(self.allocator, "{}_g", .{ basename[0..period_idx], }); defer self.allocator.free(starts_with); var max: usize = 0; var it = dir.iterate(); while (try it.next()) |entry| { switch (entry.kind) { .File => blk: { if (!std.mem.startsWith(u8, entry.name, starts_with)) break :blk {}; // we want to get the N in x_gN.ext const entry_gidx = std.mem.lastIndexOf(u8, entry.name, "_g").?; const entry_pidx_opt = std.mem.lastIndexOf(u8, entry.name, "."); if (entry_pidx_opt == null) break :blk {}; const entry_pidx = entry_pidx_opt.?; // if N isn't a number, we just ignore that file const idx_str = entry.name[entry_gidx + 2 .. entry_pidx]; const idx = std.fmt.parseInt(usize, idx_str, 10) catch |err| { break :blk {}; }; if (idx > max) max = idx; }, else => {}, } } const out_path = try std.fmt.allocPrint(self.allocator, "{}/{}{}{}", .{ dirname, starts_with, max + 1, extension, }); return out_path; } fn quicksaveCmd(self: *Runner) !void { var image = try self.getImage(); const out_path = try self.makeGlitchedPath(); try image.saveTo(out_path); } fn runQSCmd(self: *Runner, program: []const u8) !void { var image = try self.getImage(); const out_path = try self.makeGlitchedPath(); try image.saveTo(out_path); var proc = try std.ChildProcess.init( &[_][]const u8{ program, out_path }, self.allocator, ); defer proc.deinit(); std.debug.warn("running '{} {}'\n", .{ program, out_path }); _ = try proc.spawnAndWait(); } /// Run the http://lv2plug.in/plugins/eg-amp plugin over the file. fn ampCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://lv2plug.in/plugins/eg-amp", pos, params); } fn rFlangerCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://plugin.org.uk/swh-plugins/retroFlange", pos, params); } fn eqCmd(self: *Runner, position: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://plugin.org.uk/swh-plugins/dj_eq_mono", position, params); } fn phaserCmd(self: *Runner, position: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://plugin.org.uk/swh-plugins/lfoPhaser", position, params); } fn mbeqCmd(self: *Runner, position: Position, bands: []const f32) !void { var image = try self.getImage(); var params = ParamList.init(self.allocator); defer params.deinit(); for (bands) |band_value, idx| { var sym = try std.fmt.allocPrint(self.allocator, "band_{}", .{idx + 1}); try params.append(plugin.Param{ .sym = sym, .value = band_value, }); } try image.runPlugin("http://plugin.org.uk/swh-plugins/mbeq", position, params); } fn chorusCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://plugin.org.uk/swh-plugins/multivoiceChorus", pos, params); } fn pitchScalerCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://plugin.org.uk/swh-plugins/pitchScaleHQ", pos, params); } fn reverbCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://invadarecords.com/plugins/lv2/erreverb/mono", pos, params); } fn highpassCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://invadarecords.com/plugins/lv2/filter/hpf/mono", pos, params); } fn delayCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://plugin.org.uk/swh-plugins/delayorama", pos, params); } fn vinylCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://plugin.org.uk/swh-plugins/vynil", pos, params); } fn revDelayCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://plugin.org.uk/swh-plugins/revdelay", pos, params); } fn noiseCmd(self: *Runner, pos: Position, map: *ParamMap) !void { var image = try self.getImage(); try image.runCustomPlugin(custom.RandomNoise, pos, *ParamMap, map); } fn wildNoiseCmd(self: *Runner, pos: Position, map: *ParamMap) !void { var image = try self.getImage(); try image.runCustomPlugin(custom.WildNoise, pos, *ParamMap, map); } fn writeCmd(self: *Runner, pos: Position, map: *ParamMap) !void { var image = try self.getImage(); try image.runCustomPlugin(custom.Write, pos, *ParamMap, map); } fn embedCmd(self: *Runner, pos: Position, path: []const u8) !void { var image = try self.getImage(); try image.runCustomPlugin(custom.Embed, pos, []const u8, path); } fn rotateCmd( self: *Runner, deg: f32, bgfill: []const u8, ) !void { var image = try self.getImage(); var c_bgfill = try std.cstr.addNullByte(self.allocator, bgfill); defer self.allocator.free(c_bgfill); try magick.runRotate(image, deg, c_bgfill); } fn gateCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://hippie.lt/lv2/gate", pos, params); } fn detuneCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://drobilla.net/plugins/mda/Detune", pos, params); } fn overdriveCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://drobilla.net/plugins/mda/Overdrive", pos, params); } fn degradeCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://drobilla.net/plugins/mda/Degrade", pos, params); } fn repsychoCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://drobilla.net/plugins/mda/RePsycho", pos, params); } fn talkboxCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://drobilla.net/plugins/mda/TalkBox", pos, params); } fn dynCompCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://gareus.org/oss/lv2/darc#mono", pos, params); } fn foverdriveCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://plugin.org.uk/swh-plugins/foverdrive", pos, params); } fn thruZeroCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://drobilla.net/plugins/mda/ThruZero", pos, params); } fn gverbCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://plugin.org.uk/swh-plugins/gverb", pos, params); } fn invertCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://plugin.org.uk/swh-plugins/inv", pos, params); } fn tapedelayCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); try image.runPlugin("http://plugin.org.uk/swh-plugins/tapeDelay", pos, params); } fn runCommand(self: *Runner, cmd: *lang.Command) !void { var params = ParamList.init(self.allocator); defer params.deinit(); var map = ParamMap.init(self.allocator); defer map.deinit(); return switch (cmd.command) { .Noop => {}, .Load => blk: { var path = cmd.args.at(0); try self.loadCmd(path); // TODO is this needed? break :blk; }, .Quicksave => try self.quicksaveCmd(), .RunQS => try self.runQSCmd(cmd.args.at(0)), .Amp => blk: { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "gain"); try self.ampCmd(pos, params); }, .RFlanger => blk: { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "delay_depth_avg"); try cmd.appendParam(¶ms, "law_freq"); try self.rFlangerCmd(pos, params); }, .Eq => blk: { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "lo"); try cmd.appendParam(¶ms, "mid"); try cmd.appendParam(¶ms, "hi"); try self.eqCmd(pos, params); }, .Phaser => blk: { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "lfo_rate"); try cmd.appendParam(¶ms, "lfo_depth"); try cmd.appendParam(¶ms, "fb"); try cmd.appendParam(¶ms, "spread"); try self.phaserCmd(pos, params); }, .Mbeq => blk: { const pos = try cmd.consumePosition(); const bands = try cmd.floatArgMany(self.allocator, 2, 15, @as(f32, 0)); defer self.allocator.free(bands); try self.mbeqCmd(pos, bands); }, .Chorus => blk: { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "voices"); try cmd.appendParam(¶ms, "delay_base"); try cmd.appendParam(¶ms, "voice_spread"); try cmd.appendParam(¶ms, "detune"); try cmd.appendParam(¶ms, "law_freq"); try cmd.appendParam(¶ms, "attendb"); try self.chorusCmd(pos, params); }, .PitchScaler => blk: { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "mult"); try self.pitchScalerCmd(pos, params); }, .Reverb => blk: { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "roomLength"); try cmd.appendParam(¶ms, "roomWidth"); try cmd.appendParam(¶ms, "roomHeight"); try cmd.appendParam(¶ms, "sourceLR"); try cmd.appendParam(¶ms, "sourceFB"); try cmd.appendParam(¶ms, "listLR"); try cmd.appendParam(¶ms, "listFB"); try cmd.appendParam(¶ms, "hpf"); try cmd.appendParam(¶ms, "warmth"); try cmd.appendParam(¶ms, "diffusion"); try self.reverbCmd(pos, params); }, .Highpass => blk: { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "freq"); try cmd.appendParam(¶ms, "gain"); try cmd.appendParam(¶ms, "noClip"); try self.highpassCmd(pos, params); }, .Delay => blk: { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "seed"); try cmd.appendParam(¶ms, "gain"); try cmd.appendParam(¶ms, "feedback_pc"); try cmd.appendParam(¶ms, "tap_count"); try cmd.appendParam(¶ms, "first_delay"); try cmd.appendParam(¶ms, "delay_range"); try cmd.appendParam(¶ms, "delay_scale"); try cmd.appendParam(¶ms, "delay_rand_pc"); try cmd.appendParam(¶ms, "gain_scale"); try cmd.appendParam(¶ms, "wet"); try self.delayCmd(pos, params); }, .Vinyl => blk: { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "year"); try cmd.appendParam(¶ms, "rpm"); try cmd.appendParam(¶ms, "warp"); try cmd.appendParam(¶ms, "click"); try cmd.appendParam(¶ms, "wear"); try self.vinylCmd(pos, params); }, .RevDelay => blk: { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "delay_time"); try cmd.appendParam(¶ms, "dry_level"); try cmd.appendParam(¶ms, "wet_level"); try cmd.appendParam(¶ms, "feedback"); try cmd.appendParam(¶ms, "xfade_samp"); try self.revDelayCmd(pos, params); }, .Noise => blk: { const pos = try cmd.consumePosition(); try cmd.appendParamMap(&map, "seed"); try cmd.appendParamMap(&map, "fill_bytes"); try self.noiseCmd(pos, &map); }, .WildNoise => blk: { const pos = try cmd.consumePosition(); try cmd.appendParamMap(&map, "seed"); try cmd.appendParamMap(&map, "fill_bytes"); try self.wildNoiseCmd(pos, &map); }, .Write => blk: { const pos = try cmd.consumePosition(); try cmd.appendParamMap(&map, "data"); try self.writeCmd(pos, &map); }, .Embed => blk: { const pos = try cmd.consumePosition(); const path = cmd.args.at(2); try self.embedCmd(pos, path); }, .Rotate => blk: { const deg = try cmd.floatArgAt(0); const bgfill = try cmd.argAt(1); try self.rotateCmd(deg, bgfill); }, .Gate => { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "switch"); try cmd.appendParam(¶ms, "threshold"); try cmd.appendParam(¶ms, "attack"); try cmd.appendParam(¶ms, "hold"); try cmd.appendParam(¶ms, "decay"); try cmd.appendParam(¶ms, "gaterange"); try self.gateCmd(pos, params); }, .Detune => { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "detune"); try cmd.appendParam(¶ms, "mix"); try cmd.appendParam(¶ms, "output"); try cmd.appendParam(¶ms, "latency"); try self.detuneCmd(pos, params); }, .Overdrive => { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "drive"); try cmd.appendParam(¶ms, "muffle"); try cmd.appendParam(¶ms, "output"); try self.overdriveCmd(pos, params); }, .Degrade => { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "headroom"); try cmd.appendParam(¶ms, "quant"); try cmd.appendParam(¶ms, "rate"); try cmd.appendParam(¶ms, "post_filt"); try cmd.appendParam(¶ms, "non_lin"); try cmd.appendParam(¶ms, "output"); try self.degradeCmd(pos, params); }, .RePsycho => { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "tune"); try cmd.appendParam(¶ms, "fine"); try cmd.appendParam(¶ms, "decay"); try cmd.appendParam(¶ms, "thresh"); try cmd.appendParam(¶ms, "hold"); try cmd.appendParam(¶ms, "mix"); try cmd.appendParam(¶ms, "quality"); try self.repsychoCmd(pos, params); }, .TalkBox => { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "wet"); try cmd.appendParam(¶ms, "dry"); try cmd.appendParam(¶ms, "carrier"); try cmd.appendParam(¶ms, "quality"); try self.talkboxCmd(pos, params); }, .DynComp => { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "enable"); try cmd.appendParam(¶ms, "hold"); try cmd.appendParam(¶ms, "inputgain"); try cmd.appendParam(¶ms, "threshold"); try cmd.appendParam(¶ms, "ratio"); try cmd.appendParam(¶ms, "attack"); try cmd.appendParam(¶ms, "release"); try cmd.appendParam(¶ms, "gain_min"); try cmd.appendParam(¶ms, "gain_max"); try cmd.appendParam(¶ms, "rms"); try self.dynCompCmd(pos, params); }, .ThruZero => { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "rate"); try cmd.appendParam(¶ms, "mix"); try cmd.appendParam(¶ms, "feedback"); try cmd.appendParam(¶ms, "depth_mod"); try self.thruZeroCmd(pos, params); }, .Foverdrive => { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "drive"); try self.foverdriveCmd(pos, params); }, .Gverb => { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "roomsize"); try cmd.appendParam(¶ms, "revtime"); try cmd.appendParam(¶ms, "damping"); try cmd.appendParam(¶ms, "inputbandwidth"); try cmd.appendParam(¶ms, "drylevel"); try cmd.appendParam(¶ms, "earlylevel"); try cmd.appendParam(¶ms, "taillevel"); try self.gverbCmd(pos, params); }, .Invert => { const pos = try cmd.consumePosition(); try self.gverbCmd(pos, params); }, .TapeDelay => { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "speed"); try cmd.appendParam(¶ms, "da_db"); try cmd.appendParam(¶ms, "t1d"); try cmd.appendParam(¶ms, "t1a_db"); try cmd.appendParam(¶ms, "t2d"); try cmd.appendParam(¶ms, "t2a_db"); try cmd.appendParam(¶ms, "t3d"); try cmd.appendParam(¶ms, "t3a_db"); try cmd.appendParam(¶ms, "t4d"); try cmd.appendParam(¶ms, "t4a_db"); try self.tapedelayCmd(pos, params); }, else => blk: { std.debug.warn("Unsupported command: {}\n", .{cmd.command}); break :blk RunError.UnknownCommand; }, }; } /// Run a list of commands. pub fn runCommands( self: *Runner, cmds: lang.CommandList, debug_flag: bool, ) !void { for (cmds.toSlice()) |const_cmd| { if (debug_flag) const_cmd.print(); // copy the command so we own its memory var cmd = try const_cmd.copy(self.allocator); defer self.allocator.destroy(cmd); try self.runCommand(cmd); } } }; test "running noop" { const allocator = std.heap.direct_allocator; var cmds = lang.CommandList.init(allocator); defer cmds.deinit(); var cmd_ptr = try allocator.create(lang.Command); cmd_ptr.* = lang.Command{ .command = .Noop, .args = lang.ArgList.init(allocator), }; try cmds.append(cmd_ptr); var runner = Runner.init(allocator); defer runner.deinit(); try runner.runCommands(cmds, false); }