// Copyright 2007-2016 David Robillard // Copyright 2019 Luna Mendes // // the main script is a fork/port of lv2apply, licensed under the ISC license. // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. const std = @import("std"); const c = @cImport({ @cInclude("assert.h"); @cInclude("math.h"); @cInclude("sndfile.h"); @cInclude("stdarg.h"); @cInclude("stdio.h"); @cInclude("stdlib.h"); @cInclude("string.h"); @cInclude("lilv/lilv.h"); @cInclude("lv2/core/lv2.h"); @cDefine("LILV_VERSION", "0.24.4"); }); const LV2_CORE_URI = "http://lv2plug.in/ns/lv2core"; fn Lv2Core(comptime ns: []const u8) []const u8 { return (LV2_CORE_URI ++ ns)[0..]; } const LV2_CORE__InputPort = Lv2Core("#InputPort"); const LV2_CORE__OutputPort = Lv2Core("#OutputPort"); const LV2_CORE__AudioPort = Lv2Core("#AudioPort"); const LV2_CORE__ControlPort = Lv2Core("#ControlPort"); const LV2_CORE__connectionOptional = Lv2Core("#connectionOptional"); /// Control port value set from the command line const Param = struct { /// Port symbol sym: []const u8, /// Control value value: f32, }; const ParamList = std.ArrayList(Param); const PortType = enum { Control, Audio, }; /// Runtime port information. const Port = struct { lilv_port: *const c.LilvPort, ptype: PortType, index: u32, value: f32, is_input: bool, optional: bool, }; /// Application state const LV2Apply = struct { allocator: *std.mem.Allocator, world: *c.LilvWorld = undefined, plugin: *const c.LilvPlugin = undefined, instance: *c.LilvInstance = undefined, in_path: []u8 = undefined, out_path: []u8 = undefined, in_file: *c.SNDFILE = undefined, out_file: *c.SNDFILE = undefined, n_params: u32, params: ParamList, n_audio_in: u32, n_audio_out: u32, ports: []*Port = undefined, pub fn deinit(self: *LV2Apply) void { sclose(self.in_path, self.in_file); sclose(self.out_path, self.out_file); c.lilv_instance_free(self.instance); c.lilv_world_free(self.world); self.allocator.free(self.ports); self.params.deinit(); } pub fn createPorts(self: *LV2Apply) !i32 { var world = self.world; const n_ports: u32 = c.lilv_plugin_get_num_ports(self.plugin); self.ports = try self.allocator.realloc(self.ports, n_ports); var values: []f32 = try self.allocator.alloc(f32, n_ports); defer self.allocator.free(values); c.lilv_plugin_get_port_ranges_float(self.plugin, null, null, values.ptr); var lv2_InputPort = c.lilv_new_uri(world, LV2_CORE__InputPort.ptr); defer std.heap.c_allocator.destroy(lv2_InputPort); var lv2_OutputPort = c.lilv_new_uri(world, LV2_CORE__OutputPort.ptr); defer std.heap.c_allocator.destroy(lv2_OutputPort); var lv2_AudioPort = c.lilv_new_uri(world, LV2_CORE__AudioPort.ptr); defer std.heap.c_allocator.destroy(lv2_AudioPort); var lv2_ControlPort = c.lilv_new_uri(world, LV2_CORE__ControlPort.ptr); defer std.heap.c_allocator.destroy(lv2_ControlPort); var lv2_connectionOptional = c.lilv_new_uri(world, LV2_CORE__connectionOptional.ptr); defer std.heap.c_allocator.destroy(lv2_connectionOptional); var i: u32 = 0; while (i < n_ports) : (i += 1) { var port: *Port = self.ports[i]; const lport = c.lilv_plugin_get_port_by_index(self.plugin, i).?; port.lilv_port = lport; port.index = i; if (std.math.isNan(values[i])) { port.value = f32(0); } else { port.value = values[i]; } port.optional = c.lilv_port_has_property(self.plugin, lport, lv2_connectionOptional); if (c.lilv_port_is_a(self.plugin, lport, lv2_InputPort)) { port.is_input = true; } else if (!c.lilv_port_is_a(self.plugin, lport, lv2_OutputPort) and !port.optional) { std.debug.warn("Port {} is neither input or output\n", i); return error.UnassignedIOPort; } // check if port is an audio or control port if (c.lilv_port_is_a(self.plugin, lport, lv2_ControlPort)) { port.ptype = .Control; } else if (c.lilv_port_is_a(self.plugin, lport, lv2_AudioPort)) { port.ptype = .Audio; if (port.is_input) { self.n_audio_in += 1; } else { self.n_audio_out += 1; } } else if (!port.optional) { std.debug.warn("Port {} has unsupported type\n", i); return error.UnsupportedPortType; } } return 0; } }; fn sopen(path: []const u8, mode: i32, fmt: *c.SF_INFO) ?*c.SNDFILE { var file = c.sf_open(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 null; } return file; } fn sclose(path: []const u8, file: *c.SNDFILE) void { var st: i32 = c.sf_close(file); if (st != 0) { std.debug.warn( "Failed to close {} ({})\n", path, c.sf_error_number(st), ); } } ///Read a single frame from a file into an interleaved buffer. ///If more channels are required than are available in the file, the remaining ///channels are distributed in a round-robin fashion (LRLRL). fn sread(file: *c.SNDFILE, file_chans: c_int, buf: []f32) bool { 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; } fn print_version() !void { const stdout_file = try std.io.getStdOut(); try stdout_file.write("lv2apply (lilv) 0.24.4\n"); try stdout_file.write("Copyright 2007-2016 David Robillard \n"); try stdout_file.write("Copyright 2019 Luna Mendes \n"); } fn print_usage() !void { const stdout_file = try std.io.getStdOut(); try stdout_file.write("Usage: lv2apply [OPTION]... PLUGIN_URI\n"); try stdout_file.write(" -i IN_FILE Input file\n"); try stdout_file.write(" -o OUT_FILE Output file\n"); try stdout_file.write(" -o OUT_FILE Output file\n"); try stdout_file.write("-c SYM VAL Control value\n"); try stdout_file.write("--help print help\n"); try stdout_file.write("--version display version\n"); } pub fn main() !void { std.debug.warn("awoo.\n"); var arena = std.heap.ArenaAllocator.init(std.heap.c_allocator); const allocator = &arena.allocator; var in_path: ?[]u8 = null; var out_path: ?[]u8 = null; var self = LV2Apply{ .allocator = allocator, .n_params = 0, .params = ParamList.init(allocator), .n_audio_in = 0, .n_audio_out = 0, .ports = try allocator.alloc(*Port, 0), }; defer self.deinit(); var args_it = std.process.args(); _ = args_it.skip(); var plugin_uri_opt: ?[]u8 = null; while (args_it.next(allocator)) |arg_err| { var arg = try arg_err; if (std.mem.eql(u8, arg, "--version")) { try print_version(); return; } else if (std.mem.eql(u8, arg, "--help")) { try print_usage(); return; } else if (std.mem.eql(u8, arg, "-i")) { in_path = try args_it.next(allocator).?; } else if (std.mem.eql(u8, arg, "-o")) { out_path = try args_it.next(allocator).?; } else if (std.mem.eql(u8, arg, "-c")) { var param_name = try args_it.next(allocator).?; var value_arg = try args_it.next(allocator).?; var param_value = try std.fmt.parseFloat(f32, value_arg); try self.params.append(Param{ .sym = param_name, .value = param_value }); } else if (arg[0] == '-') { try print_usage(); return; } else { plugin_uri_opt = arg; } } if (in_path == null or out_path == null or plugin_uri_opt == null) { try print_usage(); return; } self.in_path = in_path.?; self.out_path = out_path.?; var plugin_uri = plugin_uri_opt.?; self.world = c.lilv_world_new().?; var uri: *c.LilvNode = c.lilv_new_uri(self.world, plugin_uri.ptr) orelse blk: { std.debug.warn("Invalid plugin URI <{}>\n", plugin_uri); return; }; c.lilv_world_load_all(self.world); const plugins: *const c.LilvPlugins = c.lilv_world_get_all_plugins(self.world); const plugin_opt = c.lilv_plugins_get_by_uri(plugins, uri); c.lilv_node_free(uri); if (plugin_opt) |plugin| { self.plugin = plugin; } else { std.debug.warn("Plugin <{}> not found\n", plugin_uri); return; } var in_fmt = 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), }; var in_file_opt = sopen(self.in_path, c.SFM_READ, &in_fmt); if (in_file_opt) |in_file| { self.in_file = in_file; } else { std.debug.warn("Failed to call sopen().\n"); return; } _ = try self.createPorts(); if (self.n_audio_in == 0 or (in_fmt.channels != @intCast(c_int, self.n_audio_in) and in_fmt.channels != 1)) { std.debug.warn("unable to map {} inputs to {} ports\n", in_fmt.channels, self.n_audio_in); } var it = self.params.iterator(); while (it.next()) |param| { var sym = c.lilv_new_string(self.world, param.sym.ptr); const port = c.lilv_plugin_get_port_by_symbol(self.plugin, sym); c.lilv_node_free(sym); std.debug.warn("param control: set {} to {}\n", param.sym, param.value); self.ports[c.lilv_port_get_index(self.plugin, port)].value = param.value; } var out_fmt = c.SF_INFO{ .frames = c_int(0), .samplerate = c_int(44100), .channels = @intCast(c_int, self.n_audio_out), .format = c.SF_FORMAT_ULAW | c.SF_FORMAT_RAW, .sections = c_int(0), .seekable = c_int(0), }; var out_file_opt = sopen(self.out_path, c.SFM_WRITE, &out_fmt); if (out_file_opt) |out_file| { self.out_file = out_file; } else { std.debug.warn("Failed to call sopen() for outfile.\n"); return; } const n_ports: u32 = c.lilv_plugin_get_num_ports(self.plugin); var in_buf = try self.allocator.alloc(f32, self.n_audio_in); var out_buf = try self.allocator.alloc(f32, self.n_audio_out); var instance_opt = c.lilv_plugin_instantiate( self.plugin, @intToFloat(f64, in_fmt.samplerate), null, ); if (instance_opt) |instance| { self.instance = instance; } else { std.debug.warn("failed to instantiate\n"); return; } var i: u32 = 0; var o: u32 = 0; for (self.ports) |port, p_idx| { var ptype = port.ptype; var p = @intCast(u32, p_idx); if (ptype == .Control) { c.lilv_instance_connect_port(self.instance, p, &port.value); } else if (ptype == .Audio) { if (port.is_input) { c.lilv_instance_connect_port(self.instance, p, &in_buf[i]); i += 1; } else { c.lilv_instance_connect_port(self.instance, p, &in_buf[o]); o += 1; } } else { c.lilv_instance_connect_port(self.instance, p, null); } } // Ports are now connected to buffers in interleaved format, so we can run // a single frame at a time and avoid having to interleave buffers to // read/write from/to sndfile. std.debug.warn("almost there!\n"); c.lilv_instance_activate(self.instance); const START = 2 * 44100; // for the first START frames, copy from in to out i = 0; while (i < START) : (i += 1) { _ = sread(self.in_file, in_fmt.channels, in_buf); _ = c.sf_writef_float(self.out_file, in_buf.ptr, 1); } // seek to START then run plugin over the rest var seeked = c.sf_seek(self.in_file, START, c.SEEK_SET); std.debug.warn("{} seeked frames\n", seeked); while (sread(self.in_file, in_fmt.channels, in_buf)) { c.lilv_instance_run(self.instance, 1); if (c.sf_writef_float(self.out_file, out_buf.ptr, 1) != 1) { std.debug.warn("failed to write to output file\n"); return; } } c.lilv_instance_deactivate(self.instance); return; }