const std = @import("std"); const lv2 = @import("lv2_helpers.zig"); const c = lv2.c; const custom = @import("custom.zig"); const plugins = @import("plugin.zig"); /// Approximate size of the BMP header, in bytes. pub const BMPHeaderSize: usize = 82000; /// 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. 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; } return file.?; } 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 read {}, got {}\n", frames, count); return ImageError.WriteFail; } } fn sseek(file: *c.SNDFILE, offset: usize) void { const frames = c.sf_seek(file, @intCast(i64, offset), c.SEEK_SET); if (frames != @intCast(i64, offset)) { std.debug.warn("failed to seek to {}\n", offset); } } fn sf_tell(file: *c.SNDFILE) i64 { var frames = c.sf_seek(file, 0, c.SEEK_CUR); std.debug.warn("\t\t{} frames\n", frames); return -frames; } /// 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 = 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; } fn mkSfInfo() c.SF_INFO { return c.SF_INFO{ .frames = c_int(0), .samplerate = c_int(44100), .channels = c_int(1), .format = c.SF_FORMAT_ULAW | c.SF_FORMAT_RAW | c.SF_ENDIAN_BIG, .sections = c_int(0), .seekable = 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 > i64(0)); std.debug.assert(in_fmt.seekable == i32(1)); image.* = Image{ .allocator = allocator, .sndfile = sndfile, .path = path, .curpath = path, .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); self.curpath = path; self.frames = @intCast(usize, in_fmt.frames); std.debug.warn("\timage: reopened on '{}'\n", self.curpath); } /// 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, 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); 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, params: *plugins.ParamMap, ) !void { var plugin_opt: ?Plugin = Plugin.init(self.allocator, params); if (plugin_opt == null) { return ImageError.PluginLoadFail; } var plugin = plugin_opt.?; defer plugin.deinit(); // 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, 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); } };