diff --git a/.gitignore b/.gitignore index 3cef7be..c0c219b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,124 @@ -zig-cache/ +# 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/ diff --git a/build.zig b/build.zig deleted file mode 100644 index 65518df..0000000 --- a/build.zig +++ /dev/null @@ -1,20 +0,0 @@ -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 new file mode 100644 index 0000000..e4c2998 --- /dev/null +++ b/scritcher/__init__.py @@ -0,0 +1,2 @@ +from .main import main +__all__ = ['main'] diff --git a/scritcher/error.py b/scritcher/error.py new file mode 100644 index 0000000..52507c6 --- /dev/null +++ b/scritcher/error.py @@ -0,0 +1,2 @@ +class InterpreterError(Exception): + pass diff --git a/scritcher/executer.py b/scritcher/executer.py new file mode 100644 index 0000000..2b3a8a1 --- /dev/null +++ b/scritcher/executer.py @@ -0,0 +1,78 @@ +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 new file mode 100644 index 0000000..8a16764 --- /dev/null +++ b/scritcher/image.py @@ -0,0 +1,17 @@ +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 new file mode 100644 index 0000000..572c068 --- /dev/null +++ b/scritcher/main.py @@ -0,0 +1,32 @@ +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 new file mode 100644 index 0000000..fe5d775 --- /dev/null +++ b/scritcher/utils.py @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000..852efa9 --- /dev/null +++ b/setup.py @@ -0,0 +1,14 @@ +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 deleted file mode 100644 index fc61a2c..0000000 --- a/src/main.zig +++ /dev/null @@ -1,501 +0,0 @@ -// 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; -}