2019-07-08 15:38:16 +00:00
|
|
|
const std = @import("std");
|
|
|
|
const lang = @import("lang.zig");
|
2019-07-08 17:43:58 +00:00
|
|
|
const images = @import("image.zig");
|
2019-07-09 01:40:52 +00:00
|
|
|
const plugin = @import("plugin.zig");
|
2019-07-13 20:42:46 +00:00
|
|
|
const custom = @import("custom.zig");
|
2019-07-22 22:28:55 +00:00
|
|
|
const magick = @import("magick.zig");
|
2019-07-08 15:38:16 +00:00
|
|
|
|
2022-04-27 23:01:09 +00:00
|
|
|
const log = std.log.scoped(.scritcher_runner);
|
|
|
|
|
2019-07-10 20:15:57 +00:00
|
|
|
const Position = plugin.Position;
|
|
|
|
const ParamList = plugin.ParamList;
|
2019-07-13 21:17:44 +00:00
|
|
|
const ParamMap = plugin.ParamMap;
|
2019-07-10 20:15:57 +00:00
|
|
|
|
2019-07-08 17:43:58 +00:00
|
|
|
const Image = images.Image;
|
|
|
|
|
|
|
|
pub const RunError = error{
|
|
|
|
UnknownCommand,
|
|
|
|
NoBMP,
|
2019-07-08 18:40:31 +00:00
|
|
|
ImageRequired,
|
2019-07-08 17:43:58 +00:00
|
|
|
};
|
2019-07-08 15:38:16 +00:00
|
|
|
|
|
|
|
pub const Runner = struct {
|
2022-04-27 23:01:12 +00:00
|
|
|
allocator: std.mem.Allocator,
|
2019-10-06 18:16:41 +00:00
|
|
|
|
|
|
|
/// The currently opened image in the runner
|
2019-07-08 17:43:58 +00:00
|
|
|
image: ?*Image = null,
|
2019-07-08 15:38:16 +00:00
|
|
|
|
2019-10-06 18:16:41 +00:00
|
|
|
/// If the runner is in REPL mode
|
2020-06-02 20:16:02 +00:00
|
|
|
repl: bool,
|
2019-10-06 18:16:41 +00:00
|
|
|
|
2020-11-08 21:47:40 +00:00
|
|
|
args: []const [:0]u8,
|
2020-06-02 20:12:34 +00:00
|
|
|
|
2022-04-27 23:01:12 +00:00
|
|
|
pub fn init(allocator: std.mem.Allocator, repl: bool) Runner {
|
2019-07-09 03:04:01 +00:00
|
|
|
return Runner{
|
|
|
|
.allocator = allocator,
|
2019-10-06 18:16:41 +00:00
|
|
|
.repl = repl,
|
2020-06-02 20:12:34 +00:00
|
|
|
.args = std.process.argsAlloc(allocator) catch unreachable,
|
2019-07-09 03:04:01 +00:00
|
|
|
};
|
2019-07-08 15:38:16 +00:00
|
|
|
}
|
|
|
|
|
2019-07-08 17:43:58 +00:00
|
|
|
pub fn deinit(self: *Runner) void {
|
|
|
|
if (self.image) |image| {
|
|
|
|
image.close();
|
|
|
|
}
|
2020-08-18 20:58:04 +00:00
|
|
|
|
|
|
|
std.process.argsFree(self.allocator, self.args);
|
2019-07-08 17:43:58 +00:00
|
|
|
}
|
|
|
|
|
2019-09-11 01:00:07 +00:00
|
|
|
pub fn clone(self: *Runner) !Runner {
|
|
|
|
var cloned_image = if (self.image) |image| try image.clone() else null;
|
2020-06-02 20:12:34 +00:00
|
|
|
return Runner{
|
|
|
|
.allocator = self.allocator,
|
|
|
|
.image = cloned_image,
|
2020-06-02 20:15:50 +00:00
|
|
|
.repl = self.repl,
|
2020-06-02 20:12:34 +00:00
|
|
|
.args = self.args,
|
|
|
|
};
|
2019-09-11 00:31:23 +00:00
|
|
|
}
|
|
|
|
|
2019-07-08 16:55:54 +00:00
|
|
|
fn resolveArg(self: *Runner, load_path: []const u8) ![]const u8 {
|
|
|
|
if (load_path[0] == ':') {
|
|
|
|
// parse the index from 1 to end
|
2019-10-06 18:16:41 +00:00
|
|
|
var index = try std.fmt.parseInt(usize, load_path[1..], 10);
|
|
|
|
|
2020-06-02 19:59:53 +00:00
|
|
|
// if it isn't in the repl, args look like this:
|
|
|
|
// 'scritcher ./script ./image'
|
|
|
|
// if it is, it looks like this
|
|
|
|
// 'scritcher repl ./script ./image'
|
|
|
|
|
|
|
|
// ':0' should ALWAYS point to the image.
|
2022-04-27 23:01:09 +00:00
|
|
|
if (self.repl) index += 3 else index += 3;
|
2020-06-02 19:59:53 +00:00
|
|
|
|
2020-06-02 20:12:34 +00:00
|
|
|
for (self.args) |arg, idx| {
|
2022-04-27 23:01:09 +00:00
|
|
|
log.debug("arg{d} = {s}\n", .{ idx, arg });
|
2019-07-08 16:55:54 +00:00
|
|
|
}
|
2022-04-27 23:01:09 +00:00
|
|
|
log.debug("fetch arg idx={d}, val={s}\n", .{ index, self.args[index] });
|
2020-06-02 20:12:34 +00:00
|
|
|
return self.args[index];
|
2019-07-08 16:55:54 +00:00
|
|
|
} else {
|
|
|
|
return load_path;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-18 23:57:33 +00:00
|
|
|
// Caller owns returned memory.
|
2019-07-08 16:55:54 +00:00
|
|
|
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,
|
2019-12-08 15:14:31 +00:00
|
|
|
&[_][]const u8{path},
|
2019-07-08 16:55:54 +00:00
|
|
|
);
|
2019-07-08 17:43:58 +00:00
|
|
|
|
2019-07-08 16:55:54 +00:00
|
|
|
return resolved_path;
|
|
|
|
}
|
|
|
|
|
2019-07-08 17:43:58 +00:00
|
|
|
fn loadCmd(self: *Runner, path_or_argidx: []const u8) !void {
|
2020-08-18 23:57:33 +00:00
|
|
|
const load_path = try self.resolveArgPath(path_or_argidx);
|
2022-04-27 23:01:09 +00:00
|
|
|
log.debug("\tload path: {s}\n", .{load_path});
|
2019-07-08 17:43:58 +00:00
|
|
|
|
|
|
|
// 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.
|
2019-11-02 00:43:45 +00:00
|
|
|
if (!std.mem.endsWith(u8, load_path, ".bmp") and !std.mem.endsWith(u8, load_path, ".ppm")) {
|
2022-04-27 23:01:09 +00:00
|
|
|
log.debug("Only BMP files are allowed to be loaded. Got path '{s}'\n", .{load_path});
|
2019-07-08 17:43:58 +00:00
|
|
|
return RunError.NoBMP;
|
|
|
|
}
|
|
|
|
|
2019-07-10 15:00:43 +00:00
|
|
|
// 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.
|
2020-08-19 00:00:07 +00:00
|
|
|
if (self.image) |image| image.close();
|
2019-07-08 17:43:58 +00:00
|
|
|
self.image = try Image.open(self.allocator, load_path);
|
2019-07-08 16:55:54 +00:00
|
|
|
}
|
|
|
|
|
2019-07-08 18:40:31 +00:00
|
|
|
fn getImage(self: *Runner) !*Image {
|
|
|
|
if (self.image) |image| {
|
|
|
|
return image;
|
|
|
|
} else {
|
2022-04-27 23:01:09 +00:00
|
|
|
log.debug("image is required!\n", .{});
|
2019-07-08 18:40:31 +00:00
|
|
|
return RunError.ImageRequired;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-18 20:58:04 +00:00
|
|
|
/// Caller owns returned memory.
|
2019-07-11 00:43:23 +00:00
|
|
|
fn makeGlitchedPath(self: *Runner) ![]const u8 {
|
2019-07-08 18:40:31 +00:00
|
|
|
// 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).?;
|
|
|
|
|
2020-03-26 19:35:58 +00:00
|
|
|
var dir = try std.fs.cwd().openDir(dirname, .{ .iterate = true });
|
2019-07-08 18:40:31 +00:00
|
|
|
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.
|
2021-04-04 01:16:04 +00:00
|
|
|
const starts_with = try std.fmt.allocPrint(self.allocator, "{s}_g", .{
|
2019-07-08 18:40:31 +00:00
|
|
|
basename[0..period_idx],
|
2020-01-15 01:31:20 +00:00
|
|
|
});
|
2019-08-13 13:33:08 +00:00
|
|
|
defer self.allocator.free(starts_with);
|
2019-07-08 18:40:31 +00:00
|
|
|
|
|
|
|
var max: usize = 0;
|
|
|
|
|
2019-10-31 22:02:36 +00:00
|
|
|
var it = dir.iterate();
|
|
|
|
|
|
|
|
while (try it.next()) |entry| {
|
2019-07-08 18:40:31 +00:00
|
|
|
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| {
|
2022-04-27 23:01:12 +00:00
|
|
|
log.debug("ignoring file {s}", .{@errorName(err)});
|
|
|
|
|
2019-07-08 18:40:31 +00:00
|
|
|
break :blk {};
|
|
|
|
};
|
|
|
|
|
|
|
|
if (idx > max) max = idx;
|
|
|
|
},
|
|
|
|
else => {},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-04 01:16:04 +00:00
|
|
|
const out_path = try std.fmt.allocPrint(self.allocator, "{s}/{s}{d}{s}", .{
|
2019-07-10 02:04:05 +00:00
|
|
|
dirname,
|
|
|
|
starts_with,
|
|
|
|
max + 1,
|
|
|
|
extension,
|
2020-01-15 01:31:20 +00:00
|
|
|
});
|
2019-07-10 02:04:05 +00:00
|
|
|
|
2019-07-11 00:43:23 +00:00
|
|
|
return out_path;
|
|
|
|
}
|
|
|
|
|
|
|
|
fn quicksaveCmd(self: *Runner) !void {
|
|
|
|
var image = try self.getImage();
|
|
|
|
const out_path = try self.makeGlitchedPath();
|
2020-08-18 20:58:04 +00:00
|
|
|
defer self.allocator.free(out_path);
|
2019-07-10 02:04:05 +00:00
|
|
|
try image.saveTo(out_path);
|
2019-07-08 18:40:31 +00:00
|
|
|
}
|
|
|
|
|
2020-06-02 01:47:21 +00:00
|
|
|
fn runQSCmd(self: *Runner, cmd: lang.Command) !void {
|
|
|
|
const runqs = cmd.cast(lang.Command.RunQS).?;
|
2019-07-11 00:43:23 +00:00
|
|
|
var image = try self.getImage();
|
|
|
|
const out_path = try self.makeGlitchedPath();
|
2020-08-18 20:58:04 +00:00
|
|
|
defer self.allocator.free(out_path);
|
2019-07-11 00:43:23 +00:00
|
|
|
try image.saveTo(out_path);
|
|
|
|
|
|
|
|
var proc = try std.ChildProcess.init(
|
2020-06-02 01:47:21 +00:00
|
|
|
&[_][]const u8{ runqs.program, out_path },
|
2019-07-11 00:43:23 +00:00
|
|
|
self.allocator,
|
|
|
|
);
|
|
|
|
defer proc.deinit();
|
|
|
|
|
2022-04-27 23:01:09 +00:00
|
|
|
log.debug("running '{s} {s}'\n", .{ runqs.program, out_path });
|
2019-07-11 00:43:23 +00:00
|
|
|
_ = try proc.spawnAndWait();
|
|
|
|
}
|
|
|
|
|
2020-06-02 01:06:15 +00:00
|
|
|
fn rotateCmd(self: *Runner, cmd: lang.Command) !void {
|
|
|
|
const rotate_cmd = cmd.cast(lang.Command.Rotate).?;
|
|
|
|
|
2019-07-22 22:28:55 +00:00
|
|
|
var image = try self.getImage();
|
2020-06-02 01:06:15 +00:00
|
|
|
var c_bgfill = try std.cstr.addNullByte(self.allocator, rotate_cmd.bgfill);
|
2019-07-23 01:22:01 +00:00
|
|
|
defer self.allocator.free(c_bgfill);
|
|
|
|
|
2020-06-02 01:06:15 +00:00
|
|
|
try magick.runRotate(image, rotate_cmd.deg, c_bgfill);
|
2019-07-22 22:28:55 +00:00
|
|
|
}
|
|
|
|
|
2020-07-23 19:31:32 +00:00
|
|
|
fn executeLV2Command(self: *@This(), command: anytype) !void {
|
2020-05-31 18:20:19 +00:00
|
|
|
const pos = plugin.Position{
|
|
|
|
.split = command.split,
|
|
|
|
.index = command.index,
|
|
|
|
};
|
|
|
|
|
|
|
|
var params = ParamList.init(self.allocator);
|
|
|
|
defer params.deinit();
|
|
|
|
|
|
|
|
const typ = @TypeOf(command);
|
|
|
|
|
2020-05-31 20:11:40 +00:00
|
|
|
inline for (@typeInfo(@TypeOf(command.parameters)).Struct.fields) |cmd_field| {
|
2020-05-31 18:20:19 +00:00
|
|
|
try params.append(plugin.Param{
|
|
|
|
.sym = cmd_field.name,
|
2020-05-31 20:11:40 +00:00
|
|
|
.value = @field(command.parameters, cmd_field.name),
|
2020-05-31 18:20:19 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
var image = try self.getImage();
|
|
|
|
try image.runPlugin(typ.lv2_url, pos, params);
|
|
|
|
}
|
|
|
|
|
2020-07-23 19:31:32 +00:00
|
|
|
fn executeCustomCommand(self: *@This(), command: anytype) !void {
|
2020-06-01 00:47:31 +00:00
|
|
|
const pos = plugin.Position{
|
|
|
|
.split = command.split,
|
|
|
|
.index = command.index,
|
|
|
|
};
|
|
|
|
|
|
|
|
var image = try self.getImage();
|
2020-06-01 01:28:10 +00:00
|
|
|
try image.runCustomPlugin(@TypeOf(command).plugin_type, pos, command.parameters);
|
2020-06-01 00:47:31 +00:00
|
|
|
}
|
|
|
|
|
2020-06-02 00:54:10 +00:00
|
|
|
fn runSingleCommand(
|
2020-05-31 01:56:45 +00:00
|
|
|
self: *@This(),
|
2020-05-31 20:18:36 +00:00
|
|
|
cmd: lang.Command,
|
|
|
|
comptime tag: lang.Command.Tag,
|
2020-05-31 01:56:45 +00:00
|
|
|
) !void {
|
2022-04-27 23:01:12 +00:00
|
|
|
const typ = lang.Command.tagToType(tag);
|
2020-05-31 01:56:45 +00:00
|
|
|
const command = cmd.cast(typ).?;
|
2020-06-02 01:34:48 +00:00
|
|
|
const ctype = typ.command_type;
|
|
|
|
switch (ctype) {
|
|
|
|
.lv2_command => try self.executeLV2Command(command.*),
|
|
|
|
.custom_command => try self.executeCustomCommand(command.*),
|
2020-05-31 18:20:19 +00:00
|
|
|
}
|
2020-05-31 01:56:45 +00:00
|
|
|
}
|
|
|
|
|
2020-06-02 00:54:10 +00:00
|
|
|
fn runCommand(self: *@This(), cmd: lang.Command) !void {
|
2020-05-31 01:56:45 +00:00
|
|
|
switch (cmd.tag) {
|
2020-06-02 01:47:21 +00:00
|
|
|
.noop => {},
|
2020-05-31 18:20:19 +00:00
|
|
|
.load => {
|
2020-05-31 20:18:36 +00:00
|
|
|
const command = cmd.cast(lang.Command.Load).?;
|
2020-05-31 18:20:19 +00:00
|
|
|
try self.loadCmd(command.path);
|
|
|
|
},
|
2020-06-02 01:06:15 +00:00
|
|
|
.quicksave => try self.quicksaveCmd(),
|
|
|
|
.rotate => try self.rotateCmd(cmd),
|
2020-06-02 01:47:21 +00:00
|
|
|
.runqs => try self.runQSCmd(cmd),
|
2020-06-02 00:54:10 +00:00
|
|
|
|
2020-06-02 01:06:15 +00:00
|
|
|
.amp => try self.runSingleCommand(cmd, .amp),
|
2020-06-02 00:54:10 +00:00
|
|
|
.rflanger => try self.runSingleCommand(cmd, .rflanger),
|
|
|
|
.eq => try self.runSingleCommand(cmd, .eq),
|
|
|
|
.phaser => try self.runSingleCommand(cmd, .phaser),
|
|
|
|
.mbeq => try self.runSingleCommand(cmd, .mbeq),
|
|
|
|
.chorus => try self.runSingleCommand(cmd, .chorus),
|
|
|
|
.pitchscaler => try self.runSingleCommand(cmd, .pitchscaler),
|
|
|
|
.reverb => try self.runSingleCommand(cmd, .reverb),
|
|
|
|
.highpass => try self.runSingleCommand(cmd, .highpass),
|
|
|
|
.delay => try self.runSingleCommand(cmd, .delay),
|
|
|
|
.vinyl => try self.runSingleCommand(cmd, .vinyl),
|
|
|
|
.revdelay => try self.runSingleCommand(cmd, .revdelay),
|
|
|
|
.gate => try self.runSingleCommand(cmd, .gate),
|
|
|
|
.detune => try self.runSingleCommand(cmd, .detune),
|
|
|
|
.overdrive => try self.runSingleCommand(cmd, .overdrive),
|
|
|
|
.degrade => try self.runSingleCommand(cmd, .degrade),
|
|
|
|
.repsycho => try self.runSingleCommand(cmd, .repsycho),
|
|
|
|
.talkbox => try self.runSingleCommand(cmd, .talkbox),
|
|
|
|
.dyncomp => try self.runSingleCommand(cmd, .dyncomp),
|
|
|
|
.thruzero => try self.runSingleCommand(cmd, .thruzero),
|
|
|
|
.foverdrive => try self.runSingleCommand(cmd, .foverdrive),
|
|
|
|
.gverb => try self.runSingleCommand(cmd, .gverb),
|
|
|
|
.invert => try self.runSingleCommand(cmd, .invert),
|
|
|
|
.tapedelay => try self.runSingleCommand(cmd, .tapedelay),
|
|
|
|
.moddelay => try self.runSingleCommand(cmd, .moddelay),
|
|
|
|
.multichorus => try self.runSingleCommand(cmd, .multichorus),
|
|
|
|
.saturator => try self.runSingleCommand(cmd, .saturator),
|
|
|
|
.vintagedelay => try self.runSingleCommand(cmd, .vintagedelay),
|
|
|
|
|
|
|
|
.noise => try self.runSingleCommand(cmd, .noise),
|
|
|
|
.wildnoise => try self.runSingleCommand(cmd, .wildnoise),
|
|
|
|
.write => try self.runSingleCommand(cmd, .write),
|
|
|
|
.embed => try self.runSingleCommand(cmd, .embed),
|
2020-05-31 01:56:45 +00:00
|
|
|
}
|
|
|
|
}
|
2019-07-08 17:43:58 +00:00
|
|
|
/// Run a list of commands.
|
2019-07-08 16:13:03 +00:00
|
|
|
pub fn runCommands(
|
|
|
|
self: *Runner,
|
|
|
|
cmds: lang.CommandList,
|
|
|
|
debug_flag: bool,
|
|
|
|
) !void {
|
2022-04-27 23:01:12 +00:00
|
|
|
_ = debug_flag;
|
2020-08-18 23:40:06 +00:00
|
|
|
for (cmds.list.items) |cmd| {
|
2020-05-31 01:39:45 +00:00
|
|
|
cmd.print();
|
2020-06-02 00:54:10 +00:00
|
|
|
try self.runCommand(cmd.*);
|
2019-07-08 15:38:16 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2019-08-08 19:48:31 +00:00
|
|
|
|
|
|
|
test "running noop" {
|
2022-04-27 23:01:12 +00:00
|
|
|
const allocator = std.testing.allocator;
|
2019-08-08 19:48:31 +00:00
|
|
|
var cmds = lang.CommandList.init(allocator);
|
|
|
|
defer cmds.deinit();
|
|
|
|
|
2022-04-27 23:01:12 +00:00
|
|
|
var command = lang.Command{ .tag = .noop };
|
|
|
|
var noop = lang.Command.Noop{ .base = command };
|
|
|
|
try cmds.append(&noop.base);
|
2019-08-08 19:48:31 +00:00
|
|
|
|
2022-04-27 23:01:12 +00:00
|
|
|
var runner = Runner.init(allocator, false);
|
2019-08-08 19:48:31 +00:00
|
|
|
defer runner.deinit();
|
|
|
|
|
|
|
|
try runner.runCommands(cmds, false);
|
|
|
|
}
|