scritcher/src/main.zig

534 lines
16 KiB
Zig
Raw Normal View History

2019-07-07 05:26:05 +00:00
// Copyright 2007-2016 David Robillard <http://drobilla.net>
// Copyright 2019 Luna Mendes <https://l4.pm>
//
// 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.
2019-07-05 19:59:45 +00:00
const std = @import("std");
2019-07-08 02:03:55 +00:00
const langs = @import("lang.zig");
2019-07-05 19:59:45 +00:00
2019-07-07 05:26:05 +00:00
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(ns: []const u8) ![]const u8 {
var allocator = std.heap.direct_allocator;
2019-07-07 05:26:05 +00:00
return try std.cstr.addNullByte(
allocator,
try std.fmt.allocPrint(allocator, "{}{}", LV2_CORE_URI, ns),
);
}
2019-07-07 05:26:05 +00:00
2019-07-07 18:04:25 +00:00
fn makeCStr(data: []const u8) ![]u8 {
var allocator = std.heap.direct_allocator;
return std.cstr.addNullByte(allocator, data);
}
2019-07-07 05:26:05 +00:00
/// Control port value set from the command line
const Param = struct {
/// Port symbol
sym: []const u8,
/// Control value
value: f32,
};
2019-07-07 17:26:49 +00:00
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);
}
}
2019-07-07 05:26:05 +00:00
const ParamList = std.ArrayList(Param);
const PortType = enum {
Control,
Audio,
};
/// Runtime port information.
const Port = struct {
2019-07-07 18:54:38 +00:00
lilv_port: ?*const c.LilvPort,
2019-07-07 05:26:05 +00:00
ptype: PortType,
index: u32,
value: f32,
is_input: bool,
optional: bool,
};
/// Application state
const LV2Apply = struct {
allocator: *std.mem.Allocator,
2019-07-07 17:26:49 +00:00
world: ?*c.LilvWorld = null,
plugin: ?*const c.LilvPlugin = null,
instance: ?*c.LilvInstance = null,
in_path: ?[]u8 = undefined,
out_path: ?[]u8 = undefined,
2019-07-07 05:26:05 +00:00
2019-07-07 17:26:49 +00:00
in_file: ?*c.SNDFILE = null,
out_file: ?*c.SNDFILE = null,
2019-07-07 05:26:05 +00:00
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);
2019-07-07 17:26:49 +00:00
if (self.instance) |instance| {
c.lilv_instance_free(self.instance);
}
if (self.world) |world| {
c.lilv_world_free(self.world);
}
2019-07-07 05:26:05 +00:00
self.allocator.free(self.ports);
self.params.deinit();
}
2019-07-07 18:04:25 +00:00
pub fn makeCStr(self: *LV2Apply, data: []const u8) ![]u8 {
return std.cstr.addNullByte(self.allocator, data);
}
2019-07-07 05:26:05 +00:00
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);
2019-07-07 18:54:38 +00:00
for (self.ports) |port_ptr, idx| {
var port = try self.allocator.create(Port);
port.* = Port{
.lilv_port = null,
.ptype = .Control,
.index = f32(0),
.value = f32(0),
.is_input = false,
.optional = false,
};
self.ports[idx] = port;
}
2019-07-07 05:26:05 +00:00
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);
// 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");
2019-07-07 05:26:05 +00:00
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];
2019-07-07 18:54:38 +00:00
2019-07-07 05:26:05 +00:00
const lport = c.lilv_plugin_get_port_by_index(self.plugin, i).?;
port.lilv_port = lport;
port.index = i;
2019-07-07 18:54:38 +00:00
2019-07-07 05:26:05 +00:00
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;
}
};
2019-07-07 17:26:49 +00:00
fn sopen(path_opt: ?[]const u8, mode: i32, fmt: *c.SF_INFO) ?*c.SNDFILE {
if (path_opt == null) return null;
var path = path_opt.?;
2019-07-07 05:26:05 +00:00
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;
}
2019-07-07 17:26:49 +00:00
fn sclose(path_opt: ?[]const u8, file_opt: ?*c.SNDFILE) void {
if (path_opt == null) return;
if (file_opt == null) return;
var path = path_opt.?;
var file = file_opt.?;
2019-07-07 05:26:05 +00:00
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).
2019-07-07 17:26:49 +00:00
fn sread(file_opt: ?*c.SNDFILE, file_chans: c_int, buf: []f32) bool {
var file = file_opt.?;
2019-07-07 05:26:05 +00:00
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 <http://drobilla.net>\n");
try stdout_file.write("Copyright 2019 Luna Mendes <http://l4.pm>\n");
}
fn print_usage() !void {
const stdout_file = try std.io.getStdOut();
try stdout_file.write("Usage: lv2apply [OPTION]... PLUGIN_URI\n");
2019-07-07 17:26:49 +00:00
try stdout_file.write("\t-i IN_FILE Input file\n");
try stdout_file.write("\t-o OUT_FILE Output file\n");
try stdout_file.write("\t-c SYM VAL Control value\n");
try stdout_file.write("\t--help print help\n");
try stdout_file.write("\t--version display version\n");
2019-07-07 05:26:05 +00:00
}
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.c_allocator);
2019-07-08 02:03:55 +00:00
const allocator = &arena.allocator;
var lang = langs.Lang.init(allocator);
//defer lang.deinit();
var args_it = std.process.args();
const exe_name = try (args_it.next(allocator) orelse @panic("expected exe name"));
// args[1] is the path to scri file
const scri_path = try (args_it.next(allocator) orelse @panic("expected scri path"));
std.debug.warn("path: '{}'\n", scri_path);
var file = try std.fs.File.openRead(scri_path);
defer file.close();
// sadly, we read it all into memory. such is life
const total_bytes = try file.getEndPos();
var data = try allocator.alloc(u8, total_bytes);
_ = try file.read(data);
2019-07-08 03:09:34 +00:00
var cmds = try lang.parse(data);
2019-07-08 02:03:55 +00:00
var it = cmds.iterator();
while (it.next()) |cmd| {
cmd.print();
}
}
pub fn oldMain() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.c_allocator);
2019-07-07 05:26:05 +00:00
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;
}
2019-07-07 18:04:25 +00:00
self.in_path = try self.makeCStr(in_path.?);
self.out_path = try self.makeCStr(out_path.?);
var plugin_uri = try self.makeCStr(plugin_uri_opt.?);
2019-07-07 05:26:05 +00:00
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| {
2019-07-07 18:54:38 +00:00
var param_sym = try self.makeCStr(param.sym);
var sym = c.lilv_new_string(self.world, param_sym.ptr);
const port = c.lilv_plugin_get_port_by_symbol(self.plugin, sym).?;
2019-07-07 05:26:05 +00:00
c.lilv_node_free(sym);
2019-07-07 18:54:38 +00:00
var idx = c.lilv_port_get_index(self.plugin, port);
self.ports[idx].value = param.value;
2019-07-07 05:26:05 +00:00
}
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) {
2019-07-07 17:26:49 +00:00
lilv_instance_connect_port(self.instance, p, &port.value);
2019-07-07 05:26:05 +00:00
} else if (ptype == .Audio) {
if (port.is_input) {
2019-07-07 17:26:49 +00:00
lilv_instance_connect_port(self.instance, p, &in_buf[i]);
2019-07-07 05:26:05 +00:00
i += 1;
} else {
2019-07-07 19:05:10 +00:00
lilv_instance_connect_port(self.instance, p, &out_buf[o]);
2019-07-07 05:26:05 +00:00
o += 1;
}
} else {
2019-07-07 17:26:49 +00:00
lilv_instance_connect_port(self.instance, p, null);
2019-07-07 05:26:05 +00:00
}
}
// 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.
2019-07-07 17:26:49 +00:00
lilv_instance_activate(self.instance);
2019-07-07 05:26:05 +00:00
2019-07-07 19:05:10 +00:00
const START = 10 * 44100;
2019-07-07 05:26:05 +00:00
// 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)) {
2019-07-07 17:26:49 +00:00
lilv_instance_run(self.instance, 1);
2019-07-07 19:05:10 +00:00
2019-07-07 05:26:05 +00:00
if (c.sf_writef_float(self.out_file, out_buf.ptr, 1) != 1) {
std.debug.warn("failed to write to output file\n");
return;
}
}
2019-07-07 17:26:49 +00:00
lilv_instance_deactivate(self.instance);
2019-07-07 05:26:05 +00:00
return;
2019-07-05 19:59:45 +00:00
}