const std = @import("std"); const lang = @import("lang.zig"); const images = @import("image.zig"); const plugin = @import("plugin.zig"); const Image = images.Image; pub const RunError = error{ UnknownCommand, NoBMP, ImageRequired, }; fn appendParam(params: *plugin.ParamList, sym: []const u8, value: f32) !void { try params.append(plugin.Param{ .sym = sym, .value = value }); } pub const Runner = struct { allocator: *std.mem.Allocator, image: ?*Image = null, pub fn init(allocator: *std.mem.Allocator) Runner { return Runner{ .allocator = allocator, }; } pub fn deinit(self: *Runner) void { if (self.image) |image| { image.close(); } } fn resolveArg(self: *Runner, load_path: []const u8) ![]const u8 { if (load_path[0] == ':') { // parse the index from 1 to end const index = try std.fmt.parseInt(usize, load_path[1..], 10); 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("load 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")) { 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 quicksaveCmd(self: *Runner) !void { // 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(self.allocator, 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], ); var max: usize = 0; while (try dir.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, ); try image.saveTo(out_path); } /// Run the http://lv2plug.in/plugins/eg-amp plugin over the file. fn ampCmd(self: *Runner, split: usize, index: usize, gain: f32) !void { var image = try self.getImage(); var params = plugin.ParamList.init(self.allocator); defer params.deinit(); try params.append(plugin.Param{ .sym = "gain", .value = gain }); // TODO if we could detect that the next command is a quicksave then // we don't need the temporary file in runPlugin and instead we // dump it all on the output image for it. try image.runPlugin( "http://lv2plug.in/plugins/eg-amp", plugin.Position{ .split = split, .index = index }, params, ); } fn rFlangerCmd( self: *Runner, split: usize, index: usize, delay_depth_avg: f32, law_freq: f32, ) !void { var image = try self.getImage(); var params = plugin.ParamList.init(self.allocator); defer params.deinit(); try params.append(plugin.Param{ .sym = "delay_depth_avg", .value = delay_depth_avg }); try params.append(plugin.Param{ .sym = "law_freq", .value = law_freq }); try image.runPlugin( "http://plugin.org.uk/swh-plugins/retroFlange", plugin.Position{ .split = split, .index = index }, params, ); } fn eqCmd( self: *Runner, position: plugin.Position, lo: f32, mid: f32, high: f32, ) !void { var image = try self.getImage(); var params = plugin.ParamList.init(self.allocator); defer params.deinit(); try appendParam(¶ms, "lo", lo); try appendParam(¶ms, "mid", mid); try appendParam(¶ms, "hi", high); try image.runPlugin("http://plugin.org.uk/swh-plugins/dj_eq_mono", position, params); } fn phaserCmd( self: *Runner, position: plugin.Position, lfo_rate: f32, lfo_depth: f32, fb: f32, spread: f32, ) !void { var image = try self.getImage(); var params = plugin.ParamList.init(self.allocator); defer params.deinit(); try appendParam(¶ms, "lfo_rate", lfo_rate); try appendParam(¶ms, "lfo_depth", lfo_depth); try appendParam(¶ms, "fb", fb); try appendParam(¶ms, "spread", spread); try image.runPlugin("http://plugin.org.uk/swh-plugins/lfoPhaser", position, params); } fn mbeqCmd( self: *Runner, position: plugin.Position, bands: []const f32, ) !void { var image = try self.getImage(); var params = plugin.ParamList.init(self.allocator); defer params.deinit(); for (bands) |band_value, idx| { var sym = try std.fmt.allocPrint(self.allocator, "band_{}", idx + 1); try appendParam(¶ms, sym, band_value); } try image.runPlugin("http://plugin.org.uk/swh-plugins/mbeq", position, params); } fn chorusCmd( self: *Runner, pos: plugin.Position, params: plugin.ParamList, ) !void { var image = try self.getImage(); try image.runPlugin("http://plugin.org.uk/swh-plugins/multivoiceChorus", pos, params); } fn runCommand(self: *Runner, cmd: *lang.Command) !void { var params = plugin.ParamList.init(self.allocator); defer params.deinit(); return switch (cmd.command) { .Noop => {}, .Load => blk: { var path = cmd.args.at(0); try self.loadCmd(path); break :blk; }, .Quicksave => try self.quicksaveCmd(), .Amp => blk: { const split = try cmd.usizeArgAt(0); const index = try cmd.usizeArgAt(1); const gain = try cmd.floatArgAt(2); try self.ampCmd(split, index, gain); }, .RFlanger => blk: { const split = try cmd.usizeArgAt(0); const index = try cmd.usizeArgAt(1); const delay_depth_avg = try cmd.floatArgAt(2); const law_freq = try cmd.floatArgAt(3); try self.rFlangerCmd(split, index, delay_depth_avg, law_freq); }, .Eq => blk: { const pos = try cmd.consumePosition(); const lo = try cmd.floatArgAt(2); const mid = try cmd.floatArgAt(3); const high = try cmd.floatArgAt(4); try self.eqCmd(pos, lo, mid, high); }, .Phaser => blk: { const pos = try cmd.consumePosition(); const lfo_rate = try cmd.floatArgAt(2); const lfo_depth = try cmd.floatArgAt(3); const fb = try cmd.floatArgAt(4); const spread = try cmd.floatArgAt(5); try self.phaserCmd(pos, lfo_rate, lfo_depth, fb, spread); }, .Mbeq => blk: { const pos = try cmd.consumePosition(); const bands = try cmd.floatArgMany(self.allocator, 2, 15, f32(0)); try self.mbeqCmd(pos, bands); }, .Chorus => blk: { const pos = try cmd.consumePosition(); try cmd.appendParam(¶ms, "voices", 2); try cmd.appendParam(¶ms, "delay_base", 3); try cmd.appendParam(¶ms, "voice_spread", 4); try cmd.appendParam(¶ms, "detune", 5); try cmd.appendParam(¶ms, "law_freq", 6); try cmd.appendParam(¶ms, "attendb", 7); try self.chorusCmd(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 { var it = cmds.iterator(); while (it.next()) |cmd| { if (debug_flag) cmd.print(); try self.runCommand(cmd); } } };