const std = @import("std"); const lv2 = @import("lv2_helpers.zig"); const c = lv2.c; const bmp = @import("bmp_valid.zig"); const log = std.log.scoped(.scritcher_image); const plugins = @import("plugin.zig"); /// Buffer size for main image copying. pub const BufferSize: usize = 300000; pub const ImageError = error{ OpenFail, InvalidPlugin, UnknownPlugin, InvalidSymbol, InstantiateFail, WriteFail, PluginLoadFail, }; /// Low level integration function with libsndfile. pub fn sopen( allocator: std.mem.Allocator, path: []const u8, mode: i32, fmt: *c.SF_INFO, ) !*c.SNDFILE { var cstr_path = try std.cstr.addNullByte(allocator, path); defer allocator.free(cstr_path); var file = c.sf_open(cstr_path.ptr, mode, fmt); const st: i32 = c.sf_error(file); if (st != 0) { log.debug("Failed to open {s} ({s})", .{ path, c.sf_error_number(st), }); return ImageError.OpenFail; } const frames_on_end = c.sf_seek(file, 0, c.SEEK_END); _ = c.sf_seek(file, 0, c.SEEK_SET); try std.testing.expectEqual(fmt.frames, frames_on_end); const frames_on_end_by_end = c.sf_seek(file, frames_on_end, c.SEEK_SET); try std.testing.expectEqual(frames_on_end, frames_on_end_by_end); log.debug("frames on end: {}, frame on end (2): {}", .{ frames_on_end, frames_on_end_by_end }); return file.?; } pub fn swrite(file: *c.SNDFILE, buf: [*]f32, frames: i64) !void { const count = c.sf_writef_float(file, buf, frames); if (count != frames) { log.debug("Wanted to write {}, got {}", .{ frames, count }); return ImageError.WriteFail; } } pub fn sseek(file: *c.SNDFILE, offset: usize) void { const offset_i64 = @intCast(i64, offset); const frames = c.sf_seek(file, offset_i64, c.SEEK_SET); const frames_current = c.sf_seek(file, 0, c.SEEK_CUR); std.debug.assert(frames == frames_current); if (frames != offset_i64) { log.debug("failed to seek to {} (seeked {} frames, offset_i64={})", .{ offset, frames, offset_i64 }); } } /// Caller owns the returned memory. pub fn temporaryName(allocator: std.mem.Allocator) ![]u8 { const template_start = "/temp/temp_"; const template = "/tmp/temp_XXXXXXXXXXXXXXXXXXXXX"; var nam = try allocator.alloc(u8, template.len); std.mem.copy(u8, nam, template); const seed = @truncate(u64, @bitCast(u128, std.time.nanoTimestamp())); var r = std.rand.DefaultPrng.init(seed); var fill = nam[template_start.len..nam.len]; var i: usize = 0; while (i < 100) : (i += 1) { // generate a random uppercase letter, that is, 65 + random number. for (fill) |_, f_idx| { var idx = @intCast(u8, r.random().uintLessThan(u5, 24)); var letter = @as(u8, 65) + idx; fill[f_idx] = letter; } // if we fail to access it, we assume it doesn't exist and return it. var tmp_file: std.fs.File = std.fs.cwd().openFile( nam, .{ .mode = .read_only }, ) catch |err| { if (err == error.FileNotFound) return nam else continue; }; // if we actually found someone, close the handle so that we don't // get EMFILE later on. tmp_file.close(); } return error.TempGenFail; } pub fn mkSfInfo() c.SF_INFO { return c.SF_INFO{ .frames = @as(c_int, 0), .samplerate = @as(c_int, 44100), .channels = @as(c_int, 1), .format = c.SF_FORMAT_ULAW | c.SF_FORMAT_RAW | c.SF_ENDIAN_BIG, .sections = @as(c_int, 0), .seekable = @as(c_int, 0), }; } pub const Image = struct { allocator: std.mem.Allocator, /// Pointer to the underlying libsndfile's SNDFILE struct. sndfile: *c.SNDFILE, /// Current sound file's framecount. frames: usize, /// The original image file path. path: []const u8, /// Represents the current path being worked on. curpath: []const u8, /// Open a BMP image for later. pub fn open(allocator: std.mem.Allocator, path: []const u8) !*Image { var in_fmt = mkSfInfo(); var sndfile = try sopen(allocator, path, c.SFM_READ, &in_fmt); var image = try allocator.create(Image); std.debug.assert(in_fmt.frames > @as(i64, 0)); std.debug.assert(in_fmt.seekable == @as(i32, 1)); image.* = Image{ .allocator = allocator, .sndfile = sndfile, .path = path, .curpath = path, .frames = @intCast(usize, in_fmt.frames), }; return image; } pub fn clone(self: *Image) !*Image { var in_fmt = mkSfInfo(); // clone sndfile var sndfile = try sopen(self.allocator, self.curpath, c.SFM_READ, &in_fmt); std.debug.assert(self.frames == @intCast(usize, in_fmt.frames)); var image = try self.allocator.create(Image); std.debug.assert(in_fmt.frames > @as(i64, 0)); std.debug.assert(in_fmt.seekable == @as(i32, 1)); image.* = Image{ .allocator = self.allocator, .sndfile = sndfile, .path = self.path, .curpath = self.curpath, .frames = @intCast(usize, in_fmt.frames), }; return image; } pub fn close(self: *Image) void { var st: i32 = c.sf_close(self.sndfile); if (st != 0) { log.debug("Failed to close {s} ({s})", .{ self.path, c.sf_error_number(st), }); } self.allocator.free(self.path); self.allocator.free(self.curpath); var allocator = self.allocator; self.* = undefined; allocator.destroy(self); } pub fn read(self: *Image, file_chans: c_int, buf: []f32) bool { const n_read: c.sf_count_t = c.sf_readf_float(self.sndfile, buf.ptr, 1); const buf_chans = @intCast(c_int, buf.len); var i = file_chans - 1; while (i < buf_chans) : (i += 1) { //buf[@intCast(usize, i)] = buf[i % file_chans]; buf[@intCast(usize, i)] = buf[@intCast(usize, @mod(i, file_chans))]; } return n_read == 1; } /// Copy bytes from the current file to out_file. fn copyBytes( self: *Image, out_file: *c.SNDFILE, start: usize, end: usize, ) !void { var buf = try self.allocator.alloc(f32, BufferSize); defer self.allocator.free(buf); var i: usize = start; // we do sf_seek() calls to make sure we are actually on the start // and actually end at the end position for the file. sseek(self.sndfile, start); sseek(out_file, start); while (i <= end) : (i += buf.len) { log.debug("\t\ti={d}, buf.len={d}, end={d}", .{ i, buf.len, end }); sseek(self.sndfile, i); sseek(out_file, i); const bytes_until_end = end - i; var read_bytes: i64 = undefined; var view: []f32 = buf[0..buf.len]; if (bytes_until_end < buf.len) { read_bytes = c.sf_readf_float(self.sndfile, buf.ptr, @intCast(i64, bytes_until_end)); view = buf[0..bytes_until_end]; } else { read_bytes = c.sf_readf_float(self.sndfile, buf.ptr, @intCast(i64, buf.len)); } try swrite(out_file, view.ptr, @intCast(i64, view.len)); } sseek(self.sndfile, end); sseek(out_file, end); } fn getSeekPos(self: *Image, position: plugins.Position) plugins.SeekPos { const file_end = self.frames; var seek_pos = position.seekPos(file_end); log.debug("\tstart {d} end {d}", .{ seek_pos.start, seek_pos.end }); return seek_pos; } pub fn reopen(self: *Image, path: []const u8) !void { var in_fmt = mkSfInfo(); self.sndfile = try sopen(self.allocator, path, c.SFM_READ, &in_fmt); // std.testing.expectEqual(self.frames, @intCast(usize, in_fmt.frames)); self.curpath = path; self.frames = @intCast(usize, in_fmt.frames); log.debug("\timage: reopened on '{s}' (frames={d}, fmt.frames={d})", .{ self.curpath, self.frames, in_fmt.frames, }); } pub fn checkValid(self: *Image) !void { var file = try std.fs.cwd().openFile(self.path, .{ .mode = .read_only }); defer file.close(); // main bmp header: // 2 bytes for magic header // 4 bytes for size in bytes // 2 bytes ? // 2 bytes ? // 4 bytes for pixel array offset var magic = [2]u8{ 0, 0 }; _ = try file.read(&magic); if (std.mem.endsWith(u8, self.path, ".bmp")) try bmp.magicValid(&magic); } /// Run a plugin over the image. /// This setups a new lilv world/plugin among other things. /// The internal SNDFILE pointer is modified to point to the output of the /// plugin run. pub fn runPlugin( self: *Image, plugin_uri: []const u8, position: plugins.Position, params: plugins.ParamList, ) !void { var timer = try std.time.Timer.start(); var ctx = try plugins.makeContext(self.allocator, plugin_uri); defer ctx.deinit(); var ports = try lv2.setupPorts(&ctx); defer ctx.allocator.free(ports); if (ctx.n_audio_in > 2) { log.debug("plugin <{s}> has more than two inputs.", .{plugin_uri}); return ImageError.InvalidPlugin; } if (ctx.n_audio_out > 2) { log.debug("plugin <{s}> has more than two outputs.", .{plugin_uri}); return ImageError.InvalidPlugin; } // now, for each param for the plugin, we find its port, and set // the value for the port there. for (params.items) |param| { var sym_cstr = try std.cstr.addNullByte(self.allocator, param.sym); defer self.allocator.free(sym_cstr); var sym = c.lilv_new_string(ctx.world, sym_cstr.ptr); const port = c.lilv_plugin_get_port_by_symbol(ctx.plugin, sym) orelse { log.debug("assert fail: symbol {s} not found on port", .{param.sym}); return ImageError.InvalidSymbol; }; c.lilv_node_free(sym); var idx = c.lilv_port_get_index(ctx.plugin, port); log.debug("\tset sym={s}, idx={d} to val={}", .{ param.sym, idx, param.value, }); (&ports[idx]).value = param.value; } // now we need to generate a temporary file and put the output of // running the plugin on that file var tmpnam = try temporaryName(self.allocator); log.debug("\trunning plugin from '{s}' to '{s}'", .{ self.curpath, tmpnam }); var out_fmt = mkSfInfo(); var out_file = try sopen(self.allocator, tmpnam, c.SFM_WRITE, &out_fmt); var rctx = try plugins.RunContext.init(self.allocator, ctx.plugin); defer rctx.deinit(); rctx.connectPorts(ports); lv2.lilv_instance_activate(rctx.instance); // now that we have everything setup, we need to make the part where we // just copy the original image and the part where we run the plugin // over the image. const seek_pos = self.getSeekPos(position); // there are four main stages: // - the bmp header copy // - pre-plugin // - plugin // - post-plugin // pre-plugin copy, merged with bmp header copy try self.copyBytes( out_file, @as(usize, 0), seek_pos.start, ); sseek(self.sndfile, seek_pos.start); var i: usize = seek_pos.start; log.debug("\tseek pos start: {d} end: {d}", .{ seek_pos.start, seek_pos.end }); var inbuf = &rctx.buffers.in; var outbuf = &rctx.buffers.out; while (i <= seek_pos.end) : (i += 1) { inbuf[0] = 0; inbuf[1] = 0; const read_bytes = c.sf_readf_float(self.sndfile, inbuf, 1); if (read_bytes == 0) { log.debug("WARN! reached EOF at idx={d}", .{i}); break; } // trick plugins into having correct stereo signal from // my mono input inbuf[1] = inbuf[0]; lv2.lilv_instance_run(rctx.instance, 1); try swrite(out_file, outbuf, 1); } sseek(self.sndfile, seek_pos.end); // post-plugin copy try self.copyBytes( out_file, seek_pos.end + 1, self.frames, ); c.sf_write_sync(out_file); _ = c.sf_close(out_file); _ = c.sf_close(self.sndfile); try self.reopen(tmpnam); try self.checkValid(); var time_taken = timer.read(); log.debug("\ttook {d:.2}ms running plugin", .{time_taken / std.time.us_per_ms}); } pub fn saveTo(self: *Image, out_path: []const u8) !void { log.debug("\timg: copy from '{s}' to '{s}'", .{ self.curpath, out_path }); try std.fs.copyFileAbsolute(self.curpath, out_path, .{}); } pub fn runCustomPlugin( self: *Image, comptime Plugin: type, position: plugins.Position, extra: anytype, ) !void { var plugin_opt: ?Plugin = Plugin.init(self.allocator, extra); if (plugin_opt == null) { return ImageError.PluginLoadFail; } var plugin = plugin_opt.?; defer plugin.deinit(); const decls = comptime std.meta.declarations(Plugin); inline for (decls) |decl| { if (comptime std.mem.eql(u8, decl.name, "setup")) { try plugin.setup(); } } // the code here is a copypaste of runPlugin() without the specific // lilv things. var tmpnam = try temporaryName(self.allocator); log.debug("\trunning CUSTOM plugin from '{s}' to '{s}'", .{ self.curpath, tmpnam }); var out_fmt = mkSfInfo(); var out_file = try sopen(self.allocator, tmpnam, c.SFM_WRITE, &out_fmt); var bufs = plugins.RunBuffers{}; const seek_pos = self.getSeekPos(position); // make sure we start from 0 sseek(self.sndfile, 0); // there are four main stages: // - the bmp header copy // - pre-plugin // - CUSTOM plugin // - post-plugin // pre-plugin copy, merged with bmp header copy try self.copyBytes( out_file, @as(usize, 0), seek_pos.start, ); sseek(self.sndfile, seek_pos.start); var i: usize = seek_pos.start; log.debug("\tseek pos start: {d} end: {d}", .{ seek_pos.start, seek_pos.end }); var inbuf = &bufs.in; var outbuf = &bufs.out; while (i <= seek_pos.end) : (i += 1) { const read_bytes = c.sf_readf_float(self.sndfile, inbuf, 1); if (read_bytes == 0) { log.debug("WARN! reached EOF at idx={d}", .{i}); break; } plugin.run(&bufs); try swrite(out_file, outbuf, 1); } sseek(self.sndfile, seek_pos.end); // post-plugin copy try self.copyBytes( out_file, seek_pos.end + 1, self.frames, ); c.sf_write_sync(out_file); _ = c.sf_close(out_file); _ = c.sf_close(self.sndfile); // reopen the file as SFM_READ so we can run plugin chains etc try self.reopen(tmpnam); try self.checkValid(); } };