diff --git a/.gitignore b/.gitignore index 346c11d..3cef7be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1 @@ zig-cache/ -zig-out/ -*.mp3 -*.wav -build_runner.zig diff --git a/README.md b/README.md index 7d4da76..1d172d9 100644 --- a/README.md +++ b/README.md @@ -9,24 +9,20 @@ glitch art "framework", ???????? language??? something? # how do? -## build depedencies (headers included): +## build depedencies: - zig at https://ziglang.org - libc, lilv and libsndfile - - graphicsmagick for the `rotate` command - - readline (for repl) + - an appreciation for glitched anime girls on your hard drive + - optional: imagemagick to convert from whatever to bmp -## plugin depedencies (only required at runtime): - - lv2 default plugins (most specifically the eg-amp plugin, - will likely be there by default) +## plugin depedencies: + - lv2 default plugins (most specifically the eg-amp plugin) - the SWH plugins ( https://github.com/swh/lv2 ) - the Invada Studio plugins ( https://launchpad.net/invada-studio/ ) - - abGate plugin - - MDA plugins - - Calf plugins ```bash # build and install -# assumes lilv headers are at /usr/include/lilv-0, edit build.zig if different +# assumes lilv headers are at /usr/include/lilv-0, check build.zig if different git clone https://gitdab.com/luna/scritcher.git cd scritcher @@ -35,33 +31,14 @@ zig build install --prefix ~/.local/ # on your input image file convert blah.jpg blah.bmp -# if you got the lv2 default amp plugin, this is runnable scritcher examples/middle_amp.scri blah.bmp -# if not, the noise plugin does not depend on any lv2 plugin. -scritcher examples/noise.scri blah.bmp - // scritcher saves it on an incremental basis, so the first run will save to // blah_g1.bmp, the second saves to blah_g2.bmp, etc. $your_image_viewer blah_g1.bmp ``` -# using the repl +## todo -using repl works via `scritcher repl scri_file.scri input_image.bmp` - -you type commands as you'd write the specific scritcher commands -(`doc/README.md`), with four repl-specific ones (semicolons do not apply): - - `push`, to push the last written command to the queue - - `save`, to write the queue to the given `scri_file.scri` file - - `list`, to print the current contents of the queue - - `quit`, to exit - -After a non-REPL command, such as an effect, the program pointed by -`SCRITCHER_RUNNER` will run as argument to the `runqs` command. By default, -the program run will be `ristretto` (as it is my preffered image viewer, -considering it was able to handle when some images went broke) - -this allows for quicker iteration of commands, as you can type a command, see -the image changes as fast as possible, tweak its arguments, -and when satisfied, `push` it, and work on the next command, etc. + - search other plugins + - conquer the world for glitchy anime girls diff --git a/build.zig b/build.zig index 5f2ff5e..65518df 100644 --- a/build.zig +++ b/build.zig @@ -1,73 +1,20 @@ -const std = @import("std"); +const Builder = @import("std").build.Builder; -fn setupLinks(step: *std.Build.Step.Compile) void { - step.linkSystemLibrary("c"); +pub fn build(b: *Builder) void { + const mode = b.standardReleaseOptions(); + const exe = b.addExecutable("scritcher", "src/main.zig"); + exe.setBuildMode(mode); + exe.install(); - step.linkSystemLibrary("lilv-0"); - step.linkSystemLibrary("sndfile"); - step.linkSystemLibrary("readline"); + exe.linkSystemLibrary("lilv-0"); + exe.linkSystemLibrary("sndfile"); + exe.linkSystemLibrary("c"); - step.linkSystemLibrary("GraphicsMagickWand"); - step.linkSystemLibrary("GraphicsMagick"); + exe.addIncludeDir("/usr/include/lilv-0"); - step.addIncludePath(.{ .path = "/usr/include/GraphicsMagick" }); - step.addIncludePath(.{ .path = "/usr/include" }); - - const possible_lilv_include_dirs = [_][]const u8{ - "/usr/include/lilv-0/lilv", - "/usr/include/lilv-0", - }; - - var found_any_lilv = false; - - for (possible_lilv_include_dirs) |possible_lilv_dir| { - var possible_dir = std.fs.cwd().openDir(possible_lilv_dir, .{}) catch |err| { - std.debug.print("possible lilv {s} fail: {s}\n", .{ possible_lilv_dir, @errorName(err) }); - continue; - }; - - possible_dir.close(); - found_any_lilv = true; - - std.debug.print("found lilv at '{s}'\n", .{possible_lilv_dir}); - step.addIncludePath(.{ .path = possible_lilv_dir }); - } - - if (!found_any_lilv) { - std.debug.print("No LILV library was found :(\n", .{}); - @panic("no lilv found"); - } -} - -pub fn build(b: *std.Build) void { - const target = b.standardTargetOptions(.{}); - const optimize = b.standardOptimizeOption(.{}); - - const exe = b.addExecutable(.{ - .name = "scritcher", - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - }); - setupLinks(exe); - b.installArtifact(exe); - - const run_cmd = b.addRunArtifact(exe); - - if (b.args) |args| { - run_cmd.addArgs(args); - } + 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); - - const test_step = b.addTest(.{ - .root_source_file = b.path("src/main.zig"), - .target = target, - .optimize = optimize, - }); - setupLinks(test_step); - const run_unit_tests = b.addRunArtifact(test_step); - const test_cmd = b.step("test", "run unit tests"); - test_cmd.dependOn(&run_unit_tests.step); } diff --git a/doc/README.md b/doc/README.md index c02ebda..95995be 100644 --- a/doc/README.md +++ b/doc/README.md @@ -23,8 +23,7 @@ where in the file you want the plugin to be ran. so, if you did `plugin 3 1...`, it would split the file in 3, then get the part that is of index 1 (starting at 0). -**Keep in mind parts can start from either top or bottom of the image, -it depends of the file format** +**Keep in mind parts start from the bottom of the file.** ## `load path_or_arg` @@ -39,9 +38,8 @@ Run the eg-amp plugin over the given slice of the file. Run the Retro Flanger script from the SWH plugins. -Parameters: - - `delay_depth_avg`: Average stall (ms), 0..10, default 2.5 - - `law_freq`: Flange frequency (Hz), 0.5..8, default 1 + - `delay_depth_avg` is for the `Average stall (ms)` parameter of the plugin. + - `law_freq` is for the `Flange frequency` parameter of the plugin. ## `eq split index lo mid hi` @@ -91,21 +89,11 @@ Runs the Higher Quality Pitch Scaler from the SWH plugins. The `mult` parameter is the pitch coefficient, range from 0.5..2, default 1. -## `reverb split index roomLength roomWidth roomHeight sourceLR sourceFB listLR listFB hpf warmth diffusion` +## `reverb split index roomLength roomHeight sourceLR sourceFB listLR listFB hpf warmth diffusion` Run the Early Reflection Reverb from the Invada Studio plugins. -Parameters: - - `roomLength`: Room Length, 3..100, default 25 - - `roomWidth`: Room Length, 3..100, default 30 - - `roomHeight`: Room Length, 3..100, default 10 - - `sourceLR`: Source Pan, -1..1, default -0.01 - - `sourceFB`: Source (F/B), 0.5..1, default 0.8 - - `listLR`: Listener Pan, -1..1, default 0.01 - - `listFB`: Listener (F/B), 0..0.5, default 0.2 - - `hpf`: HPF (High Pass Filter), 20..2000, default 1000 (most likely you want 20) - - `warmth`: Warmth, 0..100, default 50 - - `diffusion`: Diffusion, 0..100, default 50 +**TODO** Parameter list ## `highpass split index freq gain noClip` @@ -116,7 +104,7 @@ Parameters: - `gain`: Gain, 0..12, default 0 - `noClip`: Soft Clip (assumed boolean), 0..1, default 0 -## `delay split index seed gain feedback_pc tap_count first_delay delay_range delay_scale delay_rand_pc gain_scale wet` +## `delay seed gain feedback_pc tap_count first_delay delay_range delay_scale delay_rand_pc gain_scale wet` Parameters: - `seed`: Random seed, 0..1000, default 0 @@ -130,7 +118,7 @@ Parameters: - `gain_scale`: Amplitude change, 0.2..5, default 1 - `wet`: Dry/wet mix, 0..1, default 1 -## `vinyl split index year warp click wear` +## TODO `vinyl split index year warp click wear` VyNil effect from SWH. @@ -161,16 +149,9 @@ Parameters: - `repeat_bytes`, Amount of bytes to preload with random data and repeat throughout the image slice -## `rotate deg bgfill` +## TODO `echo split index delay` -Rotate the image by `deg` degrees, filling the resulting triangles with `bgfill`. - -`bgfill` is a hex string, e.g `#000000`. - -## `embed split index path_to_file` - -embed an audio file in the given image. the file is opened with libsndfile, -which means some formats, like mp3, will not be opened. +Run an echo filter on the given loaded file. ## `quicksave` @@ -179,201 +160,3 @@ with a suffix on the filename (before extension). Doing consecutive `quicksave`s will not overwrite any files, the suffixes will just be different. - -## `gate split index switch threshold attack hold decay gaterange` - -**TODO:** find good parameters - - - switch (bool): 0..1, default 0 - - threshold (dB): -70..12, default -70 - - attack (ms): 0.1..500, default 30 - - hold (ms): 5..3000, default 500 - - decay (ms): 5..4000, default 1000 - - gaterange (dB): -90..-20, default -90 - -## `detune split index detune mix output latency` - -> A low-quality stereo pitch shifter for the sort of chorus and detune effects found on multi-effects hardware. - - - detune (cents, left channel is lowered in pitch, right channel is raised): 0..1, default 0.2 - - mix (wet/dry mix): 0..1, default 0.9 - - output (level trim): 0..1, default 0.5 - - latency (trade-off between latency and low-freq response): 0..1, default 0.5 - -other presets: - - stereo detune: 0.2 0.9 0.5 0.5 - - out of tune: 0.8 0.7 0.5 0.5 - - -## `overdrive split index drive muffle output` - -> Possible uses include adding body to drum loops, fuzz guitar, and that 'standing outside a nightclub' sound. This plug does not simulate valve distortion, and any attempt to process organ sounds through it will be extremely unrewarding! - - - drive (amount of distortion): 0..1, default 0 - - muffle (gentle low-pass filter): 0..1, default 0 - - output (level trim): 0..1, default 0.5 - -## `degrade split index headroom quant rate post_filt non_lin output` - -> Sample quality reduction - -**NOTE:** finding the right parameters is HARD for this plugin. - - - headroom (peak clipping threshold): 0..1, default 0.8 - - quant (bit depth, typically 8 or below for "telephone" quality): 0..1, default 0.5 - - rate (sample rate): 0..1, default 0.65 - - post_filt (low-pass filter to muffle the distortion): 0..1, default 0.9 - - non_lin (additional harmonic distortion "thickening"): 0..1, default 0.58 - - output: 0..1, default 0.5 - -## `repsycho split index tune fine decay thresh hold mix quality` - -**NOTE:** HARD to find good parameters - - - tune (coarse tune, semitones): 0..1, default 1 - - fine (fine tune, cents): 0..1, default 1 - - decay (adjust envelope of each trunk, a fast decay can be useful while setting up): 0..1, default 0.5 - - thresh (trigger level to divide the input into chunks): 0..1, default 0.6 - - hold (minimum chunk length): 0..1, default 0.45 - - mix (mix original signal with output): 0..1, default 1 - - quality (quality, bool. the high=1 setting uses smoother pitch-shifting and allows stereo): 0..1, default 0 - -## `talkbox split index wet dry carrier quality` - -> High resolution vocoder - - - wet: 0..1, default 0.5 - - dry: 0..1, default 0 - - carrier: 0..1, default 0 - - quality: 0..1, default 1 - -## `dyncomp split index enable hold inputgain threshold ratio attack release gain_min gain_max rms` - - - enable (bool): 0..1, default 1 - - hold (bool): 0..1, default 0 - - inputgain (dB): -10..30, default 0 - - threshold (dB): -50..-10, default -30 - - ratio (???): 0..1, default 0 - - attack (seconds): 0.001..0.1, default 0.01 - - release (seconds): 0.03..3.0, default 0.3 - - gain\_min (dB): -20..40 - - gain\_max (dB): -20..40 - - rms (signal level, dB): -80..10 - -## `thruzero split index rate mix feedback depth_mod` - -> Tape flanger and ADT - -> This plug simulates tape-flanging, where two copies of a signal cancel out completely as the tapes pass each other. It can also be used for other "modulated delay" effects such as phasing and simple chorusing. - - - rate (modulation rate, set to minimum for static comb filtering): 0..1, default 0.3 - - mix (wet/dry mix, set to 50% for complete cancelling): 0..1, default 0.47 - - feedback (add positive or negative feedback for harsher or "ringing" sound): 0..1, default 0.3 - - depth_mod (modulation depth, set to less than 100% to limit build up of low frequencies with feedback): 0..1, default 1 - -## `foverdrive split index drive` - -Fast Overdrive from SWH plugins. - - - drive: 1..3, default 1 - -## `gverb split index roomsize revtime damping drylevel earlylevel taillevel` - -GVerb algorithm from SWH plugins. - - - roomsize (meters): 1..300, default 75.75 - - revtime (reverb time, seconds): 0.1..30, default 7.575 - - damping: 0..1, default 0.5 - - inputbandwidth: 0..1, default 0.75 - - drylevel (dB): -70..0, default 0 - - earlylevel (dB): -70..0, default 0 - - taillevel (dB): -70..0, default -17.5 - -## `invert split index` - -> A utility plugin that inverts the signal, also (wrongly) known as a 180 degree phase shift. - -## `tapedelay split index speed da_db t1d t1a_db...` - -**TODO:** gives 0 output - -> Correctly models the tape motion and some of the smear effect, there is no simulation fo the head saturation yet, as I don't have a good model of it. When I get one I will add it. - -> The way the tape accelerates and decelerates gives a nicer delay effect for many purposes. - - - speed (inches/sec, 1=normal): 0..10, default 1 - - da\_db (dry level, dB): -90..0, default -90 - - t1d (tap 1 distance, inches): 0..4, default 0 - - t1a\_db (tap 1 level, dB): -90..0, default 0 - - t2d (tap 2 distance, inches): 0..4, default 1 - - t2a\_db (tap 2 level, dB): -90..0, default -90 - - t3d (tap 3 distance, inches): 0..4, default 2 - - t3a\_db (tap 3 level, dB): -90..0, default -90 - - t4d (tap 4 distance, inches): 0..4, default 3 - - t4a\_db (tap 4 level, dB): -90..0, default -90 - -## `moddelay split index base` - -> A delay whose tap can be modulated at audio rate. - - - base (base delay, seconds): 0..1, default 1 - -## `multichorus split index min_delay mod_depth mod_rate stereo voices vphase amount dry freq freq2 q overlap level_in level_out lfo` - -Calf Multi Chorus - - - `min_delay` (ms): 0.1..10, default 5 - - `mod_depth` (ms): 0.1..10, default 6 - - `mod_rate` (hz): 0.1..20, default 0.1 - - `stereo` (degrees): 0..360, default 180 - - `voices`: 1..8, default 4 - - `vphase` (inter-voice phase, degrees): 0..360, default 64 - - `amount`: 0..4, default 0.5 - - `dry`: 0..4, default 0.5 - - `freq` (center frq 1, hz): 10..20000, default 100 - - `freq2` (center frq 2, hz): 10..20000, default 5000 - - `q` (???): 0.125..8, default 0.125 - - `overlap`: 0..1, default 0.75 - - `level_in` (Input Gain): 0.0156250..64, default 1 - - `level_out` (Output Gain): 0.0156250..64, default 1 - - `lfo` (toggle): 0..1, default 1 - -## `saturator split index bypass level_in level_out mix drive blend lp_pre_freq hp_pre_fre lp_post_freq hp_post_freq p_freq p_level p_q pre post` - - - `bypass` (toggle): 0..1, default 0 - - `level_in` (Input Gain): 0.0156250..64, default 1 - - `level_out` (Output Gain): 0.0156250..64, default 1 - - `mix`: 0..1, default 1 - - `drive` (saturation, coef): 0.1..10, default 5 - - `blend` (coef): -10..10, default 10 - - `lp_pre_freq` (lowpass, hz): 10..20000, default 20000 - - `hp_pre_freq` (highpass, hz): 10..20000, default 10 - - `lp_post_freq` (lowpass, hz): 10..20000, default 20000 - - `hp_post_freq` (highpass, hz): 10..20000, default 10 - - `p_freq` (Tone, hz): 80..8000, default 2000 - - `p_level` (Amount): 0.0625..16, default 1 - - `p_q` (???, coef): 0.1..10, default 1 - - `pre` (Activate Pre, toggle): 0..1, default 0 - - `post` (Activate Post, toggle): 0..1, default 0 - -## `vintagedelay split index ...` - - - `level_in` (Input Gain): 0.0156250..64, default 1 - - `level_out` (Output Gain): 0.0156250..64, default 1 - - `subdiv` (int): 1..16, default 4 - - `time_l` (int): 1..16, default 3 - - `time_r` (int): 1..16, default 5 - - `feedback`: 0..1, default 0.5 - - `amount` (Wet): 0..4, default 0.25 - - `mix_mode` (enum): Stereo=0, Ping-Pong=1, L then R=2, R then L=3, default 1 - - `medium` (enum): Plain=0, Tape=1, Old Tape=2, default 1 - - `dry` (dry): 0..4, default 1 - - `width` (stereo width, strict): -1..1, default 1 - - `fragmentation` (enum): Repeating=0, Pattern=1, default 0 - - `pbeats` (Pattern Beats, int): 1..8, default 4 - - `pfrag` (Pattern Fragmentation, int): 1..8, default 4 - - `timing` (enum): BPM=0, ms=1, Hz=2, Sync=3, default 0 - - `bpm`: 30..300, default 120 - - `ms` (int): 10..2000, default 500 - - `hz`: 0.01..100, default 2 - - `bpm_host` (strict): 1..300, default 120 diff --git a/examples/degrade.scri b/examples/degrade.scri deleted file mode 100644 index 09c04c0..0000000 --- a/examples/degrade.scri +++ /dev/null @@ -1,8 +0,0 @@ -load :0; -degrade 8 1 0.8 0.5 0.65 0.9 0.58 0.5; -degrade 8 2 0.1 1 0.65 0.5 0.5 0.4; -degrade 8 3 0.1 1 0.65 0.9 0.58 0.5; -degrade 8 4 0 1 1 0 0 1; -degrade 8 5 0 1 1 0 0 0; -degrade 8 6 0 0 0 0 0 0; -quicksave; diff --git a/examples/detune.scri b/examples/detune.scri deleted file mode 100644 index 269e80d..0000000 --- a/examples/detune.scri +++ /dev/null @@ -1,3 +0,0 @@ -load :0; -detune 3 1 0.2 0.9 0.5 0.5; -quicksave; diff --git a/examples/dyncomp.scri b/examples/dyncomp.scri deleted file mode 100644 index d1d9e7b..0000000 --- a/examples/dyncomp.scri +++ /dev/null @@ -1,3 +0,0 @@ -load :0; -dyncomp 3 1 1 0 0 -30 0 0.01 0.3 0 0 0; -quicksave; diff --git a/examples/embed.scri b/examples/embed.scri deleted file mode 100644 index b78f7c9..0000000 --- a/examples/embed.scri +++ /dev/null @@ -1,3 +0,0 @@ -load :0; -embed 3 1 ./file.wav; -quicksave; diff --git a/examples/foverdrive.scri b/examples/foverdrive.scri deleted file mode 100644 index a4ac785..0000000 --- a/examples/foverdrive.scri +++ /dev/null @@ -1,3 +0,0 @@ -load :0; -foverdrive 3 1 100; -quicksave; diff --git a/examples/gate.scri b/examples/gate.scri deleted file mode 100644 index 7299d00..0000000 --- a/examples/gate.scri +++ /dev/null @@ -1,3 +0,0 @@ -load :0; -gate 3 1 0 -70 30 500 1000 -90; -quicksave; diff --git a/examples/gverb.scri b/examples/gverb.scri deleted file mode 100644 index f9aced0..0000000 --- a/examples/gverb.scri +++ /dev/null @@ -1,3 +0,0 @@ -load :0; -gverb 3 1 75.75 7.575 0.5 0.75 0 0 -17.5; -quicksave; diff --git a/examples/invert.scri b/examples/invert.scri deleted file mode 100644 index bd7b695..0000000 --- a/examples/invert.scri +++ /dev/null @@ -1,3 +0,0 @@ -load :0; -invert 3 1; -quicksave; diff --git a/examples/mbeq.scri b/examples/mbeq.scri index de0eef4..9afbee1 100644 --- a/examples/mbeq.scri +++ b/examples/mbeq.scri @@ -1,3 +1,3 @@ load :0; -mbeq 3 1 0 0 0 0.3 0 0 7 0 0 0 0 0 0.1 0 0; +mbeq 3 1 1 4 0.2 2.1; quicksave; diff --git a/examples/middle_amp.scri b/examples/middle_amp.scri index a5928fd..a1ed655 100644 --- a/examples/middle_amp.scri +++ b/examples/middle_amp.scri @@ -1,3 +1,8 @@ load :0; -amp 3 1 20; +amp 80 70 10; +amp 80 73 20; +amp 80 75 20; +amp 80 76 20; +amp 7 4 2; +amp 5 1 8; quicksave; diff --git a/examples/moddelay.scri b/examples/moddelay.scri deleted file mode 100644 index 202cafb..0000000 --- a/examples/moddelay.scri +++ /dev/null @@ -1,3 +0,0 @@ -load :0; -moddelay 3 1 1; -quicksave; diff --git a/examples/multichorus.scri b/examples/multichorus.scri deleted file mode 100644 index 2355d16..0000000 --- a/examples/multichorus.scri +++ /dev/null @@ -1,3 +0,0 @@ -load :0; -multichorus 3 1 5 6 0.1 180 4 64 0.5 0.5 100 5000 0.125 0.75 1 1 1; -quicksave; diff --git a/examples/overdrive.scri b/examples/overdrive.scri deleted file mode 100644 index 585f8ad..0000000 --- a/examples/overdrive.scri +++ /dev/null @@ -1,5 +0,0 @@ -load :0; -overdrive 5 1 0.5 0 0.4; -overdrive 5 2 0.6 0.2 0.3; -overdrive 5 3 0.7 0.3 0.2; -quicksave; diff --git a/examples/pitchscaler.scri b/examples/pitch.scri similarity index 100% rename from examples/pitchscaler.scri rename to examples/pitch.scri diff --git a/examples/repsycho.scri b/examples/repsycho.scri deleted file mode 100644 index 757c728..0000000 --- a/examples/repsycho.scri +++ /dev/null @@ -1,8 +0,0 @@ -load :0; -repsycho 8 1 1 1 0.5 0.6 0.45 1 0; -repsycho 8 2 1 1 1 0.6 0.45 1 0; -repsycho 8 3 1 1 0.5 0.8 0.45 1 0; -repsycho 8 4 1 1 0.5 0.6 0.1 1 0; -repsycho 8 5 1 1 0.5 0.6 0.45 0.5 0; -repsycho 8 6 1 1 0.5 0.6 0.45 1 1; -quicksave; diff --git a/examples/reverb.scri b/examples/reverb.scri index 61de89c..9148bf7 100644 --- a/examples/reverb.scri +++ b/examples/reverb.scri @@ -1,3 +1,3 @@ load :0; -reverb 3 1 25 30 10 0 0.8 0 0.2 1000 50 50; +reverb 3 1 25 30 0 0.8 0 0.2 1000 50 50; quicksave; diff --git a/examples/rflanger.scri b/examples/rflanger.scri deleted file mode 100644 index f1fe18d..0000000 --- a/examples/rflanger.scri +++ /dev/null @@ -1,3 +0,0 @@ -load :0; -rflanger 3 1 2.5 1; -quicksave; diff --git a/examples/rotate.scri b/examples/rotate.scri deleted file mode 100644 index acde24d..0000000 --- a/examples/rotate.scri +++ /dev/null @@ -1,3 +0,0 @@ -load :0; -rotate 30 #ff00ff; -quicksave; diff --git a/examples/saturator.scri b/examples/saturator.scri deleted file mode 100644 index 864c27a..0000000 --- a/examples/saturator.scri +++ /dev/null @@ -1,3 +0,0 @@ -load :0; -saturator 3 1 0 1 1 1 5 10 20000 10 20000 10 2000 1 1 0 0; -quicksave; diff --git a/examples/talkbox.scri b/examples/talkbox.scri deleted file mode 100644 index 2f3c386..0000000 --- a/examples/talkbox.scri +++ /dev/null @@ -1,3 +0,0 @@ -load :0; -talkbox 3 1 0.5 0 0 1; -quicksave; diff --git a/examples/tapedelay.scri b/examples/tapedelay.scri deleted file mode 100644 index 4b5d8c0..0000000 --- a/examples/tapedelay.scri +++ /dev/null @@ -1,3 +0,0 @@ -load :0; -tapedelay 3 1 1 -90 0 0 1 -90 2 -90 3 -90; -quicksave; diff --git a/examples/thruzero.scri b/examples/thruzero.scri deleted file mode 100644 index 99b4899..0000000 --- a/examples/thruzero.scri +++ /dev/null @@ -1,3 +0,0 @@ -load :0; -thruzero 3 1 0.3 0.47 0.3 1; -quicksave; diff --git a/examples/vintagedelay.scri b/examples/vintagedelay.scri deleted file mode 100644 index 156638a..0000000 --- a/examples/vintagedelay.scri +++ /dev/null @@ -1,3 +0,0 @@ -load :0; -vintagedelay 3 1 1 1 4 3 5 0.5 0.25 1 1 1 1 0 4 4 0 120 500 2 120; -quicksave; diff --git a/examples/write.scri b/examples/write.scri deleted file mode 100644 index f7906c3..0000000 --- a/examples/write.scri +++ /dev/null @@ -1,3 +0,0 @@ -load :0; -write 3 1 1; -quicksave; diff --git a/src/bmp_valid.zig b/src/bmp_valid.zig deleted file mode 100644 index 6e256e8..0000000 --- a/src/bmp_valid.zig +++ /dev/null @@ -1,25 +0,0 @@ -const std = @import("std"); -const log = std.log.scoped(.scritcher_bmp); -pub const BMPValidError = error{InvalidMagic}; - -const VALID_MAGICS = [_][]const u8{ - "BM", - "BA", - "CI", - "CP", - "IC", - "PT", -}; - -pub fn magicValid(magic: []const u8) !void { - var valid = false; - - for (VALID_MAGICS) |valid_magic| { - if (std.mem.eql(u8, magic, valid_magic)) valid = true; - } - - if (!valid) { - log.debug("\tINVALID HEADER: '{s}'", .{magic}); - return BMPValidError.InvalidMagic; - } -} diff --git a/src/custom.zig b/src/custom.zig index d942a3b..d1e7269 100644 --- a/src/custom.zig +++ b/src/custom.zig @@ -2,78 +2,74 @@ const std = @import("std"); const lv2 = @import("lv2_helpers.zig"); const plugins = @import("plugin.zig"); -const image = @import("image.zig"); -const log = std.log.scoped(.scritcher_custom); const c = lv2.c; const RunBuffers = plugins.RunBuffers; pub const RandomNoise = struct { r: std.rand.DefaultPrng, - rand_buf: ?[]f32 = null, - allocator: ?std.mem.Allocator = null, + rand_buf: ?[]f32, cnt: usize = 0, pub fn init( - allocator: std.mem.Allocator, - params: anytype, - ) ?RandomNoise { - var r = std.rand.DefaultPrng.init(params.seed); + allocator: *std.mem.Allocator, + params: *plugins.ParamMap, + ) !RandomNoise { + const seed = @floatToInt(u64, params.get("seed").?.value); + const fillbytes = @floatToInt(usize, params.get("fill_bytes").?.value); - if (params.fill_bytes > 0) { - var rand_buf = allocator.alloc(f32, params.fill_bytes) catch return null; + var r = std.rand.DefaultPrng.init(seed); - for (rand_buf, 0..) |_, idx| { - rand_buf[idx] = r.random().float(f32); + if (fillbytes > 0) { + var rand_buf = try allocator.alloc(f32, fillbytes); + + for (rand_buf) |_, idx| { + rand_buf[idx] = r.random.float(f32); } return RandomNoise{ .r = r, - .allocator = allocator, .rand_buf = rand_buf, }; } else { return RandomNoise{ .r = r, + .rand_buf = null, }; } } - pub fn deinit(self: *RandomNoise) void { - if (self.allocator == null) return; - if (self.rand_buf == null) return; - self.allocator.?.free(self.rand_buf.?); - } - pub fn run(self: *RandomNoise, bufs: *RunBuffers) void { if (self.rand_buf) |rand_buf| { if (self.cnt >= rand_buf.len) self.cnt = 0; bufs.out[0] = rand_buf[self.cnt]; self.cnt += 1; } else { - bufs.out[0] = self.r.random().float(f32); + bufs.out[0] = self.r.random.float(f32); } } }; pub const WildNoise = struct { r: std.rand.DefaultPrng, - rand_buf: ?[]f32 = null, - allocator: ?std.mem.Allocator = null, + rand_buf: ?[]f32, cnt: usize = 0, pub fn init( - allocator: std.mem.Allocator, - params: anytype, - ) ?WildNoise { - var r = std.rand.DefaultPrng.init(params.seed); + allocator: *std.mem.Allocator, + params: *plugins.ParamMap, + ) !WildNoise { + const seed = @floatToInt(u64, params.get("seed").?.value); + const fillbytes = @floatToInt(usize, params.get("fill_bytes").?.value); - if (params.fill_bytes > 0) { - var rand_buf = allocator.alloc(f32, params.fill_bytes) catch return null; + var r = std.rand.DefaultPrng.init(seed); - for (rand_buf, 0..) |_, idx| { - rand_buf[idx] = @as(f32, @floatFromInt(r.random().int(u1))); + if (fillbytes > 0) { + var rand_buf = try allocator.alloc(f32, fillbytes); + + for (rand_buf) |_, idx| { + rand_buf[idx] = @intToFloat(f32, r.random.int(u1)); } return WildNoise{ @@ -83,110 +79,18 @@ pub const WildNoise = struct { } else { return WildNoise{ .r = r, + .rand_buf = null, }; } } - pub fn deinit(self: *WildNoise) void { - if (self.allocator == null) return; - if (self.rand_buf == null) return; - self.allocator.?.free(self.rand_buf.?); - } - pub fn run(self: *WildNoise, bufs: *RunBuffers) void { if (self.rand_buf) |rand_buf| { if (self.cnt >= rand_buf.len) self.cnt = 0; bufs.out[0] = rand_buf[self.cnt]; self.cnt += 1; } else { - bufs.out[0] = @as(f32, @floatFromInt(self.r.random().int(u1))); + bufs.out[0] = @intToFloat(f32, self.r.random.int(u1)); } } }; - -/// Write any float to the image. -/// Keep in mind that the bit representation of the float will clash with -/// the format of BMP pixel data, which means writing 0 everywhere won't give -/// you the black color. -pub const Write = struct { - data: f32, - - pub fn init( - allocator: std.mem.Allocator, - params: anytype, - ) Write { - _ = allocator; - return Write{ - .data = params.data, - }; - } - - pub fn deinit(self: *Write) void { - _ = self; - } - - pub fn run(self: *Write, bufs: *RunBuffers) void { - bufs.out[0] = self.data; - } -}; - -pub const Embed = struct { - allocator: std.mem.Allocator, - filepath: []const u8, - - sndfile: *c.SNDFILE = undefined, - buf: []f32 = undefined, - - pub fn init(allocator: std.mem.Allocator, params: anytype) @This() { - return Embed{ - .allocator = allocator, - .filepath = params.path, - }; - } - - pub fn setup(self: *@This()) !void { - var in_fmt = c.SF_INFO{ - .frames = @as(c_int, 0), - .samplerate = @as(c_int, 0), - .channels = @as(c_int, 0), - .format = @as(c_int, 0), - .sections = @as(c_int, 0), - .seekable = @as(c_int, 0), - }; - - self.sndfile = try image.sopen( - self.allocator, - self.filepath, - c.SFM_READ, - &in_fmt, - ); - - image.sseek(self.sndfile, 0); - - self.buf = try self.allocator.alloc(f32, @as(usize, @intCast(in_fmt.channels))); - } - - pub fn deinit(self: *@This()) void { - _ = self; - } - - pub fn run(self: *@This(), bufs: *RunBuffers) void { - const read_bytes = c.sf_readf_float(self.sndfile, self.buf.ptr, 1); - - if (read_bytes == 0) { - bufs.out[0] = bufs.in[0]; - return; - } - - if (read_bytes < 0) { - const st: i32 = c.sf_error(self.sndfile); - log.debug("Failed to read {s} ({s})", .{ - self.filepath, - c.sf_error_number(st), - }); - return; - } - - bufs.out[0] = bufs.in[0] + self.buf[0]; - } -}; diff --git a/src/image.zig b/src/image.zig index e20cfe7..020d893 100644 --- a/src/image.zig +++ b/src/image.zig @@ -1,13 +1,14 @@ const std = @import("std"); const lv2 = @import("lv2_helpers.zig"); const c = lv2.c; -const bmp = @import("bmp_valid.zig"); -const log = std.log.scoped(.scritcher_image); const plugins = @import("plugin.zig"); -/// Buffer size for main image copying. -pub const BufferSize: usize = 300000; +/// Approximate size of the BMP header. +pub const BMPHeaderSize: usize = 82000; + +/// Buffer size for main copying +pub const BufferSize: usize = 60000; pub const ImageError = error{ OpenFail, @@ -16,278 +17,184 @@ pub const ImageError = error{ InvalidSymbol, InstantiateFail, WriteFail, - PluginLoadFail, }; /// Low level integration function with libsndfile. -pub fn sopen( - allocator: std.mem.Allocator, +fn sopen( + allocator: *std.mem.Allocator, path: []const u8, mode: i32, fmt: *c.SF_INFO, ) !*c.SNDFILE { - const cstr_path = try allocator.dupeZ(u8, path); + var cstr_path = try std.cstr.addNullByte(allocator, path); defer allocator.free(cstr_path); - const file = c.sf_open(cstr_path.ptr, mode, fmt); + var file = c.sf_open(cstr_path.ptr, mode, fmt); const st: i32 = c.sf_error(file); if (st != 0) { - log.debug("Failed to open {s} ({s})", .{ + std.debug.warn( + "Failed to open {} ({})\n", path, c.sf_error_number(st), - }); + ); return ImageError.OpenFail; } - const frames_on_end = c.sf_seek(file, 0, c.SEEK_END); - _ = c.sf_seek(file, 0, c.SEEK_SET); - try std.testing.expectEqual(fmt.frames, frames_on_end); - - const frames_on_end_by_end = c.sf_seek(file, frames_on_end, c.SEEK_SET); - try std.testing.expectEqual(frames_on_end, frames_on_end_by_end); - - log.debug("frames on end: {}, frame on end (2): {}", .{ frames_on_end, frames_on_end_by_end }); - return file.?; } -pub fn swrite(file: *c.SNDFILE, buf: [*]f32, frames: i64) !void { +fn swrite(file: *c.SNDFILE, buf: [*]f32, frames: i64) !void { const count = c.sf_writef_float(file, buf, frames); if (count != frames) { - log.debug("Wanted to write {}, got {}", .{ frames, count }); + std.debug.warn("Wanted to read {}, got {}\n", frames, count); return ImageError.WriteFail; } } -pub fn sseek(file: *c.SNDFILE, offset: usize) void { - const offset_i64 = @as(i64, @intCast(offset)); - const frames = c.sf_seek(file, offset_i64, c.SEEK_SET); - const frames_current = c.sf_seek(file, 0, c.SEEK_CUR); - std.debug.assert(frames == frames_current); - - if (frames != offset_i64) { - log.debug("failed to seek to {} (seeked {} frames, offset_i64={})", .{ offset, frames, offset_i64 }); - } +fn getEndPos(path: []const u8) !usize { + var file = try std.fs.File.openRead(path); + defer file.close(); + return file.getEndPos(); } -/// Caller owns the returned memory. -pub fn temporaryName(allocator: std.mem.Allocator) ![]u8 { +fn temporaryName(allocator: *std.mem.Allocator) ![]u8 { const template_start = "/temp/temp_"; - const template = "/tmp/temp_XXXXXXXXXXXXXXXXXXXXX"; + const template = "/tmp/temp_XXXXXXXX"; var nam = try allocator.alloc(u8, template.len); - std.mem.copyForwards(u8, nam, template); + std.mem.copy(u8, nam, template); - const seed = @as(u64, @truncate(@as(u128, @bitCast(std.time.nanoTimestamp())))); - var r = std.rand.DefaultPrng.init(seed); + var r = std.rand.DefaultPrng.init(std.time.timestamp()); var fill = nam[template_start.len..nam.len]; var i: usize = 0; while (i < 100) : (i += 1) { + // generate a random uppercase letter, that is, 65 + random number. - for (fill, 0..) |_, f_idx| { - const idx = @as(u8, @intCast(r.random().uintLessThan(u5, 24))); - const letter = @as(u8, 65) + idx; + for (fill) |_, f_idx| { + var idx = @intCast(u8, r.random.uintLessThan(u5, 24)); + var letter = u8(65) + idx; fill[f_idx] = letter; } // if we fail to access it, we assume it doesn't exist and return it. - var tmp_file: std.fs.File = std.fs.cwd().openFile( - nam, - .{ .mode = .read_only }, - ) catch |err| { - if (err == error.FileNotFound) return nam else continue; + std.fs.File.access(nam) catch |err| { + if (err == error.FileNotFound) { + return nam; + } }; - - // if we actually found someone, close the handle so that we don't - // get EMFILE later on. - tmp_file.close(); } return error.TempGenFail; } -pub fn mkSfInfo() c.SF_INFO { +fn mkSfInfo() c.SF_INFO { return c.SF_INFO{ - .frames = @as(c_int, 0), - .samplerate = @as(c_int, 44100), - .channels = @as(c_int, 1), + .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 = @as(c_int, 0), - .seekable = @as(c_int, 0), + .sections = c_int(0), + .seekable = c_int(0), }; } pub const Image = struct { - allocator: std.mem.Allocator, + allocator: *std.mem.Allocator, /// Pointer to the underlying libsndfile's SNDFILE struct. sndfile: *c.SNDFILE, - /// Current sound file's framecount. - frames: usize, - /// The original image file path. path: []const u8, /// Represents the current path being worked on. curpath: []const u8, - /// Open a BMP image for later. - pub fn open(allocator: std.mem.Allocator, path: []const u8) !*Image { + /// Open a BMP file. + pub fn open(allocator: *std.mem.Allocator, path: []const u8) !*Image { var in_fmt = mkSfInfo(); - const sndfile = try sopen(allocator, path, c.SFM_READ, &in_fmt); - const image = try allocator.create(Image); - - std.debug.assert(in_fmt.frames > @as(i64, 0)); - std.debug.assert(in_fmt.seekable == @as(i32, 1)); + var sndfile = try sopen(allocator, path, c.SFM_READ, &in_fmt); + var image = try allocator.create(Image); image.* = Image{ .allocator = allocator, .sndfile = sndfile, .path = path, .curpath = path, - .frames = @as(usize, @intCast(in_fmt.frames)), - }; - - return image; - } - - pub fn clone(self: *Image) !*Image { - var in_fmt = mkSfInfo(); - // clone sndfile - const sndfile = try sopen(self.allocator, self.curpath, c.SFM_READ, &in_fmt); - std.debug.assert(self.frames == @as(usize, @intCast(in_fmt.frames))); - - const image = try self.allocator.create(Image); - - std.debug.assert(in_fmt.frames > @as(i64, 0)); - std.debug.assert(in_fmt.seekable == @as(i32, 1)); - - image.* = Image{ - .allocator = self.allocator, - .sndfile = sndfile, - .path = self.path, - .curpath = self.curpath, - .frames = @as(usize, @intCast(in_fmt.frames)), }; return image; } pub fn close(self: *Image) void { - const st: i32 = c.sf_close(self.sndfile); + var st: i32 = c.sf_close(self.sndfile); + if (st != 0) { - log.debug("Failed to close {s} ({s})", .{ + std.debug.warn( + "Failed to close {} ({})\n", self.path, c.sf_error_number(st), - }); + ); } - - self.allocator.free(self.path); - self.allocator.free(self.curpath); - - var allocator = self.allocator; - self.* = undefined; - allocator.destroy(self); } pub fn read(self: *Image, file_chans: c_int, buf: []f32) bool { - const n_read: c.sf_count_t = c.sf_readf_float(self.sndfile, buf.ptr, 1); - const buf_chans = @as(c_int, @intCast(buf.len)); + 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[@as(usize, @intCast(i))] = buf[@as(usize, @intCast(@mod(i, file_chans)))]; + buf[@intCast(usize, i)] = buf[@intCast(usize, @mod(i, file_chans))]; } return n_read == 1; } - /// Copy bytes from the current file to out_file. fn copyBytes( self: *Image, out_file: *c.SNDFILE, + buf: []f32, start: usize, end: usize, ) !void { - var buf = try self.allocator.alloc(f32, BufferSize); - defer self.allocator.free(buf); - + const total_bytes = end - start; var i: usize = start; // we do sf_seek() calls to make sure we are actually on the start // and actually end at the end position for the file. - sseek(self.sndfile, start); - sseek(out_file, start); + _ = c.sf_seek(self.sndfile, @intCast(i64, start), c.SEEK_SET); while (i <= end) : (i += buf.len) { - log.debug("\t\ti={d}, buf.len={d}, end={d}", .{ i, buf.len, end }); - sseek(self.sndfile, i); - sseek(out_file, i); - - const bytes_until_end = end - i; + std.debug.warn("i={}, buf.len={}, end={}\n", i, buf.len, end); + const excess = @intCast(i64, i + buf.len) - @intCast(i64, end); var read_bytes: i64 = undefined; var view: []f32 = buf[0..buf.len]; - if (bytes_until_end < buf.len) { - read_bytes = c.sf_readf_float(self.sndfile, buf.ptr, @as(i64, @intCast(bytes_until_end))); - view = buf[0..bytes_until_end]; + if (excess > 0) { + std.debug.warn( + "excess of {} bytes, reading {} instead\n", + excess, + @intCast(i64, buf.len) - excess, + ); + read_bytes = c.sf_readf_float(self.sndfile, buf.ptr, @intCast(i64, buf.len) - excess); + view = buf[0..@intCast(usize, excess)]; } else { - read_bytes = c.sf_readf_float(self.sndfile, buf.ptr, @as(i64, @intCast(buf.len))); + read_bytes = c.sf_readf_float(self.sndfile, buf.ptr, @intCast(i64, buf.len)); } - try swrite(out_file, view.ptr, @as(i64, @intCast(view.len))); + try swrite(out_file, view.ptr, @intCast(i64, view.len)); } - sseek(self.sndfile, end); - sseek(out_file, end); - } - - fn getSeekPos(self: *Image, position: plugins.Position) plugins.SeekPos { - const file_end = self.frames; - const seek_pos = position.seekPos(file_end); - log.debug("\tstart {d} end {d}", .{ seek_pos.start, seek_pos.end }); - return seek_pos; - } - - pub fn reopen(self: *Image, path: []const u8) !void { - var in_fmt = mkSfInfo(); - - self.sndfile = try sopen(self.allocator, path, c.SFM_READ, &in_fmt); - // std.testing.expectEqual(self.frames, @intCast(usize, in_fmt.frames)); - - self.curpath = path; - self.frames = @as(usize, @intCast(in_fmt.frames)); - - log.debug("\timage: reopened on '{s}' (frames={d}, fmt.frames={d})", .{ - self.curpath, - self.frames, - in_fmt.frames, - }); - } - - pub fn checkValid(self: *Image) !void { - var file = try std.fs.cwd().openFile(self.path, .{ .mode = .read_only }); - defer file.close(); - - // main bmp header: - // 2 bytes for magic header - // 4 bytes for size in bytes - // 2 bytes ? - // 2 bytes ? - // 4 bytes for pixel array offset - var magic = [2]u8{ 0, 0 }; - _ = try file.read(&magic); - - if (std.mem.endsWith(u8, self.path, ".bmp")) - try bmp.magicValid(&magic); + _ = c.sf_seek(self.sndfile, @intCast(i64, end), c.SEEK_SET); } /// Run a plugin over the image. @@ -300,54 +207,51 @@ pub const Image = struct { position: plugins.Position, params: plugins.ParamList, ) !void { - var timer = try std.time.Timer.start(); - var ctx = try plugins.makeContext(self.allocator, plugin_uri); defer ctx.deinit(); var ports = try lv2.setupPorts(&ctx); - defer ctx.allocator.free(ports); if (ctx.n_audio_in > 2) { - log.debug("plugin <{s}> has more than two inputs.", .{plugin_uri}); + std.debug.warn("plugin <{}> accepts more than two channels.\n", plugin_uri); return ImageError.InvalidPlugin; } - if (ctx.n_audio_out > 2) { - log.debug("plugin <{s}> has more than two outputs.", .{plugin_uri}); - return ImageError.InvalidPlugin; - } + // TODO check n_audio_out > 2 // now, for each param for the plugin, we find its port, and set // the value for the port there. - for (params.items) |param| { - const sym_cstr = try self.allocator.dupeZ(u8, param.sym); + var it = params.iterator(); + + while (it.next()) |param| { + var sym_cstr = try std.cstr.addNullByte(self.allocator, param.sym); defer self.allocator.free(sym_cstr); - const sym = c.lilv_new_string(ctx.world, sym_cstr.ptr); - const port = c.lilv_plugin_get_port_by_symbol(ctx.plugin, sym) orelse { - log.debug("assert fail: symbol {s} not found on port", .{param.sym}); + var sym = c.lilv_new_string(ctx.world, sym_cstr.ptr); + const port = c.lilv_plugin_get_port_by_symbol(ctx.plugin, sym) orelse blk: { + std.debug.warn("assert fail: symbol {} not found on port\n", param.sym); return ImageError.InvalidSymbol; }; c.lilv_node_free(sym); - const idx = c.lilv_port_get_index(ctx.plugin, port); - log.debug("\tset sym={s}, idx={d} to val={}", .{ + var idx = c.lilv_port_get_index(ctx.plugin, port); + std.debug.warn( + "\tset sym={}, idx={} to val={}\n", param.sym, idx, param.value, - }); - (&ports[idx]).value = param.value; + ); + ports[idx].value = param.value; } // now we need to generate a temporary file and put the output of // running the plugin on that file - const tmpnam = try temporaryName(self.allocator); - log.debug("\trunning plugin from '{s}' to '{s}'", .{ self.curpath, tmpnam }); + var tmpnam = try temporaryName(self.allocator); + std.debug.warn("\trunning plugin from '{}' to '{}'\n", self.curpath, tmpnam); var out_fmt = mkSfInfo(); - const out_file = try sopen(self.allocator, tmpnam, c.SFM_WRITE, &out_fmt); + var out_file = try sopen(self.allocator, tmpnam, c.SFM_WRITE, &out_fmt); var rctx = try plugins.RunContext.init(self.allocator, ctx.plugin); defer rctx.deinit(); @@ -359,7 +263,12 @@ pub const Image = struct { // just copy the original image and the part where we run the plugin // over the image. - const seek_pos = self.getSeekPos(position); + const file_end = try getEndPos(self.curpath); + const seek_pos = position.seekPos(file_end); + std.debug.warn("\tstart {} end {}\n", seek_pos.start, seek_pos.end); + + // make sure we start from 0 + _ = c.sf_seek(self.sndfile, 0, c.SEEK_SET); // there are four main stages: // - the bmp header copy @@ -367,138 +276,44 @@ pub const Image = struct { // - plugin // - post-plugin + var file_copy_buf = try self.allocator.alloc(f32, BufferSize); + defer self.allocator.free(file_copy_buf); + // pre-plugin copy, merged with bmp header copy try self.copyBytes( out_file, - @as(usize, 0), - seek_pos.start, + file_copy_buf, + usize(0), + seek_pos.start + @mod(seek_pos.start, BufferSize), ); - sseek(self.sndfile, seek_pos.start); + _ = c.sf_seek(self.sndfile, @intCast(i64, seek_pos.start), c.SEEK_SET); var i: usize = seek_pos.start; - log.debug("\tseek pos start: {d} end: {d}", .{ seek_pos.start, seek_pos.end }); + std.debug.warn("\tseek pos start: {} end: {}\n", seek_pos.start, seek_pos.end); - var inbuf = &rctx.buffers.in; - const outbuf = &rctx.buffers.out; + var inbuf = rctx.buffers.in; + var outbuf = rctx.buffers.out; while (i <= seek_pos.end) : (i += 1) { - inbuf[0] = 0; - inbuf[1] = 0; - - const read_bytes = c.sf_readf_float(self.sndfile, inbuf, 1); + const read_bytes = c.sf_readf_float(self.sndfile, inbuf.ptr, 1); if (read_bytes == 0) { - log.debug("WARN! reached EOF at idx={d}", .{i}); + std.debug.warn("WARN! reached EOF at idx={}\n", i); break; } - // trick plugins into having correct stereo signal from - // my mono input - inbuf[1] = inbuf[0]; - lv2.lilv_instance_run(rctx.instance, 1); - try swrite(out_file, outbuf, 1); + try swrite(out_file, outbuf.ptr, 1); } - sseek(self.sndfile, seek_pos.end); + _ = c.sf_seek(self.sndfile, @intCast(i64, seek_pos.end), c.SEEK_SET); // post-plugin copy try self.copyBytes( out_file, + file_copy_buf, seek_pos.end + 1, - self.frames, - ); - - c.sf_write_sync(out_file); - _ = c.sf_close(out_file); - _ = c.sf_close(self.sndfile); - - try self.reopen(tmpnam); - try self.checkValid(); - - const time_taken = timer.read(); - log.debug("\ttook {d:.2}ms running plugin", .{time_taken / std.time.us_per_ms}); - } - - pub fn saveTo(self: *Image, out_path: []const u8) !void { - log.debug("\timg: copy from '{s}' to '{s}'", .{ self.curpath, out_path }); - try std.fs.copyFileAbsolute(self.curpath, out_path, .{}); - } - - pub fn runCustomPlugin( - self: *Image, - comptime Plugin: type, - position: plugins.Position, - extra: anytype, - ) !void { - const plugin_opt: ?Plugin = Plugin.init(self.allocator, extra); - if (plugin_opt == null) { - return ImageError.PluginLoadFail; - } - - var plugin = plugin_opt.?; - defer plugin.deinit(); - - const decls = comptime std.meta.declarations(Plugin); - inline for (decls) |decl| { - if (comptime std.mem.eql(u8, decl.name, "setup")) { - try plugin.setup(); - } - } - - // the code here is a copypaste of runPlugin() without the specific - // lilv things. - const tmpnam = try temporaryName(self.allocator); - log.debug("\trunning CUSTOM plugin from '{s}' to '{s}'", .{ self.curpath, tmpnam }); - - var out_fmt = mkSfInfo(); - const out_file = try sopen(self.allocator, tmpnam, c.SFM_WRITE, &out_fmt); - - var bufs = plugins.RunBuffers{}; - const seek_pos = self.getSeekPos(position); - - // make sure we start from 0 - sseek(self.sndfile, 0); - - // there are four main stages: - // - the bmp header copy - // - pre-plugin - // - CUSTOM plugin - // - post-plugin - - // pre-plugin copy, merged with bmp header copy - try self.copyBytes( - out_file, - @as(usize, 0), - seek_pos.start, - ); - - sseek(self.sndfile, seek_pos.start); - - var i: usize = seek_pos.start; - log.debug("\tseek pos start: {d} end: {d}", .{ seek_pos.start, seek_pos.end }); - - const inbuf = &bufs.in; - const outbuf = &bufs.out; - - while (i <= seek_pos.end) : (i += 1) { - const read_bytes = c.sf_readf_float(self.sndfile, inbuf, 1); - if (read_bytes == 0) { - log.debug("WARN! reached EOF at idx={d}", .{i}); - break; - } - - plugin.run(&bufs); - try swrite(out_file, outbuf, 1); - } - - sseek(self.sndfile, seek_pos.end); - - // post-plugin copy - try self.copyBytes( - out_file, - seek_pos.end + 1, - self.frames, + file_end + @mod(file_end, BufferSize), ); c.sf_write_sync(out_file); @@ -506,7 +321,93 @@ pub const Image = struct { _ = c.sf_close(self.sndfile); // reopen the file as SFM_READ so we can run plugin chains etc - try self.reopen(tmpnam); - try self.checkValid(); + self.sndfile = try sopen(self.allocator, tmpnam, c.SFM_READ, &out_fmt); + self.curpath = tmpnam; + } + + pub fn saveTo(self: *Image, out_path: []const u8) !void { + std.debug.warn("saved to '{}'\n", out_path); + try std.fs.copyFile(self.curpath, out_path); + } + + pub fn runCustomPlugin( + self: *Image, + comptime Plugin: type, + position: plugins.Position, + params: *plugins.ParamMap, + ) !void { + var plugin = try Plugin.init(self.allocator, params); + + // the code here is a copypaste of runPlugin() without the specific + // lilv things. + var tmpnam = try temporaryName(self.allocator); + std.debug.warn("\trunning CUSTOM plugin from '{}' to '{}'\n", self.curpath, tmpnam); + + var out_fmt = mkSfInfo(); + var out_file = try sopen(self.allocator, tmpnam, c.SFM_WRITE, &out_fmt); + + var bufs = try plugins.RunBuffers.init(self.allocator); + defer bufs.deinit(); + + const file_end = try getEndPos(self.curpath); + const seek_pos = position.seekPos(file_end); + std.debug.warn("\tstart {} end {}\n", seek_pos.start, seek_pos.end); + + // make sure we start from 0 + _ = c.sf_seek(self.sndfile, 0, c.SEEK_SET); + + // there are four main stages: + // - the bmp header copy + // - pre-plugin + // - CUSTOM plugin + // - post-plugin + + var file_copy_buf = try self.allocator.alloc(f32, BufferSize); + defer self.allocator.free(file_copy_buf); + + // pre-plugin copy, merged with bmp header copy + try self.copyBytes( + out_file, + file_copy_buf, + usize(0), + seek_pos.start + @mod(seek_pos.start, BufferSize), + ); + + _ = c.sf_seek(self.sndfile, @intCast(i64, seek_pos.start), c.SEEK_SET); + + var i: usize = seek_pos.start; + std.debug.warn("\tseek pos start: {} end: {}\n", seek_pos.start, seek_pos.end); + + var inbuf = bufs.in; + var outbuf = bufs.out; + + while (i <= seek_pos.end) : (i += 1) { + const read_bytes = c.sf_readf_float(self.sndfile, bufs.in.ptr, 1); + if (read_bytes == 0) { + std.debug.warn("WARN! reached EOF at idx={}\n", i); + break; + } + + plugin.run(&bufs); + try swrite(out_file, bufs.out.ptr, 1); + } + + _ = c.sf_seek(self.sndfile, @intCast(i64, seek_pos.end), c.SEEK_SET); + + // post-plugin copy + try self.copyBytes( + out_file, + file_copy_buf, + seek_pos.end + 1, + file_end + @mod(file_end, BufferSize), + ); + + c.sf_write_sync(out_file); + _ = c.sf_close(out_file); + _ = c.sf_close(self.sndfile); + + // reopen the file as SFM_READ so we can run plugin chains etc + self.sndfile = try sopen(self.allocator, tmpnam, c.SFM_READ, &out_fmt); + self.curpath = tmpnam; } }; diff --git a/src/lang.zig b/src/lang.zig index 31e5a73..6b6b980 100644 --- a/src/lang.zig +++ b/src/lang.zig @@ -1,762 +1,208 @@ const std = @import("std"); const plugin = @import("plugin.zig"); -const custom = @import("custom.zig"); -const log = std.log.scoped(.scritcher_lang); -pub const ParseError = error{ParseFail}; +pub const ParseError = error{ + NoCommandGiven, + UnknownCommand, + ArgRequired, +}; pub const CommandType = enum { - /// "LV2 Commands" are commands that receive split, index, and then receive - /// any f64 arguments. - lv2_command, + Noop, + Load, + Quicksave, + RunQS, - custom_command, + Amp, + RFlanger, + Eq, + Phaser, + Mbeq, + Chorus, + PitchScaler, + Reverb, + Highpass, + Delay, + Vinyl, + RevDelay, + Noise, + WildNoise, }; -fn LV2Command( - comptime tag: Command.Tag, - comptime plugin_url: []const u8, - comptime LV2Parameters: type, -) type { - return struct { - pub const base_tag = tag; - pub const command_type = CommandType.lv2_command; - pub const lv2_url = plugin_url; - - base: Command, - split: usize, - index: usize, - parameters: LV2Parameters, - }; -} - -fn CustomCommand( - comptime tag: Command.Tag, - comptime Plugin: type, - comptime PluginParameters: type, -) type { - return struct { - pub const base_tag = tag; - pub const command_type = CommandType.custom_command; - pub const plugin_type = Plugin; - - base: Command, - split: usize, - index: usize, - parameters: PluginParameters, - }; -} - pub const Command = struct { - tag: Tag, + command: CommandType, + args: ArgList, + cur_idx: usize = 0, - pub const Tag = enum { - noop, - load, - quicksave, - runqs, - - amp, - rflanger, - eq, - phaser, - mbeq, - chorus, - pitchscaler, - reverb, - highpass, - delay, - vinyl, - revdelay, - gate, - detune, - overdrive, - degrade, - repsycho, - talkbox, - dyncomp, - thruzero, - foverdrive, - gverb, - invert, - tapedelay, - moddelay, - multichorus, - saturator, - vintagedelay, - - noise, - wildnoise, - write, - embed, - - rotate, - }; - - pub fn tagToType(comptime tag: Tag) type { - return switch (tag) { - .noop => Noop, - .load => Load, - .quicksave => Quicksave, - .runqs => RunQS, - - .amp => Amp, - .rflanger => RFlanger, - .eq => Eq, - .phaser => Phaser, - .mbeq => Mbeq, - .chorus => Chorus, - .pitchscaler => Pitchscaler, - .reverb => Reverb, - .highpass => Highpass, - .delay => Delay, - .vinyl => Vinyl, - .revdelay => Revdelay, - .gate => Gate, - .detune => Detune, - .overdrive => Overdrive, - .degrade => Degrade, - .repsycho => Repsycho, - .talkbox => Talkbox, - .dyncomp => Dyncomp, - .thruzero => Thruzero, - .foverdrive => Foverdrive, - .gverb => Gverb, - .invert => Invert, - .tapedelay => Tapedelay, - .moddelay => Moddelay, - .multichorus => Multichorus, - .saturator => Saturator, - .vintagedelay => Vintagedelay, - - .noise => Noise, - .wildnoise => Wildnoise, - .write => Write, - .embed => Embed, - - .rotate => Rotate, - }; + pub fn print(self: *const Command) void { + std.debug.warn("cmd:{}\n", self.command); } - pub fn cast(base: *const @This(), comptime T: type) ?*const T { - if (base.tag != T.base_tag) - return null; + pub fn argAt(self: *const Command, idx: usize) ![]const u8 { + const args = self.args.toSliceConst(); - //const baseInt = @intFromPtr(base); - - //log.debug("casting from {d}", .{baseInt}); - //log.debug("aligns from 8? {d}", .{baseInt % 8}); - //log.debug("align T: {d} {s}", .{ @alignOf(*T), @typeName(T) }); - //log.debug("align base: {d} {s}", .{ @alignOf(*const @This()), @typeName(@This()) }); - const base_aligned: *const @This() = @alignCast(base); - - const parented = @as(*const T, @alignCast(@fieldParentPtr("base", base_aligned))); - - const ptr: *const T = @alignCast(parented); - //log.debug("align: {d}\n", .{@alignOf(@TypeOf(ptr))}); - return ptr; - } - - pub fn print(base: *const @This()) void { - log.debug("tag: {}", .{base.tag}); - } - - pub const Noop = struct { - pub const base_tag = Tag.noop; - base: Command, - }; - - pub const Load = struct { - pub const base_tag = Tag.load; - base: Command, - path: []const u8, - }; - - pub const Quicksave = struct { - pub const base_tag = Tag.quicksave; - base: Command, - }; - - pub const RunQS = struct { - pub const base_tag = Tag.runqs; - base: Command, - program: []const u8, - }; - - pub const Noise = CustomCommand(Tag.noise, custom.RandomNoise, struct { - seed: u64, - fill_bytes: usize, - }); - - pub const Wildnoise = CustomCommand(Tag.wildnoise, custom.WildNoise, struct { - seed: u64, - fill_bytes: usize, - }); - - pub const Write = CustomCommand(Tag.write, custom.Write, struct { - data: f32, - }); - - pub const Embed = CustomCommand(Tag.write, custom.Embed, struct { - path: []const u8, - }); - - pub const Rotate = struct { - pub const base_tag = Tag.rotate; - base: Command, - deg: f32, - bgfill: []const u8, - }; - - pub const Amp = LV2Command( - .amp, - "http://lv2plug.in/plugins/eg-amp", - struct { gain: f32 }, - ); - - pub const RFlanger = LV2Command( - .rflanger, - "http://plugin.org.uk/swh-plugins/retroFlange", - struct { delay_depth_avg: f32, law_freq: f32 }, - ); - - pub const Eq = LV2Command( - .rflanger, - "http://plugin.org.uk/swh-plugins/dj_eq_mono", - struct { lo: f32, mid: f32, hi: f32 }, - ); - - pub const Phaser = LV2Command( - .phaser, - "http://plugin.org.uk/swh-plugins/lfoPhaser", - struct { lfo_rate: f32, lfo_depth: f32, fb: f32, spread: f32 }, - ); - - pub const Mbeq = LV2Command( - .mbeq, - "http://plugin.org.uk/swh-plugins/mbeq", - struct { - band_1: f32, - band_2: f32, - band_3: f32, - band_4: f32, - band_5: f32, - band_6: f32, - band_7: f32, - band_8: f32, - band_9: f32, - band_10: f32, - band_11: f32, - band_12: f32, - band_13: f32, - band_14: f32, - band_15: f32, - }, - ); - - pub const Chorus = LV2Command( - .chorus, - "http://plugin.org.uk/swh-plugins/multivoiceChorus", - struct { - voices: f32, - delay_base: f32, - voice_spread: f32, - detune: f32, - law_freq: f32, - attendb: f32, - }, - ); - - pub const Pitchscaler = LV2Command( - .pitchscaler, - "http://plugin.org.uk/swh-plugins/pitchScaleHQ", - struct { mult: f32 }, - ); - - pub const Reverb = LV2Command( - .reverb, - "http://invadarecords.com/plugins/lv2/erreverb/mono", - struct { - roomLength: f32, - roomWidth: f32, - roomHeight: f32, - sourceLR: f32, - sourceFB: f32, - listLR: f32, - listFB: f32, - hpf: f32, - warmth: f32, - diffusion: f32, - }, - ); - - pub const Highpass = LV2Command(.highpass, "http://invadarecords.com/plugins/lv2/filter/hpf/mono", struct { - freq: f32, - gain: f32, - noClip: f32, - }); - - pub const Delay = LV2Command(.delay, "http://plugin.org.uk/swh-plugins/delayorama", struct { - seed: f32, - gain: f32, - feedback_pc: f32, - tap_count: f32, - first_delay: f32, - delay_range: f32, - delay_scale: f32, - delay_rand_pc: f32, - gain_scale: f32, - wet: f32, - }); - - pub const Vinyl = LV2Command(.vinyl, "http://plugin.org.uk/swh-plugins/vynil", struct { - year: f32, - rpm: f32, - warp: f32, - click: f32, - wear: f32, - }); - - pub const Revdelay = LV2Command(.revdelay, "http://plugin.org.uk/swh-plugins/revdelay", struct { - delay_time: f32, - dry_level: f32, - wet_level: f32, - feedback: f32, - xfade_samp: f32, - }); - // pub const Noise= LV2Command(.,,struct{}); - pub const Gate = LV2Command(.gate, "http://hippie.lt/lv2/gate", struct { - @"switch": f32, - threshold: f32, - attack: f32, - hold: f32, - decay: f32, - gaterange: f32, - }); - pub const Detune = LV2Command(.detune, "http://drobilla.net/plugins/mda/Detune", struct { - detune: f32, - mix: f32, - output: f32, - latency: f32, - }); - pub const Overdrive = LV2Command(.overdrive, "http://drobilla.net/plugins/mda/Overdrive", struct { - drive: f32, - muffle: f32, - output: f32, - }); - pub const Degrade = LV2Command(.degrade, "http://drobilla.net/plugins/mda/Degrade", struct { - headroom: f32, - quant: f32, - rate: f32, - post_filt: f32, - non_lin: f32, - output: f32, - }); - pub const Repsycho = LV2Command(.repsycho, "http://drobilla.net/plugins/mda/RePsycho", struct { - tune: f32, - fine: f32, - decay: f32, - thresh: f32, - hold: f32, - mix: f32, - quality: f32, - }); - pub const Talkbox = LV2Command(.talkbox, "http://drobilla.net/plugins/mda/TalkBox", struct { - wet: f32, - dry: f32, - carrier: f32, - quality: f32, - }); - pub const Dyncomp = LV2Command(.dyncomp, "http://gareus.org/oss/lv2/darc#mono", struct { - enable: f32, - hold: f32, - inputgain: f32, - threshold: f32, - Ratio: f32, - attack: f32, - release: f32, - gain_min: f32, - gain_max: f32, - rms: f32, - }); - pub const Foverdrive = LV2Command(.foverdrive, "http://plugin.org.uk/swh-plugins/foverdrive", struct { - drive: f32, - }); - pub const Thruzero = LV2Command(.thruzero, "http://drobilla.net/plugins/mda/ThruZero", struct { rate: f32, mix: f32, feedback: f32, depth_mod: f32 }); - - pub const Gverb = LV2Command(.gverb, "http://plugin.org.uk/swh-plugins/gverb", struct { - roomsize: f32, - revtime: f32, - damping: f32, - inputbandwidth: f32, - drylevel: f32, - earlylevel: f32, - taillevel: f32, - }); - pub const Invert = LV2Command(.invert, "http://plugin.org.uk/swh-plugins/inv", struct {}); - pub const Tapedelay = LV2Command(.tapedelay, "http://plugin.org.uk/swh-plugins/tapeDelay", struct { - speed: f32, - da_db: f32, - - t1d: f32, - t1a_db: f32, - - t2d: f32, - t2a_db: f32, - - t3d: f32, - t3a_db: f32, - - t4d: f32, - t4a_db: f32, - }); - - pub const Moddelay = LV2Command( - .moddelay, - "http://plugin.org.uk/swh-plugins/modDelay", - struct { - base: f32, - }, - ); - - pub const Multichorus = LV2Command(.multichorus, "http://calf.sourceforge.net/plugins/MultiChorus", struct { - min_delay: f32, - mod_depth: f32, - mod_rate: f32, - stereo: f32, - voices: f32, - vphase: f32, - amount: f32, - dry: f32, - freq: f32, - freq2: f32, - q: f32, - overlap: f32, - level_in: f32, - level_out: f32, - lfo: f32, - }); - pub const Saturator = LV2Command(.saturator, "http://calf.sourceforge.net/plugins/Saturator", struct { - bypass: f32, - level_in: f32, - level_out: f32, - mix: f32, - drive: f32, - blend: f32, - lp_pre_freq: f32, - hp_pre_freq: f32, - lp_post_freq: f32, - hp_post_freq: f32, - p_freq: f32, - p_level: f32, - p_q: f32, - pre: f32, - post: f32, - }); - pub const Vintagedelay = LV2Command(.vintagedelay, "http://calf.sourceforge.net/plugins/VintageDelay", struct { - level_in: f32, - level_out: f32, - subdiv: f32, - time_l: f32, - time_r: f32, - feedback: f32, - amount: f32, - mix_mode: f32, - medium: f32, - dry: f32, - width: f32, - fragmentation: f32, - pbeats: f32, - pfrag: f32, - timing: f32, - bpm: f32, - ms: f32, - hz: f32, - bpm_host: f32, - }); -}; - -const CmdArrayList = std.ArrayList(*Command); - -pub const CommandList = struct { - list: CmdArrayList, - const Self = @This(); - - pub fn init(allocator: std.mem.Allocator) Self { - return .{ - .list = CmdArrayList.init(allocator), - }; - } - - pub fn deinit(self: *Self) void { - for (self.list.items) |cmd_ptr| { - inline for (@typeInfo(Command.Tag).Enum.fields) |field| { - if (cmd_ptr.tag == @field(Command.Tag, field.name)) { - const actual_tag = - @field(Command.Tag, field.name); - // if we find a match on the tag, we can get the type - const typ = Command.tagToType(actual_tag); - - const inner_command = cmd_ptr.cast(typ).?; - inline for (@typeInfo(typ).Struct.fields) |cmd_field| { - switch (cmd_field.type) { - []u8, []const u8 => self.list.allocator.free(@field(inner_command, cmd_field.name)), - else => {}, - } - } - } - } - - //TODO this is ian invalid free - //self.list.allocator.destroy(cmd_ptr); + if (idx > (args.len - 1)) { + std.debug.warn("Expected argument at index {}\n", idx); + return ParseError.ArgRequired; } - self.list.deinit(); + + return args[idx]; } - pub fn append(self: *Self, cmd: *Command) !void { - return try self.list.append(cmd); + 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 consumePosition(self: *Command) !plugin.Position { + self.cur_idx = 2; + return plugin.Position{ + .split = try self.usizeArgAt(0), + .index = try self.usizeArgAt(1), + }; + } + + 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 fn floatArgMany( + self: *const Command, + allocator: *std.mem.Allocator, + start_index: usize, + elements: usize, + default: f32, + ) ![]const f32 { + var i: usize = start_index; + var arr = std.ArrayList(f32).init(allocator); + + while (i < elements) : (i += 1) { + var value: f32 = self.floatArgAt(i) catch |err| blk: { + std.debug.warn("\tdoing default on arg {}\n", i); + break :blk default; + }; + + try arr.append(value); + } + + return arr.toSliceConst(); + } + + pub fn appendParam( + self: *Command, + params: *plugin.ParamList, + symbol: []const u8, + ) !void { + var val = try self.floatArgAt(self.cur_idx); + self.cur_idx += 1; + + try params.append(plugin.Param{ + .sym = symbol, + .value = val, + }); + } + + pub fn appendParamMap( + self: *Command, + map: *plugin.ParamMap, + symbol: []const u8, + ) !void { + var val = try self.floatArgAt(self.cur_idx); + self.cur_idx += 1; + _ = try map.put(symbol, val); } }; -/// A parser. +pub const CommandList = std.ArrayList(*Command); +pub const ArgList = std.ArrayList([]const u8); + +pub const PluginKeywords = [_]CommandType{ .Amp, .RFlanger }; +pub const KeywordMap = std.AutoHashMap([]const u8, CommandType); + pub const Lang = struct { - allocator: std.mem.Allocator, + allocator: *std.mem.Allocator, + keywords: KeywordMap, - has_error: bool = false, - line: usize = 0, - - pub fn init(allocator: std.mem.Allocator) Lang { + pub fn init(allocator: *std.mem.Allocator) Lang { return Lang{ .allocator = allocator, + .keywords = KeywordMap.init(allocator), }; } - pub fn deinit(self: *Lang) void { - _ = self; - } + fn fillKeywords(self: *Lang) !void { + _ = try self.keywords.put("noop", .Noop); + _ = try self.keywords.put("load", .Load); + _ = try self.keywords.put("quicksave", .Quicksave); + _ = try self.keywords.put("runqs", .RunQS); - pub fn reset(self: *Lang) void { - self.has_error = false; - self.line = 0; - } + _ = try self.keywords.put("amp", .Amp); + _ = try self.keywords.put("rflanger", .RFlanger); + _ = try self.keywords.put("eq", .Eq); + _ = try self.keywords.put("mbeq", .Mbeq); + _ = try self.keywords.put("phaser", .Phaser); + _ = try self.keywords.put("chorus", .Chorus); + _ = try self.keywords.put("pitchscaler", .PitchScaler); + _ = try self.keywords.put("reverb", .Reverb); + _ = try self.keywords.put("highpass", .Highpass); + _ = try self.keywords.put("delay", .Delay); + _ = try self.keywords.put("vinyl", .Vinyl); + _ = try self.keywords.put("revdelay", .RevDelay); - fn doError(self: *Lang, comptime fmt: []const u8, args: anytype) void { - log.warn("ERROR! at line {}: ", .{self.line}); - log.warn(fmt, args); - self.has_error = true; - } - - fn parseCommandArguments( - self: *@This(), - comptime command_struct: type, - tok_it: *std.mem.SplitIterator(u8, .sequence), - commands: *CommandList, - ) !void { - // Based on the command struct fields, we can parse the arguments. - var cmd = try self.allocator.create(command_struct); - - // we already add the command to the list to prevent memory leaks - // by commands that error out - try commands.append(&cmd.base); - - const is_lv2_command = switch (command_struct.base_tag) { - .noop, .load, .quicksave, .runqs, .rotate => false, - else => true, - }; - - if (is_lv2_command) { - const split = tok_it.next(); - if (split == null) { - self.doError("Expected split parameter, got EOL", .{}); - return; - } - - const index = tok_it.next(); - if (index == null) { - self.doError("Expected index parameter, got EOL", .{}); - return; - } - - cmd.split = try std.fmt.parseInt(usize, split.?, 10); - cmd.index = try std.fmt.parseInt(usize, index.?, 10); - - inline for (@typeInfo(@TypeOf(cmd.parameters)).Struct.fields) |cmd_field| { - const maybe_arg = tok_it.next(); - if (maybe_arg == null) { - self.doError("Expected parameter for {s}, got nothing", .{cmd_field.name}); - return; - } - - const arg = maybe_arg.?; - const arg_value = switch (cmd_field.type) { - f32 => try std.fmt.parseFloat(f32, arg), - u64 => try std.fmt.parseInt(u64, arg, 10), - usize => try std.fmt.parseInt(usize, arg, 10), - []const u8 => @as([]const u8, try self.allocator.dupe(u8, arg)), - else => @compileError("parameter struct has unsupported type " ++ @typeName(cmd_field.field_type)), - }; - - @field(cmd.parameters, cmd_field.name) = arg_value; - } - } else { - inline for (@typeInfo(command_struct).Struct.fields) |cmd_field| { - comptime { - if (std.mem.eql(u8, cmd_field.name, "base")) { - continue; - } - } - - const arg_opt = tok_it.next(); - if (arg_opt == null) { - self.doError("Expected parameter for {s}, got nothing", .{cmd_field.name}); - return; - } - const arg = arg_opt.?; - - const argument_value = switch (cmd_field.type) { - usize => try std.fmt.parseInt(usize, arg, 10), - i32 => try std.fmt.parseInt(i32, arg, 10), - f32 => try std.fmt.parseFloat(f32, arg), - []const u8 => @as([]const u8, try self.allocator.dupe(u8, arg)), - else => @compileError("Invalid parameter type (" ++ @typeName(cmd_field.field_type) ++ ") left on command struct " ++ @typeName(command_struct) ++ "."), - }; - - log.debug("parsing {s}, arg of type {s} => {any}", .{ - @typeName(command_struct), - @typeName(@TypeOf(argument_value)), - argument_value, - }); - - @field(cmd, cmd_field.name) = argument_value; - } - } - - cmd.base.tag = command_struct.base_tag; - const command = cmd.base.cast(command_struct).?; - log.debug("cmd: {}", .{command}); + // custom implementations (not lv2) + _ = try self.keywords.put("noise", .Noise); + _ = try self.keywords.put("wildnoise", .WildNoise); } pub fn parse(self: *Lang, data: []const u8) !CommandList { - var splitted_it = std.mem.split(u8, data, ";"); + var splitted_it = std.mem.separate(data, ";"); + try self.fillKeywords(); var cmds = CommandList.init(self.allocator); - errdefer cmds.deinit(); while (splitted_it.next()) |stmt_orig| { - self.line += 1; var stmt = std.mem.trimRight(u8, stmt_orig, "\n"); stmt = std.mem.trimLeft(u8, stmt, "\n"); if (stmt.len == 0) continue; - if (std.mem.startsWith(u8, stmt, "#")) continue; + if (stmt[0] == '#') continue; - // TODO better tokenizer instead of just tokenize(" ")...maybe???? - var tok_it = std.mem.splitSequence(u8, stmt, " "); + // TODO better tokenizer instead of just tokenize(" "); + var tok_it = std.mem.tokenize(stmt, " "); - const cmd_opt = tok_it.next(); - if (cmd_opt == null) { - self.doError("No command given", .{}); - continue; - } - const command_string = cmd_opt.?; + var cmd_opt = tok_it.next(); + if (cmd_opt == null) return ParseError.NoCommandGiven; + var command = cmd_opt.?; - var found: bool = false; - - inline for (@typeInfo(Command).Struct.decls) |cmd_struct_decl| { - const struct_name = cmd_struct_decl.name; - const cmd_struct_type = @field(Command, struct_name); - - const info_of_info = @typeInfo(@TypeOf(cmd_struct_type)); - - switch (info_of_info) { - .Type => {}, - else => continue, - } - - const info = @typeInfo(cmd_struct_type); - - switch (info) { - .Struct => {}, - else => continue, - } - - comptime var lowered_command_name = [_]u8{0} ** struct_name.len; - var runtime_lowered_command_name = [_]u8{0} ** struct_name.len; - comptime { - for (struct_name, 0..) |c, i| { - lowered_command_name[i] = std.ascii.toLower(c); - } - } - const c_l = lowered_command_name; - std.mem.copyForwards(u8, &runtime_lowered_command_name, &c_l); - - // if we have a match, we know the proper struct type - // to use. this actually works compared to storing command_struct - // in a variable because then that variable must be comptime. - - // the drawback of this approach is that our emitted code is basically linear - // because we don't use the hashmap anymore. - // - // Attempting to use ComptimeHashMap hits compiler bugs and I'm - // not sure if we can map strings to *types* in it. - - if ((!found) and std.mem.eql(u8, &runtime_lowered_command_name, command_string)) { - found = true; - try self.parseCommandArguments(cmd_struct_type, &tok_it, &cmds); - } + var kv_opt = self.keywords.get(command); + var ctype: CommandType = undefined; + if (kv_opt) |kv| { + ctype = kv.value; + } else { + std.debug.warn("Unknown command: '{}'\n", command); + return ParseError.UnknownCommand; } - if (!found) { - self.doError("Unknown command '{s}' ({d} bytes)", .{ command_string, command_string.len }); - continue; + var args = ArgList.init(self.allocator); + + while (tok_it.next()) |arg| { + try args.append(arg); } + + // construct final Command based on command + var cmd_ptr = try self.allocator.create(Command); + cmd_ptr.* = Command{ .command = ctype, .args = args }; + try cmds.append(cmd_ptr); } - if (self.has_error) return ParseError.ParseFail; - return cmds; } }; -test "noop" { - var lang = Lang.init(std.testing.allocator); - defer lang.deinit(); - - var cmds = try lang.parse("noop;"); - defer cmds.deinit(); - - try std.testing.expectEqual(cmds.list.items.len, 1); - try std.testing.expectEqual(cmds.list.items[0].tag, .noop); -} - -test "load, phaser, quicksave" { - var lang = Lang.init(std.testing.allocator); - defer lang.deinit(); - - const prog = - \\load :0; - \\phaser 3 1 25 0.25 0 1; - \\quicksave; - ; - - var cmds = try lang.parse(prog); - defer cmds.deinit(); - - try std.testing.expectEqual(cmds.list.items.len, 3); - try std.testing.expectEqual(cmds.list.items[0].tag, .load); - try std.testing.expectEqual(cmds.list.items[1].tag, .phaser); - try std.testing.expectEqual(cmds.list.items[2].tag, .quicksave); -} - -test "load, phaser with errors, quicksave" { - var lang = Lang.init(std.testing.allocator); - defer lang.deinit(); - - const prog = - \\load :0; - \\phaser 3 1 25; - \\quicksave; - ; - - try std.testing.expectError(error.ParseFail, lang.parse(prog)); -} +// TODO tests diff --git a/src/lv2_helpers.zig b/src/lv2_helpers.zig index a59babd..834a813 100644 --- a/src/lv2_helpers.zig +++ b/src/lv2_helpers.zig @@ -1,29 +1,27 @@ const std = @import("std"); const plugin = @import("plugin.zig"); -const log = std.log.scoped(.scritcher_lv2); - pub const c = @cImport({ @cInclude("sndfile.h"); @cInclude("lilv/lilv.h"); - @cInclude("lv2.h"); + @cInclude("lv2/core/lv2.h"); }); -pub fn Lv2Core(comptime ns: []const u8) []const u8 { - const LV2_CORE_URI = "http://lv2plug.in/ns/lv2core"; - return LV2_CORE_URI ++ ns ++ [_]u8{0}; -} +const LV2_CORE_URI = "http://lv2plug.in/ns/lv2core"; -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"); +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: ?*anyopaque, + data_location: ?*c_void, ) void { instance.?.*.lv2_descriptor.?.*.connect_port.?(instance.?.*.lv2_handle, port_index, data_location); } @@ -61,48 +59,56 @@ pub const Port = struct { /// 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. -/// -/// Caller owns returned memory. -pub fn setupPorts(ctx: *plugin.Context) ![]Port { - const world = ctx.world; +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); + var ports = try ctx.allocator.alloc(*Port, n_ports); - for (ports, 0..) |_, idx| { - const port: *Port = &ports[idx]; + for (ports) |port_ptr, idx| { + var port = try ctx.allocator.create(Port); port.* = Port{ .lilv_port = null, .ptype = .Control, - .index = @as(f32, 0), - .value = @as(f32, 0), + .index = f32(0), + .value = f32(0), .is_input = false, .optional = false, }; + + ports[idx] = port; } - const values: []f32 = try ctx.allocator.alloc(f32, n_ports); + 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); - const lv2_InputPort = c.lilv_new_uri(world, LV2_CORE__InputPort.ptr).?; - //defer std.heap.c_allocator.destroy(lv2_InputPort); - const lv2_OutputPort = c.lilv_new_uri(world, LV2_CORE__OutputPort.ptr).?; - //defer std.heap.c_allocator.destroy(lv2_OutputPort); + // 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"); - const lv2_AudioPort = c.lilv_new_uri(world, LV2_CORE__AudioPort.ptr).?; - //defer std.heap.c_allocator.destroy(lv2_AudioPort); + var lv2_InputPort = c.lilv_new_uri(world, LV2_CORE__InputPort.ptr); + defer std.heap.c_allocator.destroy(lv2_InputPort); - const lv2_ControlPort = c.lilv_new_uri(world, LV2_CORE__ControlPort.ptr).?; - //defer std.heap.c_allocator.destroy(lv2_ControlPort); + var lv2_OutputPort = c.lilv_new_uri(world, LV2_CORE__OutputPort.ptr); + defer std.heap.c_allocator.destroy(lv2_OutputPort); - const lv2_connection_string = c.lilv_new_uri(world, LV2_CORE__connectionOptional.ptr).?; - //defer std.heap.c_allocator.destroy(lv2_connection_string); + 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]; + var port: *Port = ports[i]; const lport = c.lilv_plugin_get_port_by_index(ctx.plugin, i).?; @@ -110,17 +116,17 @@ pub fn setupPorts(ctx: *plugin.Context) ![]Port { port.index = i; if (std.math.isNan(values[i])) { - port.value = @as(f32, 0); + port.value = f32(0); } else { port.value = values[i]; } - port.optional = c.lilv_port_has_property(ctx.plugin, lport, lv2_connection_string); + 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) { - log.debug("Port {d} is neither input or output", .{i}); + std.debug.warn("Port {} is neither input or output\n", i); return error.UnassignedIOPort; } @@ -136,7 +142,7 @@ pub fn setupPorts(ctx: *plugin.Context) ![]Port { ctx.n_audio_out += 1; } } else if (!port.optional) { - log.debug("Port {d} has unsupported type", .{i}); + std.debug.warn("Port {} has unsupported type\n", i); return error.UnsupportedPortType; } } diff --git a/src/magick.zig b/src/magick.zig deleted file mode 100644 index ab59f0a..0000000 --- a/src/magick.zig +++ /dev/null @@ -1,85 +0,0 @@ -// imagemagick plugins -const std = @import("std"); -const images = @import("image.zig"); - -const log = std.log.scoped(.scritcher_magick); -const Image = images.Image; - -const mc = @cImport({ - @cInclude("wand/magick_wand.h"); -}); - -pub const MagickContext = struct { - wand: *mc.MagickWand, - - pub fn init() !MagickContext { - mc.InitializeMagick(null); - - const wand = mc.NewMagickWand(); - if (wand == null) return error.WandCreateFail; - - return MagickContext{ - .wand = wand.?, - }; - } - - pub fn deinit(self: *MagickContext) void { - _ = mc.DestroyMagickWand(self.wand); - mc.DestroyMagick(); - } - - pub fn doErr(self: *MagickContext) !void { - _ = self; - return error.WandError; - } -}; - -fn magickLoad(image: *Image) !MagickContext { - var mctx = try MagickContext.init(); - errdefer mctx.deinit(); - - const curpath = try image.allocator.dupeZ(u8, image.curpath); - defer image.allocator.free(curpath); - - log.debug("loading '{s}'", .{curpath}); - - if (mc.MagickReadImage(mctx.wand, curpath.ptr) != 1) - return error.MagickReadFail; - - return mctx; -} - -fn magickSave(image: *Image, wand: *mc.MagickWand) !void { - const allocator = image.allocator; - - const tmpnam = try images.temporaryName(allocator); - const c_tmpnam = try allocator.dupeZ(u8, tmpnam); - defer allocator.free(c_tmpnam); - - log.debug("\tmagick: saving to '{s}'..", .{c_tmpnam}); - - if (mc.MagickWriteImage(wand, c_tmpnam.ptr) != 1) - return error.MagickWriteFail; - - log.debug("OK", .{}); - - try image.reopen(tmpnam); -} - -/// Rotate the given image. -/// bgfill must point to a null-terminated string. -pub fn runRotate(image: *Image, deg: f32, bgfill: []const u8) !void { - var mctx = try magickLoad(image); - defer mctx.deinit(); - - const bg = mc.NewPixelWand(); - defer mc.DestroyPixelWand(bg); - - if (mc.PixelSetColor(bg, bgfill.ptr) != 1) - return error.PixelSetColorFail; - - if (mc.MagickRotateImage(mctx.wand, bg, deg) != 1) - return error.RotateFail; - - try magickSave(image, mctx.wand); -} diff --git a/src/main.zig b/src/main.zig index f21087e..bdbf8a6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,258 +1,33 @@ const std = @import("std"); const langs = @import("lang.zig"); const runners = @import("runner.zig"); -const printer = @import("printer.zig"); -const log = std.log.scoped(.scritcher); +pub fn main() !void { + var arena = std.heap.ArenaAllocator.init(std.heap.c_allocator); + defer arena.deinit(); -test "scritcher" { - _ = @import("lang.zig"); - _ = @import("runner.zig"); -} - -const readline = @cImport({ - @cInclude("stdio.h"); - @cInclude("stdlib.h"); - - @cInclude("readline/readline.h"); - @cInclude("readline/history.h"); -}); - -fn wrapInCmdList(allocator: std.mem.Allocator, cmd: *langs.Command) !langs.CommandList { - var cmds = langs.CommandList.init(allocator); - try cmds.append(cmd); - return cmds; -} - -fn copyCommandToHeap(allocator: std.mem.Allocator, command: langs.Command, comptime tag: langs.Command.Tag) !*langs.Command { - const CommandStruct = langs.Command.tagToType(tag); - const casted = command.cast(CommandStruct).?; - var heap_cmd = try allocator.create(CommandStruct); - - heap_cmd.* = casted.*; - log.debug("casted: {}", .{casted}); - log.debug("heap_cmd: {}", .{heap_cmd}); - - return &heap_cmd.base; -} - -pub fn doRepl(allocator: std.mem.Allocator, args_it: anytype) !void { - var stdout_file = std.io.getStdOut(); - const stdout = &stdout_file.writer(); - const scri_path = (args_it.next() orelse @panic("expected scri path")); - errdefer allocator.free(scri_path); - defer allocator.free(scri_path); - - var file_read_opt: ?std.fs.File = std.fs.cwd().openFile(scri_path, .{}) catch |err| blk: { - if (err == error.FileNotFound) break :blk null; - return err; - }; - const total_bytes = if (file_read_opt) |file_read| try file_read.getEndPos() else 0; - - var cmds = langs.CommandList.init(allocator); - defer cmds.deinit(); + const allocator = &arena.allocator; var lang = langs.Lang.init(allocator); - defer lang.deinit(); + //defer lang.deinit(); - if (total_bytes > 0) { - const scri_existing = try allocator.alloc(u8, total_bytes); - _ = try file_read_opt.?.read(scri_existing); - defer allocator.free(scri_existing); - - // we can't defer this directly because we copy the - // Command pointers to the cmds list. running deinit() directly - // would cause those pointers to be freed. - var existing_cmds = try lang.parse(scri_existing); - defer existing_cmds.list.deinit(); - - // copy the existing command list into the repl's command list - for (existing_cmds.list.items) |existing_cmd| { - try cmds.append(existing_cmd); - } - } else { - // if there isn't any commands on the file, we load our default - // 'load :0' command - - // TODO: deliberate memleak here. we only allocate this - // command once, for the start of the file, so. - var load_cmd = try allocator.create(langs.Command.Load); - load_cmd.path = ":0"; - load_cmd.base.tag = langs.Command.Tag.load; - - // taking address is fine, because load_cmd lives in the lifetime - // of the allocator. - try cmds.append(&load_cmd.base); - } - - if (file_read_opt) |file_read| { - file_read.close(); - } - - var file = try std.fs.cwd().createFile(scri_path, .{ - .read = false, - .truncate = true, - }); - defer file.close(); - - var out = file.writer(); - const stream = &out; - - // since we opened the file for writing, it becomes empty, so, to ensure - // we don't fuck up later on, we print cmds before starting the repl - try printer.printList(cmds, stdout); - try printer.printList(cmds, stream); - - // we keep - // - a CommandList with the full commands we have right now - // - a Command with the current last typed successful command - - // - one runner that contains the current full state of the image - // as if the current cmds was ran over it (TODO better wording) - // - one runner that gets copied from the original on every new - // command the user issues - - var current: langs.Command = undefined; - - var runner = runners.Runner.init(allocator, true); + var runner = runners.Runner.init(allocator); defer runner.deinit(); - // run the load command - try runner.runCommands(cmds, true); + var args_it = std.process.args(); - const wanted_runner: []const u8 = std.posix.getenv("SCRITCHER_RUNNER") orelse "ristretto"; + const exe_name = try (args_it.next(allocator) orelse @panic("expected exe name")); - var runqs_cmd = langs.Command.RunQS{ - .base = langs.Command{ .tag = langs.Command.Tag.runqs }, - .program = wanted_runner, - }; + // args[1] is the path to scri file + const scri_path = try (args_it.next(allocator) orelse @panic("expected scri path")); - while (true) { - lang.reset(); - - var rd_line = readline.readline("> "); - if (rd_line == null) { - log.debug("leaving from eof", .{}); - break; - } - readline.add_history(rd_line); - //defer std.heap.c_allocator.destroy(rd_line); - - const line = rd_line[0..std.mem.len(rd_line)]; - - if (std.mem.eql(u8, line, "push")) { - const heap_cmd = switch (current.tag) { - .noop => try copyCommandToHeap(allocator, current, .noop), - .load => try copyCommandToHeap(allocator, current, .load), - .quicksave => try copyCommandToHeap(allocator, current, .quicksave), - .runqs => try copyCommandToHeap(allocator, current, .runqs), - .amp => try copyCommandToHeap(allocator, current, .amp), - .rflanger => try copyCommandToHeap(allocator, current, .rflanger), - .eq => try copyCommandToHeap(allocator, current, .eq), - .phaser => try copyCommandToHeap(allocator, current, .phaser), - .mbeq => try copyCommandToHeap(allocator, current, .mbeq), - .chorus => try copyCommandToHeap(allocator, current, .chorus), - .pitchscaler => try copyCommandToHeap(allocator, current, .pitchscaler), - .reverb => try copyCommandToHeap(allocator, current, .reverb), - .highpass => try copyCommandToHeap(allocator, current, .highpass), - .delay => try copyCommandToHeap(allocator, current, .delay), - .vinyl => try copyCommandToHeap(allocator, current, .vinyl), - .revdelay => try copyCommandToHeap(allocator, current, .revdelay), - .gate => try copyCommandToHeap(allocator, current, .gate), - .detune => try copyCommandToHeap(allocator, current, .detune), - .overdrive => try copyCommandToHeap(allocator, current, .overdrive), - .degrade => try copyCommandToHeap(allocator, current, .degrade), - .repsycho => try copyCommandToHeap(allocator, current, .repsycho), - .talkbox => try copyCommandToHeap(allocator, current, .talkbox), - .dyncomp => try copyCommandToHeap(allocator, current, .dyncomp), - .thruzero => try copyCommandToHeap(allocator, current, .thruzero), - .foverdrive => try copyCommandToHeap(allocator, current, .foverdrive), - .gverb => try copyCommandToHeap(allocator, current, .gverb), - .invert => try copyCommandToHeap(allocator, current, .invert), - .tapedelay => try copyCommandToHeap(allocator, current, .tapedelay), - .moddelay => try copyCommandToHeap(allocator, current, .moddelay), - .multichorus => try copyCommandToHeap(allocator, current, .multichorus), - .saturator => try copyCommandToHeap(allocator, current, .saturator), - .vintagedelay => try copyCommandToHeap(allocator, current, .vintagedelay), - .noise => try copyCommandToHeap(allocator, current, .noise), - .wildnoise => try copyCommandToHeap(allocator, current, .wildnoise), - .write => try copyCommandToHeap(allocator, current, .write), - .embed => try copyCommandToHeap(allocator, current, .embed), - .rotate => try copyCommandToHeap(allocator, current, .rotate), - }; - - try cmds.append(heap_cmd); - - // run the current added command to main cmds list - // with the main parent runner - var cmds_wrapped = try wrapInCmdList(allocator, heap_cmd); - defer cmds_wrapped.deinit(); - try runner.runCommands(cmds_wrapped, true); - - continue; - } else if (std.mem.eql(u8, line, "list")) { - try printer.printList(cmds, stdout); - continue; - } else if (std.mem.eql(u8, line, "save")) { - // seek to 0 instead of appending the new command - // NOTE appending single command might be faster - try file.seekTo(0); - try printer.printList(cmds, stream); - continue; - } else if (std.mem.eql(u8, line, "quit") or std.mem.eql(u8, line, "q")) { - log.debug("leaving", .{}); - break; - } else if (std.mem.startsWith(u8, line, "#")) { - continue; - } - - var cmds_parsed = lang.parse(line) catch |err| { - log.debug("repl: error while parsing: {}", .{err}); - continue; - }; - - // no command? ignore! - if (cmds_parsed.list.items.len == 0) continue; - current = cmds_parsed.list.items[0].*; - - // by cloning the parent runner, we can iteratively write - // whatever command we want and only commit the good results - // back to the parent runner - var runner_clone = try runner.clone(); - defer runner_clone.deinit(); - - // taking address is fine, because runqs_cmd lives in the lifetime - // of this function. - try cmds_parsed.append(&runqs_cmd.base); - - try runner_clone.runCommands(cmds_parsed, true); - _ = try stdout.write("\n"); - } -} - -fn doHelp() void { - log.debug("scritcher!", .{}); - log.debug("usage: scritcher [run|help|repl]", .{}); - log.debug("\tscritcher run path_to_script.scri path_to_input_file.bmp", .{}); - log.debug("\tscritcher repl path_to_script.scri path_to_input_file.bmp", .{}); -} - -fn doRun(allocator: std.mem.Allocator, args_it: anytype) !void { - var lang = langs.Lang.init(allocator); - defer lang.deinit(); - - var runner = runners.Runner.init(allocator, false); - defer runner.deinit(); - - const scri_path = (args_it.next() orelse @panic("run: expected scri path")); - - var file = try std.fs.cwd().openFile(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(); - const data = try allocator.alloc(u8, total_bytes); + var data = try allocator.alloc(u8, total_bytes); defer allocator.free(data); _ = try file.read(data); @@ -261,33 +36,3 @@ fn doRun(allocator: std.mem.Allocator, args_it: anytype) !void { try runner.runCommands(cmds, true); } - -pub fn main() !void { - var allocator_instance = std.heap.GeneralPurposeAllocator(.{}){}; - defer { - _ = allocator_instance.deinit(); - } - const allocator = allocator_instance.allocator(); - - var args_it = try std.process.argsWithAllocator(allocator); - defer args_it.deinit(); - - _ = args_it.skip(); - const cli_command_opt = args_it.next(); - if (cli_command_opt == null) { - return doHelp(); - } - - const cli_command = cli_command_opt.?; - - if (std.mem.eql(u8, cli_command, "help")) { - return doHelp(); - } else if (std.mem.eql(u8, cli_command, "repl")) { - return try doRepl(allocator, &args_it); - } else if (std.mem.eql(u8, cli_command, "run")) { - return try doRun(allocator, &args_it); - } else { - log.debug("unknown command: '{s}'", .{cli_command}); - return error.UnknownCommand; - } -} diff --git a/src/plugin.zig b/src/plugin.zig index 29ec224..7794c9e 100644 --- a/src/plugin.zig +++ b/src/plugin.zig @@ -3,7 +3,6 @@ const std = @import("std"); const lv2 = @import("lv2_helpers.zig"); const c = lv2.c; -const log = std.log.scoped(.scritcher_plugin); const ImageError = @import("image.zig").ImageError; /// Control port @@ -17,7 +16,7 @@ pub const Param = struct { /// List of parameters to be set to control ports. pub const ParamList = std.ArrayList(Param); -pub const ParamMap = std.StringHashMap(f32); +pub const ParamMap = std.AutoHashMap([]const u8, f32); /// Represents an absolute position in the image. pub const SeekPos = struct { @@ -35,8 +34,7 @@ pub const Position = struct { index: usize, pub fn seekPos(self: Position, total_size: usize) SeekPos { - std.debug.assert(self.index <= self.split); - const tot = total_size / self.split; + var tot = total_size / self.split; return SeekPos{ .start = self.index * tot, @@ -47,7 +45,7 @@ pub const Position = struct { /// Represents the starting context for a single plugin run. pub const Context = struct { - allocator: std.mem.Allocator, + allocator: *std.mem.Allocator, world: *c.LilvWorld, plugin: *const c.LilvPlugin, @@ -62,11 +60,29 @@ pub const Context = struct { /// Specific run context for non-plugins. pub const RunBuffers = struct { - // we use [2]f32 to account for stereo plugins, however - // we only use in_buf[0] and out_buf[0], and don't use the - // (supposedly) right side of neither input or output. - in: [2]f32 = [_]f32{0} ** 2, - out: [2]f32 = [_]f32{0} ** 2, + allocator: *std.mem.Allocator, + + in: []f32, + out: []f32, + + pub fn init(allocator: *std.mem.Allocator) !RunBuffers { + // we allocate []f32 with size 2 to account for stereo plugins, however + // we only use &in_buf[0] and &out_buf[0], and don't use the + // (supposedly) right side of neither input or output. + var in_buf = try allocator.alloc(f32, 2); + std.mem.secureZero(f32, in_buf); + + return RunBuffers{ + .allocator = allocator, + .in = in_buf, + .out = try allocator.alloc(f32, 2), + }; + } + + pub fn deinit(self: *RunBuffers) void { + self.allocator.free(self.in); + self.allocator.free(self.out); + } }; /// Represents the specific run context of plugin instantation. @@ -75,86 +91,76 @@ pub const RunContext = struct { instance: *c.LilvInstance, pub fn init( - allocator: std.mem.Allocator, + allocator: *std.mem.Allocator, plugin: *const c.LilvPlugin, ) !RunContext { - _ = allocator; // TODO batch RunBuffers? - - const instance = c.lilv_plugin_instantiate(plugin, @as(f64, 44100), null); - errdefer c.lilv_instance_free(instance); - + var instance = c.lilv_plugin_instantiate(plugin, f64(44100), null); if (instance == null) { return ImageError.InstantiateFail; } + // we allocate []f32 with size 2 to account for stereo plugins, however + // we only use &in_buf[0] and &out_buf[0], and don't use the + // (supposedly) right side of neither input or output. + var in_buf = try allocator.alloc(f32, 2); + std.mem.secureZero(f32, in_buf); + return RunContext{ - .buffers = RunBuffers{}, + .buffers = try RunBuffers.init(allocator), .instance = instance.?, }; } pub fn deinit(self: *RunContext) void { c.lilv_instance_free(self.instance); + self.buffers.deinit(); } - pub fn connectPorts(self: *RunContext, ports: []lv2.Port) void { + pub fn connectPorts(self: *RunContext, ports: []*lv2.Port) void { var i: usize = 0; var o: usize = 0; - for (ports, 0..) |_, p_idx| { - const p = @as(u32, @intCast(p_idx)); - var port: *lv2.Port = &ports[p_idx]; + var in_buf = self.buffers.in; + var out_buf = self.buffers.out; + + for (ports) |port, p_idx| { + var p = @intCast(u32, p_idx); switch (port.ptype) { .Control => lv2.lilv_instance_connect_port(self.instance, p, &port.value), - .Audio => { + .Audio => blk: { if (port.is_input) { - lv2.lilv_instance_connect_port( - self.instance, - p, - &self.buffers.in[i], - ); + lv2.lilv_instance_connect_port(self.instance, p, &in_buf[i]); i += 1; } else { - lv2.lilv_instance_connect_port( - self.instance, - p, - &self.buffers.out[o], - ); + lv2.lilv_instance_connect_port(self.instance, p, &out_buf[o]); o += 1; } }, - // else => lv2.lilv_instance_connect_port(self.instance, p, null), + else => lv2.lilv_instance_connect_port(self.instance, p, null), } } } }; -pub fn makeContext(allocator: std.mem.Allocator, plugin_uri: []const u8) !Context { - const cstr_plugin_uri = try allocator.dupeZ(u8, plugin_uri); - defer allocator.free(cstr_plugin_uri); - - const world: *c.LilvWorld = c.lilv_world_new().?; - errdefer c.lilv_world_free(world); +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); - - const uri: *c.LilvNode = c.lilv_new_uri(world, cstr_plugin_uri.ptr) orelse { - log.debug("Invalid plugin URI <{s}>", .{plugin_uri}); + 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; }; - defer c.lilv_node_free(uri); - const plugins: *const c.LilvPlugins = c.lilv_world_get_all_plugins(world).?; + const plugins: *const c.LilvPlugins = c.lilv_world_get_all_plugins(world); - const plugin: *const c.LilvPlugin = c.lilv_plugins_get_by_uri(plugins, uri) orelse { - log.debug("Plugin <{s}> not found", .{plugin_uri}); + 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; }; - return Context{ - .allocator = allocator, - .world = world, - .plugin = plugin, - }; + c.lilv_node_free(uri); + + return Context{ .allocator = allocator, .world = world, .plugin = plugin }; } diff --git a/src/printer.zig b/src/printer.zig deleted file mode 100644 index b3da89b..0000000 --- a/src/printer.zig +++ /dev/null @@ -1,88 +0,0 @@ -const std = @import("std"); -const langs = @import("lang.zig"); - -const log = std.log.scoped(.scritcher_printer); - -fn printCommandWithParams(stream: anytype, command: anytype) !void { - const Parameters = @TypeOf(command.parameters); - try stream.print(" {d} {d}", .{ command.split, command.index }); - inline for (@typeInfo(Parameters).Struct.fields) |field| { - if (field.type == f32 or field.type == f64) { - try stream.print(" {}", .{@field(command.parameters, field.name)}); - } else if (field.type == usize or field.type == u64) { - try stream.print(" {d}", .{@field(command.parameters, field.name)}); - } else { - try stream.print(" {s}", .{@field(command.parameters, field.name)}); - } - } -} - -fn printCommand(stream: anytype, cmd: *langs.Command, comptime tag: langs.Command.Tag) !void { - const CommandStruct = langs.Command.tagToType(tag); - const casted = cmd.cast(CommandStruct).?; - - const ctype = CommandStruct.command_type; - switch (ctype) { - .lv2_command => try printCommandWithParams(stream, casted), - .custom_command => try printCommandWithParams(stream, casted), - } -} - -pub fn printList(list: langs.CommandList, stream: anytype) !void { - for (list.list.items) |cmd| { - const command = @tagName(cmd.tag); - try stream.print("{s}", .{command}); - - switch (cmd.tag) { - .load => { - const load = cmd.cast(langs.Command.Load).?; - try stream.print(" {s}", .{load.path}); - }, - .runqs => { - const runqs = cmd.cast(langs.Command.RunQS).?; - try stream.print(" {s}", .{runqs.program}); - }, - .noop, .quicksave => {}, - .rotate => { - const rotate = cmd.cast(langs.Command.Rotate).?; - try stream.print(" {d} {s}", .{ rotate.deg, rotate.bgfill }); - }, - - .amp => try printCommand(stream, cmd, .amp), - .rflanger => try printCommand(stream, cmd, .rflanger), - .eq => try printCommand(stream, cmd, .eq), - .phaser => try printCommand(stream, cmd, .phaser), - .mbeq => try printCommand(stream, cmd, .mbeq), - .chorus => try printCommand(stream, cmd, .chorus), - .pitchscaler => try printCommand(stream, cmd, .pitchscaler), - .reverb => try printCommand(stream, cmd, .reverb), - .highpass => try printCommand(stream, cmd, .highpass), - .delay => try printCommand(stream, cmd, .delay), - .vinyl => try printCommand(stream, cmd, .vinyl), - .revdelay => try printCommand(stream, cmd, .revdelay), - .gate => try printCommand(stream, cmd, .gate), - .detune => try printCommand(stream, cmd, .detune), - .overdrive => try printCommand(stream, cmd, .overdrive), - .degrade => try printCommand(stream, cmd, .degrade), - .repsycho => try printCommand(stream, cmd, .repsycho), - .talkbox => try printCommand(stream, cmd, .talkbox), - .dyncomp => try printCommand(stream, cmd, .dyncomp), - .thruzero => try printCommand(stream, cmd, .thruzero), - .foverdrive => try printCommand(stream, cmd, .foverdrive), - .gverb => try printCommand(stream, cmd, .gverb), - .invert => try printCommand(stream, cmd, .invert), - .tapedelay => try printCommand(stream, cmd, .tapedelay), - .moddelay => try printCommand(stream, cmd, .moddelay), - .multichorus => try printCommand(stream, cmd, .multichorus), - .saturator => try printCommand(stream, cmd, .saturator), - .vintagedelay => try printCommand(stream, cmd, .vintagedelay), - - .noise => try printCommand(stream, cmd, .noise), - .wildnoise => try printCommand(stream, cmd, .wildnoise), - .write => try printCommand(stream, cmd, .write), - .embed => try printCommand(stream, cmd, .embed), - } - - _ = try stream.write(";\n"); - } -} diff --git a/src/runner.zig b/src/runner.zig index 85b8d79..c835ba9 100644 --- a/src/runner.zig +++ b/src/runner.zig @@ -3,9 +3,6 @@ const lang = @import("lang.zig"); const images = @import("image.zig"); const plugin = @import("plugin.zig"); const custom = @import("custom.zig"); -const magick = @import("magick.zig"); - -const log = std.log.scoped(.scritcher_runner); const Position = plugin.Position; const ParamList = plugin.ParamList; @@ -20,21 +17,12 @@ pub const RunError = error{ }; pub const Runner = struct { - allocator: std.mem.Allocator, - - /// The currently opened image in the runner + allocator: *std.mem.Allocator, image: ?*Image = null, - /// If the runner is in REPL mode - repl: bool, - - args: []const [:0]u8, - - pub fn init(allocator: std.mem.Allocator, repl: bool) Runner { + pub fn init(allocator: *std.mem.Allocator) Runner { return Runner{ .allocator = allocator, - .repl = repl, - .args = std.process.argsAlloc(allocator) catch unreachable, }; } @@ -42,59 +30,41 @@ pub const Runner = struct { if (self.image) |image| { image.close(); } - - std.process.argsFree(self.allocator, self.args); - } - - pub fn clone(self: *Runner) !Runner { - const cloned_image = if (self.image) |image| try image.clone() else null; - return Runner{ - .allocator = self.allocator, - .image = cloned_image, - .repl = self.repl, - .args = self.args, - }; } fn resolveArg(self: *Runner, load_path: []const u8) ![]const u8 { - std.debug.assert(load_path.len > 0); if (load_path[0] == ':') { // parse the index from 1 to end - var index = try std.fmt.parseInt(usize, load_path[1..], 10); + const index = try std.fmt.parseInt(usize, load_path[1..], 10); + var args_it = std.process.args(); + _ = args_it.skip(); - // if it isn't in the repl, args look like this: - // 'scritcher ./script ./image' - // if it is, it looks like this - // 'scritcher repl ./script ./image' - - // ':0' should ALWAYS point to the image. - if (self.repl) index += 3 else index += 3; - - for (self.args, 0..) |arg, idx| { - log.debug("arg{d} = {s}", .{ idx, arg }); + var i: usize = 0; + while (i <= index) : (i += 1) { + _ = args_it.skip(); } - log.debug("fetch arg idx={d}", .{index}); - log.debug("fetch arg val={s}", .{self.args[index]}); - return self.args[index]; + + const arg = try (args_it.next(self.allocator) orelse @panic("expected argument")); + + return arg; } else { return load_path; } } - // Caller owns returned memory. fn resolveArgPath(self: *Runner, path_or_argidx: []const u8) ![]const u8 { const path = try self.resolveArg(path_or_argidx); const resolved_path = try std.fs.path.resolve( self.allocator, - &[_][]const u8{path}, + [_][]const u8{path}, ); return resolved_path; } fn loadCmd(self: *Runner, path_or_argidx: []const u8) !void { - const load_path = try self.resolveArgPath(path_or_argidx); - log.debug("\tload path: {s}", .{load_path}); + var load_path = try self.resolveArgPath(path_or_argidx); + std.debug.warn("load path: {}\n", load_path); // we could use ImageMagick to convert from X to BMP // but i can't find an easy way to do things in memory. @@ -103,15 +73,14 @@ pub const Runner = struct { // before loading the file into scritcher. for example, you can start // krita/gimp and make it export a bmp and while in the program you can // apply filters, etc. - if (!std.mem.endsWith(u8, load_path, ".bmp") and !std.mem.endsWith(u8, load_path, ".ppm")) { - log.debug("Only BMP files are allowed to be loaded. Got path '{s}'", .{load_path}); + if (!std.mem.endsWith(u8, load_path, ".bmp")) { + std.debug.warn("Only BMP files are allowed to be loaded.\n"); return RunError.NoBMP; } // we don't copy load_path into a temporary file because we're already // loading it under the SFM_READ mode, which won't cause any destructive // operations on the file. - if (self.image) |image| image.close(); self.image = try Image.open(self.allocator, load_path); } @@ -119,21 +88,20 @@ pub const Runner = struct { if (self.image) |image| { return image; } else { - log.debug("image is required!", .{}); + std.debug.warn("image is required!\n"); return RunError.ImageRequired; } } - /// Caller owns returned memory. fn makeGlitchedPath(self: *Runner) ![]const u8 { // we want to transform basename, if it is "x.bmp" to "x_gN.bmp", where // N is the maximum non-used integer. - const image = try self.getImage(); + var image = try self.getImage(); const basename = std.fs.path.basename(image.path); const dirname = std.fs.path.dirname(image.path).?; - var dir = try std.fs.cwd().openDir(dirname, .{ .iterate = true }); + var dir = try std.fs.Dir.open(self.allocator, dirname); defer dir.close(); const period_idx = std.mem.lastIndexOf(u8, basename, ".").?; @@ -141,18 +109,17 @@ pub const Runner = struct { // starts_with would be "x_g", we want to find all files in the directory // that start with that name. - const starts_with = try std.fmt.allocPrint(self.allocator, "{s}_g", .{ + const starts_with = try std.fmt.allocPrint( + self.allocator, + "{}_g", basename[0..period_idx], - }); - defer self.allocator.free(starts_with); + ); var max: usize = 0; - var it = dir.iterate(); - - while (try it.next()) |entry| { + while (try dir.next()) |entry| { switch (entry.kind) { - .file => blk: { + .File => blk: { if (!std.mem.startsWith(u8, entry.name, starts_with)) break :blk {}; // we want to get the N in x_gN.ext @@ -166,8 +133,6 @@ pub const Runner = struct { // if N isn't a number, we just ignore that file const idx_str = entry.name[entry_gidx + 2 .. entry_pidx]; const idx = std.fmt.parseInt(usize, idx_str, 10) catch |err| { - log.debug("ignoring file {s}", .{@errorName(err)}); - break :blk {}; }; @@ -177,12 +142,14 @@ pub const Runner = struct { } } - const out_path = try std.fmt.allocPrint(self.allocator, "{s}/{s}{d}{s}", .{ + const out_path = try std.fmt.allocPrint( + self.allocator, + "{}/{}{}{}", dirname, starts_with, max + 1, extension, - }); + ); return out_path; } @@ -190,156 +157,284 @@ pub const Runner = struct { fn quicksaveCmd(self: *Runner) !void { var image = try self.getImage(); const out_path = try self.makeGlitchedPath(); - defer self.allocator.free(out_path); try image.saveTo(out_path); } - fn runQSCmd(self: *Runner, cmd: *lang.Command) !void { - const runqs = cmd.cast(lang.Command.RunQS).?; + fn runQSCmd(self: *Runner, program: []const u8) !void { var image = try self.getImage(); const out_path = try self.makeGlitchedPath(); - defer self.allocator.free(out_path); try image.saveTo(out_path); - var proc = std.ChildProcess.init( - &[_][]const u8{ runqs.program, out_path }, + var proc = try std.ChildProcess.init( + [_][]const u8{ program, out_path }, self.allocator, ); - //defer proc.deinit(); + defer proc.deinit(); - log.debug("running '{s} {s}'", .{ runqs.program, out_path }); + std.debug.warn("running '{} {}'", program, out_path); _ = try proc.spawnAndWait(); } - fn rotateCmd(self: *Runner, cmd: *lang.Command) !void { - const rotate_cmd = cmd.cast(lang.Command.Rotate).?; - - const image = try self.getImage(); - const c_bgfill = try self.allocator.dupeZ(u8, rotate_cmd.bgfill); - defer self.allocator.free(c_bgfill); - - try magick.runRotate(image, rotate_cmd.deg, c_bgfill); + /// Run the http://lv2plug.in/plugins/eg-amp plugin over the file. + fn ampCmd(self: *Runner, pos: Position, params: ParamList) !void { + var image = try self.getImage(); + try image.runPlugin("http://lv2plug.in/plugins/eg-amp", pos, params); } - fn executeLV2Command(self: *@This(), command: anytype) !void { - const pos = plugin.Position{ - .split = command.split, - .index = command.index, - }; + fn rFlangerCmd(self: *Runner, pos: Position, params: ParamList) !void { + var image = try self.getImage(); + try image.runPlugin("http://plugin.org.uk/swh-plugins/retroFlange", pos, params); + } + fn eqCmd(self: *Runner, position: Position, params: ParamList) !void { + var image = try self.getImage(); + try image.runPlugin("http://plugin.org.uk/swh-plugins/dj_eq_mono", position, params); + } + + fn phaserCmd(self: *Runner, position: Position, params: ParamList) !void { + var image = try self.getImage(); + try image.runPlugin("http://plugin.org.uk/swh-plugins/lfoPhaser", position, params); + } + + fn mbeqCmd(self: *Runner, position: Position, bands: []const f32) !void { + var image = try self.getImage(); var params = ParamList.init(self.allocator); defer params.deinit(); - const typ = @TypeOf(command); - - inline for (@typeInfo(@TypeOf(command.parameters)).Struct.fields) |cmd_field| { + for (bands) |band_value, idx| { + var sym = try std.fmt.allocPrint(self.allocator, "band_{}", idx + 1); try params.append(plugin.Param{ - .sym = cmd_field.name, - .value = @field(command.parameters, cmd_field.name), + .sym = sym, + .value = band_value, }); } + try image.runPlugin("http://plugin.org.uk/swh-plugins/mbeq", position, params); + } + + fn chorusCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); - try image.runPlugin(typ.lv2_url, pos, params); + try image.runPlugin("http://plugin.org.uk/swh-plugins/multivoiceChorus", pos, params); } - fn executeCustomCommand(self: *@This(), command: anytype) !void { - const pos = plugin.Position{ - .split = command.split, - .index = command.index, - }; - + fn pitchScalerCmd(self: *Runner, pos: Position, params: ParamList) !void { var image = try self.getImage(); - try image.runCustomPlugin(@TypeOf(command).plugin_type, pos, command.parameters); + try image.runPlugin("http://plugin.org.uk/swh-plugins/pitchScaleHQ", pos, params); } - fn runSingleCommand( - self: *@This(), - cmd: *lang.Command, - comptime tag: lang.Command.Tag, - ) !void { - const typ = lang.Command.tagToType(tag); - const command = cmd.cast(typ).?; - const ctype = typ.command_type; - switch (ctype) { - .lv2_command => try self.executeLV2Command(command.*), - .custom_command => try self.executeCustomCommand(command.*), - } + fn reverbCmd(self: *Runner, pos: Position, params: ParamList) !void { + var image = try self.getImage(); + try image.runPlugin("http://invadarecords.com/plugins/lv2/erreverb/mono", pos, params); } - fn runCommand(self: *@This(), cmd: *lang.Command) !void { - switch (cmd.tag) { - .noop => {}, - .load => { - const command = cmd.cast(lang.Command.Load).?; - try self.loadCmd(command.path); + fn highpassCmd(self: *Runner, pos: Position, params: ParamList) !void { + var image = try self.getImage(); + try image.runPlugin("http://invadarecords.com/plugins/lv2/filter/hpf/mono", pos, params); + } + + fn delayCmd(self: *Runner, pos: Position, params: ParamList) !void { + var image = try self.getImage(); + try image.runPlugin("http://plugin.org.uk/swh-plugins/delayorama", pos, params); + } + + fn vinylCmd(self: *Runner, pos: Position, params: ParamList) !void { + var image = try self.getImage(); + try image.runPlugin("http://plugin.org.uk/swh-plugins/vynil", pos, params); + } + + fn revDelayCmd(self: *Runner, pos: Position, params: ParamList) !void { + var image = try self.getImage(); + try image.runPlugin("http://plugin.org.uk/swh-plugins/revdelay", pos, params); + } + + fn noiseCmd(self: *Runner, pos: Position, map: *ParamMap) !void { + var image = try self.getImage(); + try image.runCustomPlugin(custom.RandomNoise, pos, map); + } + + fn wildNoiseCmd(self: *Runner, pos: Position, map: *ParamMap) !void { + var image = try self.getImage(); + try image.runCustomPlugin(custom.WildNoise, pos, map); + } + + fn runCommand(self: *Runner, cmd: *lang.Command) !void { + var params = ParamList.init(self.allocator); + defer params.deinit(); + + var map = ParamMap.init(self.allocator); + defer map.deinit(); + + return switch (cmd.command) { + .Noop => {}, + .Load => blk: { + var path = cmd.args.at(0); + try self.loadCmd(path); + break :blk; }, - .quicksave => try self.quicksaveCmd(), - .rotate => try self.rotateCmd(cmd), - .runqs => try self.runQSCmd(cmd), + .Quicksave => try self.quicksaveCmd(), + .RunQS => try self.runQSCmd(cmd.args.at(0)), - .amp => try self.runSingleCommand(cmd, .amp), - .rflanger => try self.runSingleCommand(cmd, .rflanger), - .eq => try self.runSingleCommand(cmd, .eq), - .phaser => try self.runSingleCommand(cmd, .phaser), - .mbeq => try self.runSingleCommand(cmd, .mbeq), - .chorus => try self.runSingleCommand(cmd, .chorus), - .pitchscaler => try self.runSingleCommand(cmd, .pitchscaler), - .reverb => try self.runSingleCommand(cmd, .reverb), - .highpass => try self.runSingleCommand(cmd, .highpass), - .delay => try self.runSingleCommand(cmd, .delay), - .vinyl => try self.runSingleCommand(cmd, .vinyl), - .revdelay => try self.runSingleCommand(cmd, .revdelay), - .gate => try self.runSingleCommand(cmd, .gate), - .detune => try self.runSingleCommand(cmd, .detune), - .overdrive => try self.runSingleCommand(cmd, .overdrive), - .degrade => try self.runSingleCommand(cmd, .degrade), - .repsycho => try self.runSingleCommand(cmd, .repsycho), - .talkbox => try self.runSingleCommand(cmd, .talkbox), - .dyncomp => try self.runSingleCommand(cmd, .dyncomp), - .thruzero => try self.runSingleCommand(cmd, .thruzero), - .foverdrive => try self.runSingleCommand(cmd, .foverdrive), - .gverb => try self.runSingleCommand(cmd, .gverb), - .invert => try self.runSingleCommand(cmd, .invert), - .tapedelay => try self.runSingleCommand(cmd, .tapedelay), - .moddelay => try self.runSingleCommand(cmd, .moddelay), - .multichorus => try self.runSingleCommand(cmd, .multichorus), - .saturator => try self.runSingleCommand(cmd, .saturator), - .vintagedelay => try self.runSingleCommand(cmd, .vintagedelay), + .Amp => blk: { + const pos = try cmd.consumePosition(); + try cmd.appendParam(¶ms, "gain"); + try self.ampCmd(pos, params); + }, - .noise => try self.runSingleCommand(cmd, .noise), - .wildnoise => try self.runSingleCommand(cmd, .wildnoise), - .write => try self.runSingleCommand(cmd, .write), - .embed => try self.runSingleCommand(cmd, .embed), - } + .RFlanger => blk: { + const pos = try cmd.consumePosition(); + try cmd.appendParam(¶ms, "delay_depth_avg"); + try cmd.appendParam(¶ms, "law_freq"); + try self.rFlangerCmd(pos, params); + }, + + .Eq => blk: { + const pos = try cmd.consumePosition(); + try cmd.appendParam(¶ms, "lo"); + try cmd.appendParam(¶ms, "mid"); + try cmd.appendParam(¶ms, "hi"); + + try self.eqCmd(pos, params); + }, + + .Phaser => blk: { + const pos = try cmd.consumePosition(); + + try cmd.appendParam(¶ms, "lfo_rate"); + try cmd.appendParam(¶ms, "lfo_depth"); + try cmd.appendParam(¶ms, "fb"); + try cmd.appendParam(¶ms, "spread"); + + try self.phaserCmd(pos, params); + }, + + .Mbeq => blk: { + const pos = try cmd.consumePosition(); + const bands = try cmd.floatArgMany(self.allocator, 2, 15, f32(0)); + try self.mbeqCmd(pos, bands); + }, + + .Chorus => blk: { + const pos = try cmd.consumePosition(); + + try cmd.appendParam(¶ms, "voices"); + try cmd.appendParam(¶ms, "delay_base"); + try cmd.appendParam(¶ms, "voice_spread"); + try cmd.appendParam(¶ms, "detune"); + try cmd.appendParam(¶ms, "law_freq"); + try cmd.appendParam(¶ms, "attendb"); + + try self.chorusCmd(pos, params); + }, + + .PitchScaler => blk: { + const pos = try cmd.consumePosition(); + try cmd.appendParam(¶ms, "mult"); + try self.pitchScalerCmd(pos, params); + }, + + .Reverb => blk: { + const pos = try cmd.consumePosition(); + + try cmd.appendParam(¶ms, "roomLength"); + try cmd.appendParam(¶ms, "roomHeight"); + try cmd.appendParam(¶ms, "sourceLR"); + try cmd.appendParam(¶ms, "sourceFB"); + try cmd.appendParam(¶ms, "listLR"); + try cmd.appendParam(¶ms, "listFB"); + try cmd.appendParam(¶ms, "hpf"); + try cmd.appendParam(¶ms, "warmth"); + try cmd.appendParam(¶ms, "diffusion"); + + try self.reverbCmd(pos, params); + }, + + .Highpass => blk: { + const pos = try cmd.consumePosition(); + + try cmd.appendParam(¶ms, "freq"); + try cmd.appendParam(¶ms, "gain"); + try cmd.appendParam(¶ms, "noClip"); + + try self.highpassCmd(pos, params); + }, + + .Delay => blk: { + const pos = try cmd.consumePosition(); + + try cmd.appendParam(¶ms, "seed"); + try cmd.appendParam(¶ms, "gain"); + try cmd.appendParam(¶ms, "feedback_pc"); + try cmd.appendParam(¶ms, "tap_count"); + try cmd.appendParam(¶ms, "first_delay"); + try cmd.appendParam(¶ms, "delay_range"); + try cmd.appendParam(¶ms, "delay_scale"); + try cmd.appendParam(¶ms, "delay_rand_pc"); + try cmd.appendParam(¶ms, "gain_scale"); + try cmd.appendParam(¶ms, "wet"); + + try self.delayCmd(pos, params); + }, + + .Vinyl => blk: { + const pos = try cmd.consumePosition(); + + try cmd.appendParam(¶ms, "year"); + try cmd.appendParam(¶ms, "rpm"); + try cmd.appendParam(¶ms, "warp"); + try cmd.appendParam(¶ms, "click"); + try cmd.appendParam(¶ms, "wear"); + + try self.vinylCmd(pos, params); + }, + + .RevDelay => blk: { + const pos = try cmd.consumePosition(); + + try cmd.appendParam(¶ms, "delay_time"); + try cmd.appendParam(¶ms, "dry_level"); + try cmd.appendParam(¶ms, "wet_level"); + try cmd.appendParam(¶ms, "feedback"); + try cmd.appendParam(¶ms, "xfade_samp"); + + try self.revDelayCmd(pos, params); + }, + + .Noise => blk: { + const pos = try cmd.consumePosition(); + + try cmd.appendParamMap(&map, "seed"); + try cmd.appendParamMap(&map, "fill_bytes"); + + try self.noiseCmd(pos, &map); + }, + + .WildNoise => blk: { + const pos = try cmd.consumePosition(); + + try cmd.appendParamMap(&map, "seed"); + try cmd.appendParamMap(&map, "fill_bytes"); + + try self.wildNoiseCmd(pos, &map); + }, + + else => blk: { + std.debug.warn("Unsupported command: {}\n", cmd.command); + break :blk RunError.UnknownCommand; + }, + }; } + /// Run a list of commands. pub fn runCommands( self: *Runner, cmds: lang.CommandList, debug_flag: bool, ) !void { - _ = debug_flag; - for (cmds.list.items) |cmd| { - cmd.print(); + var it = cmds.iterator(); + + while (it.next()) |cmd| { + if (debug_flag) cmd.print(); try self.runCommand(cmd); } } }; - -test "running noop" { - const allocator = std.testing.allocator; - var cmds = lang.CommandList.init(allocator); - defer cmds.deinit(); - - const command = lang.Command{ .tag = .noop }; - - var noop = try allocator.create(lang.Command.Noop); - noop.* = lang.Command.Noop{ .base = command }; - try cmds.append(&noop.base); - - var runner = Runner.init(allocator, false); - defer runner.deinit(); - - try runner.runCommands(cmds, false); -}