const std = @import("std"); const lv2 = @import("lv2_helpers.zig"); const c = lv2.c; const bmp = @import("bmp_valid.zig"); 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) { std.debug.warn( "Failed to open {} ({})\n", 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); std.testing.expectEqual(fmt.frames, frames_on_end); const frames_on_end_by_end = c.sf_seek(file, frames_on_end, c.SEEK_SET); std.testing.expectEqual(frames_on_end, frames_on_end_by_end); std.debug.warn("frames on end: {}, frame on end (2): {}\n", 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) { std.debug.warn("Wanted to write {}, got {}\n", 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.testing.expectEqual(frames, frames_current); if (frames != offset_i64) { std.debug.warn("failed to seek to {} (seeked {} frames, offset_i64={})\n", 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_XXXXXXXXXXX"; var nam = try allocator.alloc(u8, template.len); std.mem.copy(u8, nam, template); var r = std.rand.DefaultPrng.init(std.time.timestamp()); 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. std.fs.File.access(nam) catch |err| { if (err == error.FileNotFound) { return nam; } }; } 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.testing.expectEqual(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 { //self.allocator.free(self.path); //self.allocator.free(self.curpath); var st: i32 = c.sf_close(self.sndfile); if (st != 0) { std.debug.warn( "Failed to close {} ({})\n", self.path, c.sf_error_number(st), ); } } pub fn read(self: *Image, file_chans: c_int, buf: []f32) bool { var file = file_opt.?; const n_read: c.sf_count_t = c.sf_readf_float(file, 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) { std.debug.warn("\t\ti={}, buf.len={}, end={}\n", 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); std.debug.warn("\tstart {} end {}\n", 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); std.debug.warn( "\timage: reopened on '{}' (frames={}, fmt.frames={})\n", self.curpath, self.frames, in_fmt.frames, ); } pub fn checkValid(self: *Image) !void { var file = try std.fs.File.openRead(self.path); 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); if (ctx.n_audio_in > 2) { std.debug.warn("plugin <{}> has more than two inputs.\n", plugin_uri); return ImageError.InvalidPlugin; } if (ctx.n_audio_out > 2) { std.debug.warn("plugin <{}> has more than two outputs.\n", plugin_uri); return ImageError.InvalidPlugin; } // now, for each param for the plugin, we find its port, and set // the value for the port there. var it = params.iterator(); while (it.next()) |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 blk: { std.debug.warn("assert fail: symbol {} not found on port\n", param.sym); return ImageError.InvalidSymbol; }; c.lilv_node_free(sym); var idx = c.lilv_port_get_index(ctx.plugin, port); std.debug.warn( "\tset sym={}, idx={} to val={}\n", 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); std.debug.warn("\trunning plugin from '{}' to '{}'\n", 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; std.debug.warn("\tseek pos start: {} end: {}\n", seek_pos.start, seek_pos.end); var inbuf = rctx.buffers.in; var outbuf = rctx.buffers.out; while (i <= seek_pos.end) : (i += 1) { const read_bytes = c.sf_readf_float(self.sndfile, inbuf.ptr, 1); if (read_bytes == 0) { std.debug.warn("WARN! reached EOF at idx={}\n", i); break; } lv2.lilv_instance_run(rctx.instance, 1); try swrite(out_file, outbuf.ptr, 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(); std.debug.warn("\ttook {d:.2}ms running plugin\n", time_taken / std.time.millisecond); } pub fn saveTo(self: *Image, out_path: []const u8) !void { std.debug.warn("\timg: copy from '{}' to '{}'\n", self.curpath, out_path); try std.fs.copyFile(self.curpath, out_path); } pub fn runCustomPlugin( self: *Image, comptime Plugin: type, position: plugins.Position, comptime ExtraType: type, extra: ExtraType, ) !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); std.debug.warn("\trunning CUSTOM plugin from '{}' to '{}'\n", self.curpath, tmpnam); var out_fmt = mkSfInfo(); var out_file = try sopen(self.allocator, tmpnam, c.SFM_WRITE, &out_fmt); var bufs = try plugins.RunBuffers.init(self.allocator); defer bufs.deinit(); 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; std.debug.warn("\tseek pos start: {} end: {}\n", 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, bufs.in.ptr, 1); if (read_bytes == 0) { std.debug.warn("WARN! reached EOF at idx={}\n", i); break; } plugin.run(&bufs); try swrite(out_file, bufs.out.ptr, 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(); } };