diff --git a/examples/middle_echo.scri b/examples/middle_echo.scri index 18e29f6..45bc8d1 100644 --- a/examples/middle_echo.scri +++ b/examples/middle_echo.scri @@ -1,3 +1,4 @@ load :0; -echo 3 0 1 0.2; +amp 3 1 10; +# echo 3 0 1 0.2; quicksave; diff --git a/src/image.zig b/src/image.zig index 0def9ba..f165896 100644 --- a/src/image.zig +++ b/src/image.zig @@ -1,9 +1,20 @@ const std = @import("std"); +const lv2 = @import("lv2_helpers.zig"); + const c = @cImport({ @cInclude("sndfile.h"); + + @cInclude("lilv/lilv.h"); + @cInclude("lv2/core/lv2.h"); }); -pub const ImageError = error{OpenFail}; +const plugins = @import("plugin.zig"); + +pub const ImageError = error{ + OpenFail, + InvalidPlugin, + UnknownPlugin, +}; /// Low level integration function with libsndfile. fn sopen( @@ -70,4 +81,35 @@ pub const Image = struct { ); } } + + 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; + } + + pub fn runPlugin( + self: *Image, + plugin_uri: []const u8, + pos: plugins.Position, + params: plugins.ParamList, + ) !void { + var context = try plugins.makeContext(self.allocator, plugin_uri); + std.debug.warn("world: {}\n", context.world); + std.debug.warn("plugin: {}\n", context.plugin); + + var ports = try lv2.setupPorts(&context); + for (ports) |port| { + std.debug.warn("port: {}\n", port.*); + } + } }; diff --git a/src/lang.zig b/src/lang.zig index 8b91e8f..ed5b402 100644 --- a/src/lang.zig +++ b/src/lang.zig @@ -3,6 +3,7 @@ const std = @import("std"); pub const ParseError = error{ NoCommandGiven, UnknownCommand, + ArgRequired, }; pub const CommandType = enum { @@ -19,6 +20,32 @@ pub const Command = struct { pub fn print(self: *const Command) void { std.debug.warn("cmd:{}\n", self.command); } + + pub fn argAt(self: *const Command, idx: usize) ![]const u8 { + const args = self.args.toSliceConst(); + + if (idx > (args.len - 1)) { + std.debug.warn("Expected argument at index {}\n", idx); + return ParseError.ArgRequired; + } + + return args[idx]; + } + + pub fn usizeArgAt(self: *const Command, idx: usize) !usize { + var arg = try self.argAt(idx); + return try std.fmt.parseInt(usize, arg, 10); + } + + pub fn intArgAt(self: *const Command, idx: usize) !i32 { + var arg = try self.argAt(idx); + return try std.fmt.parseInt(i32, arg, 10); + } + + pub fn floatArgAt(self: *const Command, idx: usize) !f32 { + var arg = try self.argAt(idx); + return try std.fmt.parseFloat(f32, arg); + } }; pub const CommandList = std.ArrayList(*Command); @@ -55,6 +82,7 @@ pub const Lang = struct { stmt = std.mem.trimLeft(u8, stmt, "\n"); if (stmt.len == 0) continue; + if (stmt[0] == '#') continue; // TODO better tokenizer instead of just tokenize(" "); var tok_it = std.mem.tokenize(stmt, " "); diff --git a/src/lv2_helpers.zig b/src/lv2_helpers.zig new file mode 100644 index 0000000..8a6dc57 --- /dev/null +++ b/src/lv2_helpers.zig @@ -0,0 +1,150 @@ +const std = @import("std"); +const plugin = @import("plugin.zig"); + +pub const c = @cImport({ + @cInclude("lilv/lilv.h"); + @cInclude("lv2/core/lv2.h"); +}); + +const LV2_CORE_URI = "http://lv2plug.in/ns/lv2core"; + +pub fn Lv2Core(ns: []const u8) ![]const u8 { + var allocator = std.heap.direct_allocator; + + return try std.cstr.addNullByte( + allocator, + try std.fmt.allocPrint(allocator, "{}{}", LV2_CORE_URI, ns), + ); +} + +pub fn lilv_instance_connect_port( + instance: [*c]c.LilvInstance, + port_index: u32, + data_location: ?*c_void, +) void { + instance.?.*.lv2_descriptor.?.*.connect_port.?(instance.?.*.lv2_handle, port_index, data_location); +} + +pub fn lilv_instance_activate(instance: [*c]c.LilvInstance) void { + if (instance.?.*.lv2_descriptor.?.*.activate != null) { + instance.?.*.lv2_descriptor.?.*.activate.?(instance.?.*.lv2_handle); + } +} + +pub fn lilv_instance_run(instance: [*c]c.LilvInstance, sample_count: u32) void { + instance.?.*.lv2_descriptor.?.*.run.?(instance.?.*.lv2_handle, sample_count); +} + +pub fn lilv_instance_deactivate(instance: [*c]c.LilvInstance) void { + if (instance.?.*.lv2_descriptor.?.*.deactivate != null) { + instance.?.*.lv2_descriptor.?.*.deactivate.?(instance.?.*.lv2_handle); + } +} + +pub const PortType = enum { + Control, + Audio, +}; + +pub const Port = struct { + lilv_port: ?*const c.LilvPort, + ptype: PortType, + index: u32, + value: f32, + is_input: bool, + optional: bool, +}; + +/// Setup ports for a given plugin. Gives an array to pointers of Port structs. +/// This setup is required so we link the plugin to the ports later on, and +/// also link our buffers, and control values. +pub fn setupPorts(ctx: *plugin.Context) ![]*Port { + var world = ctx.world; + const n_ports: u32 = c.lilv_plugin_get_num_ports(ctx.plugin); + + var ports = try ctx.allocator.alloc(*Port, n_ports); + + for (ports) |port_ptr, idx| { + var port = try ctx.allocator.create(Port); + port.* = Port{ + .lilv_port = null, + .ptype = .Control, + .index = f32(0), + .value = f32(0), + .is_input = false, + .optional = false, + }; + + ports[idx] = port; + } + + var values: []f32 = try ctx.allocator.alloc(f32, n_ports); + defer ctx.allocator.free(values); + + c.lilv_plugin_get_port_ranges_float(ctx.plugin, null, null, values.ptr); + + // bad solution, but it really do be like that + const LV2_CORE__InputPort = try Lv2Core("#InputPort"); + const LV2_CORE__OutputPort = try Lv2Core("#OutputPort"); + const LV2_CORE__AudioPort = try Lv2Core("#AudioPort"); + const LV2_CORE__ControlPort = try Lv2Core("#ControlPort"); + const LV2_CORE__connectionOptional = try Lv2Core("#connectionOptional"); + + 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 = ports[i]; + + const lport = c.lilv_plugin_get_port_by_index(ctx.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(ctx.plugin, lport, lv2_connectionOptional); + + if (c.lilv_port_is_a(ctx.plugin, lport, lv2_InputPort)) { + port.is_input = true; + } else if (!c.lilv_port_is_a(ctx.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(ctx.plugin, lport, lv2_ControlPort)) { + port.ptype = .Control; + } else if (c.lilv_port_is_a(ctx.plugin, lport, lv2_AudioPort)) { + port.ptype = .Audio; + + if (port.is_input) { + ctx.n_audio_in += 1; + } else { + ctx.n_audio_out += 1; + } + } else if (!port.optional) { + std.debug.warn("Port {} has unsupported type\n", i); + return error.UnsupportedPortType; + } + } + + return ports; +} diff --git a/src/main.zig b/src/main.zig index ddfeea5..2b2fcf6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -60,30 +60,6 @@ const Param = struct { value: f32, }; -pub fn lilv_instance_connect_port( - instance: [*c]c.LilvInstance, - port_index: u32, - data_location: ?*c_void, -) void { - instance.?.*.lv2_descriptor.?.*.connect_port.?(instance.?.*.lv2_handle, port_index, data_location); -} - -pub fn lilv_instance_activate(instance: [*c]c.LilvInstance) void { - if (instance.?.*.lv2_descriptor.?.*.activate != null) { - instance.?.*.lv2_descriptor.?.*.activate.?(instance.?.*.lv2_handle); - } -} - -pub fn lilv_instance_run(instance: [*c]c.LilvInstance, sample_count: u32) void { - instance.?.*.lv2_descriptor.?.*.run.?(instance.?.*.lv2_handle, sample_count); -} - -pub fn lilv_instance_deactivate(instance: [*c]c.LilvInstance) void { - if (instance.?.*.lv2_descriptor.?.*.deactivate != null) { - instance.?.*.lv2_descriptor.?.*.deactivate.?(instance.?.*.lv2_handle); - } -} - const ParamList = std.ArrayList(Param); const PortType = enum { diff --git a/src/plugin.zig b/src/plugin.zig new file mode 100644 index 0000000..a555b11 --- /dev/null +++ b/src/plugin.zig @@ -0,0 +1,56 @@ +const std = @import("std"); + +const c = @import("lv2_helpers.zig").c; + +const ImageError = @import("image.zig").ImageError; + +/// Control port +pub const Param = struct { + /// Port symbol + sym: []const u8, + + /// Control value + value: f32, +}; + +/// List of parameters to be set to control ports. +pub const ParamList = std.ArrayList(Param); + +/// Represents a relative position in the image +pub const Position = struct { + split: usize, + index: usize, +}; + +/// Represents the starting context for a single plugin run. +pub const Context = struct { + allocator: *std.mem.Allocator, + world: *c.LilvWorld, + plugin: ?*const c.LilvPlugin, + + // they should both be 1. + n_audio_in: usize = 0, + n_audio_out: usize = 0, +}; + +pub fn makeContext(allocator: *std.mem.Allocator, plugin_uri: []const u8) !Context { + const cstr_plugin_uri = try std.cstr.addNullByte(allocator, plugin_uri); + var world = c.lilv_world_new().?; + + c.lilv_world_load_all(world); + var uri: *c.LilvNode = c.lilv_new_uri(world, cstr_plugin_uri.ptr) orelse blk: { + std.debug.warn("Invalid plugin URI <{}>\n", plugin_uri); + return ImageError.InvalidPlugin; + }; + + const plugins: *const c.LilvPlugins = c.lilv_world_get_all_plugins(world); + + var plugin: *const c.LilvPlugin = c.lilv_plugins_get_by_uri(plugins, uri) orelse blk: { + std.debug.warn("Plugin <{}> not found\n", plugin_uri); + return ImageError.UnknownPlugin; + }; + + c.lilv_node_free(uri); + + return Context{ .allocator = allocator, .world = world, .plugin = plugin }; +} diff --git a/src/runner.zig b/src/runner.zig index 90ccd08..0be0451 100644 --- a/src/runner.zig +++ b/src/runner.zig @@ -1,6 +1,7 @@ const std = @import("std"); const lang = @import("lang.zig"); const images = @import("image.zig"); +const plugin = @import("plugin.zig"); const Image = images.Image; @@ -15,7 +16,9 @@ pub const Runner = struct { image: ?*Image = null, pub fn init(allocator: *std.mem.Allocator) Runner { - return Runner{ .allocator = allocator }; + return Runner{ + .allocator = allocator, + }; } pub fn deinit(self: *Runner) void { @@ -70,6 +73,8 @@ pub const Runner = struct { return RunError.NoBMP; } + // TODO, copy load_path into a temporary file, then use that + // file to work on things. self.image = try Image.open(self.allocator, load_path); } @@ -135,6 +140,24 @@ pub const Runner = struct { std.debug.warn("out path: {}\n", out_path); } + /// Run the http://lv2plug.in/plugins/eg-amp plugin over the file. + fn ampCmd(self: *Runner, split: usize, index: usize, gain: f32) !void { + var image = try self.getImage(); + var param = try self.allocator.create(plugin.Param); + defer self.allocator.destroy(param); + + param.* = plugin.Param{ .sym = "gain", .value = gain }; + + var params = plugin.ParamList.init(self.allocator); + defer params.deinit(); + + try image.runPlugin( + "http://lv2plug.in/plugins/eg-amp", + plugin.Position{ .split = split, .index = index }, + params, + ); + } + fn runCommand(self: *Runner, cmd: *lang.Command) !void { return switch (cmd.command) { .Noop => {}, @@ -143,8 +166,15 @@ pub const Runner = struct { try self.loadCmd(path); break :blk; }, - .Quicksave => try self.quicksaveCmd(), + + .Amp => blk: { + const split = try cmd.usizeArgAt(0); + const index = try cmd.usizeArgAt(1); + const gain = try cmd.floatArgAt(2); + try self.ampCmd(split, index, gain); + }, + else => blk: { std.debug.warn("Unknown command: {}\n", cmd.command); break :blk RunError.UnknownCommand;