Compare commits

...

213 Commits

Author SHA1 Message Date
Luna 3af4dc575a fix command ptr alignment 2024-06-01 16:53:01 -03:00
Luna c0da5e68b7 base port to zig 0.12 2024-06-01 15:46:37 -03:00
Luna c73b98a356 fix for zig 0.11 2023-08-04 21:50:59 -03:00
Luna 7cc93c2976 zig fmt 2023-08-04 21:34:51 -03:00
Luna ed67a52b15 fixes for some version of zig 2023-08-04 21:34:17 -03:00
Luna 0f6ac055a4 remove extra newlines from log calls 2022-10-08 15:14:56 -03:00
Luna 43dad638f2 use stage1 2022-10-08 15:06:14 -03:00
Luna ef6c68705d port to latest zig 2022-10-08 15:05:15 -03:00
Luna 6b2ce7e425 properly fix memleaks 2022-04-28 00:28:07 -03:00
Luna 2796b654e5 port main() code path to latest zig 2022-04-28 00:08:52 -03:00
Luna 1987c4d497 fix command parsing 2022-04-27 20:01:14 -03:00
Luna 5d1bcf33ee fixes for latest zig: a lot of stuff 2022-04-27 20:01:12 -03:00
Luna c817170a04 port to latest zig: use std.log 2022-04-27 20:01:09 -03:00
Luna 268be1074c port to latest zig fs api 2022-04-27 20:01:06 -03:00
Luna d7d4385242 fix memory leak on string arguments 2022-04-27 20:01:00 -03:00
Luna b195577dee prevent memleaks on parse errors 2021-04-04 20:47:18 -03:00
Luna ea3850b99d add test for parse errors 2021-04-04 18:15:42 -03:00
Luna b28f208e65 add error for non-lv2 args not getting enough args 2021-04-04 17:56:59 -03:00
Luna 2a801a129b add note about comptime hash map 2021-04-04 17:45:14 -03:00
Luna 78cc7fab4b fix tests 2021-04-04 17:08:53 -03:00
Luna a9e4c5823a remove implemented TODO 2021-04-03 23:22:22 -03:00
Luna 79d7e137d2 refactor into 'doRun' function 2021-04-03 23:21:50 -03:00
Luna 73c5214146 add 'help' command 2021-04-03 23:20:17 -03:00
Luna 32b01976d8 change default scri path to 'run' cli command 2021-04-03 23:05:56 -03:00
Luna 9937365433 update to latest zig 2021-04-03 22:16:04 -03:00
Luna 667e6cbdac zig fmt pass 2021-04-03 21:09:44 -03:00
Luna 2acb45fdca build.zig: fix for latest zig 2021-04-03 21:09:30 -03:00
Luna ebf716de17 fix type for Runner.args 2020-11-08 18:47:40 -03:00
Luna 5e8093b26f free lv2 ports slice 2020-08-18 21:02:43 -03:00
Luna 7097334201 turn on gpa 2020-08-18 21:00:16 -03:00
Luna c2834f8254 properly free Image contents 2020-08-18 21:00:07 -03:00
Luna 77cceab288 make Image free its paths 2020-08-18 20:57:33 -03:00
Luna 9527426d48 remove memleak when getting args 2020-08-18 20:54:04 -03:00
Luna 0cd47be7ac remove need to dupe strings to command structs 2020-08-18 20:53:54 -03:00
Luna 708ef45600 fix use-after-free on repl 2020-08-18 20:41:57 -03:00
Luna f61e9b013f make CommandList free its command pointers 2020-08-18 20:40:06 -03:00
Luna 4adf80e51a fix some memory leaks 2020-08-18 17:58:04 -03:00
Luna 353d8d6947 fix for latest zig 2020-08-18 17:49:23 -03:00
Luna 8aa65958d0 cast ptr 2020-07-23 16:32:33 -03:00
Luna fa7e993ec2 zig fmt pass 2020-07-23 16:31:32 -03:00
luna 973d6e63c5 Merge pull request 'Better LILV/LV2 integration' (#15) from better-lilv into master
Reviewed-on: #15
2020-06-09 22:38:08 +00:00
Luna 8d312cd987 use lv2.h provided at root of /usr/include
fixes ubuntu issues
2020-06-02 22:24:43 -03:00
Luna c80a6d0df7 fix typos and actually add dirs to step 2020-06-02 21:54:39 -03:00
Luna 0ba2c5ffcc add basics of searching for lilv 2020-06-02 21:42:26 -03:00
luna 18924adac4 Merge pull request 'Use comptime for fully declarative LV2 and Custom commands' (#14) from declarative-commands into master
Reviewed-on: #14
2020-06-02 21:37:46 +00:00
Luna 6d8614e678 revert cloned runner using parsed cmd list 2020-06-02 17:25:03 -03:00
Luna af0ea574e1 remove default value for Runner.repl field 2020-06-02 17:16:02 -03:00
Luna 1b59705eae cloned runners inherit repl flag from parent 2020-06-02 17:15:50 -03:00
Luna a2ea8fb53e make args live through lifetime of runner
we just assign cloned runners the args of the parent runner and don't
free the parent's, maintaining okay memory usage.
2020-06-02 17:12:34 -03:00
Luna c78ca9dd5b refactor argument fetching for load cmd 2020-06-02 16:59:53 -03:00
Luna dc98c7a22f use better seed 2020-06-02 16:59:36 -03:00
Luna aeb76fe6c0 add more possibilities on temporary file paths 2020-06-02 16:35:42 -03:00
Luna 542ba75b01 make repl cloned runner run the runqs cmd 2020-06-02 16:16:49 -03:00
Luna c7eb70a06f ignore lines without commands 2020-06-02 16:16:43 -03:00
Luna 0240b10a3c close handles while making temporary paths 2020-06-02 16:11:30 -03:00
Luna d6c92c0231 add split and index when printing custom/lv2 cmds 2020-06-02 16:00:54 -03:00
Luna 5235482ab4 print float parameters with decimal notation 2020-06-01 23:58:56 -03:00
Luna ee7ffd1be7 move repl commands to the heap 2020-06-01 23:10:30 -03:00
Luna 0453f37305 add noop, runqs support to runner 2020-06-01 22:47:21 -03:00
Luna 7f008db540 part 2 of printing commands 2020-06-01 22:45:35 -03:00
Luna b06bab9ec5 remove unecessary compileError call 2020-06-01 22:35:29 -03:00
Luna f973d6807d add basics of printing under new command structure 2020-06-01 22:35:16 -03:00
Luna 128f58c502 re-enable repl 2020-06-01 22:35:07 -03:00
Luna e71eba583e make load command own its path memory 2020-06-01 22:34:56 -03:00
Luna 9cb82e3180 remove unecessary code 2020-06-01 22:34:48 -03:00
Luna b00ab8e839 remove the big switch when printing commands 2020-06-01 22:14:12 -03:00
Luna 0b816a512e lang: remove unused declarations 2020-06-01 22:08:01 -03:00
Luna b0525f2386 add proper support on rotateCmd for lang.Command.Rotate 2020-06-01 22:06:15 -03:00
Luna 54919110a5 refactor: remove 'new' prefix from functions 2020-06-01 21:54:10 -03:00
Luna 76b353e593 remove code for old runner commands 2020-06-01 21:51:51 -03:00
Luna b238517b33 update symbols for Dyncomp 2020-06-01 21:41:10 -03:00
Luna 36937a5fde remove ParamMap creation from runner 2020-05-31 22:28:10 -03:00
Luna 72379e63ee add support for more types on lv2 parameter structs 2020-05-31 22:27:56 -03:00
Luna 690ab89cfd allow any typed command to use lv2 param parse logic 2020-05-31 22:27:38 -03:00
Luna 30da41293a fix type defs for noise cmds 2020-05-31 22:27:24 -03:00
Luna 89afa8af10 convert from ParamMap to ducktyped param struct 2020-05-31 22:27:11 -03:00
Luna 9801e303c0 flatten switch values 2020-05-31 21:55:48 -03:00
Luna 2b4f428890 fix typo 2020-05-31 21:55:44 -03:00
Luna 83996b889f make rotate not be a typed command 2020-05-31 21:55:27 -03:00
Luna 10b2c69605 fix typos 2020-05-31 21:55:22 -03:00
Luna 303a40758d make custom plugins always receive ParamMap 2020-05-31 21:55:03 -03:00
Luna 1c1e525b1d add support for plugin command types 2020-05-31 21:47:31 -03:00
Luna 1fac8c7312 fix putting KV on a string 2020-05-31 21:40:31 -03:00
Luna e8808c501b make Embed follow existing custom plugin structure 2020-05-31 21:38:18 -03:00
Luna e669b74ffb add CustomCommand function 2020-05-31 21:34:49 -03:00
Luna ca751e58f7 add draft declarations for custom commands 2020-05-31 21:33:19 -03:00
Luna 82dc99d7d5 remove unecessary switch 2020-05-31 21:26:27 -03:00
Luna d9358ed794 add lv2 parameter validation 2020-05-31 21:25:25 -03:00
Luna 8ce844ceed add validation for split/index args 2020-05-31 21:10:43 -03:00
Luna 7543ecafaa remove unused blocks of code 2020-05-31 18:30:39 -03:00
Luna 325e7b1102 add support for the rest of lv2 commands 2020-05-31 18:29:42 -03:00
Luna 0de2d05fa3 fix detune command definition 2020-05-31 17:54:43 -03:00
Luna 3a7009f9bf convert more commands to new form 2020-05-31 17:53:33 -03:00
Luna c6c31f5a60 rename NewCommand to Command 2020-05-31 17:18:36 -03:00
Luna 8e0ebbe5e0 remove old Command struct 2020-05-31 17:16:57 -03:00
Luna d09603c005 add runner support for LV2Command() spec 2020-05-31 17:11:40 -03:00
Luna 5412934f27 fix fetching of non-lv2 params 2020-05-31 17:11:32 -03:00
Luna b5512c45fb fix fetching of lv2 params 2020-05-31 17:11:26 -03:00
Luna dd1b493da2 fix typo 2020-05-31 17:11:17 -03:00
Luna dca5d7b644 parse lv2 commands' split/index automatically 2020-05-31 17:08:35 -03:00
Luna b21960d372 remove unused code off parser 2020-05-31 16:57:12 -03:00
Luna 94c9695110 add helper to make lv2 command structs 2020-05-31 16:55:50 -03:00
Luna f9c1851734 add support for generic lv2 command running 2020-05-31 15:20:19 -03:00
Luna 9c6387973f convert command list to ptr command list, fixing mem issues 2020-05-30 23:57:42 -03:00
Luna d518369314 add random debug statements 2020-05-30 23:57:34 -03:00
Luna 7d519b73b6 add runner support on command cast 2020-05-30 22:56:45 -03:00
Luna 6497fc1dd8 add split/index decls to Amp, RFlanger 2020-05-30 22:42:41 -03:00
Luna eb18d01cdd copy struct base tag into command base tag 2020-05-30 22:39:52 -03:00
Luna 47a97636cd enable command debug printing 2020-05-30 22:39:45 -03:00
Luna c3b83c3e4b replace f64 to f32 on some commands 2020-05-30 22:29:13 -03:00
Luna f20079b807 disable codepaths not supporting new commands 2020-05-30 22:25:43 -03:00
Luna 154032f9b1 revamp command structure for tags and base types 2020-05-30 22:25:30 -03:00
Luna 27f499dfbb add basics of generic command parsing 2020-05-30 17:48:14 -03:00
Luna d86e9efe43 add main command struct and make fillKeywords mostly-comptime 2020-05-30 16:57:00 -03:00
Luna e462b4e9d6 fix for latest zig 2020-05-30 15:56:58 -03:00
Luna c8f949fe96 remove embedded magick_wand in favor of cimport 2020-05-12 17:48:20 -03:00
Luna 3de84b5a23 finish arraylist updates 2020-05-12 17:48:15 -03:00
Luna 0c2cd5638e update because of arraylist api changes 2020-05-12 17:26:46 -03:00
Luna 043556e798 Update README on new env var 2020-04-10 00:20:46 -03:00
Luna 9614b96c71 Add SCRITCHER_RUNNER env var
Closes #13
2020-04-10 00:01:23 -03:00
Luna 8bb09f1573 fixes for latest zig
- arraylist breaking changes
 - mem.separate => mem.split
 - heap.page_allocator
2020-04-09 23:51:33 -03:00
Luna 8261d202bd multiple fixes for latest zig 2020-03-26 16:35:58 -03:00
Luna fe7e347762 fix mbeq example 2020-02-08 01:40:35 -03:00
Luna 2c0716e11d move pitch.scri => pitchscaler.scri 2020-01-26 15:45:09 -03:00
Luna 08d9923e75 add saturator, vintagedelay cmd 2020-01-26 14:50:50 -03:00
Luna bc8ab98689 add multichorus cmd 2020-01-26 14:03:01 -03:00
Luna 127ea389fd add moddelay cmd 2020-01-26 02:05:43 -03:00
Luna a0a75579dd add tapedelay cmd (broken) 2020-01-26 00:28:59 -03:00
Luna 8c3c5a3ac2 add invert cmd 2020-01-26 00:13:53 -03:00
Luna 299a39fc27 add gverb cmd 2020-01-26 00:03:15 -03:00
Luna c73b7440a6 add foverdrive cmd
- lang: remove oldGetCommand
2020-01-25 23:52:22 -03:00
Luna fa8740e0db add thruzero command 2020-01-25 23:32:24 -03:00
Luna 046e43a68c add dyncomp example file 2020-01-25 22:46:15 -03:00
Luna 519eb51206 remove heap allocation for RunBuffers 2020-01-25 22:46:00 -03:00
Luna 5a0e6934e7 add dyncomp cmd 2020-01-25 22:45:37 -03:00
Luna 7dae50b030 README: add note about MDA dep 2020-01-25 18:53:29 -03:00
Luna 7c4dead5cf add many experimental plugins 2020-01-25 18:52:45 -03:00
Luna 9a6caa2453 add gate cmd 2020-01-25 17:47:20 -03:00
Luna 182d368363 fix for latest zig 2020-01-14 22:31:20 -03:00
Luna a10416e56f fixes for latest zig 2019-12-08 12:14:31 -03:00
Luna 09471e564a allow args to be deinited on error, fix argAt 2019-12-02 20:13:19 -03:00
Luna 46d576fc13 image: remove expectEqual check (caused problems for rotate) 2019-11-10 13:46:48 -03:00
Luna c57c40db61 use at-as builtin 2019-11-10 13:37:59 -03:00
Luna a411659d6b add exampels/rflanger.scri 2019-11-01 22:29:14 -03:00
Luna fb0516a1db allow ppm files as inputs
- only run bmp.magicValid when path ends in bmp
 - remove uneeced header size constants
2019-11-01 21:43:45 -03:00
Luna 91de05d3b1 runner: fix for latest zig 2019-10-31 19:02:36 -03:00
Luna 1c17b714a7 doc: add embed cmd 2019-10-25 07:09:39 -03:00
Luna 430677fc07 enhance embed custom plugin
- seek to 0 on setup
 - ignore and copy original bytes if we reached end of audio file
2019-10-22 19:16:09 -03:00
Luna c8b5327634 libsndfile cant open mp3s 2019-10-22 19:09:05 -03:00
Luna 1c6ef1d4d5 Revert "use sf_open_fd"
This reverts commit 6e66850d90.
2019-10-22 19:08:38 -03:00
Luna 6e66850d90 use sf_open_fd 2019-10-22 18:57:36 -03:00
Luna 2d64eaa3ef add embed example script 2019-10-22 18:30:55 -03:00
Luna 612421f7b6 add embed code 2019-10-22 18:23:05 -03:00
Luna 2e27390058 add the rest for embed command
- allow any extra param as a generic to custom plugin init
2019-10-22 18:16:15 -03:00
Luna 87bab879ed add embed cmd 2019-10-22 17:08:19 -03:00
Luna 005d685723 doc: remove unused parameters part of eq command 2019-10-20 13:17:23 -03:00
Luna bb501d9c1e repl: add output when leaving from c-d 2019-10-20 13:13:32 -03:00
Luna c75db19523 repl: ignore comments
closes #10
2019-10-20 13:12:49 -03:00
Luna ecc9546de2 add readline to readme 2019-10-20 13:08:54 -03:00
Luna cdca993048 remove 'use-after-free' 2019-10-20 11:59:15 -03:00
Luna a18809eb5b repl: handle when original file doesn't exist 2019-10-20 11:37:11 -03:00
Luna c05be26dbd runner: ignore one arg when its in repl
closes #8
2019-10-06 15:16:41 -03:00
Luna 7cf16f9ffa deinit existing_cmds (not long lived) 2019-10-06 15:12:19 -03:00
Luna 80036c4c0d fix other things related to reading scri file on repl 2019-10-06 15:07:30 -03:00
Luna 66b262beb2 repl: open file as read and append to cmds before open as write
closes #7
2019-10-06 14:39:15 -03:00
Luna cb689c3ade repl: use readline 2019-10-06 10:53:09 -03:00
Luna 8c5f42b3db add quit/q commands to repl 2019-10-05 21:44:44 -03:00
Luna f8814ca94d add assertions about seek state and seek position 2019-10-05 21:42:16 -03:00
Luna 0e323f6631 repl tweaks 2019-09-29 23:57:24 -03:00
Luna 52950f9a6f README: add repl instructions 2019-09-10 22:27:35 -03:00
Luna 66a7356502 runner: add newline to runqs message 2019-09-10 22:08:35 -03:00
Luna 49cc7221b0 fix some clones, finish list of commands for printer 2019-09-10 22:00:07 -03:00
Luna 84d582cfd1 image: open curpath instead of path when cloning 2019-09-10 21:35:22 -03:00
Luna 67c42e39d4 add cloning of Runner and Image + basic Runner REPL logic 2019-09-10 21:31:23 -03:00
Luna 88a73e64d8 better repl logic 2019-09-10 15:44:10 -03:00
Luna 058bf8deb9 add push and list commands 2019-09-10 14:51:41 -03:00
Luna c90590e3b5 add Lang.reset, open scri_file and write current commands to it 2019-09-10 12:12:10 -03:00
Luna 8b67ccc1bf add printing of commands to any stream 2019-09-10 11:45:04 -03:00
Luna 736277db04 add parsing of given repl line 2019-09-08 23:28:09 -03:00
Luna 34d3b12c56 add basic main loop 2019-09-08 17:27:59 -03:00
Luna fc8dcf4583 add doRepl func 2019-09-08 17:06:19 -03:00
Luna 7e8023a383 add validation of bmp magic bytes 2019-09-08 11:55:17 -03:00
Luna b3bb56279a remove usage of command pointers for most things
this serves as a workaround for the compiler bugs found on zig trunk

 - use std.StringHashMap
2019-09-03 12:45:17 -03:00
Luna cdf2d843d2 update README 2019-08-27 14:40:41 -03:00
Luna fecd3b2e69 lang: fix RevDelay's arg count 2019-08-27 14:17:15 -03:00
Luna cfec45eb6c make internal ports a slice of Port structs 2019-08-14 22:35:22 -03:00
Luna 1d86abff5b main: use direct allocator 2019-08-14 16:11:09 -03:00
Luna a33db9f060 custom: add Write.deinit and fix WildNoise.deinit 2019-08-14 09:13:06 -03:00
Luna e142285a32 use Plugin.deinit() 2019-08-14 09:10:54 -03:00
Luna f8d8ed067d add deinit() calls to custom plugins 2019-08-14 09:10:09 -03:00
Luna 0ad1c88274 free some things in runner 2019-08-13 10:33:08 -03:00
Luna b66070c4e4 lang: use std.mem.startsWith 2019-08-13 10:19:39 -03:00
Luna b0ddb35ae9 fix mbeq arg validation 2019-08-12 13:07:06 -03:00
Luna af34f71c8f move lv2 core constants to comptime 2019-08-10 14:36:51 -03:00
Luna 0cab58429c fix std.AutoHashMap usage 2019-08-09 18:40:11 -03:00
Luna c0269ead20 image: bump BufferSize 2019-08-09 14:03:32 -03:00
Luna 99382d0a66 add better freeing procedures w/ errdefer 2019-08-09 13:55:40 -03:00
Luna 4aa9e9d7b9 runner: bump ident of load path msg 2019-08-09 11:29:10 -03:00
Luna 6fd4cd0ecd add better error messages for no cmd and unknown cmd 2019-08-09 11:28:40 -03:00
Luna a0bf1e2723 lang: do as many semantic errors as possible before ParseFail 2019-08-08 20:30:24 -03:00
Luna 90bc830a9a lang: show line where error happened 2019-08-08 20:27:24 -03:00
Luna ab2e8884b8 lang: add arg checks for the other commands 2019-08-08 20:24:25 -03:00
Luna 422cd2502c lang: add basic argument validation 2019-08-08 20:13:49 -03:00
Luna 9fd41a61bf add runner test + 'zig build test' subcommand 2019-08-08 16:48:31 -03:00
Luna 704eaac081 lang: defer deinit() on tests 2019-08-07 21:09:04 -03:00
Luna 7e6b85ef48 lang: add tests
- lang: add deinit()
2019-08-07 21:04:51 -03:00
Luna b8dcb84294 image: fix typo 2019-08-06 23:35:02 -03:00
Luna 981b52c05b lang: add hack while AutoHashMap is broken
- image: add timer for runPlugin
2019-08-06 23:34:11 -03:00
Luna fc3972d65c remove unecessary allocation in RunContext 2019-08-06 19:25:47 -03:00
Luna 75eeea14f3 add check for n_audio_out>2 2019-08-06 19:25:31 -03:00
Luna 8172066851 add write cmd
- make custom plugins return ?Plugin instead of !Plugin
2019-07-29 11:51:40 -03:00
Luna ff78044587 magick: reopen the image post MagickWriteImage 2019-07-23 18:30:16 -03:00
Luna c0b70a81b1 magick: reopen the file instead of just setting curpath 2019-07-22 22:48:37 -03:00
36 changed files with 2067 additions and 4853 deletions

4
.gitignore vendored
View File

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

View File

@ -9,20 +9,24 @@ glitch art "framework", ???????? language??? something?
# how do?
## build depedencies:
## build depedencies (headers included):
- zig at https://ziglang.org
- libc, lilv and libsndfile
- an appreciation for glitched anime girls on your hard drive
- graphicsmagick for the `rotate` command
- readline (for repl)
## plugin depedencies:
- lv2 default plugins (most specifically the eg-amp plugin)
## plugin depedencies (only required at runtime):
- lv2 default plugins (most specifically the eg-amp plugin,
will likely be there by default)
- 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, check build.zig if different
# assumes lilv headers are at /usr/include/lilv-0, edit build.zig if different
git clone https://gitdab.com/luna/scritcher.git
cd scritcher
@ -31,9 +35,33 @@ 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
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.

View File

@ -1,24 +1,73 @@
const Builder = @import("std").build.Builder;
const std = @import("std");
pub fn build(b: *Builder) void {
const mode = b.standardReleaseOptions();
const exe = b.addExecutable("scritcher", "src/main.zig");
exe.setBuildMode(mode);
exe.install();
fn setupLinks(step: *std.Build.Step.Compile) void {
step.linkSystemLibrary("c");
exe.linkSystemLibrary("lilv-0");
exe.linkSystemLibrary("sndfile");
exe.linkSystemLibrary("c");
step.linkSystemLibrary("lilv-0");
step.linkSystemLibrary("sndfile");
step.linkSystemLibrary("readline");
exe.linkSystemLibrary("GraphicsMagickWand");
exe.linkSystemLibrary("GraphicsMagick");
step.linkSystemLibrary("GraphicsMagickWand");
step.linkSystemLibrary("GraphicsMagick");
exe.addIncludeDir("/usr/include/lilv-0");
exe.addIncludeDir("/usr/include/GraphicsMagick");
step.addIncludePath(.{ .path = "/usr/include/GraphicsMagick" });
step.addIncludePath(.{ .path = "/usr/include" });
const run_cmd = exe.run();
run_cmd.step.dependOn(b.getInstallStep());
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_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,7 +23,8 @@ 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 start from the bottom of the file.**
**Keep in mind parts can start from either top or bottom of the image,
it depends of the file format**
## `load path_or_arg`
@ -38,8 +39,9 @@ Run the eg-amp plugin over the given slice of the file.
Run the Retro Flanger script from the SWH plugins.
- `delay_depth_avg` is for the `Average stall (ms)` parameter of the plugin.
- `law_freq` is for the `Flange frequency` parameter of the plugin.
Parameters:
- `delay_depth_avg`: Average stall (ms), 0..10, default 2.5
- `law_freq`: Flange frequency (Hz), 0.5..8, default 1
## `eq split index lo mid hi`
@ -165,6 +167,11 @@ 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.
## `quicksave`
Save the file on the same directory of the file specified by `load`, but
@ -172,3 +179,201 @@ 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

8
examples/degrade.scri Normal file
View File

@ -0,0 +1,8 @@
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;

3
examples/detune.scri Normal file
View File

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

3
examples/dyncomp.scri Normal file
View File

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

3
examples/embed.scri Normal file
View File

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

3
examples/foverdrive.scri Normal file
View File

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

3
examples/gate.scri Normal file
View File

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

3
examples/gverb.scri Normal file
View File

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

3
examples/invert.scri Normal file
View File

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

View File

@ -1,3 +1,3 @@
load :0;
mbeq 3 1 1 4 0.2 2.1;
mbeq 3 1 0 0 0 0.3 0 0 7 0 0 0 0 0 0.1 0 0;
quicksave;

3
examples/moddelay.scri Normal file
View File

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

View File

@ -0,0 +1,3 @@
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;

5
examples/overdrive.scri Normal file
View File

@ -0,0 +1,5 @@
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;

8
examples/repsycho.scri Normal file
View File

@ -0,0 +1,8 @@
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;

3
examples/rflanger.scri Normal file
View File

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

3
examples/saturator.scri Normal file
View File

@ -0,0 +1,3 @@
load :0;
saturator 3 1 0 1 1 1 5 10 20000 10 20000 10 2000 1 1 0 0;
quicksave;

3
examples/talkbox.scri Normal file
View File

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

3
examples/tapedelay.scri Normal file
View File

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

3
examples/thruzero.scri Normal file
View File

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

View File

@ -0,0 +1,3 @@
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;

3
examples/write.scri Normal file
View File

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

25
src/bmp_valid.zig Normal file
View File

@ -0,0 +1,25 @@
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,74 +2,78 @@
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,
rand_buf: ?[]f32 = null,
allocator: ?std.mem.Allocator = null,
cnt: usize = 0,
pub fn init(
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);
allocator: std.mem.Allocator,
params: anytype,
) ?RandomNoise {
var r = std.rand.DefaultPrng.init(params.seed);
var r = std.rand.DefaultPrng.init(seed);
if (params.fill_bytes > 0) {
var rand_buf = allocator.alloc(f32, params.fill_bytes) catch return null;
if (fillbytes > 0) {
var rand_buf = try allocator.alloc(f32, fillbytes);
for (rand_buf) |_, idx| {
rand_buf[idx] = r.random.float(f32);
for (rand_buf, 0..) |_, 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,
rand_buf: ?[]f32 = null,
allocator: ?std.mem.Allocator = null,
cnt: usize = 0,
pub fn init(
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);
allocator: std.mem.Allocator,
params: anytype,
) ?WildNoise {
var r = std.rand.DefaultPrng.init(params.seed);
var r = std.rand.DefaultPrng.init(seed);
if (params.fill_bytes > 0) {
var rand_buf = allocator.alloc(f32, params.fill_bytes) catch return null;
if (fillbytes > 0) {
var rand_buf = try allocator.alloc(f32, fillbytes);
for (rand_buf) |_, idx| {
rand_buf[idx] = @intToFloat(f32, r.random.int(u1));
for (rand_buf, 0..) |_, idx| {
rand_buf[idx] = @as(f32, @floatFromInt(r.random().int(u1)));
}
return WildNoise{
@ -79,18 +83,110 @@ 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] = @intToFloat(f32, self.r.random.int(u1));
bufs.out[0] = @as(f32, @floatFromInt(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,14 +1,13 @@
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");
/// Approximate size of the BMP header.
pub const BMPHeaderSize: usize = 82000;
/// Buffer size for main copying
pub const BufferSize: usize = 60000;
/// Buffer size for main image copying.
pub const BufferSize: usize = 300000;
pub const ImageError = error{
OpenFail,
@ -17,100 +16,113 @@ pub const ImageError = error{
InvalidSymbol,
InstantiateFail,
WriteFail,
PluginLoadFail,
};
/// Low level integration function with libsndfile.
fn sopen(
allocator: *std.mem.Allocator,
pub fn sopen(
allocator: std.mem.Allocator,
path: []const u8,
mode: i32,
fmt: *c.SF_INFO,
) !*c.SNDFILE {
var cstr_path = try std.cstr.addNullByte(allocator, path);
const cstr_path = try allocator.dupeZ(u8, path);
defer allocator.free(cstr_path);
var file = c.sf_open(cstr_path.ptr, mode, fmt);
const file = c.sf_open(cstr_path.ptr, mode, fmt);
const st: i32 = c.sf_error(file);
if (st != 0) {
std.debug.warn(
"Failed to open {} ({})\n",
log.debug("Failed to open {s} ({s})", .{
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.?;
}
fn swrite(file: *c.SNDFILE, buf: [*]f32, frames: i64) !void {
pub fn swrite(file: *c.SNDFILE, buf: [*]f32, frames: i64) !void {
const count = c.sf_writef_float(file, buf, frames);
if (count != frames) {
std.debug.warn("Wanted to read {}, got {}\n", frames, count);
log.debug("Wanted to write {}, got {}", .{ frames, count });
return ImageError.WriteFail;
}
}
fn sseek(file: *c.SNDFILE, offset: usize) void {
const frames = c.sf_seek(file, @intCast(i64, offset), c.SEEK_SET);
if (frames != @intCast(i64, offset)) {
std.debug.warn("failed to seek to {}\n", offset);
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 sf_tell(file: *c.SNDFILE) i64 {
var frames = c.sf_seek(file, 0, c.SEEK_CUR);
std.debug.warn("\t\t{} frames\n", frames);
return -frames;
}
pub fn temporaryName(allocator: *std.mem.Allocator) ![]u8 {
/// Caller owns the returned memory.
pub fn temporaryName(allocator: std.mem.Allocator) ![]u8 {
const template_start = "/temp/temp_";
const template = "/tmp/temp_XXXXXXXX";
const template = "/tmp/temp_XXXXXXXXXXXXXXXXXXXXX";
var nam = try allocator.alloc(u8, template.len);
std.mem.copy(u8, nam, template);
std.mem.copyForwards(u8, nam, template);
var r = std.rand.DefaultPrng.init(std.time.timestamp());
const seed = @as(u64, @truncate(@as(u128, @bitCast(std.time.nanoTimestamp()))));
var r = std.rand.DefaultPrng.init(seed);
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) |_, f_idx| {
var idx = @intCast(u8, r.random.uintLessThan(u5, 24));
var letter = u8(65) + idx;
for (fill, 0..) |_, f_idx| {
const idx = @as(u8, @intCast(r.random().uintLessThan(u5, 24)));
const letter = @as(u8, 65) + idx;
fill[f_idx] = letter;
}
// if we fail to access it, we assume it doesn't exist and return it.
std.fs.File.access(nam) catch |err| {
if (err == error.FileNotFound) {
return nam;
}
var tmp_file: std.fs.File = std.fs.cwd().openFile(
nam,
.{ .mode = .read_only },
) catch |err| {
if (err == error.FileNotFound) return nam else continue;
};
// if we actually found someone, close the handle so that we don't
// get EMFILE later on.
tmp_file.close();
}
return error.TempGenFail;
}
fn mkSfInfo() c.SF_INFO {
pub fn mkSfInfo() c.SF_INFO {
return c.SF_INFO{
.frames = c_int(0),
.samplerate = c_int(44100),
.channels = c_int(1),
.frames = @as(c_int, 0),
.samplerate = @as(c_int, 44100),
.channels = @as(c_int, 1),
.format = c.SF_FORMAT_ULAW | c.SF_FORMAT_RAW | c.SF_ENDIAN_BIG,
.sections = c_int(0),
.seekable = c_int(0),
.sections = @as(c_int, 0),
.seekable = @as(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,
@ -125,55 +137,79 @@ pub const Image = struct {
curpath: []const u8,
/// Open a BMP image for later.
pub fn open(allocator: *std.mem.Allocator, path: []const u8) !*Image {
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);
var sndfile = try sopen(allocator, path, c.SFM_READ, &in_fmt);
var image = try allocator.create(Image);
const image = try allocator.create(Image);
std.debug.assert(in_fmt.frames > i64(0));
std.debug.assert(in_fmt.seekable == i32(1));
std.debug.assert(in_fmt.frames > @as(i64, 0));
std.debug.assert(in_fmt.seekable == @as(i32, 1));
image.* = Image{
.allocator = allocator,
.sndfile = sndfile,
.path = path,
.curpath = path,
.frames = @intCast(usize, in_fmt.frames),
.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 {
//self.allocator.free(self.path);
//self.allocator.free(self.curpath);
var st: i32 = c.sf_close(self.sndfile);
const st: i32 = c.sf_close(self.sndfile);
if (st != 0) {
std.debug.warn(
"Failed to close {} ({})\n",
log.debug("Failed to close {s} ({s})", .{
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 {
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);
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 i = file_chans - 1;
while (i < buf_chans) : (i += 1) {
//buf[@intCast(usize, i)] = buf[i % file_chans];
buf[@intCast(usize, i)] = buf[@intCast(usize, @mod(i, file_chans))];
buf[@as(usize, @intCast(i))] = buf[@as(usize, @intCast(@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,
@ -191,7 +227,7 @@ pub const Image = struct {
sseek(out_file, start);
while (i <= end) : (i += buf.len) {
std.debug.warn("i={}, buf.len={}, end={}\n", i, buf.len, end);
log.debug("\t\ti={d}, buf.len={d}, end={d}", .{ i, buf.len, end });
sseek(self.sndfile, i);
sseek(out_file, i);
@ -201,13 +237,13 @@ pub const Image = struct {
var view: []f32 = buf[0..buf.len];
if (bytes_until_end < buf.len) {
read_bytes = c.sf_readf_float(self.sndfile, buf.ptr, @intCast(i64, bytes_until_end));
read_bytes = c.sf_readf_float(self.sndfile, buf.ptr, @as(i64, @intCast(bytes_until_end)));
view = buf[0..bytes_until_end];
} else {
read_bytes = c.sf_readf_float(self.sndfile, buf.ptr, @intCast(i64, buf.len));
read_bytes = c.sf_readf_float(self.sndfile, buf.ptr, @as(i64, @intCast(buf.len)));
}
try swrite(out_file, view.ptr, @intCast(i64, view.len));
try swrite(out_file, view.ptr, @as(i64, @intCast(view.len)));
}
sseek(self.sndfile, end);
@ -216,11 +252,44 @@ pub const Image = struct {
fn getSeekPos(self: *Image, position: plugins.Position) plugins.SeekPos {
const file_end = self.frames;
var seek_pos = position.seekPos(file_end);
std.debug.warn("\tstart {} end {}\n", seek_pos.start, seek_pos.end);
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);
}
/// Run a plugin over the image.
/// This setups a new lilv world/plugin among other things.
/// The internal SNDFILE pointer is modified to point to the output of the
@ -231,51 +300,54 @@ 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) {
std.debug.warn("plugin <{}> accepts more than two channels.\n", plugin_uri);
log.debug("plugin <{s}> has more than two inputs.", .{plugin_uri});
return ImageError.InvalidPlugin;
}
// TODO check n_audio_out > 2
if (ctx.n_audio_out > 2) {
log.debug("plugin <{s}> has more than two outputs.", .{plugin_uri});
return ImageError.InvalidPlugin;
}
// now, for each param for the plugin, we find its port, and set
// the value for the port there.
var it = params.iterator();
while (it.next()) |param| {
var sym_cstr = try std.cstr.addNullByte(self.allocator, param.sym);
for (params.items) |param| {
const sym_cstr = try self.allocator.dupeZ(u8, param.sym);
defer self.allocator.free(sym_cstr);
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);
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});
return ImageError.InvalidSymbol;
};
c.lilv_node_free(sym);
var idx = c.lilv_port_get_index(ctx.plugin, port);
std.debug.warn(
"\tset sym={}, idx={} to val={}\n",
const idx = c.lilv_port_get_index(ctx.plugin, port);
log.debug("\tset sym={s}, idx={d} to val={}", .{
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
var tmpnam = try temporaryName(self.allocator);
std.debug.warn("\trunning plugin from '{}' to '{}'\n", self.curpath, tmpnam);
const tmpnam = try temporaryName(self.allocator);
log.debug("\trunning plugin from '{s}' to '{s}'", .{ self.curpath, tmpnam });
var out_fmt = mkSfInfo();
var out_file = try sopen(self.allocator, tmpnam, c.SFM_WRITE, &out_fmt);
const 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();
@ -298,27 +370,34 @@ pub const Image = struct {
// pre-plugin copy, merged with bmp header copy
try self.copyBytes(
out_file,
usize(0),
@as(usize, 0),
seek_pos.start,
);
sseek(self.sndfile, seek_pos.start);
var i: usize = seek_pos.start;
std.debug.warn("\tseek pos start: {} end: {}\n", seek_pos.start, seek_pos.end);
log.debug("\tseek pos start: {d} end: {d}", .{ seek_pos.start, seek_pos.end });
var inbuf = rctx.buffers.in;
var outbuf = rctx.buffers.out;
var inbuf = &rctx.buffers.in;
const outbuf = &rctx.buffers.out;
while (i <= seek_pos.end) : (i += 1) {
const read_bytes = c.sf_readf_float(self.sndfile, inbuf.ptr, 1);
inbuf[0] = 0;
inbuf[1] = 0;
const read_bytes = c.sf_readf_float(self.sndfile, inbuf, 1);
if (read_bytes == 0) {
std.debug.warn("WARN! reached EOF at idx={}\n", i);
log.debug("WARN! reached EOF at idx={d}", .{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.ptr, 1);
try swrite(out_file, outbuf, 1);
}
sseek(self.sndfile, seek_pos.end);
@ -334,35 +413,48 @@ pub const Image = struct {
_ = 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;
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 {
std.debug.warn("\timg: copy from '{}' to '{}'\n", self.curpath, out_path);
try std.fs.copyFile(self.curpath, out_path);
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,
params: *plugins.ParamMap,
extra: anytype,
) !void {
var plugin = try Plugin.init(self.allocator, params);
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.
var tmpnam = try temporaryName(self.allocator);
std.debug.warn("\trunning CUSTOM plugin from '{}' to '{}'\n", self.curpath, tmpnam);
const tmpnam = try temporaryName(self.allocator);
log.debug("\trunning CUSTOM plugin from '{s}' to '{s}'", .{ 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 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
@ -377,27 +469,27 @@ pub const Image = struct {
// pre-plugin copy, merged with bmp header copy
try self.copyBytes(
out_file,
usize(0),
@as(usize, 0),
seek_pos.start,
);
sseek(self.sndfile, seek_pos.start);
var i: usize = seek_pos.start;
std.debug.warn("\tseek pos start: {} end: {}\n", seek_pos.start, seek_pos.end);
log.debug("\tseek pos start: {d} end: {d}", .{ seek_pos.start, seek_pos.end });
var inbuf = bufs.in;
var outbuf = bufs.out;
const inbuf = &bufs.in;
const outbuf = &bufs.out;
while (i <= seek_pos.end) : (i += 1) {
const read_bytes = c.sf_readf_float(self.sndfile, bufs.in.ptr, 1);
const read_bytes = c.sf_readf_float(self.sndfile, inbuf, 1);
if (read_bytes == 0) {
std.debug.warn("WARN! reached EOF at idx={}\n", i);
log.debug("WARN! reached EOF at idx={d}", .{i});
break;
}
plugin.run(&bufs);
try swrite(out_file, bufs.out.ptr, 1);
try swrite(out_file, outbuf, 1);
}
sseek(self.sndfile, seek_pos.end);
@ -414,7 +506,7 @@ pub const Image = struct {
_ = 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;
try self.reopen(tmpnam);
try self.checkValid();
}
};

View File

@ -1,213 +1,762 @@
const std = @import("std");
const plugin = @import("plugin.zig");
const custom = @import("custom.zig");
pub const ParseError = error{
NoCommandGiven,
UnknownCommand,
ArgRequired,
};
const log = std.log.scoped(.scritcher_lang);
pub const ParseError = error{ParseFail};
pub const CommandType = enum {
Noop,
Load,
Quicksave,
RunQS,
/// "LV2 Commands" are commands that receive split, index, and then receive
/// any f64 arguments.
lv2_command,
Amp,
RFlanger,
Eq,
Phaser,
Mbeq,
Chorus,
PitchScaler,
Reverb,
Highpass,
Delay,
Vinyl,
RevDelay,
Noise,
WildNoise,
Rotate,
custom_command,
};
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 {
command: CommandType,
args: ArgList,
cur_idx: usize = 0,
tag: Tag,
pub fn print(self: *const Command) void {
std.debug.warn("cmd:{}\n", self.command);
}
pub const Tag = enum {
noop,
load,
quicksave,
runqs,
pub fn argAt(self: *const Command, idx: usize) ![]const u8 {
const args = self.args.toSliceConst();
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,
if (idx > (args.len - 1)) {
std.debug.warn("Expected argument at index {}\n", idx);
return ParseError.ArgRequired;
}
noise,
wildnoise,
write,
embed,
return args[idx];
}
rotate,
};
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 tagToType(comptime tag: Tag) type {
return switch (tag) {
.noop => Noop,
.load => Load,
.quicksave => Quicksave,
.runqs => RunQS,
pub fn consumePosition(self: *Command) !plugin.Position {
self.cur_idx = 2;
return plugin.Position{
.split = try self.usizeArgAt(0),
.index = try self.usizeArgAt(1),
.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 intArgAt(self: *const Command, idx: usize) !i32 {
var arg = try self.argAt(idx);
return try std.fmt.parseInt(i32, arg, 10);
pub fn cast(base: *const @This(), comptime T: type) ?*const T {
if (base.tag != T.base_tag)
return null;
//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 floatArgAt(self: *const Command, idx: usize) !f32 {
var arg = try self.argAt(idx);
return try std.fmt.parseFloat(f32, arg);
pub fn print(base: *const @This()) void {
log.debug("tag: {}", .{base.tag});
}
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);
pub const Noop = struct {
pub const base_tag = Tag.noop;
base: Command,
};
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;
};
pub const Load = struct {
pub const base_tag = Tag.load;
base: Command,
path: []const u8,
};
try arr.append(value);
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);
}
return arr.toSliceConst();
self.list.deinit();
}
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);
pub fn append(self: *Self, cmd: *Command) !void {
return try self.list.append(cmd);
}
};
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);
/// A parser.
pub const Lang = struct {
allocator: *std.mem.Allocator,
keywords: KeywordMap,
allocator: std.mem.Allocator,
pub fn init(allocator: *std.mem.Allocator) Lang {
has_error: bool = false,
line: usize = 0,
pub fn init(allocator: std.mem.Allocator) Lang {
return Lang{
.allocator = allocator,
.keywords = KeywordMap.init(allocator),
};
}
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 deinit(self: *Lang) void {
_ = self;
}
_ = 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);
pub fn reset(self: *Lang) void {
self.has_error = false;
self.line = 0;
}
// custom implementations (not lv2)
_ = try self.keywords.put("noise", .Noise);
_ = try self.keywords.put("wildnoise", .WildNoise);
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;
}
// even more custom
_ = try self.keywords.put("rotate", .Rotate);
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});
}
pub fn parse(self: *Lang, data: []const u8) !CommandList {
var splitted_it = std.mem.separate(data, ";");
try self.fillKeywords();
var splitted_it = std.mem.split(u8, data, ";");
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 (stmt[0] == '#') continue;
if (std.mem.startsWith(u8, stmt, "#")) continue;
// TODO better tokenizer instead of just tokenize(" ");
var tok_it = std.mem.tokenize(stmt, " ");
// TODO better tokenizer instead of just tokenize(" ")...maybe????
var tok_it = std.mem.splitSequence(u8, stmt, " ");
var cmd_opt = tok_it.next();
if (cmd_opt == null) return ParseError.NoCommandGiven;
var command = cmd_opt.?;
const cmd_opt = tok_it.next();
if (cmd_opt == null) {
self.doError("No command given", .{});
continue;
}
const command_string = cmd_opt.?;
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;
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 args = ArgList.init(self.allocator);
while (tok_it.next()) |arg| {
try args.append(arg);
if (!found) {
self.doError("Unknown command '{s}' ({d} bytes)", .{ command_string, command_string.len });
continue;
}
// 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;
}
};
// TODO tests
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));
}

View File

@ -1,27 +1,29 @@
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/core/lv2.h");
@cInclude("lv2.h");
});
const LV2_CORE_URI = "http://lv2plug.in/ns/lv2core";
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 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__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 lilv_instance_connect_port(
instance: [*c]c.LilvInstance,
port_index: u32,
data_location: ?*c_void,
data_location: ?*anyopaque,
) void {
instance.?.*.lv2_descriptor.?.*.connect_port.?(instance.?.*.lv2_handle, port_index, data_location);
}
@ -59,56 +61,48 @@ 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.
pub fn setupPorts(ctx: *plugin.Context) ![]*Port {
var world = ctx.world;
///
/// Caller owns returned memory.
pub fn setupPorts(ctx: *plugin.Context) ![]Port {
const 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) |port_ptr, idx| {
var port = try ctx.allocator.create(Port);
for (ports, 0..) |_, idx| {
const port: *Port = &ports[idx];
port.* = Port{
.lilv_port = null,
.ptype = .Control,
.index = f32(0),
.value = f32(0),
.index = @as(f32, 0),
.value = @as(f32, 0),
.is_input = false,
.optional = false,
};
ports[idx] = port;
}
var values: []f32 = try ctx.allocator.alloc(f32, n_ports);
const 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);
// 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_OutputPort = c.lilv_new_uri(world, LV2_CORE__OutputPort.ptr).?;
//defer std.heap.c_allocator.destroy(lv2_OutputPort);
var lv2_InputPort = c.lilv_new_uri(world, LV2_CORE__InputPort.ptr);
defer std.heap.c_allocator.destroy(lv2_InputPort);
const lv2_AudioPort = c.lilv_new_uri(world, LV2_CORE__AudioPort.ptr).?;
//defer std.heap.c_allocator.destroy(lv2_AudioPort);
var lv2_OutputPort = c.lilv_new_uri(world, LV2_CORE__OutputPort.ptr);
defer std.heap.c_allocator.destroy(lv2_OutputPort);
const lv2_ControlPort = c.lilv_new_uri(world, LV2_CORE__ControlPort.ptr).?;
//defer std.heap.c_allocator.destroy(lv2_ControlPort);
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);
const lv2_connection_string = c.lilv_new_uri(world, LV2_CORE__connectionOptional.ptr).?;
//defer std.heap.c_allocator.destroy(lv2_connection_string);
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).?;
@ -116,17 +110,17 @@ pub fn setupPorts(ctx: *plugin.Context) ![]*Port {
port.index = i;
if (std.math.isNan(values[i])) {
port.value = f32(0);
port.value = @as(f32, 0);
} else {
port.value = values[i];
}
port.optional = c.lilv_port_has_property(ctx.plugin, lport, lv2_connectionOptional);
port.optional = c.lilv_port_has_property(ctx.plugin, lport, lv2_connection_string);
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) {
std.debug.warn("Port {} is neither input or output\n", i);
log.debug("Port {d} is neither input or output", .{i});
return error.UnassignedIOPort;
}
@ -142,7 +136,7 @@ pub fn setupPorts(ctx: *plugin.Context) ![]*Port {
ctx.n_audio_out += 1;
}
} else if (!port.optional) {
std.debug.warn("Port {} has unsupported type\n", i);
log.debug("Port {d} has unsupported type", .{i});
return error.UnsupportedPortType;
}
}

View File

@ -2,9 +2,12 @@
const std = @import("std");
const images = @import("image.zig");
const log = std.log.scoped(.scritcher_magick);
const Image = images.Image;
const mc = @import("magick_wand.zig");
const mc = @cImport({
@cInclude("wand/magick_wand.h");
});
pub const MagickContext = struct {
wand: *mc.MagickWand,
@ -12,7 +15,7 @@ pub const MagickContext = struct {
pub fn init() !MagickContext {
mc.InitializeMagick(null);
var wand = mc.NewMagickWand();
const wand = mc.NewMagickWand();
if (wand == null) return error.WandCreateFail;
return MagickContext{
@ -26,6 +29,7 @@ pub const MagickContext = struct {
}
pub fn doErr(self: *MagickContext) !void {
_ = self;
return error.WandError;
}
};
@ -34,10 +38,10 @@ fn magickLoad(image: *Image) !MagickContext {
var mctx = try MagickContext.init();
errdefer mctx.deinit();
var curpath = try std.cstr.addNullByte(image.allocator, image.curpath);
const curpath = try image.allocator.dupeZ(u8, image.curpath);
defer image.allocator.free(curpath);
std.debug.warn("loading '{}'\n", curpath);
log.debug("loading '{s}'", .{curpath});
if (mc.MagickReadImage(mctx.wand, curpath.ptr) != 1)
return error.MagickReadFail;
@ -48,17 +52,18 @@ fn magickLoad(image: *Image) !MagickContext {
fn magickSave(image: *Image, wand: *mc.MagickWand) !void {
const allocator = image.allocator;
var tmpnam = try images.temporaryName(allocator);
var c_tmpnam = try std.cstr.addNullByte(allocator, tmpnam);
const tmpnam = try images.temporaryName(allocator);
const c_tmpnam = try allocator.dupeZ(u8, tmpnam);
defer allocator.free(c_tmpnam);
std.debug.warn("\tmagick: saving to '{}'..", c_tmpnam);
log.debug("\tmagick: saving to '{s}'..", .{c_tmpnam});
if (mc.MagickWriteImage(wand, c_tmpnam.ptr) != 1)
return error.MagickWriteFail;
image.curpath = tmpnam;
std.debug.warn("OK\n");
log.debug("OK", .{});
try image.reopen(tmpnam);
}
/// Rotate the given image.
@ -67,7 +72,7 @@ pub fn runRotate(image: *Image, deg: f32, bgfill: []const u8) !void {
var mctx = try magickLoad(image);
defer mctx.deinit();
var bg = mc.NewPixelWand();
const bg = mc.NewPixelWand();
defer mc.DestroyPixelWand(bg);
if (mc.PixelSetColor(bg, bgfill.ptr) != 1)

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,258 @@
const std = @import("std");
const langs = @import("lang.zig");
const runners = @import("runner.zig");
const printer = @import("printer.zig");
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.c_allocator);
defer arena.deinit();
const log = std.log.scoped(.scritcher);
const allocator = &arena.allocator;
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();
var lang = langs.Lang.init(allocator);
//defer lang.deinit();
defer lang.deinit();
var runner = runners.Runner.init(allocator);
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);
defer runner.deinit();
var args_it = std.process.args();
// run the load command
try runner.runCommands(cmds, true);
const exe_name = try (args_it.next(allocator) orelse @panic("expected exe name"));
const wanted_runner: []const u8 = std.posix.getenv("SCRITCHER_RUNNER") orelse "ristretto";
// args[1] is the path to scri file
const scri_path = try (args_it.next(allocator) orelse @panic("expected scri path"));
var runqs_cmd = langs.Command.RunQS{
.base = langs.Command{ .tag = langs.Command.Tag.runqs },
.program = wanted_runner,
};
var file = try std.fs.File.openRead(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, .{});
defer file.close();
// sadly, we read it all into memory. such is life
const total_bytes = try file.getEndPos();
var data = try allocator.alloc(u8, total_bytes);
const data = try allocator.alloc(u8, total_bytes);
defer allocator.free(data);
_ = try file.read(data);
@ -36,3 +261,33 @@ pub fn main() !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,6 +3,7 @@ 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
@ -16,7 +17,7 @@ pub const Param = struct {
/// List of parameters to be set to control ports.
pub const ParamList = std.ArrayList(Param);
pub const ParamMap = std.AutoHashMap([]const u8, f32);
pub const ParamMap = std.StringHashMap(f32);
/// Represents an absolute position in the image.
pub const SeekPos = struct {
@ -34,7 +35,8 @@ pub const Position = struct {
index: usize,
pub fn seekPos(self: Position, total_size: usize) SeekPos {
var tot = total_size / self.split;
std.debug.assert(self.index <= self.split);
const tot = total_size / self.split;
return SeekPos{
.start = self.index * tot,
@ -45,7 +47,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,
@ -60,29 +62,11 @@ pub const Context = struct {
/// Specific run context for non-plugins.
pub const RunBuffers = struct {
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);
}
// 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,
};
/// Represents the specific run context of plugin instantation.
@ -91,76 +75,86 @@ pub const RunContext = struct {
instance: *c.LilvInstance,
pub fn init(
allocator: *std.mem.Allocator,
allocator: std.mem.Allocator,
plugin: *const c.LilvPlugin,
) !RunContext {
var instance = c.lilv_plugin_instantiate(plugin, f64(44100), null);
_ = allocator; // TODO batch RunBuffers?
const instance = c.lilv_plugin_instantiate(plugin, @as(f64, 44100), null);
errdefer c.lilv_instance_free(instance);
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 = try RunBuffers.init(allocator),
.buffers = RunBuffers{},
.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;
var in_buf = self.buffers.in;
var out_buf = self.buffers.out;
for (ports) |port, p_idx| {
var p = @intCast(u32, p_idx);
for (ports, 0..) |_, p_idx| {
const p = @as(u32, @intCast(p_idx));
var port: *lv2.Port = &ports[p_idx];
switch (port.ptype) {
.Control => lv2.lilv_instance_connect_port(self.instance, p, &port.value),
.Audio => blk: {
.Audio => {
if (port.is_input) {
lv2.lilv_instance_connect_port(self.instance, p, &in_buf[i]);
lv2.lilv_instance_connect_port(
self.instance,
p,
&self.buffers.in[i],
);
i += 1;
} else {
lv2.lilv_instance_connect_port(self.instance, p, &out_buf[o]);
lv2.lilv_instance_connect_port(
self.instance,
p,
&self.buffers.out[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 std.cstr.addNullByte(allocator, plugin_uri);
var world = c.lilv_world_new().?;
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);
c.lilv_world_load_all(world);
var uri: *c.LilvNode = c.lilv_new_uri(world, cstr_plugin_uri.ptr) orelse blk: {
std.debug.warn("Invalid plugin URI <{}>\n", plugin_uri);
const uri: *c.LilvNode = c.lilv_new_uri(world, cstr_plugin_uri.ptr) orelse {
log.debug("Invalid plugin URI <{s}>", .{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).?;
var plugin: *const c.LilvPlugin = c.lilv_plugins_get_by_uri(plugins, uri) orelse blk: {
std.debug.warn("Plugin <{}> not found\n", plugin_uri);
const plugin: *const c.LilvPlugin = c.lilv_plugins_get_by_uri(plugins, uri) orelse {
log.debug("Plugin <{s}> not found", .{plugin_uri});
return ImageError.UnknownPlugin;
};
c.lilv_node_free(uri);
return Context{ .allocator = allocator, .world = world, .plugin = plugin };
return Context{
.allocator = allocator,
.world = world,
.plugin = plugin,
};
}

88
src/printer.zig Normal file
View File

@ -0,0 +1,88 @@
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

@ -5,6 +5,8 @@ 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;
const ParamMap = plugin.ParamMap;
@ -18,12 +20,21 @@ pub const RunError = error{
};
pub const Runner = struct {
allocator: *std.mem.Allocator,
allocator: std.mem.Allocator,
/// The currently opened image in the runner
image: ?*Image = null,
pub fn init(allocator: *std.mem.Allocator) Runner {
/// If the runner is in REPL mode
repl: bool,
args: []const [:0]u8,
pub fn init(allocator: std.mem.Allocator, repl: bool) Runner {
return Runner{
.allocator = allocator,
.repl = repl,
.args = std.process.argsAlloc(allocator) catch unreachable,
};
}
@ -31,41 +42,59 @@ 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
const index = try std.fmt.parseInt(usize, load_path[1..], 10);
var args_it = std.process.args();
_ = args_it.skip();
var index = try std.fmt.parseInt(usize, load_path[1..], 10);
var i: usize = 0;
while (i <= index) : (i += 1) {
_ = 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 });
}
const arg = try (args_it.next(self.allocator) orelse @panic("expected argument"));
return arg;
log.debug("fetch arg idx={d}", .{index});
log.debug("fetch arg val={s}", .{self.args[index]});
return self.args[index];
} 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 {
var load_path = try self.resolveArgPath(path_or_argidx);
std.debug.warn("load path: {}\n", load_path);
const load_path = try self.resolveArgPath(path_or_argidx);
log.debug("\tload path: {s}", .{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.
@ -74,14 +103,15 @@ 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")) {
std.debug.warn("Only BMP files are allowed to be loaded.\n");
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});
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);
}
@ -89,20 +119,21 @@ pub const Runner = struct {
if (self.image) |image| {
return image;
} else {
std.debug.warn("image is required!\n");
log.debug("image is required!", .{});
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.
var image = try self.getImage();
const 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.Dir.open(self.allocator, dirname);
var dir = try std.fs.cwd().openDir(dirname, .{ .iterate = true });
defer dir.close();
const period_idx = std.mem.lastIndexOf(u8, basename, ".").?;
@ -110,17 +141,18 @@ 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,
"{}_g",
const starts_with = try std.fmt.allocPrint(self.allocator, "{s}_g", .{
basename[0..period_idx],
);
});
defer self.allocator.free(starts_with);
var max: usize = 0;
while (try dir.next()) |entry| {
var it = dir.iterate();
while (try it.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
@ -134,6 +166,8 @@ 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 {};
};
@ -143,14 +177,12 @@ pub const Runner = struct {
}
}
const out_path = try std.fmt.allocPrint(
self.allocator,
"{}/{}{}{}",
const out_path = try std.fmt.allocPrint(self.allocator, "{s}/{s}{d}{s}", .{
dirname,
starts_with,
max + 1,
extension,
);
});
return out_path;
}
@ -158,303 +190,156 @@ 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, program: []const u8) !void {
fn runQSCmd(self: *Runner, cmd: *lang.Command) !void {
const runqs = cmd.cast(lang.Command.RunQS).?;
var image = try self.getImage();
const out_path = try self.makeGlitchedPath();
defer self.allocator.free(out_path);
try image.saveTo(out_path);
var proc = try std.ChildProcess.init(
[_][]const u8{ program, out_path },
var proc = std.ChildProcess.init(
&[_][]const u8{ runqs.program, out_path },
self.allocator,
);
defer proc.deinit();
//defer proc.deinit();
std.debug.warn("running '{} {}'", program, out_path);
log.debug("running '{s} {s}'", .{ runqs.program, out_path });
_ = try proc.spawnAndWait();
}
/// 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 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);
}
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 executeLV2Command(self: *@This(), command: anytype) !void {
const pos = plugin.Position{
.split = command.split,
.index = command.index,
};
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();
for (bands) |band_value, idx| {
var sym = try std.fmt.allocPrint(self.allocator, "band_{}", idx + 1);
const typ = @TypeOf(command);
inline for (@typeInfo(@TypeOf(command.parameters)).Struct.fields) |cmd_field| {
try params.append(plugin.Param{
.sym = sym,
.value = band_value,
.sym = cmd_field.name,
.value = @field(command.parameters, cmd_field.name),
});
}
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("http://plugin.org.uk/swh-plugins/multivoiceChorus", pos, params);
try image.runPlugin(typ.lv2_url, pos, params);
}
fn pitchScalerCmd(self: *Runner, pos: Position, params: ParamList) !void {
var image = try self.getImage();
try image.runPlugin("http://plugin.org.uk/swh-plugins/pitchScaleHQ", pos, params);
}
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 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 rotateCmd(
self: *Runner,
deg: f32,
bgfill: []const u8,
) !void {
var image = try self.getImage();
var c_bgfill = try std.cstr.addNullByte(self.allocator, bgfill);
defer self.allocator.free(c_bgfill);
try magick.runRotate(image, deg, c_bgfill);
}
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(),
.RunQS => try self.runQSCmd(cmd.args.at(0)),
.Amp => blk: {
const pos = try cmd.consumePosition();
try cmd.appendParam(&params, "gain");
try self.ampCmd(pos, params);
},
.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, "roomWidth");
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);
},
.Rotate => blk: {
const deg = try cmd.floatArgAt(0);
const bgfill = try cmd.argAt(1);
try self.rotateCmd(deg, bgfill);
},
else => blk: {
std.debug.warn("Unsupported command: {}\n", cmd.command);
break :blk RunError.UnknownCommand;
},
fn executeCustomCommand(self: *@This(), command: anytype) !void {
const pos = plugin.Position{
.split = command.split,
.index = command.index,
};
var image = try self.getImage();
try image.runCustomPlugin(@TypeOf(command).plugin_type, pos, command.parameters);
}
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 runCommand(self: *@This(), cmd: *lang.Command) !void {
switch (cmd.tag) {
.noop => {},
.load => {
const command = cmd.cast(lang.Command.Load).?;
try self.loadCmd(command.path);
},
.quicksave => try self.quicksaveCmd(),
.rotate => try self.rotateCmd(cmd),
.runqs => try self.runQSCmd(cmd),
.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),
.noise => try self.runSingleCommand(cmd, .noise),
.wildnoise => try self.runSingleCommand(cmd, .wildnoise),
.write => try self.runSingleCommand(cmd, .write),
.embed => try self.runSingleCommand(cmd, .embed),
}
}
/// Run a list of commands.
pub fn runCommands(
self: *Runner,
cmds: lang.CommandList,
debug_flag: bool,
) !void {
var it = cmds.iterator();
while (it.next()) |cmd| {
if (debug_flag) cmd.print();
_ = debug_flag;
for (cmds.list.items) |cmd| {
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);
}