diff --git a/.gitignore b/.gitignore index c0c219b..3cef7be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,124 +1 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ +zig-cache/ diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..65518df --- /dev/null +++ b/build.zig @@ -0,0 +1,20 @@ +const Builder = @import("std").build.Builder; + +pub fn build(b: *Builder) void { + const mode = b.standardReleaseOptions(); + const exe = b.addExecutable("scritcher", "src/main.zig"); + exe.setBuildMode(mode); + exe.install(); + + exe.linkSystemLibrary("lilv-0"); + exe.linkSystemLibrary("sndfile"); + exe.linkSystemLibrary("c"); + + exe.addIncludeDir("/usr/include/lilv-0"); + + const run_cmd = exe.run(); + run_cmd.step.dependOn(b.getInstallStep()); + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); +} diff --git a/scritcher/__init__.py b/scritcher/__init__.py deleted file mode 100644 index e4c2998..0000000 --- a/scritcher/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .main import main -__all__ = ['main'] diff --git a/scritcher/error.py b/scritcher/error.py deleted file mode 100644 index 52507c6..0000000 --- a/scritcher/error.py +++ /dev/null @@ -1,2 +0,0 @@ -class InterpreterError(Exception): - pass diff --git a/scritcher/executer.py b/scritcher/executer.py deleted file mode 100644 index 2b3a8a1..0000000 --- a/scritcher/executer.py +++ /dev/null @@ -1,78 +0,0 @@ -import os -import shlex -import tempfile -import shutil - -from pathlib import Path -from PIL import Image - -from .utils import load_file_path -from .error import InterpreterError -from .image import GlitchImage - - -class Interpreter: - """Interpreter for scritcher instructions/statements.""" - def __init__(self): - self.orig_path = None - self.img = None - - def _cmd_noop(self): - pass - - def _cmd_load(self, loadpath: str): - source_path = load_file_path(loadpath) - self.orig_path = source_path - - # create a temporary file to hold bmp data - handle, bmp_path = tempfile.mkstemp('.bmp') - os.close(handle) - - self.img = GlitchImage(source_path, Path(bmp_path)) - self.img.load() - - def _cmd_quicksave(self): - suffix = self.img.original.suffix - name = self.img.original.name.replace(suffix, '') - parent_folder = self.orig_path.parents[0] - - # we need to search all files that match the pattern NAME_g* - # then we save on the one _g suffix above it. - - # e.g if the original path is "miya.png", we want to search for - # files named "miya_g1.raw", "miya_g2.raw", and then we want to write - # on "miya_g3.raw". - index = 0 - for glitched_out in parent_folder.glob(f'{name}_g*'): - try: - idx = int(glitched_out.name.strip(name + '_g')[0]) - - # glob() doesnt seem to show a stable order. anyways, we can - # just only update our index when its the maximum found - if idx > index: - index = idx - except (IndexError, ValueError): - continue - - # create our next glitched path - out_path = shutil.copyfile( - self.img.path, parent_folder / f'{name}_g{index + 1}.raw') - print('saved to', out_path) - - - def run(self, line: str): - """Run a single line.""" - print(f'running {line!r}') - - args = shlex.split(line) - command = args[0] - - if command != 'load' and self.img is None: - print('warn: no file loaded.') - - try: - method = getattr(self, f'_cmd_{command}') - except AttributeError: - raise InterpreterError(f'Command {command!r} not found') - - method(*args[1:]) diff --git a/scritcher/image.py b/scritcher/image.py deleted file mode 100644 index 8a16764..0000000 --- a/scritcher/image.py +++ /dev/null @@ -1,17 +0,0 @@ -import logging -from PIL import Image - -log = logging.getLogger(__name__) - -class GlitchImage: - """A wrapper class around PIL.Image""" - def __init__(self, orig_path, target_path): - self.original = orig_path - self.path = target_path - - def load(self): - """Load the given image, convert it to BMP, and write it on the - given target path.""" - log.info('opening %r into %r', self.original, self.path) - img = Image.open(self.original) - img.save(self.path, 'BMP') diff --git a/scritcher/main.py b/scritcher/main.py deleted file mode 100644 index 572c068..0000000 --- a/scritcher/main.py +++ /dev/null @@ -1,32 +0,0 @@ -import sys -import logging -from pathlib import Path - -from .executer import Interpreter -from .error import InterpreterError - -logging.basicConfig(level=logging.DEBUG) - -def main(): - try: - scri_path = Path(sys.argv[1]).resolve() - except IndexError: - print(f'usage: {sys.argv[0]} path/to/file.scri') - return - - full_text = scri_path.read_text() - full_text = full_text.replace('\n', '') - stmts = full_text.split(';') - - interp = Interpreter() - - try: - for stmt in stmts: - if not stmt: - continue - - interp.run(stmt) - - print('OK') - except InterpreterError as err: - print(f'Interpreter error. {err.args[0]!r}') diff --git a/scritcher/utils.py b/scritcher/utils.py deleted file mode 100644 index fe5d775..0000000 --- a/scritcher/utils.py +++ /dev/null @@ -1,16 +0,0 @@ -import sys -from pathlib import Path - -from .error import InterpreterError - -def load_file_path(arg: str) -> Path: - """load given argument.""" - - try: - index = int(arg.split(':')[1]) - except ValueError: - raise InterpreterError('Invalid argument index') - except IndexError: - return Path(arg).resolve() - - return Path(sys.argv[2 + index]) diff --git a/setup.py b/setup.py deleted file mode 100644 index 852efa9..0000000 --- a/setup.py +++ /dev/null @@ -1,14 +0,0 @@ -from setuptools import setup - -setup( - name='scritcher', - version='0.1', - py_modules=['scritcher'], - install_requires=[ - 'Pillow==6.0.0', - ], - entry_points=''' - [console_scripts] - scritcher=scritcher:main - ''', -) diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..fc61a2c --- /dev/null +++ b/src/main.zig @@ -0,0 +1,501 @@ +// 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(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), + ); +} + +fn makeCStr(data: []const u8) ![]u8 { + var allocator = std.heap.direct_allocator; + return std.cstr.addNullByte(allocator, data); +} + +/// Control port value set from the command line +const Param = struct { + /// Port symbol + sym: []const u8, + + /// Control value + 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 { + 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 = null, + plugin: ?*const c.LilvPlugin = null, + instance: ?*c.LilvInstance = null, + + in_path: ?[]u8 = undefined, + out_path: ?[]u8 = undefined, + + in_file: ?*c.SNDFILE = null, + out_file: ?*c.SNDFILE = null, + + 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); + + if (self.instance) |instance| { + c.lilv_instance_free(self.instance); + } + if (self.world) |world| { + c.lilv_world_free(self.world); + } + + self.allocator.free(self.ports); + self.params.deinit(); + } + + pub fn makeCStr(self: *LV2Apply, data: []const u8) ![]u8 { + return std.cstr.addNullByte(self.allocator, data); + } + + 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); + + 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; + } + + 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"); + + 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_opt: ?[]const u8, mode: i32, fmt: *c.SF_INFO) ?*c.SNDFILE { + if (path_opt == null) return null; + + var path = path_opt.?; + 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_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.?; + + 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_opt: ?*c.SNDFILE, 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; +} + +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("\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"); +} + +pub fn main() !void { + 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 = try self.makeCStr(in_path.?); + self.out_path = try self.makeCStr(out_path.?); + var plugin_uri = try self.makeCStr(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 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).?; + c.lilv_node_free(sym); + + var idx = c.lilv_port_get_index(self.plugin, port); + self.ports[idx].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) { + lilv_instance_connect_port(self.instance, p, &port.value); + } else if (ptype == .Audio) { + if (port.is_input) { + lilv_instance_connect_port(self.instance, p, &in_buf[i]); + i += 1; + } else { + lilv_instance_connect_port(self.instance, p, &out_buf[o]); + o += 1; + } + } else { + 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. + lilv_instance_activate(self.instance); + + const START = 10 * 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)) { + 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; + } + } + + lilv_instance_deactivate(self.instance); + return; +}