commit 853b60e12641736a7e2e4b17b02590074dc33199 Author: Ebu Date: Tue Dec 2 00:08:44 2025 +0100 Initial Air Freshener plugin implementation diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9dd90ad --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +*.blend filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text +*.kra filter=lfs diff=lfs merge=lfs -text +*.exr filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..21a2155 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,16 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "cargo", + "command": "post", + "args": ["build", "--package", "ebu-plug-core"], + "problemMatcher": ["$rustc"], + "group": { + "kind": "build", + "isDefault": true + }, + "label": "rust: cargo build" + } + ] +} diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5583b77 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1301 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "anymap3" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170433209e817da6aae2c51aa0dd443009a613425dd041ebfb2492d1c4c11a25" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "atomic_float" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62af46d040ba9df09edc6528dae9d8e49f5f3e82f55b7d2ec31a733c38dbc49d" + +[[package]] +name = "atomic_refcell" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "baseview" +version = "0.1.0" +source = "git+https://github.com/RustAudio/baseview#f7326ceab58c8dd75f3e10bc815ecf7d098e2efc" +dependencies = [ + "cocoa", + "core-foundation", + "keyboard-types", + "nix", + "objc", + "raw-window-handle", + "uuid", + "winapi", + "x11", + "x11rb", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "cc" +version = "1.2.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap-sys" +version = "0.5.0" +source = "git+https://github.com/micahrj/clap-sys.git?rev=25d7f53fdb6363ad63fbd80049cb7a42a97ac156#25d7f53fdb6363ad63fbd80049cb7a42a97ac156" + +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "ebu-dsp" +version = "0.1.0" +dependencies = [ + "nih_plug", +] + +[[package]] +name = "ebu-plug-core" +version = "0.1.0" +dependencies = [ + "baseview", + "crossbeam", + "ebu-dsp", + "femtovg", + "image", + "nih_plug", + "parking_lot", + "serde", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "femtovg" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0530af3119be5658d8c1f7e69248d46e2c59e500dc2ef373cf25b355158ef101" +dependencies = [ + "bitflags 2.10.0", + "bytemuck", + "fnv", + "glow", + "image", + "imgref", + "itertools", + "log", + "lru", + "rgb", + "rustybuzz", + "slotmap", + "unicode-bidi", + "unicode-segmentation", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", +] + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "keyboard-types" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7668b7cff6a51fe61cdde64cd27c8a220786f399501b57ebe36f7d8112fd68" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "lru" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96051b46fc183dc9cd4a223960ef37b9af631b55191852a8274bfef064cda20f" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "midi-consts" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f2dd5c7f8aaf48a76e389068ab25ed80bdbc226b887f9013844c415698c9952" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "nih_log" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0cdb52ef79af48ae110401c883bdb9c15e0306a99ab6ecf18bc52068b668e54" +dependencies = [ + "atty", + "log", + "once_cell", + "termcolor", + "time", + "windows", +] + +[[package]] +name = "nih_plug" +version = "0.0.0" +source = "git+https://github.com/robbert-vdh/nih-plug#28b149ec4d62757d0b448809148a0c3ca6e09a95" +dependencies = [ + "anyhow", + "anymap3", + "atomic_float", + "atomic_refcell", + "backtrace", + "bitflags 1.3.2", + "cfg-if", + "clap-sys", + "core-foundation", + "crossbeam", + "libc", + "log", + "midi-consts", + "nih_log", + "nih_plug_derive", + "objc", + "parking_lot", + "raw-window-handle", + "serde", + "serde_json", + "widestring", + "windows", +] + +[[package]] +name = "nih_plug_derive" +version = "0.1.0" +source = "git+https://github.com/robbert-vdh/nih-plug#28b149ec4d62757d0b448809148a0c3ca6e09a95" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "nix" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4916f159ed8e5de0082076562152a76b7a1f64a01fd9d1e0fea002c37624faf" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.10.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pxfm" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" +dependencies = [ + "num-traits", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "rgb" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.10.0", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slotmap" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.111", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "rustix", + "x11rb-protocol", + "xcursor", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5fe4a11 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "ebu-plug-core" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +ebu-dsp = { version = "0.1.0", path = "ebu-dsp" } +nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", version = "0.0.0", default-features = false } +parking_lot = "0.12.5" +baseview = { git = "https://github.com/RustAudio/baseview", version = "0.1.0", features = ["opengl"]} +crossbeam = "0.8.4" +femtovg = "0.18.1" +serde = { version = "1.0.228", features = ["derive"] } +image = { version = "0.25.9", default-features = false, features = ["png"] } + +[workspace] +members = ["ebu-dsp"] diff --git a/assets/AirFreshener/Fresh.blend b/assets/AirFreshener/Fresh.blend new file mode 100644 index 0000000..befcd09 --- /dev/null +++ b/assets/AirFreshener/Fresh.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1e3585df90783e81322d406846d03dd171fa3b3c75fe8d5308f270897642d91 +size 5143312 diff --git a/assets/AirFreshener/backgrounds.kra b/assets/AirFreshener/backgrounds.kra new file mode 100644 index 0000000..74325ac --- /dev/null +++ b/assets/AirFreshener/backgrounds.kra @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ed8ebfa6d250788e84e3dc52671b0cb823c2003dae47be1f90257be94785e2d +size 1753876 diff --git a/assets/AirFreshener/bg0.png b/assets/AirFreshener/bg0.png new file mode 100644 index 0000000..29bed14 --- /dev/null +++ b/assets/AirFreshener/bg0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8722450a8b27cf9866dc9846e6b0369c663c1af3d086af8254f69c736ed9ed3a +size 109813 diff --git a/assets/AirFreshener/bg1.png b/assets/AirFreshener/bg1.png new file mode 100644 index 0000000..11d0b54 --- /dev/null +++ b/assets/AirFreshener/bg1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad09d8634742350ab1b1870dff1744500ed18fbc1c402d5c621834d92de420fc +size 141839 diff --git a/assets/AirFreshener/sheet.png b/assets/AirFreshener/sheet.png new file mode 100644 index 0000000..e523bcd --- /dev/null +++ b/assets/AirFreshener/sheet.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:078edb3b3707182495b701ffa0611e1b04742fe56e5b957b9ba4643dd6c33eaa +size 3281280 diff --git a/assets/studio_small_09_4k.exr b/assets/studio_small_09_4k.exr new file mode 100644 index 0000000..3de17e6 --- /dev/null +++ b/assets/studio_small_09_4k.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbb83755cf731a87373bdd25e28b4a74f58c8c8a02bf348e960635265ffda558 +size 18542321 diff --git a/ebu-dsp/Cargo.toml b/ebu-dsp/Cargo.toml new file mode 100644 index 0000000..3a71db8 --- /dev/null +++ b/ebu-dsp/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "ebu-dsp" +version = "0.1.0" +edition = "2024" + +[dependencies] +nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", version = "0.0.0", default-features = false } diff --git a/ebu-dsp/src/amplitude.rs b/ebu-dsp/src/amplitude.rs new file mode 100644 index 0000000..2500019 --- /dev/null +++ b/ebu-dsp/src/amplitude.rs @@ -0,0 +1,74 @@ +use std::fmt::{Debug, Display}; + +use crate::{decibel::Decibel, math_ops_impl}; + +#[derive(Default, Copy, Clone, PartialEq, PartialOrd)] +pub struct Amplitude(pub f64); +impl From for Decibel { + fn from(value: Amplitude) -> Self { + Decibel::from(20.0 * value.0.abs().log10()) + } +} +impl Amplitude { + pub fn value(&self) -> f64 { + self.0 + } +} + +impl Display for Amplitude { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!("{:.2}", self.0)) + } +} +impl Debug for Amplitude { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!("{}", self.0)) + } +} + +math_ops_impl!(Amplitude); + +impl std::ops::Add for Amplitude { + type Output = Amplitude; + fn add(self, rhs: Amplitude) -> Self::Output { + Self(self.0 + rhs.0) + } +} +impl std::ops::Sub for Amplitude { + type Output = Amplitude; + fn sub(self, rhs: Amplitude) -> Self::Output { + Self(self.0 - rhs.0) + } +} +impl std::ops::Mul for Amplitude { + type Output = Amplitude; + fn mul(self, rhs: Amplitude) -> Self::Output { + Self(self.0 * rhs.0) + } +} +impl std::ops::Div for Amplitude { + type Output = Amplitude; + fn div(self, rhs: Amplitude) -> Self::Output { + Self(self.0 / rhs.0) + } +} +impl std::ops::AddAssign for Amplitude { + fn add_assign(&mut self, rhs: Amplitude) { + self.0 += rhs.0; + } +} +impl std::ops::SubAssign for Amplitude { + fn sub_assign(&mut self, rhs: Amplitude) { + self.0 -= rhs.0; + } +} +impl std::ops::MulAssign for Amplitude { + fn mul_assign(&mut self, rhs: Amplitude) { + self.0 *= rhs.0; + } +} +impl std::ops::DivAssign for Amplitude { + fn div_assign(&mut self, rhs: Amplitude) { + self.0 /= rhs.0; + } +} diff --git a/ebu-dsp/src/biquad.rs b/ebu-dsp/src/biquad.rs new file mode 100644 index 0000000..ae15f47 --- /dev/null +++ b/ebu-dsp/src/biquad.rs @@ -0,0 +1,348 @@ +use std::{ + f64::{self, consts::SQRT_2}, + fmt::Display, + sync::Arc, +}; + +use crate::{ + decibel::Decibel, SampleRate, + traits::{IntFormatter, Processor}, +}; + +#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum Slope { + #[default] + DB12, + DB24, + DB36, + DB48, +} + +#[derive(Default, Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum FilterMode { + Peak, + HighPass, + HighShelf, + LowPass, + LowShelf, + AllPass, + BandPass, + BandReject, + #[default] + None, +} +impl TryFrom for FilterMode { + type Error = (); + fn try_from(value: i32) -> Result { + match value { + 0 => Ok(FilterMode::Peak), + 1 => Ok(FilterMode::HighPass), + 2 => Ok(FilterMode::HighShelf), + 3 => Ok(FilterMode::LowPass), + 4 => Ok(FilterMode::LowShelf), + 5 => Ok(FilterMode::AllPass), + 6 => Ok(FilterMode::BandPass), + 7 => Ok(FilterMode::BandReject), + _ => Err(()), + } + } +} +impl From for i32 { + fn from(value: FilterMode) -> Self { + match value { + FilterMode::Peak => 0, + FilterMode::HighPass => 1, + FilterMode::HighShelf => 2, + FilterMode::LowPass => 3, + FilterMode::LowShelf => 4, + FilterMode::AllPass => 5, + FilterMode::BandPass => 6, + FilterMode::BandReject => 7, + FilterMode::None => 8, + } + } +} +impl TryFrom<&str> for FilterMode { + type Error = (); + + fn try_from(value: &str) -> Result { + match value { + "Peak" => Ok(FilterMode::Peak), + "Highpass" => Ok(FilterMode::HighPass), + "High Shelf" => Ok(FilterMode::HighShelf), + "Lowpass" => Ok(FilterMode::LowPass), + "Low Shelf" => Ok(FilterMode::LowShelf), + "Allpass" => Ok(FilterMode::AllPass), + "Bandpass" => Ok(FilterMode::BandPass), + "Notch" => Ok(FilterMode::BandReject), + _ => Err(()), + } + } +} +impl Display for FilterMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + FilterMode::Peak => "Peak", + FilterMode::HighPass => "Highpass", + FilterMode::HighShelf => "High Shelf", + FilterMode::LowPass => "Lowpass", + FilterMode::LowShelf => "Low Shelf", + FilterMode::AllPass => "Allpass", + FilterMode::BandPass => "Bandpass", + FilterMode::BandReject => "Notch", + FilterMode::None => "None", + }) + } +} + +impl IntFormatter for FilterMode { + fn v2s() -> Arc String + Send + Sync> + where + Self: Sized, + { + Arc::new(move |value| FilterMode::try_from(value).unwrap_or_default().to_string()) + } + + fn s2v() -> Arc Option + Send + Sync> + where + Self: Sized, + { + Arc::new(move |value| FilterMode::try_from(value).map(i32::from).ok()) + } +} + +#[derive(Default, Debug, Clone, Copy)] +struct FilterCoefficients { + a1: f64, + a2: f64, + b0: f64, + b1: f64, + b2: f64, +} +impl FilterCoefficients { + fn calculate( + &mut self, + mode: FilterMode, + sample_rate: SampleRate, + freq: f64, + q: f64, + gain_db: Decibel, + ) { + let v = 10f64.powf(gain_db.value().abs() / 20.0); + let k = (f64::consts::PI * freq / sample_rate.0).tan(); + + match mode { + FilterMode::Peak => { + if gain_db.value() >= 0.0 { + let norm = 1.0 / (1.0 + 1.0 / q * k + k * k); + self.b0 = (1.0 + v / q * k + k * k) * norm; + self.b1 = 2.0 * (k * k - 1.0) * norm; + self.b2 = (1.0 - v / q * k + k * k) * norm; + self.a1 = self.b1; + self.a2 = (1.0 - 1.0 / q * k + k * k) * norm; + } else { + let norm = 1.0 / (1.0 + v / q * k + k * k); + self.b0 = (1.0 + 1.0 / q * k + k * k) * norm; + self.b1 = 2.0 * (k * k - 1.0) * norm; + self.b2 = (1.0 - 1.0 / q * k + k * k) * norm; + self.a1 = self.b1; + self.a2 = (1.0 - v / q * k + k * k) * norm; + } + } + FilterMode::HighPass => { + let norm = 1.0 / (1.0 + k / q + k * k); + self.b0 = 1.0 * norm; + self.b1 = -2.0 * self.b0; + self.b2 = self.b0; + self.a1 = 2.0 * (k * k - 1.0) * norm; + self.a2 = (1.0 - k / q + k * k) * norm; + } + FilterMode::HighShelf => { + if gain_db.value() >= 0.0 { + let norm = 1.0 / (1.0 + SQRT_2 * k + k * k); + self.b0 = (v + (2.0 * v).sqrt() * k + k * k) * norm; + self.b1 = 2.0 * (k * k - v) * norm; + self.b2 = (v - (2.0 * v).sqrt() * k + k * k) * norm; + self.a1 = 2.0 * (k * k - 1.0) * norm; + self.a2 = (1.0 - SQRT_2 * k + k * k) * norm; + } else { + let norm = 1.0 / (v + (2.0 * v).sqrt() * k + k * k); + self.b0 = (1.0 + SQRT_2 * k + k * k) * norm; + self.b1 = 2.0 * (k * k - 1.0) * norm; + self.b2 = (1.0 - SQRT_2 * k + k * k) * norm; + self.a1 = 2.0 * (k * k - v) * norm; + self.a2 = (v - (2.0 * v).sqrt() * k + k * k) * norm; + } + } + FilterMode::LowPass => { + let norm = 1.0 / (1.0 + k / q + k * k); + self.b0 = k * k * norm; + self.b1 = 2.0 * self.b0; + self.b2 = self.b0; + self.a1 = 2.0 * (k * k - 1.0) * norm; + self.a2 = (1.0 - k / q + k * k) * norm; + } + FilterMode::LowShelf => { + if gain_db.value() >= 0.0 { + let norm = 1.0 / (1.0 + SQRT_2 * k + k * k); + self.b0 = (1.0 + (2.0 * v).sqrt() * k + v * k * k) * norm; + self.b1 = 2.0 * (v * k * k - 1.0) * norm; + self.b2 = (1.0 - (2.0 * v).sqrt() * k + v * k * k) * norm; + self.a1 = 2.0 * (k * k - 1.0) * norm; + self.a2 = (1.0 - SQRT_2 * k + k * k) * norm; + } else { + let norm = 1.0 / (1.0 + (2.0 * v).sqrt() * k + v * k * k); + self.b0 = (1.0 + SQRT_2 * k + k * k) * norm; + self.b1 = 2.0 * (k * k - 1.0) * norm; + self.b2 = (1.0 - SQRT_2 * k + k * k) * norm; + self.a1 = 2.0 * (v * k * k - 1.0) * norm; + self.a2 = (1.0 - (2.0 * v).sqrt() * k + v * k * k) * norm; + } + } + FilterMode::AllPass => { + let norm = 1.0 / (1.0 + k / q + k * k); + self.b0 = (1.0 - k / q + k * k) * norm; + self.b1 = 2.0 * (k * k - 1.0) * norm; + self.b2 = 1.0; + self.a1 = self.b1; + self.a2 = self.b0; + } + FilterMode::BandPass => { + let norm = 1.0 / (1.0 + k / q + k * k); + self.b0 = k / q * norm; + self.b1 = 0.0; + self.b2 = -self.b0; + self.a1 = 2.0 * (k * k - 1.0) * norm; + self.a2 = (1.0 - k / q + k * k) * norm; + } + FilterMode::BandReject => { + let norm = 1.0 / (1.0 + k / q + k * k); + self.b0 = (1.0 + k * k) * norm; + self.b1 = 2.0 * (k * k - 1.0) * norm; + self.b2 = self.b0; + self.a1 = self.b1; + self.a2 = (1.0 - k / q + k * k) * norm; + } + FilterMode::None => {} + } + } +} + +#[derive(Debug, Default, Clone, Copy)] +struct State { + s1: f64, + s2: f64, +} + +#[derive(Debug, Clone, Copy)] +pub struct BiquadFilterState { + pub mode: FilterMode, + pub cutoff: f64, + pub q: f64, + pub gain_db: Decibel, + pub slope: Slope, +} +impl Default for BiquadFilterState { + fn default() -> Self { + Self { + mode: FilterMode::None, + cutoff: 2000.0, + q: 0.707, + gain_db: Decibel::from(0.0), + slope: Slope::DB12, + } + } +} + +#[derive(Debug, Clone)] +pub struct BiquadFilter { + pub state: BiquadFilterState, + sample_rate: SampleRate, + coefficients: FilterCoefficients, + slope_states: [State; 4], + dirty: bool, +} + +impl BiquadFilter { + pub fn new(sample_rate: SampleRate) -> Self { + Self::from_state(sample_rate, BiquadFilterState::default()) + } + pub fn from_state(sample_rate: SampleRate, state: BiquadFilterState) -> Self { + Self { + sample_rate, + coefficients: FilterCoefficients::default(), + slope_states: [State::default(); 4], + dirty: true, + state + } + } + pub fn set_mode(&mut self, mode: FilterMode) { + self.dirty = true; + self.state.mode = mode; + } + pub fn set_cutoff(&mut self, cutoff: f64) { + self.dirty = true; + self.state.cutoff = cutoff; + } + pub fn set_slope(&mut self, slope: Slope) { + self.dirty = true; + self.state.slope = slope; + } + pub fn set_q(&mut self, q: f64) { + self.dirty = true; + self.state.q = q; + } + pub fn set_gain(&mut self, gain_db: T) + where + T: Into, + { + self.dirty = true; + self.state.gain_db = gain_db.into(); + } + + fn update_coefficients(&mut self) { + if !self.dirty { + return; + } + self.dirty = false; + + self.coefficients.calculate( + self.state.mode, + self.sample_rate, + self.state.cutoff, + self.state.q, + self.state.gain_db, + ); + } +} + +impl Processor for BiquadFilter { + fn process(&mut self, sample: f64) -> f64 { + self.update_coefficients(); + + let iterations = match self.state.slope { + Slope::DB12 => 1, + Slope::DB24 => 2, + Slope::DB36 => 3, + Slope::DB48 => 4, + }; + let mut current = sample; + + for i in 0..iterations { + let result = self.coefficients.b0 * current + self.slope_states[i].s1; + let s1 = self.coefficients.b1 * current - self.coefficients.a1 * result + + self.slope_states[i].s2; + let s2 = self.coefficients.b2 * current - self.coefficients.a2 * result; + + self.slope_states[i].s1 = s1; + self.slope_states[i].s2 = s2; + current = result; + } + + if current.is_nan() || current.is_infinite() { + 0.0 + } else { + current + } + } +} diff --git a/ebu-dsp/src/comp.rs b/ebu-dsp/src/comp.rs new file mode 100644 index 0000000..6de188c --- /dev/null +++ b/ebu-dsp/src/comp.rs @@ -0,0 +1,127 @@ +use crate::{ + Ratio, SampleRate, amplitude::Amplitude, decibel::Decibel, ring_buffer::RingBuffer, rms, smoother::Smoother, traits::Processor +}; +use std::time::Duration; + +#[derive(Debug, Default, Clone, Copy)] +pub enum CompressorPeakMode { + #[default] + Sample, + RMS, +} + +#[derive(Debug, Default, Clone, Copy)] +pub struct CompressorState { + pub peak_mode: CompressorPeakMode, + pub threshold: Decibel, + pub ratio: Ratio, + pub gain: Decibel, + pub attack: Duration, + pub release: Duration, + pub last_rms: f64, + pub input_db: Decibel, + pub output_static_db: Decibel, + pub gain_db: Decibel, + pub gain_smoothed_db: Decibel, + pub final_gain_db: Decibel, +} +impl CompressorState { + pub fn new() -> Self { + Self { + peak_mode: CompressorPeakMode::Sample, + threshold: Decibel::from(-24.0), + ratio: Ratio(1.0, 3.0), + gain: Decibel::from(0.0), + attack: Duration::from_millis(25), + release: Duration::from_millis(100), + last_rms: 0.0, + input_db: Decibel::from(0.0), + output_static_db: Decibel::from(0.0), + gain_db: Decibel::from(0.0), + gain_smoothed_db: Decibel::from(0.0), + final_gain_db: Decibel::from(0.0), + } + } +} + +pub struct Compressor { + pub state: CompressorState, + sample_buffer: RingBuffer, + smoother: Smoother, +} + +impl Compressor { + pub fn new(sample_rate: SampleRate) -> Self { + Self::from_state(sample_rate, CompressorState::default()) + } + pub fn from_state(sample_rate: SampleRate, state: CompressorState) -> Self { + Self { + sample_buffer: RingBuffer::new(0.0), + smoother: Smoother::new(sample_rate), + state, + } + } + pub fn set_threshold(&mut self, threshold: T) + where + T: Into, + { + self.state.threshold = threshold.into(); + } + pub fn set_attack(&mut self, attack: T) + where + T: Into, + { + self.smoother.set_attack(attack); + } + pub fn set_release(&mut self, release: T) + where + T: Into, + { + self.smoother.set_release(release); + } + pub fn set_ratio(&mut self, ratio: R) + where + R: Into, + { + self.state.ratio = ratio.into(); + } + pub fn set_gain(&mut self, gain: T) + where + T: Into, + { + self.state.gain = gain.into(); + } +} + +impl Processor for Compressor { + fn process(&mut self, sample: f64) -> f64 { + self.sample_buffer[0] = sample; + + self.state.input_db = Decibel::from(Amplitude(match self.state.peak_mode { + CompressorPeakMode::Sample => sample.abs() + f64::EPSILON, + CompressorPeakMode::RMS => { + self.state.last_rms = rms(&self.sample_buffer, self.state.last_rms); + self.state.last_rms.abs().sqrt() + f64::EPSILON + } + })); + + self.state.output_static_db = if self.state.input_db < self.state.threshold { + self.state.input_db + } else { + self.state.threshold + + (self.state.input_db - self.state.threshold) * self.state.ratio.multiplier() + }; + self.state.gain_db = self.state.output_static_db - self.state.input_db; + + self.state.gain_smoothed_db = if self.state.gain_db < self.state.gain_smoothed_db { + self.smoother.attack(self.state.gain_db) + } else { + self.smoother.release(self.state.gain_db) + }; + + self.state.final_gain_db = self.state.gain_smoothed_db + self.state.gain; + self.sample_buffer.shift(); + + sample * self.state.final_gain_db + } +} \ No newline at end of file diff --git a/ebu-dsp/src/decibel.rs b/ebu-dsp/src/decibel.rs new file mode 100644 index 0000000..05320c6 --- /dev/null +++ b/ebu-dsp/src/decibel.rs @@ -0,0 +1,141 @@ +use std::fmt::{Debug, Display}; + +use crate::{amplitude::Amplitude, math_ops_impl}; + +#[derive(Default, Copy, Clone, PartialEq, PartialOrd)] +pub struct Decibel(f64); +impl Decibel { + pub fn value(&self) -> f64 { + self.0 + } +} +impl From for Amplitude { + fn from(value: Decibel) -> Self { + Amplitude(10f64.powf(value.0 / 20.0)) + } +} + +impl Display for Decibel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.0 >= 0.0 { + f.write_str(&format!("+{:.2}dB", self.0)) + } else { + f.write_str(&format!("{:.2}dB", self.0)) + } + } +} +impl Debug for Decibel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.0 >= 0.0 { + f.write_str(&format!("+{}dB", self.0)) + } else { + f.write_str(&format!("{}dB", self.0)) + } + } +} + +impl From for Decibel { + fn from(value: f32) -> Self { + Decibel(value as f64) + } +} +impl From for f32 { + fn from(value: Decibel) -> Self { + value.0 as f32 + } +} + +impl From for Decibel { + fn from(value: f64) -> Self { + let mut value = value; + #[cfg(debug_assertions)] + { + use nih_plug::nih_warn; + if value.is_nan() { + nih_warn!("Tried making dB from NaN"); + value = 0.0; + } else if value.is_infinite() { + nih_warn!("Tried making dB from Inf"); + value = 0.0; + } else if value >= 120.0 { + nih_warn!("Impossibly large dB {value}"); + } + } + Decibel(value) + } +} +impl From for f64 { + fn from(value: Decibel) -> Self { + value.0 + } +} + +math_ops_impl!(Decibel); + +impl std::ops::Add for Decibel { + type Output = Decibel; + fn add(self, rhs: Decibel) -> Self::Output { + Self(self.0 + rhs.0) + } +} +impl std::ops::Sub for Decibel { + type Output = Decibel; + fn sub(self, rhs: Decibel) -> Self::Output { + Self(self.0 - rhs.0) + } +} +impl std::ops::AddAssign for Decibel { + fn add_assign(&mut self, rhs: Decibel) { + self.0 += rhs.0; + } +} +impl std::ops::SubAssign for Decibel { + fn sub_assign(&mut self, rhs: Decibel) { + self.0 -= rhs.0; + } +} + +impl std::ops::Mul for f32 { + type Output = f32; + fn mul(self, rhs: Decibel) -> Self::Output { + ((self as f64) * rhs) as f32 + } +} +impl std::ops::Mul for f64 { + type Output = f64; + fn mul(self, rhs: Decibel) -> Self::Output { + self * Amplitude::from(rhs).0 + } +} +impl std::ops::Div for f32 { + type Output = f32; + fn div(self, rhs: Decibel) -> Self::Output { + ((self as f64) / rhs) as f32 + } +} +impl std::ops::Div for f64 { + type Output = f64; + fn div(self, rhs: Decibel) -> Self::Output { + self / Amplitude::from(rhs).0 + } +} +impl std::ops::MulAssign for f32 { + fn mul_assign(&mut self, rhs: Decibel) { + *self = ((*self as f64) * rhs) as f32 + } +} +impl std::ops::MulAssign for f64 { + fn mul_assign(&mut self, rhs: Decibel) { + *self *= Amplitude::from(rhs).0 + } +} +impl std::ops::DivAssign for f32 { + fn div_assign(&mut self, rhs: Decibel) { + *self = ((*self as f64) / rhs) as f32 + } +} +impl std::ops::DivAssign for f64 { + fn div_assign(&mut self, rhs: Decibel) { + *self /= Amplitude::from(rhs).0 + } +} diff --git a/ebu-dsp/src/eq.rs b/ebu-dsp/src/eq.rs new file mode 100644 index 0000000..025cc31 --- /dev/null +++ b/ebu-dsp/src/eq.rs @@ -0,0 +1,31 @@ +use crate::{BiquadFilter, BiquadFilterState, Processor}; + +#[derive(Debug, Default, Clone)] +pub struct EqualizerState { + pub nodes: [Option; 6], +} + +pub struct Equalizer { + pub state: EqualizerState, +} + +impl Equalizer { + pub fn new() -> Self { + Self::from_state(EqualizerState::default()) + } + pub fn from_state(state: EqualizerState) -> Self { + Self { state } + } +} + +impl Processor for Equalizer { + fn process(&mut self, sample: f64) -> f64 { + let mut sample = sample; + for node in &mut self.state.nodes { + if let Some(filter) = node { + sample = filter.process(sample); + } + } + sample + } +} diff --git a/ebu-dsp/src/freq_split.rs b/ebu-dsp/src/freq_split.rs new file mode 100644 index 0000000..88aa4fa --- /dev/null +++ b/ebu-dsp/src/freq_split.rs @@ -0,0 +1,56 @@ +use crate::{BiquadFilter, FilterMode, SampleRate, biquad::Slope, traits::Processor}; + +pub struct FreqSplitterState { + pub split_frequency: f64, + pub slope: Slope, +} +impl Default for FreqSplitterState { + fn default() -> Self { + Self { + split_frequency: 2000.0, + slope: Slope::DB24, + } + } +} + +pub struct FreqSplitter { + pub state: FreqSplitterState, + low_band: BiquadFilter, + high_band: BiquadFilter, +} + +impl FreqSplitter { + pub fn new(sample_rate: SampleRate) -> Self { + Self::from_state(sample_rate, FreqSplitterState::default()) + } + pub fn from_state(sample_rate: SampleRate, state: FreqSplitterState) -> Self { + let mut this = Self { + state, + low_band: BiquadFilter::new(sample_rate), + high_band: BiquadFilter::new(sample_rate), + }; + this.low_band.set_mode(FilterMode::LowPass); + this.high_band.set_mode(FilterMode::HighPass); + this + } +} + +impl Processor for FreqSplitter { + fn process(&mut self, sample: f64) -> (f64, f64) { + self.low_band.set_cutoff(self.state.split_frequency); + self.low_band.set_slope(self.state.slope); + self.high_band.set_cutoff(self.state.split_frequency); + self.high_band.set_slope(self.state.slope); + ( + self.low_band.process(sample), + self.high_band.process(sample), + ) + } +} + +impl Processor for FreqSplitter { + fn process(&mut self, sample: f32) -> (f32, f32) { + let (low, high) = self.process(sample as f64); + (low as f32, high as f32) + } +} diff --git a/ebu-dsp/src/lib.rs b/ebu-dsp/src/lib.rs new file mode 100644 index 0000000..df0319e --- /dev/null +++ b/ebu-dsp/src/lib.rs @@ -0,0 +1,198 @@ +mod amplitude; +mod biquad; +mod comp; +mod decibel; +mod freq_split; +mod meter; +mod ring_buffer; +mod smoother; +mod traits; +mod eq; + +use std::{ + fmt::Debug, + ops::Add, + time::Duration, +}; + +pub use biquad::{BiquadFilter, BiquadFilterState, FilterMode, Slope}; +pub use comp::{Compressor, CompressorState}; +pub use freq_split::{FreqSplitter, FreqSplitterState}; +pub use traits::{FloatFormatter, IntFormatter, Lerp, Processor}; +pub use decibel::Decibel; +pub use amplitude::Amplitude; +pub use eq::{Equalizer, EqualizerState}; + +pub struct Rect { + pub x: T, + pub y: T, + pub width: T, + pub height: T, +} +impl Debug for Rect { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Rect") + .field("x", &self.x) + .field("y", &self.y) + .field("width", &self.width) + .field("height", &self.height) + .finish() + } +} +impl + Copy> Rect { + pub fn contains(&self, value: (T, T)) -> bool { + value.0 >= self.x + && value.1 >= self.y + && value.0 < self.x + self.width + && value.1 < self.y + self.height + } +} +impl Default for Rect +where + T: Default, +{ + fn default() -> Self { + Self { + x: Default::default(), + y: Default::default(), + width: Default::default(), + height: Default::default(), + } + } +} + +use crate::ring_buffer::RingBuffer; +use std::fmt::Display; + +#[macro_export] +macro_rules! math_ops_impl { + ($T:ty) => { + /* impls for f32 */ + impl std::ops::Add for $T { + type Output = $T; + fn add(self, rhs: f32) -> Self::Output { + Self(self.0 + rhs as f64) + } + } + impl std::ops::Sub for $T { + type Output = $T; + fn sub(self, rhs: f32) -> Self::Output { + Self(self.0 - rhs as f64) + } + } + impl std::ops::Mul for $T { + type Output = $T; + fn mul(self, rhs: f32) -> Self::Output { + Self(self.0 * rhs as f64) + } + } + impl std::ops::Div for $T { + type Output = $T; + fn div(self, rhs: f32) -> Self::Output { + Self(self.0 / rhs as f64) + } + } + impl std::ops::AddAssign for $T { + fn add_assign(&mut self, rhs: f32) { + self.0 += rhs as f64; + } + } + impl std::ops::SubAssign for $T { + fn sub_assign(&mut self, rhs: f32) { + self.0 -= rhs as f64; + } + } + impl std::ops::MulAssign for $T { + fn mul_assign(&mut self, rhs: f32) { + self.0 *= rhs as f64; + } + } + impl std::ops::DivAssign for $T { + fn div_assign(&mut self, rhs: f32) { + self.0 /= rhs as f64; + } + } + + /* impls for f64 */ + impl std::ops::Add for $T { + type Output = $T; + fn add(self, rhs: f64) -> Self::Output { + Self(self.0 + rhs) + } + } + impl std::ops::Sub for $T { + type Output = $T; + fn sub(self, rhs: f64) -> Self::Output { + Self(self.0 - rhs) + } + } + impl std::ops::Mul for $T { + type Output = $T; + fn mul(self, rhs: f64) -> Self::Output { + Self(self.0 * rhs) + } + } + impl std::ops::Div for $T { + type Output = $T; + fn div(self, rhs: f64) -> Self::Output { + Self(self.0 / rhs) + } + } + impl std::ops::AddAssign for $T { + fn add_assign(&mut self, rhs: f64) { + self.0 += rhs; + } + } + impl std::ops::SubAssign for $T { + fn sub_assign(&mut self, rhs: f64) { + self.0 -= rhs; + } + } + impl std::ops::MulAssign for $T { + fn mul_assign(&mut self, rhs: f64) { + self.0 *= rhs; + } + } + impl std::ops::DivAssign for $T { + fn div_assign(&mut self, rhs: f64) { + self.0 /= rhs; + } + } + }; +} + +#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)] +pub struct SampleRate(pub f64); +impl SampleRate { + pub fn as_samples(&self, time: D) -> f64 + where + D: Into, + { + self.0 * time.into().as_secs_f64() + } +} + +#[derive(Default, Copy, Clone, PartialEq, PartialOrd)] +pub struct Ratio(pub f64, pub f64); +impl Ratio { + pub fn multiplier(&self) -> f64 { + self.0 / self.1 + } +} +impl Display for Ratio { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!("{}:{} ({})", self.0, self.1, self.multiplier())) + } +} +impl Debug for Ratio { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!("{}:{} ({})", self.0, self.1, self.multiplier())) + } +} + +pub fn rms(sample_buffer: &RingBuffer, last_rms: f64) -> f64 { + // RMS_0 = sqrt((1/n) * (x0^2 + x1^2 + x2^2 + ... + xN^2)) + // RMS_-1 = sqrt((1/n) * ( x1^2 + x2^2 + ... + xN^2 + x(N+1)^2)) + last_rms.powf(2.0) + + (1.0 / N as f64) * (sample_buffer[0].powf(2.0) - sample_buffer[N - 1].powf(2.0)) +} diff --git a/ebu-dsp/src/meter.rs b/ebu-dsp/src/meter.rs new file mode 100644 index 0000000..e69de29 diff --git a/ebu-dsp/src/ring_buffer.rs b/ebu-dsp/src/ring_buffer.rs new file mode 100644 index 0000000..237ccc7 --- /dev/null +++ b/ebu-dsp/src/ring_buffer.rs @@ -0,0 +1,38 @@ +use std::ops::{Index, IndexMut}; + +pub struct RingBuffer { + buffer: [T; N], + index: usize, +} + +impl RingBuffer +where + T: Copy, +{ + pub const fn new(value: T) -> Self { + Self { + buffer: [value; N], + index: 0, + } + } + pub const fn len(&self) -> usize { + N + } + pub fn shift(&mut self) { + self.index = (self.index + 1) % N; + } + +} +impl Index for RingBuffer { + type Output = T; + + fn index(&self, index: usize) -> &Self::Output { + &self.buffer[(self.index + index) % N] + } +} + +impl IndexMut for RingBuffer { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.buffer[(self.index + index) % N] + } +} diff --git a/ebu-dsp/src/smoother.rs b/ebu-dsp/src/smoother.rs new file mode 100644 index 0000000..fd44406 --- /dev/null +++ b/ebu-dsp/src/smoother.rs @@ -0,0 +1,50 @@ +use std::time::Duration; + +use crate::SampleRate; + +pub struct Smoother { + sample_rate: SampleRate, + attack: Duration, + release: Duration, + last_value: f64, +} +impl Smoother { + pub const fn new(sample_rate: SampleRate) -> Self { + Self { + sample_rate, + attack: Duration::ZERO, + release: Duration::ZERO, + last_value: 0.0, + } + } + pub fn set_attack(&mut self, duration: D) + where + D: Into, + { + self.attack = duration.into(); + } + pub fn attack(&mut self, value: T) -> T + where + T: From, + T: Into, + { + let a = (-2.2 / (self.attack.as_secs_f64() * self.sample_rate.0) + f64::EPSILON).exp(); + self.last_value = a * self.last_value + (1.0 - a) * value.into(); + (self.last_value + f64::EPSILON).into() + } + pub fn set_release(&mut self, duration: D) + where + D: Into, + { + self.release = duration.into(); + } + pub fn release(&mut self, value: T) -> T + where + T: From, + T: Into, + { + let a = (-2.2 / (self.release.as_secs_f64() * self.sample_rate.0) + f64::EPSILON).exp(); + self.last_value = a * self.last_value + (1.0 - a) * value.into(); + (self.last_value + f64::EPSILON).into() + } +} diff --git a/ebu-dsp/src/traits.rs b/ebu-dsp/src/traits.rs new file mode 100644 index 0000000..0bce708 --- /dev/null +++ b/ebu-dsp/src/traits.rs @@ -0,0 +1,68 @@ +use std::{ + ops::{Add, Mul}, + sync::Arc, +}; + +pub trait FloatFormatter { + fn v2s(digits: usize) -> Arc String + Send + Sync> + where + Self: Sized; + fn s2v() -> Arc Option + Send + Sync> + where + Self: Sized; +} +pub trait IntFormatter { + fn v2s() -> Arc String + Send + Sync> + where + Self: Sized; + fn s2v() -> Arc Option + Send + Sync> + where + Self: Sized; +} + +pub trait Processor { + fn process(&mut self, sample: I) -> O; +} + +impl Processor for I +where + I: Processor, +{ + fn process(&mut self, sample: f32) -> f32 { + self.process(sample as f64) as f32 + } +} + +pub trait Lerp { + type Output; + fn lerp(&self, to: Self, t: F) -> Self::Output; + fn lerp_unbounded(&self, to: Self, t: F) -> Self::Output; +} +impl Lerp for T +where + T: Mul, + >::Output: Add, + Self: Copy, +{ + type Output = >::Output; + fn lerp(&self, to: Self, t: f64) -> Self::Output { + self.lerp_unbounded(to, t.clamp(0.0, 1.0)) + } + fn lerp_unbounded(&self, to: Self, t: f64) -> Self::Output { + *self * (1.0 - t) + to * t + } +} +impl Lerp for T +where + T: Mul, + >::Output: Add, + Self: Copy, +{ + type Output = >::Output; + fn lerp(&self, to: Self, t: f32) -> Self::Output { + self.lerp_unbounded(to, t.clamp(0.0, 1.0)) + } + fn lerp_unbounded(&self, to: Self, t: f32) -> Self::Output { + *self * (1.0 - t) + to * t + } +} diff --git a/post_build.rs b/post_build.rs new file mode 100644 index 0000000..df5b7aa --- /dev/null +++ b/post_build.rs @@ -0,0 +1,3 @@ +fn main() { + std::fs::copy("target/debug/libebu_plug_core.so", "target/debug/air_freshener.clap").unwrap(); +} \ No newline at end of file diff --git a/src/editor.rs b/src/editor.rs new file mode 100644 index 0000000..82d66df --- /dev/null +++ b/src/editor.rs @@ -0,0 +1,58 @@ +use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, +}; + +use baseview::WindowHandle; +use crossbeam::atomic::AtomicCell; +use nih_plug::params::persist::PersistentField; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct EditorState { + /// The window's size in logical pixels before applying `scale_factor`. + #[serde(with = "nih_plug::params::persist::serialize_atomic_cell")] + pub size: AtomicCell<(u32, u32)>, + /// Whether the editor's window is currently open. + #[serde(skip)] + pub open: AtomicBool, +} +impl EditorState { + pub fn from_size(size: (u32, u32)) -> Arc { + Arc::new(Self { + size: AtomicCell::new(size), + open: AtomicBool::new(false), + }) + } + pub fn size(&self) -> (u32, u32) { + self.size.load() + } + pub fn is_open(&self) -> bool { + self.open.load(Ordering::Acquire) + } +} + +impl<'a> PersistentField<'a, EditorState> for Arc { + fn set(&self, new_value: EditorState) { + self.size.store(new_value.size.load()); + } + + fn map(&self, f: F) -> R + where + F: Fn(&EditorState) -> R, + { + f(self) + } +} + +pub struct EditorHandle { + pub state: Arc, + pub window: WindowHandle +} +unsafe impl Send for EditorHandle {} +impl Drop for EditorHandle { + fn drop(&mut self) { + self.state.open.store(false, Ordering::Release); + self.window.close(); + } +} \ No newline at end of file diff --git a/src/gui.rs b/src/gui.rs new file mode 100644 index 0000000..cc14fa8 --- /dev/null +++ b/src/gui.rs @@ -0,0 +1,250 @@ +use crate::{parameters::PluginParams, window::EditorWindow}; +use baseview::{Event, EventStatus, MouseButton, MouseEvent, WindowEvent, WindowHandler}; +use ebu_dsp::Rect; +use femtovg::{Canvas, Color, ImageFlags, ImageId, Paint, Path, renderer::OpenGl}; +use nih_plug::prelude::*; +use std::sync::Arc; + +//const DROID_SANS_FONT: &'static [u8] = include_bytes!("../resources/fonts/DroidSans.ttf"); + +const FRESHENER_IMAGE: &'static [u8] = include_bytes!("../assets/AirFreshener/sheet.png"); +const NOT_SO_FRESH_BG_IMAGE: &'static [u8] = include_bytes!("../assets/AirFreshener/bg0.png"); +const FRESH_DUMBLEDORE_BG_IMAGE: &'static [u8] = + include_bytes!("../assets/AirFreshener/bg1.png"); +const FRESHENER_FRAMES: u32 = 256; +const FRESHENER_FRAMES_X: u32 = 20; +const FRESHENER_FRAMES_Y: u32 = 13; +const FRESHENER_FRAME_WIDTH: f32 = 73.0; +const FRESHENER_FRAME_HEIGHT: f32 = 144.0; + +pub struct PluginGui { + // font: FontId, + params: Arc, + canvas: Canvas, + _gui_context: Arc, + scaling_factor: f32, + + freshener_image: ImageId, + not_so_fresh_image: ImageId, + fresh_dumbledore_image: ImageId, + + freshener_bounds: Rect, + + dirty: bool, + mouse_position: (f32, f32), + drag_start_mouse_pos: (f32, f32), + drag_start_parameter_value: f32, + dragging: bool, +} + +impl PluginGui { + pub fn new( + window: &mut baseview::Window<'_>, + gui_context: Arc, + params: Arc, + scaling_factor: f32, + ) -> Self { + let context = window + .gl_context() + .expect("Failed to get window OpenGL context"); + unsafe { + context.make_current(); + } + let renderer = unsafe { OpenGl::new_from_function(|s| context.get_proc_address(s)) } + .expect("Failed to create femtovg renderer"); + let mut canvas = Canvas::new(renderer).expect("Failed to create femtovg canvas"); + let (width, height) = params.editor_state.size(); + + canvas.set_size(width, height, scaling_factor); + let le_fresh = canvas + .load_image_mem(FRESHENER_IMAGE, ImageFlags::empty()) + .expect("Failed to load le fresh"); + let not_so_fresh_image = canvas + .load_image_mem(NOT_SO_FRESH_BG_IMAGE, ImageFlags::empty()) + .expect("Failed to load not so fresh"); + let fresh_dumbledore_image = canvas + .load_image_mem(FRESH_DUMBLEDORE_BG_IMAGE, ImageFlags::empty()) + .expect("Failed to load fresh dumbledore"); + //let font = canvas + // .add_font_mem(DROID_SANS_FONT) + // .expect("Failed to load font"); + + unsafe { + context.make_not_current(); + } + Self { + //font, + params, + canvas, + _gui_context: gui_context, + scaling_factor, + dirty: true, + mouse_position: (0.0, 0.0), + drag_start_mouse_pos: (0.0, 0.0), + drag_start_parameter_value: 0.0, + dragging: false, + freshener_image: le_fresh, + fresh_dumbledore_image, + not_so_fresh_image, + freshener_bounds: Rect { + x: 120.0, + y: 20.0, + width: FRESHENER_FRAME_WIDTH, + height: FRESHENER_FRAME_HEIGHT, + }, + } + } +} + +impl WindowHandler for PluginGui { + fn on_frame(&mut self, window: &mut baseview::Window) { + const WINDOW_WIDTH: f32 = EditorWindow::WINDOW_SIZE.0 as f32; + const WINDOW_HEIGHT: f32 = EditorWindow::WINDOW_SIZE.1 as f32; + + if !self.dirty { + //return; + } + + let context = window + .gl_context() + .expect("Failed to get window OpenGL context"); + unsafe { + context.make_current(); + } + let (width, height) = (self.canvas.width(), self.canvas.height()); + self.canvas.reset(); + self.canvas + .clear_rect(0, 0, width, height, Color::rgbaf(0.0, 0.0, 0.0, 1.0)); + + let mut full_window_path = Path::new(); + full_window_path.rect( + 0.0, + 0.0, + EditorWindow::WINDOW_SIZE.0 as f32, + EditorWindow::WINDOW_SIZE.1 as f32, + ); + + let mut freshener_path = Path::new(); + freshener_path.rect( + self.freshener_bounds.x, + self.freshener_bounds.y, + self.freshener_bounds.width, + self.freshener_bounds.height, + ); + + let bbox = self.canvas.path_bbox(&freshener_path); + + let frame_index = (self.params.freshness.unmodulated_normalized_value() + * (FRESHENER_FRAMES - 1) as f32) + .floor(); + let frame_x = (frame_index % FRESHENER_FRAMES_X as f32).floor(); + let frame_y = (frame_index / FRESHENER_FRAMES_X as f32).floor(); + + self.canvas.fill_path( + &full_window_path, + &Paint::image( + self.not_so_fresh_image, + 0.0, + 0.0, + WINDOW_WIDTH, + WINDOW_HEIGHT, + 0.0, + 1.0, + ), + ); + self.canvas.fill_path( + &full_window_path, + &Paint::image( + self.fresh_dumbledore_image, + 0.0, + 0.0, + WINDOW_WIDTH, + WINDOW_HEIGHT, + 0.0, + self.params.freshness.unmodulated_normalized_value(), + ), + ); + + self.canvas.fill_path( + &freshener_path, + &Paint::image( + self.freshener_image, + bbox.minx - frame_x * FRESHENER_FRAME_WIDTH, + bbox.miny - frame_y * FRESHENER_FRAME_HEIGHT, + self.freshener_bounds.width * FRESHENER_FRAMES_X as f32, + self.freshener_bounds.height * FRESHENER_FRAMES_Y as f32, + 0.0, + 1.0, + ), + ); + /* + let debug = format!("{:#?}", self.params.comp_state.load()); + for (i, line) in debug.split('\n').enumerate() { + self.canvas + .fill_text( + 10.0, + 10.0 + i as f32 * 12.5, + line, + &Paint::color(Color::rgbf(1.0, 1.0, 1.0)) + .with_font(&[self.font]) + .with_font_size(12.5) + .with_text_baseline(femtovg::Baseline::Top), + ) + .expect("Failed to render font"); + } + */ + + self.canvas.flush(); + context.swap_buffers(); + unsafe { + context.make_not_current(); + } + self.dirty = false; + } + + fn on_event( + &mut self, + _window: &mut baseview::Window, + event: baseview::Event, + ) -> baseview::EventStatus { + let setter = ParamSetter::new(self._gui_context.as_ref()); + match event { + Event::Window(WindowEvent::Resized(size)) => { + let phys_size = size.physical_size(); + self.canvas + .set_size(phys_size.width, phys_size.height, self.scaling_factor); + self.dirty = true; + } + Event::Mouse(MouseEvent::CursorMoved { position, .. }) => { + self.mouse_position = (position.x as f32, position.y as f32); + if self.dragging { + let delta = self.mouse_position.1 - self.drag_start_mouse_pos.1; + let new_value = + (self.drag_start_parameter_value - delta * 0.01).clamp(0.0, 1.0); + setter.set_parameter_normalized(&self.params.freshness, new_value); + } + self.dirty = true; + } + Event::Mouse(MouseEvent::ButtonPressed { button, .. }) => { + self.dragging = self.freshener_bounds.contains(self.mouse_position) + && button == MouseButton::Left; + if self.dragging { + setter.begin_set_parameter(&self.params.freshness); + self.drag_start_mouse_pos = self.mouse_position; + self.drag_start_parameter_value = + self.params.freshness.unmodulated_normalized_value(); + } + self.dirty = true; + } + Event::Mouse(MouseEvent::ButtonReleased { .. }) => { + if self.dragging { + setter.end_set_parameter(&self.params.freshness); + } + self.dragging = false; + self.dirty = true; + } + _ => return EventStatus::Ignored, + } + EventStatus::Captured + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..e312614 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,215 @@ +mod editor; +mod gui; +mod parameters; +mod window; + +use ebu_dsp::{ + BiquadFilter, BiquadFilterState, Compressor, CompressorState, Decibel, Equalizer, + EqualizerState, FilterMode, FreqSplitter, FreqSplitterState, Lerp, Processor, Ratio, + SampleRate, +}; +use nih_plug::prelude::*; +use std::{num::NonZero, sync::Arc}; + +use crate::{parameters::PluginParams, window::EditorWindow}; + +struct AirFreshener { + params: Arc, + eqs: Vec, + compressors: Vec, + splitters: Vec, +} + +impl Default for AirFreshener { + fn default() -> Self { + Self { + params: Arc::new(PluginParams::default()), + eqs: Vec::new(), + compressors: Vec::new(), + splitters: Vec::new(), + } + } +} + +impl Plugin for AirFreshener { + const NAME: &'static str = "Air Freshener"; + const VENDOR: &'static str = "Simon Elberich"; + // You can use `env!("CARGO_PKG_HOMEPAGE")` to reference the homepage field from the + // `Cargo.toml` file here + const URL: &'static str = "https://youtu.be/dQw4w9WgXcQ"; + const EMAIL: &'static str = "info@example.com"; + + const VERSION: &'static str = env!("CARGO_PKG_VERSION"); + + // The first audio IO layout is used as the default. The other layouts may be selected either + // explicitly or automatically by the host or the user depending on the plugin API/backend. + const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[ + AudioIOLayout { + main_input_channels: NonZeroU32::new(2), + main_output_channels: NonZeroU32::new(2), + + aux_input_ports: &[], + aux_output_ports: &[], + + // Individual ports and the layout as a whole can be named here. By default these names + // are generated as needed. This layout will be called 'Stereo', while the other one is + // given the name 'Mono' based no the number of input and output channels. + names: PortNames::const_default(), + }, + AudioIOLayout { + main_input_channels: NonZeroU32::new(1), + main_output_channels: NonZeroU32::new(1), + ..AudioIOLayout::const_default() + }, + ]; + + const MIDI_INPUT: MidiConfig = MidiConfig::None; + // Setting this to `true` will tell the wrapper to split the buffer up into smaller blocks + // whenever there are inter-buffer parameter changes. This way no changes to the plugin are + // required to support sample accurate automation and the wrapper handles all of the boring + // stuff like making sure transport and other timing information stays consistent between the + // splits. + const SAMPLE_ACCURATE_AUTOMATION: bool = true; + + // If the plugin can send or receive SysEx messages, it can define a type to wrap around those + // messages here. The type implements the `SysExMessage` trait, which allows conversion to and + // from plain byte buffers. + type SysExMessage = (); + // More advanced plugins can use this to run expensive background tasks. See the field's + // documentation for more information. `()` means that the plugin does not have any background + // tasks. + type BackgroundTask = (); + + fn params(&self) -> Arc { + self.params.clone() + } + + fn editor(&mut self, _async_executor: AsyncExecutor) -> Option> { + Some(Box::new(EditorWindow::new(self.params.clone()))) + } + + // This plugin doesn't need any special initialization, but if you need to do anything expensive + // then this would be the place. State is kept around when the host reconfigures the + // plugin. If we do need special initialization, we could implement the `initialize()` and/or + // `reset()` methods + + fn initialize( + &mut self, + _audio_io_layout: &AudioIOLayout, + buffer_config: &BufferConfig, + _context: &mut impl InitContext, + ) -> bool { + let sample_rate = SampleRate(buffer_config.sample_rate as f64); + for _ in 0.._audio_io_layout + .main_input_channels + .unwrap_or(NonZero::new(2).unwrap()) + .get() + { + self.eqs.push(Equalizer::from_state(EqualizerState { + nodes: [ + Some(BiquadFilter::from_state( + sample_rate, + BiquadFilterState { + gain_db: Decibel::from(8.0), + cutoff: 1900.0, + q: 0.5, + mode: FilterMode::HighShelf, + ..Default::default() + }, + )), + Some(BiquadFilter::from_state( + sample_rate, + BiquadFilterState { + gain_db: Decibel::from(-3.0), + cutoff: 2600.0, + mode: FilterMode::Peak, + ..Default::default() + }, + )), + None, + None, + None, + None, + ], + })); + + self.compressors.push(Compressor::from_state( + sample_rate, + CompressorState { + threshold: Decibel::from(-30.0), + ratio: Ratio(1.0, 0.9), + ..Default::default() + }, + )); + + self.splitters.push(FreqSplitter::from_state( + sample_rate, + FreqSplitterState { + split_frequency: 3050.0, + ..Default::default() + }, + )); + } + true + } + + fn process( + &mut self, + buffer: &mut Buffer, + _aux: &mut AuxiliaryBuffers, + _context: &mut impl ProcessContext, + ) -> ProcessStatus { + _context.set_latency_samples(6); + for mut channel_samples in buffer.iter_samples() { + // Smoothing is optionally built into the parameters themselves + let freshness = self.params.freshness.smoothed.next() as f64; + + /*let frequency = self.params.frequency.smoothed.next() as f64; + let ratio = self.params.ratio.smoothed.next() as f64; + let threshold = self.params.threshold.smoothed.next() as f64; + + for compressor in &mut self.compressors { + compressor.set_ratio(Ratio(1.0, ratio)); + compressor.set_threshold(Decibel::from(threshold)); + } + for splitter in &mut self.splitters { + splitter.state.split_frequency = frequency; + } + for filter in &mut self.filters { + filter.state.cutoff = frequency; + } + */ + + let fresh_lerp_of_bel_air = freshness as f32; + for (channel, sample) in channel_samples.iter_mut().enumerate() { + *sample = sample.lerp(self.eqs[channel].process(*sample), fresh_lerp_of_bel_air); + let (low, high) = self.splitters[channel].process(*sample); + *sample = low + + high.lerp( + self.compressors[channel].process(high), + fresh_lerp_of_bel_air, + ); + } + } + + ProcessStatus::Normal + } + + // This can be used for cleaning up special resources like socket connections whenever the + // plugin is deactivated. Most plugins won't need to do anything here. + fn deactivate(&mut self) {} +} + +impl ClapPlugin for AirFreshener { + const CLAP_ID: &'static str = "com.moist-plugins-gmbh.gain"; + const CLAP_DESCRIPTION: Option<&'static str> = Some("A smoothed gain parameter example plugin"); + const CLAP_MANUAL_URL: Option<&'static str> = Some(Self::URL); + const CLAP_SUPPORT_URL: Option<&'static str> = None; + const CLAP_FEATURES: &'static [ClapFeature] = &[ + ClapFeature::AudioEffect, + ClapFeature::Stereo, + ClapFeature::Utility, + ]; +} + +nih_export_clap!(AirFreshener); diff --git a/src/parameters.rs b/src/parameters.rs new file mode 100644 index 0000000..e4999ae --- /dev/null +++ b/src/parameters.rs @@ -0,0 +1,70 @@ +use crate::{editor::EditorState, window::EditorWindow}; +use nih_plug::{ + formatters, + params::{FloatParam, Params}, + prelude::FloatRange, +}; +use std::sync::Arc; + +/// The [`Params`] derive macro gathers all of the information needed for the wrapper to know about +/// the plugin's parameters, persistent serializable fields, and nested parameter groups. You can +/// also easily implement [`Params`] by hand if you want to, for instance, have multiple instances +/// of a parameters struct for multiple identical oscillators/filters/envelopes. +#[derive(Params)] +pub struct PluginParams { + #[id = "freshness"] + pub freshness: FloatParam, + /* + #[id = "frequency"] + pub frequency: FloatParam, + #[id = "ratio"] + pub ratio: FloatParam, + #[id = "threshold"] + pub threshold: FloatParam, + */ + #[persist = "editor-state"] + pub editor_state: Arc, +} + +impl Default for PluginParams { + fn default() -> Self { + Self { + editor_state: EditorState::from_size(EditorWindow::WINDOW_SIZE), + + freshness: FloatParam::new("Freshness", 0.0, FloatRange::Linear { min: 0.0, max: 1.0 }) + .with_value_to_string(formatters::v2s_f32_percentage(2)) + .with_string_to_value(formatters::s2v_f32_percentage()), + /* + frequency: FloatParam::new( + "Frequency", + 2000.0, + FloatRange::Skewed { + min: 10.0, + max: 22050.0, + factor: FloatRange::skew_factor(-1.5), + }, + ) + .with_value_to_string(formatters::v2s_f32_hz_then_khz(2)) + .with_string_to_value(formatters::s2v_f32_hz_then_khz()), + ratio: FloatParam::new( + "Ratio", + 0.707, + FloatRange::Skewed { + min: 0.01, + max: 20.0, + factor: FloatRange::skew_factor(-1.5), + }, + ), + threshold: FloatParam::new( + "Threshold", + 0.0, + FloatRange::Linear { + min: -60.0, + max: 12.0, + }, + ) + .with_unit(" dB"), + */ + } + } +} diff --git a/src/window.rs b/src/window.rs new file mode 100644 index 0000000..4e5cb82 --- /dev/null +++ b/src/window.rs @@ -0,0 +1,76 @@ +use std::sync::{Arc, atomic::Ordering}; +use baseview::{Window, WindowOpenOptions, WindowScalePolicy, gl::GlConfig}; +use crossbeam::atomic::AtomicCell; +use nih_plug::{editor::Editor, plugin::Plugin}; +use crate::{AirFreshener, editor::EditorHandle, parameters::PluginParams, gui::PluginGui}; + +pub struct EditorWindow { + params: Arc, + scaling_factor: AtomicCell>, +} + +impl EditorWindow { + pub const WINDOW_SIZE: (u32, u32) = (230, 320); + pub fn new(params: Arc) -> Self { + Self { + params, + #[cfg(target_os = "macos")] + scaling_factor: AtomicCell::new(None), + #[cfg(not(target_os = "macos"))] + scaling_factor: AtomicCell::new(Some(1.0)), + } + } +} + +impl Editor for EditorWindow { + fn spawn( + &self, + parent: nih_plug::prelude::ParentWindowHandle, + context: Arc, + ) -> Box { + let (unscaled_width, unscaled_height) = self.params.editor_state.size(); + let scaling_factor = self.scaling_factor.load(); + let gui_context = context.clone(); + let params = self.params.clone(); + let window = Window::open_parented(&parent, WindowOpenOptions { + title: AirFreshener::NAME.to_owned(), + size: baseview::Size { width: unscaled_width as f64, height: unscaled_height as f64 }, + scale: scaling_factor.map(|factor| WindowScalePolicy::ScaleFactor(factor as f64)).unwrap_or(WindowScalePolicy::SystemScaleFactor), + gl_config: Some(GlConfig { + version: (3, 2), + red_bits: 8, + green_bits: 8, + blue_bits: 8, + alpha_bits: 8, + samples: None, + srgb: true, + double_buffer: true, + vsync: false, + ..Default::default() + }) + }, move |window: &mut baseview::Window<'_>| -> PluginGui { + PluginGui::new(window, gui_context, params, scaling_factor.unwrap_or(1.0)) + }); + self.params.editor_state.open.store(true, Ordering::Release); + Box::new(EditorHandle { + state: self.params.editor_state.clone(), + window + }) + } + + fn size(&self) -> (u32, u32) { + self.params.editor_state.size() + } + + fn set_scale_factor(&self, factor: f32) -> bool { + if self.params.editor_state.is_open() { + return false; + } + self.scaling_factor.store(Some(factor)); + true + } + + fn param_value_changed(&self, _id: &str, _normalized_value: f32) {} + fn param_modulation_changed(&self, _id: &str, _modulation_offset: f32) {} + fn param_values_changed(&self) {} +}