Molyxide

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit 8629085d0ec9ec2ea231f14608265b52d996ec36
parent 799027d1080c0796c96d0451f4f90d59d64cb781
Author: minerva-jupiter <ryouturn@gmail.com>
Date:   Thu, 30 Apr 2026 07:06:18 +0900

feat(synth): Implement initial polyphonic synthesizer

Diffstat:
ACargo.lock | 1127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MCargo.toml | 7++++---
MREADME.md | 2+-
Msrc/lib.rs | 895+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
4 files changed, 1939 insertions(+), 92 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -0,0 +1,1127 @@ +# 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.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "anymap3" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170433209e817da6aae2c51aa0dd443009a613425dd041ebfb2492d1c4c11a25" + +[[package]] +name = "assert_no_alloc" +version = "1.1.2" +source = "git+https://github.com/robbert-vdh/rust-assert-no-alloc.git?branch=feature%2Fnested-permit-forbid#a6fb4f62b9624715291e320ea5f0f70e73b035cf" +dependencies = [ + "backtrace", + "log", +] + +[[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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e4227379beff4205943696e6c3e0cd809bacdf3f0edd6e3dd153e2269571a4" + +[[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 = "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 = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "clap-sys" +version = "0.5.0" +source = "git+https://github.com/micahrj/clap-sys.git?rev=25d7f53fdb6363ad63fbd80049cb7a42a97ac156#25d7f53fdb6363ad63fbd80049cb7a42a97ac156" + +[[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 = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core", + "wasip2", + "wasip3", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "goblin" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6b4de4a8eb6c46a8c77e1d3be942cb9a8bf073c22374578e5ba4b08ed0ff68" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[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.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[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.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[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", +] + +[[package]] +name = "molyxide" +version = "0.1.0" +dependencies = [ + "nih_plug", + "rand", +] + +[[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.git#28b149ec4d62757d0b448809148a0c3ca6e09a95" +dependencies = [ + "anyhow", + "anymap3", + "assert_no_alloc", + "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.git#28b149ec4d62757d0b448809148a0c3ca6e09a95" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "nih_plug_xtask" +version = "0.1.0" +source = "git+https://github.com/robbert-vdh/nih-plug.git#28b149ec4d62757d0b448809148a0c3ca6e09a95" +dependencies = [ + "anyhow", + "cargo_metadata", + "goblin", + "reflink", + "serde", + "toml", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[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 = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[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.11.1", +] + +[[package]] +name = "reflink" +version = "0.1.3" +source = "git+https://github.com/nicokoch/reflink.git?rev=e8d93b465f5d9ad340cd052b64bbc77b8ee107e2#e8d93b465f5d9ad340cd052b64bbc77b8ee107e2" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scroll" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[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.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[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.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +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 = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[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 = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "xtask" +version = "0.1.0" +dependencies = [ + "nih_plug_xtask", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "Molyxide" +name = "molyxide" version = "0.1.0" edition = "2021" authors = ["Minerva_Juppiter <contact@minervajuppiter.net>"] @@ -16,10 +16,11 @@ crate-type = ["cdylib"] [dependencies] # Remove the `assert_process_allocs` feature to allow allocations on the audio # thread in debug builds. -nih_plug = { git = "https://github.com/robbert-vdh/nih-plug.git", features = ["assert_process_allocs"] } +# nih_plug = { git = "https://github.com/robbert-vdh/nih-plug.git", features = ["assert_process_allocs"] } # Uncomment the below line to disable the on-by-default VST3 feature to remove # the GPL compatibility requirement -# nih_plug = { git = "https://github.com/robbert-vdh/nih-plug.git", default-features = false, features = ["assert_process_allocs"] } +nih_plug = { git = "https://github.com/robbert-vdh/nih-plug.git", default-features = false, features = ["assert_process_allocs"] } +rand = "0.10.1" [profile.release] lto = "thin" diff --git a/README.md b/README.md @@ -5,5 +5,5 @@ After installing [Rust](https://rustup.rs/), you can compile Molyxide as follows: ```shell -cargo xtask bundle Molyxide --release +cargo xtask bundle molyxide --release ``` diff --git a/src/lib.rs b/src/lib.rs @@ -1,58 +1,690 @@ use nih_plug::prelude::*; use std::sync::Arc; -// This is a shortened version of the gain example with most comments removed, check out -// https://github.com/robbert-vdh/nih-plug/blob/master/plugins/examples/gain/src/lib.rs to get -// started +// --- Enums --- -struct Molyxide { - params: Arc<MolyxideParams>, +#[derive(Enum, PartialEq, Clone, Copy)] +pub enum WaveformSelection { + #[name = "Sawtooth"] + Saw, + #[name = "Square"] + Square, + #[name = "Saw + Square"] + Both, +} + +#[derive(Enum, PartialEq, Clone, Copy)] +pub enum ResonatorMode { + #[name = "Low Pass"] + LowPass, + #[name = "Band Pass"] + BandPass, + #[name = "High Pass"] + HighPass, +} + +#[derive(Enum, PartialEq, Clone, Copy)] +pub enum FilterAlgorithm { + #[name = "Biquad (Standard)"] + Biquad, + #[name = "ZDF SVF (Analog-style)"] + ZdfSvf, +} + +#[derive(Enum, PartialEq, Clone, Copy)] +pub enum FinalDecayMode { + #[name = "Lock (No Release)"] + Lock, + #[name = "Manual (Release On)"] + Manual, +} + +// --- DSP Modules --- + +/// ZDF SVF (Topology Preserving Transform) +struct ZdfSvf { + s1: f32, + s2: f32, +} + +impl ZdfSvf { + fn new() -> Self { + Self { s1: 0.0, s2: 0.0 } + } + fn process(&mut self, input: f32, mode: ResonatorMode, freq: f32, emph: f32, sr: f32) -> f32 { + let q = (0.5 + emph * 9.5).max(0.01); + let g = (std::f32::consts::PI * freq / sr).tan(); + let k = 1.0 / q; + let a1 = 1.0 / (1.0 + g * (g + k)); + let a2 = g * a1; + let a3 = g * a2; + let m_v3 = input - self.s2; + let v1 = a1 * self.s1 + a2 * m_v3; + let v2 = self.s2 + a2 * self.s1 + a3 * m_v3; + self.s1 = 2.0 * v1 - self.s1; + self.s2 = 2.0 * v2 - self.s2; + match mode { + ResonatorMode::LowPass => v2, + ResonatorMode::BandPass => v1, + ResonatorMode::HighPass => input - k * v1 - v2, + } + } +} + +/// ZDF Ladder Filter (24dB/oct) +/// Moog ă‚čă‚żă‚€ăƒ«ăź 4-pole ăƒ­ăƒŒăƒ‘ă‚čăƒ•ă‚Łăƒ«ă‚żăƒŒ +struct LadderFilter { + s: [f32; 4], +} + +impl LadderFilter { + fn new() -> Self { + Self { s: [0.0; 4] } + } + fn process(&mut self, input: f32, cutoff: f32, resonance: f32, sr: f32) -> f32 { + let g = (std::f32::consts::PI * cutoff / sr).tan(); + let k = 4.0 * resonance.clamp(0.0, 0.99); // è‡ȘćŠ±æŒŻă‚’éżă‘ă‚‹ăŸă‚ćŸźèȘżæ•Ž + + // 1-pole äż‚æ•° + let a = 1.0 / (1.0 + g); + + // ăƒ•ă‚ŁăƒŒăƒ‰ăƒăƒƒă‚ŻèŁœæ­Ł + let g_pow4 = g * g * g * g; + let sigma = g_pow4 * a * a * a * a; + let gamma = 1.0 / (1.0 + k * sigma); + + // ć†…éƒšçŠ¶æ…‹ă‹ă‚‰ăźăƒ•ă‚ŁăƒŒăƒ‰ăƒăƒƒă‚Ż + let input_fb = (input + - k * (self.s[0] * a + + self.s[1] * a * a + + self.s[2] * a * a * a + + self.s[3] * a * a * a * a)) + * gamma; + + // 4æź”ăź 1-pole ăƒ•ă‚Łăƒ«ă‚żăƒŒăźç›Žćˆ—ć‡Šç† + let mut vin = input_fb; + for i in 0..4 { + let v = (vin - self.s[i]) * g * a; + let out = v + self.s[i]; + self.s[i] = out + v; + vin = out; + } + + vin // 4æź”ç›źăźć‡ș抛 + } +} + +// --- Parameters --- + +#[derive(Params)] +pub struct MixerParams { + #[id = "m_tune"] + pub master_tune: FloatParam, + #[id = "mix_dir"] + pub direct: FloatParam, + #[id = "mix_mode"] + pub mode: FloatParam, + #[id = "mix_res"] + pub resonator: FloatParam, + #[id = "mix_vcf"] + pub vcf: FloatParam, +} + +#[derive(Params)] +pub struct OscParams { + #[id = "l_wave"] + pub lower_waveshape: EnumParam<WaveformSelection>, + #[id = "u_wave"] + pub upper_waveshape: EnumParam<WaveformSelection>, + #[id = "l_pw"] + pub lower_pw: FloatParam, + #[id = "u_pw"] + pub upper_pw: FloatParam, + #[id = "l_mix"] + pub lower_mix: FloatParam, + #[id = "u_mix"] + pub upper_mix: FloatParam, + #[id = "rank_a_tune"] + pub rank_a_tune: FloatParam, + #[id = "rank_b_beat"] + pub rank_b_beat: FloatParam, + #[id = "pwm_rate"] + pub pwm_rate: FloatParam, + #[id = "pwm_amt"] + pub pwm_amt: FloatParam, + #[id = "fm_rate"] + pub fm_rate: FloatParam, + #[id = "fm_amt"] + pub fm_amt: FloatParam, +} + +#[derive(Params)] +pub struct ResonatorParams { + #[id = "res_on"] + pub enabled: BoolParam, + #[id = "res_mode"] + pub mode: EnumParam<ResonatorMode>, + #[id = "res_l_cf"] + pub low_cf: FloatParam, + #[id = "res_l_emph"] + pub low_emph: FloatParam, + #[id = "res_l_gain"] + pub low_gain: FloatParam, + #[id = "res_m_cf"] + pub mid_cf: FloatParam, + #[id = "res_m_emph"] + pub mid_emph: FloatParam, + #[id = "res_m_gain"] + pub mid_gain: FloatParam, + #[id = "res_h_cf"] + pub high_cf: FloatParam, + #[id = "res_h_emph"] + pub high_emph: FloatParam, + #[id = "res_h_gain"] + pub high_gain: FloatParam, +} + +#[derive(Params)] +pub struct VcfParams { + #[id = "vcf_on"] + pub enabled: BoolParam, + #[id = "vcf_cutoff"] + pub cutoff: FloatParam, + #[id = "vcf_emph"] + pub emphasis: FloatParam, + #[id = "vcf_kb"] + pub kb_track: FloatParam, + #[id = "vcf_glide"] + pub kb_glide: FloatParam, + #[id = "vcf_lfo_r"] + pub lfo_rate: FloatParam, + #[id = "vcf_lfo_a"] + pub lfo_amt: FloatParam, + #[id = "vcf_lfo_sh"] + pub lfo_sh: FloatParam, + #[id = "vcf_env_a"] + pub attack: FloatParam, + #[id = "vcf_env_d"] + pub decay: FloatParam, + #[id = "vcf_env_s"] + pub sustain: FloatParam, + #[id = "vcf_env_amt"] + pub env_amt: FloatParam, +} + +#[derive(Params)] +pub struct AmpParams { + #[id = "amp_a"] + pub attack: FloatParam, + #[id = "amp_s"] + pub sustain: FloatParam, + #[id = "amp_dr"] + pub decay_release: FloatParam, + #[id = "amp_vel"] + pub velocity: FloatParam, + #[id = "amp_f_dec"] + pub final_decay: EnumParam<FinalDecayMode>, } #[derive(Params)] struct MolyxideParams { - /// The parameter's ID is used to identify the parameter in the wrappred plugin API. As long as - /// these IDs remain constant, you can rename and reorder these fields as you wish. The - /// parameters are exposed to the host in the same order they were defined. In this case, this - /// gain parameter is stored as linear gain while the values are displayed in decibels. + #[nested(group = "1. Mixer & Tune")] + pub mixer: MixerParams, + #[nested(group = "2. Oscillators")] + pub osc: OscParams, + #[nested(group = "3. Resonator")] + pub res: ResonatorParams, + #[nested(group = "4. VCF (Master Filter)")] + pub vcf: VcfParams, + #[nested(group = "5. Loudness (Per-Voice)")] + pub amp: AmpParams, #[id = "gain"] pub gain: FloatParam, } -impl Default for Molyxide { +impl Default for MolyxideParams { fn default() -> Self { Self { - params: Arc::new(MolyxideParams::default()), + mixer: MixerParams { + master_tune: FloatParam::new( + "Master Tune", + 0.0, + FloatRange::Linear { + min: -200.0, + max: 200.0, + }, + ), + direct: FloatParam::new( + "Direct Level", + 0.5, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ), + mode: FloatParam::new("Mode Level", 0.0, FloatRange::Linear { min: 0.0, max: 1.0 }), + resonator: FloatParam::new( + "Resonator Level", + 0.5, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ), + vcf: FloatParam::new("VCF Level", 0.5, FloatRange::Linear { min: 0.0, max: 1.0 }), + }, + osc: OscParams { + lower_waveshape: EnumParam::new("Lower Waveshape", WaveformSelection::Saw), + upper_waveshape: EnumParam::new("Upper Waveshape", WaveformSelection::Square), + lower_pw: FloatParam::new( + "Lower PW", + 0.5, + FloatRange::Linear { + min: 0.05, + max: 0.5, + }, + ), + upper_pw: FloatParam::new( + "Upper PW", + 0.2, + FloatRange::Linear { + min: 0.05, + max: 0.5, + }, + ), + lower_mix: FloatParam::new( + "Lower Rank Mix", + 0.5, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ), + upper_mix: FloatParam::new( + "Upper Rank Mix", + 0.5, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ), + rank_a_tune: FloatParam::new( + "Rank A Tune", + 0.0, + FloatRange::Linear { + min: -600.0, + max: 600.0, + }, + ), + rank_b_beat: FloatParam::new( + "Rank B Beat", + 0.0, + FloatRange::Linear { + min: -50.0, + max: 50.0, + }, + ), + pwm_rate: FloatParam::new( + "PWM Rate", + 0.5, + FloatRange::Skewed { + min: 0.05, + max: 25.0, + factor: FloatRange::skew_factor(-0.5), + }, + ), + pwm_amt: FloatParam::new( + "PWM Amount", + 0.0, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ), + fm_rate: FloatParam::new( + "FM Rate", + 6.0, + FloatRange::Skewed { + min: 0.05, + max: 25.0, + factor: FloatRange::skew_factor(-0.5), + }, + ), + fm_amt: FloatParam::new( + "FM Amount", + 0.0, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ), + }, + res: ResonatorParams { + enabled: BoolParam::new("Resonator Enabled", true), + mode: EnumParam::new("Pass Mode", ResonatorMode::LowPass), + low_cf: FloatParam::new( + "Low CF", + 200.0, + FloatRange::Skewed { + min: 20.0, + max: 20000.0, + factor: FloatRange::skew_factor(-0.9), + }, + ), + low_emph: FloatParam::new( + "Low Emphasis", + 0.0, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ), + low_gain: FloatParam::new( + "Low Gain", + 0.5, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ), + mid_cf: FloatParam::new( + "Mid CF", + 1000.0, + FloatRange::Skewed { + min: 20.0, + max: 20000.0, + factor: FloatRange::skew_factor(-0.9), + }, + ), + mid_emph: FloatParam::new( + "Mid Emphasis", + 0.0, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ), + mid_gain: FloatParam::new( + "Mid Gain", + 0.5, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ), + high_cf: FloatParam::new( + "High CF", + 5000.0, + FloatRange::Skewed { + min: 20.0, + max: 20000.0, + factor: FloatRange::skew_factor(-0.9), + }, + ), + high_emph: FloatParam::new( + "High Emphasis", + 0.0, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ), + high_gain: FloatParam::new( + "High Gain", + 0.5, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ), + }, + vcf: VcfParams { + enabled: BoolParam::new("VCF Enabled", true), + cutoff: FloatParam::new( + "VCF Cutoff", + 20000.0, + FloatRange::Skewed { + min: 20.0, + max: 20000.0, + factor: FloatRange::skew_factor(-0.9), + }, + ), + emphasis: FloatParam::new( + "VCF Emphasis", + 0.0, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ), + kb_track: FloatParam::new( + "Keyboard Track", + 1.0, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ), + kb_glide: FloatParam::new( + "Keyboard Glide", + 0.0, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ), + lfo_rate: FloatParam::new( + "VCF LFO Rate", + 6.0, + FloatRange::Skewed { + min: 0.05, + max: 25.0, + factor: FloatRange::skew_factor(-0.5), + }, + ), + lfo_amt: FloatParam::new( + "VCF LFO Amount", + 0.0, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ), + lfo_sh: FloatParam::new( + "VCF S&H Amount", + 0.0, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ), + attack: FloatParam::new( + "VCF Attack", + 0.01, + FloatRange::Skewed { + min: 0.01, + max: 10.0, + factor: FloatRange::skew_factor(-0.8), + }, + ), + decay: FloatParam::new( + "VCF Decay", + 0.1, + FloatRange::Skewed { + min: 0.01, + max: 10.0, + factor: FloatRange::skew_factor(-0.8), + }, + ), + sustain: FloatParam::new( + "VCF Sustain", + 1.0, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ), + env_amt: FloatParam::new( + "VCF Envelope Amount", + 0.0, + FloatRange::Linear { + min: -1.0, + max: 1.0, + }, + ), + }, + amp: AmpParams { + attack: FloatParam::new( + "Amp Attack", + 0.01, + FloatRange::Skewed { + min: 0.01, + max: 10.0, + factor: FloatRange::skew_factor(-0.8), + }, + ), + sustain: FloatParam::new( + "Amp Sustain", + 1.0, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ), + decay_release: FloatParam::new( + "Amp Decay/Release", + 0.1, + FloatRange::Skewed { + min: 0.01, + max: 10.0, + factor: FloatRange::skew_factor(-0.8), + }, + ), + velocity: FloatParam::new( + "Keyboard Dynamics", + 0.5, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ), + final_decay: EnumParam::new("Final Decay", FinalDecayMode::Manual), + }, + gain: FloatParam::new( + "Gain", + util::db_to_gain(0.0), + FloatRange::Linear { min: 0.0, max: 1.0 }, + ), } } } -impl Default for MolyxideParams { - fn default() -> Self { +// --- TOG (Top Octave Generator) --- + +struct TopOctaveGenerator { + phases: [f64; 12], + wrap_counts: [u64; 12], +} + +impl TopOctaveGenerator { + fn new() -> Self { Self { - // This gain is stored as linear gain. NIH-plug comes with useful conversion functions - // to treat these kinds of parameters as if we were dealing with decibels. Storing this - // as decibels is easier to work with, but requires a conversion for every sample. - gain: FloatParam::new( - "Gain", - util::db_to_gain(0.0), - FloatRange::Skewed { - min: util::db_to_gain(-30.0), - max: util::db_to_gain(30.0), - // This makes the range appear as if it was linear when displaying the values as - // decibels - factor: FloatRange::gain_skew_factor(-30.0, 30.0), - }, + phases: [0.0; 12], + wrap_counts: [0; 12], + } + } + fn process(&mut self, sample_rate: f32, frequencies: &[f32; 12]) { + for i in 0..12 { + self.phases[i] += frequencies[i] as f64 / sample_rate as f64; + if self.phases[i] >= 1.0 { + self.phases[i] -= 1.0; + self.wrap_counts[i] = self.wrap_counts[i].wrapping_add(1); + } + } + } +} + +// --- Poly Chip --- + +const NUM_CHIPS: usize = 71; +const BASE_NOTE: u8 = 29; // F1 + +struct PolyChip { + note: u8, + tog_index: usize, + division: f64, + gate: bool, + velocity: f32, + samples_since_on: u32, + samples_since_off: u32, + release_start_level: f32, +} + +impl PolyChip { + fn new(note: u8) -> Self { + let tog_index = (note % 12) as usize; + let division = 2.0f64.powi((108 + (tog_index as i32) - (note as i32)) / 12); + Self { + note, + tog_index, + division, + gate: false, + velocity: 0.0, + samples_since_on: 0, + samples_since_off: 0, + release_start_level: 0.0, + } + } + fn get_envelope(&self, params: &AmpParams, sr: f32) -> f32 { + let a = params.attack.value(); + let dr = params.decay_release.value(); + let s = params.sustain.value(); + if self.gate { + let t = self.samples_since_on as f32 / sr; + if t < a { + t / a.max(0.0001) + } else { + 1.0 - ((t - a) / dr.max(0.0001)).clamp(0.0, 1.0) * (1.0 - s) + } + } else { + let t = self.samples_since_off as f32 / sr; + if params.final_decay.value() == FinalDecayMode::Manual { + self.release_start_level * (1.0 - (t / dr.max(0.0001)).clamp(0.0, 1.0)) + } else { + 0.0 + } + } + } + fn process( + &mut self, + t_a: f64, + w_a: u64, + t_b: f64, + w_b: u64, + params: &MolyxideParams, + sr: f32, + pwm: f32, + ) -> f32 { + let env = self.get_envelope(&params.amp, sr); + if !self.gate && env <= 0.0 { + return 0.0; + } + let (ws, base_pw, mix) = if self.note < 49 { + ( + params.osc.lower_waveshape.value(), + params.osc.lower_pw.value(), + params.osc.lower_mix.value(), ) - // Because the gain parameter is stored as linear gain instead of storing the value as - // decibels, we need logarithmic smoothing - .with_smoother(SmoothingStyle::Logarithmic(50.0)) - .with_unit(" dB") - // There are many predefined formatters we can use here. If the gain was stored as - // decibels instead of as a linear gain value, we could have also used the - // `.with_step_size(0.1)` function to get internal rounding. - .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) - .with_string_to_value(formatters::s2v_f32_gain_to_db()), + } else { + ( + params.osc.upper_waveshape.value(), + params.osc.upper_pw.value(), + params.osc.upper_mix.value(), + ) + }; + let p_a = ((w_a as f64 + t_a) / self.division).fract(); + let sig_a = (p_a * 2.0 - 1.0) as f32; + let p_b = ((w_b as f64 + t_b) / self.division).fract(); + let sig_b = if p_b + < (base_pw as f64 + pwm as f64 * params.osc.pwm_amt.value() as f64 * 0.4) + .clamp(0.05, 0.95) + { + 1.0 + } else { + -1.0 + }; + let raw = match ws { + WaveformSelection::Saw => sig_a, + WaveformSelection::Square => sig_b, + WaveformSelection::Both => sig_a * mix + sig_b * (1.0 - mix), + }; + if self.gate { + self.samples_since_on = self.samples_since_on.wrapping_add(1); + } else { + self.samples_since_off = self.samples_since_off.wrapping_add(1); + } + raw * env * (1.0 - (1.0 - self.velocity) * params.amp.velocity.value()) + } +} + +// --- Main Plugin --- + +struct Molyxide { + params: Arc<MolyxideParams>, + sample_rate: f32, + tog_a: TopOctaveGenerator, + tog_b: TopOctaveGenerator, + chips: Vec<PolyChip>, + fm_phase: f32, + pwm_phase: f32, + res_low: ZdfSvf, + res_mid: ZdfSvf, + res_high: ZdfSvf, + vcf: LadderFilter, + vcf_samples_on: u32, + vcf_last_note_id: u8, +} + +impl Default for Molyxide { + fn default() -> Self { + let mut chips = Vec::with_capacity(NUM_CHIPS); + for i in 0..NUM_CHIPS { + chips.push(PolyChip::new(BASE_NOTE + i as u8)); + } + Self { + params: Arc::new(MolyxideParams::default()), + sample_rate: 44100.0, + tog_a: TopOctaveGenerator::new(), + tog_b: TopOctaveGenerator::new(), + chips, + fm_phase: 0.0, + pwm_phase: 0.0, + res_low: ZdfSvf::new(), + res_mid: ZdfSvf::new(), + res_high: ZdfSvf::new(), + vcf: LadderFilter::new(), + vcf_samples_on: 0, + vcf_last_note_id: 0, } } } @@ -62,96 +694,183 @@ impl Plugin for Molyxide { const VENDOR: &'static str = "Minerva_Juppiter"; const URL: &'static str = env!("CARGO_PKG_HOMEPAGE"); const EMAIL: &'static str = "contact@minervajuppiter.net"; - 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 a layout with - // only one input and output channel would be called 'Mono'. names: PortNames::const_default(), }]; - - - const MIDI_INPUT: MidiConfig = MidiConfig::None; + const MIDI_INPUT: MidiConfig = MidiConfig::Basic; const MIDI_OUTPUT: MidiConfig = MidiConfig::None; - 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<dyn Params> { self.params.clone() } - fn initialize( &mut self, - _audio_io_layout: &AudioIOLayout, - _buffer_config: &BufferConfig, - _context: &mut impl InitContext<Self>, + _: &AudioIOLayout, + bc: &BufferConfig, + _: &mut impl InitContext<Self>, ) -> bool { - // Resize buffers and perform other potentially expensive initialization operations here. - // The `reset()` function is always called right after this function. You can remove this - // function if you do not need it. + self.sample_rate = bc.sample_rate; true } - - fn reset(&mut self) { - // Reset buffers and envelopes here. This can be called from the audio thread and may not - // allocate. You can remove this function if you do not need it. - } - fn process( &mut self, buffer: &mut Buffer, - _aux: &mut AuxiliaryBuffers, - _context: &mut impl ProcessContext<Self>, + _: &mut AuxiliaryBuffers, + context: &mut impl ProcessContext<Self>, ) -> ProcessStatus { - for channel_samples in buffer.iter_samples() { - // Smoothing is optionally built into the parameters themselves - let gain = self.params.gain.smoothed.next(); + let mut next_event = context.next_event(); + let sr = self.sample_rate; + for (sample_id, channel_samples) in buffer.iter_samples().enumerate() { + while let Some(event) = next_event { + if event.timing() > sample_id as u32 { + break; + } + match event { + NoteEvent::NoteOn { note, velocity, .. } => { + if let Some(c) = self.chips.iter_mut().find(|c| c.note == note) { + c.gate = true; + c.velocity = velocity; + c.samples_since_on = 0; + c.samples_since_off = 0; + // VCF Contour ăƒȘă‚»ăƒƒăƒˆ + self.vcf_samples_on = 0; + self.vcf_last_note_id = note; + } + } + NoteEvent::NoteOff { note, .. } => { + if let Some(c) = self.chips.iter_mut().find(|c| c.note == note) { + if c.gate { + c.gate = false; + c.samples_since_off = 0; + c.release_start_level = c.get_envelope(&self.params.amp, sr); + } + } + } + _ => (), + } + next_event = context.next_event(); + } + self.fm_phase += self.params.osc.fm_rate.value() / sr; + if self.fm_phase >= 1.0 { + self.fm_phase -= 1.0; + } + let fm_lfo = (self.fm_phase * 2.0 * std::f32::consts::PI).sin(); + self.pwm_phase += self.params.osc.pwm_rate.value() / sr; + if self.pwm_phase >= 1.0 { + self.pwm_phase -= 1.0; + } + let pwm_lfo = (self.pwm_phase * 2.0 * std::f32::consts::PI).sin(); + + let tune = self.params.mixer.master_tune.value() + + fm_lfo * self.params.osc.fm_amt.value() * 50.0; + let mut freqs_a = [0.0f32; 12]; + let mut freqs_b = [0.0f32; 12]; + for i in 0..12 { + let base = util::midi_note_to_freq(108 + i as u8); + freqs_a[i] = base * 2.0f32.powf(tune / 1200.0); + freqs_b[i] = freqs_a[i] * 2.0f32.powf(self.params.osc.rank_b_beat.value() / 1200.0); + } + self.tog_a.process(sr, &freqs_a); + self.tog_b.process(sr, &freqs_b); + + let mut mixed = 0.0; + for c in &mut self.chips { + mixed += c.process( + self.tog_a.phases[c.tog_index], + self.tog_a.wrap_counts[c.tog_index], + self.tog_b.phases[c.tog_index], + self.tog_b.wrap_counts[c.tog_index], + &self.params, + sr, + pwm_lfo, + ); + } + + // Resonator Path + let mut res_out = 0.0; + if self.params.res.enabled.value() { + let mode = self.params.res.mode.value(); + res_out = self.res_low.process( + mixed, + mode, + self.params.res.low_cf.value(), + self.params.res.low_emph.value(), + sr, + ) * self.params.res.low_gain.value() + + self.res_mid.process( + mixed, + mode, + self.params.res.mid_cf.value(), + self.params.res.mid_emph.value(), + sr, + ) * self.params.res.mid_gain.value() + + self.res_high.process( + mixed, + mode, + self.params.res.high_cf.value(), + self.params.res.high_emph.value(), + sr, + ) * self.params.res.high_gain.value(); + } + + // VCF Path + let mut vcf_out = 0.0; + if self.params.vcf.enabled.value() { + // VCF Envelope (Contour) + let t = self.vcf_samples_on as f32 / sr; + let v_a = self.params.vcf.attack.value(); + let v_d = self.params.vcf.decay.value(); + let v_s = self.params.vcf.sustain.value(); + let v_env = if t < v_a { + t / v_a.max(0.0001) + } else { + 1.0 - ((t - v_a) / v_d.max(0.0001)).clamp(0.0, 1.0) * (1.0 - v_s) + }; + + // Cutoff calculation with Envelope and Keyboard Tracking + let kb_offset = + (self.vcf_last_note_id as f32 - 60.0) / 12.0 * self.params.vcf.kb_track.value(); + let cutoff = self.params.vcf.cutoff.value() + * 2.0f32.powf(kb_offset + v_env * self.params.vcf.env_amt.value() * 4.0); + + vcf_out = self.vcf.process( + mixed, + cutoff.clamp(20.0, 20000.0), + self.params.vcf.emphasis.value(), + sr, + ); + self.vcf_samples_on = self.vcf_samples_on.wrapping_add(1); + } + + let direct = mixed * self.params.mixer.direct.value(); + let res_path = res_out * self.params.mixer.resonator.value(); + let vcf_path = vcf_out * self.params.mixer.vcf.value(); + + let output = (direct + res_path + vcf_path) * 0.02 * self.params.gain.value(); for sample in channel_samples { - *sample *= gain; + *sample = output; } } - ProcessStatus::Normal } + fn reset(&mut self) {} } impl ClapPlugin for Molyxide { const CLAP_ID: &'static str = "net.minervajuppiter.Molyxide"; - const CLAP_DESCRIPTION: Option<&'static str> = Some("Simple Polyphonic Synthesizer"); + const CLAP_DESCRIPTION: Option<&'static str> = Some("A Simple Polyphonic Synthesizer"); const CLAP_MANUAL_URL: Option<&'static str> = Some(Self::URL); const CLAP_SUPPORT_URL: Option<&'static str> = None; - - // Don't forget to change these features - const CLAP_FEATURES: &'static [ClapFeature] = &[ClapFeature::AudioEffect, ClapFeature::Stereo]; -} - -impl Vst3Plugin for Molyxide { - const VST3_CLASS_ID: [u8; 16] = *b"Exactly16Chars!!"; - - // And also don't forget to change these categories - const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = - &[Vst3SubCategory::Fx, Vst3SubCategory::Dynamics]; + const CLAP_FEATURES: &'static [ClapFeature] = &[ClapFeature::Synthesizer, ClapFeature::Stereo]; } nih_export_clap!(Molyxide); -nih_export_vst3!(Molyxide);