Compare commits

..

No commits in common. "master" and "noise-command" have entirely different histories.

38 changed files with 796 additions and 2255 deletions

4
.gitignore vendored
View File

@ -1,5 +1 @@
zig-cache/
zig-out/
*.mp3
*.wav
build_runner.zig

View File

@ -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

View File

@ -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);
}

View File

@ -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

View File

@ -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;

View File

@ -1,3 +0,0 @@
load :0;
detune 3 1 0.2 0.9 0.5 0.5;
quicksave;

View File

@ -1,3 +0,0 @@
load :0;
dyncomp 3 1 1 0 0 -30 0 0.01 0.3 0 0 0;
quicksave;

View File

@ -1,3 +0,0 @@
load :0;
embed 3 1 ./file.wav;
quicksave;

View File

@ -1,3 +0,0 @@
load :0;
foverdrive 3 1 100;
quicksave;

View File

@ -1,3 +0,0 @@
load :0;
gate 3 1 0 -70 30 500 1000 -90;
quicksave;

View File

@ -1,3 +0,0 @@
load :0;
gverb 3 1 75.75 7.575 0.5 0.75 0 0 -17.5;
quicksave;

View File

@ -1,3 +0,0 @@
load :0;
invert 3 1;
quicksave;

View File

@ -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;

View File

@ -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;

View File

@ -1,3 +0,0 @@
load :0;
moddelay 3 1 1;
quicksave;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -1,3 +0,0 @@
load :0;
rflanger 3 1 2.5 1;
quicksave;

View File

@ -1,3 +0,0 @@
load :0;
rotate 30 #ff00ff;
quicksave;

View File

@ -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;

View File

@ -1,3 +0,0 @@
load :0;
talkbox 3 1 0.5 0 0 1;
quicksave;

View File

@ -1,3 +0,0 @@
load :0;
tapedelay 3 1 1 -90 0 0 1 -90 2 -90 3 -90;
quicksave;

View File

@ -1,3 +0,0 @@
load :0;
thruzero 3 1 0.3 0.47 0.3 1;
quicksave;

View File

@ -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;

View File

@ -1,3 +0,0 @@
load :0;
write 3 1 1;
quicksave;

View File

@ -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;
}
}

View File

@ -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];
}
};

View File

@ -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;
}
};

View File

@ -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

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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 };
}

View File

@ -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");
}
}

View File

@ -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(&params, "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(&params, "delay_depth_avg");
try cmd.appendParam(&params, "law_freq");
try self.rFlangerCmd(pos, params);
},
.Eq => blk: {
const pos = try cmd.consumePosition();
try cmd.appendParam(&params, "lo");
try cmd.appendParam(&params, "mid");
try cmd.appendParam(&params, "hi");
try self.eqCmd(pos, params);
},
.Phaser => blk: {
const pos = try cmd.consumePosition();
try cmd.appendParam(&params, "lfo_rate");
try cmd.appendParam(&params, "lfo_depth");
try cmd.appendParam(&params, "fb");
try cmd.appendParam(&params, "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(&params, "voices");
try cmd.appendParam(&params, "delay_base");
try cmd.appendParam(&params, "voice_spread");
try cmd.appendParam(&params, "detune");
try cmd.appendParam(&params, "law_freq");
try cmd.appendParam(&params, "attendb");
try self.chorusCmd(pos, params);
},
.PitchScaler => blk: {
const pos = try cmd.consumePosition();
try cmd.appendParam(&params, "mult");
try self.pitchScalerCmd(pos, params);
},
.Reverb => blk: {
const pos = try cmd.consumePosition();
try cmd.appendParam(&params, "roomLength");
try cmd.appendParam(&params, "roomHeight");
try cmd.appendParam(&params, "sourceLR");
try cmd.appendParam(&params, "sourceFB");
try cmd.appendParam(&params, "listLR");
try cmd.appendParam(&params, "listFB");
try cmd.appendParam(&params, "hpf");
try cmd.appendParam(&params, "warmth");
try cmd.appendParam(&params, "diffusion");
try self.reverbCmd(pos, params);
},
.Highpass => blk: {
const pos = try cmd.consumePosition();
try cmd.appendParam(&params, "freq");
try cmd.appendParam(&params, "gain");
try cmd.appendParam(&params, "noClip");
try self.highpassCmd(pos, params);
},
.Delay => blk: {
const pos = try cmd.consumePosition();
try cmd.appendParam(&params, "seed");
try cmd.appendParam(&params, "gain");
try cmd.appendParam(&params, "feedback_pc");
try cmd.appendParam(&params, "tap_count");
try cmd.appendParam(&params, "first_delay");
try cmd.appendParam(&params, "delay_range");
try cmd.appendParam(&params, "delay_scale");
try cmd.appendParam(&params, "delay_rand_pc");
try cmd.appendParam(&params, "gain_scale");
try cmd.appendParam(&params, "wet");
try self.delayCmd(pos, params);
},
.Vinyl => blk: {
const pos = try cmd.consumePosition();
try cmd.appendParam(&params, "year");
try cmd.appendParam(&params, "rpm");
try cmd.appendParam(&params, "warp");
try cmd.appendParam(&params, "click");
try cmd.appendParam(&params, "wear");
try self.vinylCmd(pos, params);
},
.RevDelay => blk: {
const pos = try cmd.consumePosition();
try cmd.appendParam(&params, "delay_time");
try cmd.appendParam(&params, "dry_level");
try cmd.appendParam(&params, "wet_level");
try cmd.appendParam(&params, "feedback");
try cmd.appendParam(&params, "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);
}