diff --git a/Cargo.lock b/Cargo.lock index 9e6e79f..36c984f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,12 +1,49 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "addr2line" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" + +[[package]] +name = "alsa" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb213f6b3e4b1480a60931ca2035794aa67b73103d254715b1db7b70dcb3c934" +dependencies = [ + "alsa-sys", + "bitflags", + "libc", + "nix", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "ansi_term" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -15,25 +52,208 @@ version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf8dcb5b4bbaa28653b647d8c77bd4ed40183b48882e130c1f1ffb73de069fd7" +[[package]] +name = "arpegiator" +version = "0.1.0" +dependencies = [ + "async-channel", + "async-std", + "bincode", + "build-info", + "build-info-build", + "coremidi", + "futures-lite", + "ipc-channel", + "itertools", + "log", + "mach", + "midir", + "num-traits", + "util", + "uuid", + "vst", +] + +[[package]] +name = "arpegiator_pattern_receiver" +version = "0.1.0" +dependencies = [ + "async-channel", + "async-std", + "bincode", + "build-info", + "build-info-build", + "futures-lite", + "ipc-channel", + "itertools", + "log", + "mach", + "serde", + "util", + "vst", +] + +[[package]] +name = "async-channel" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb877970c7b440ead138f6321a3b5395d6061183af779340b65e20c0fede9146" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "once_cell", + "vec-arena", +] + +[[package]] +name = "async-global-executor" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9586ec52317f36de58453159d48351bc244bc24ced3effc1fce22f3d48664af6" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-mutex", + "blocking", + "futures-lite", + "num_cpus", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9315f8f07556761c3e48fec2e6b276004acf426e6dc068b2c2251854d65ee0fd" +dependencies = [ + "concurrent-queue", + "fastrand", + "futures-lite", + "libc", + "log", + "nb-connect", + "once_cell", + "parking", + "polling", + "vec-arena", + "waker-fn", + "winapi 0.3.9", +] + +[[package]] +name = "async-lock" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6a8ea61bf9947a1007c5cada31e647dbc77b103c679858150003ba697ea798b" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-mutex" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-std" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f06685bad74e0570f5213741bea82158279a4103d988e57bfada11ad230341" +dependencies = [ + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils 0.8.1", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "num_cpus", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91831deabf0d6d7ec49552e489aed63b7456a7a3c46cff62adad428110b0af0" + +[[package]] +name = "atomic-waker" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" + +[[package]] +name = "audio_data" +version = "0.1.0" +dependencies = [ + "build-info", + "build-info-build", + "log", + "util", + "vst", +] + [[package]] name = "autocfg" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "backtrace" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5140344c85b01f9bbb4d4b7288a8aa4b3287ccef913a14bcc78a1063623598" +dependencies = [ + "addr2line", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" -version = "0.12.3" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "bincode" -version = "1.3.1" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" dependencies = [ - "byteorder", "serde", ] @@ -43,11 +263,25 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "blocking" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e170dbede1f740736619b776d7251cb1b9095c435c34d8ca9f57fcd2f335e9" +dependencies = [ + "async-channel", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", + "once_cell", +] + [[package]] name = "build-info" -version = "0.0.20" +version = "0.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69844574061700533300a9b5ddaaa97f474408df3711bb4d469082c0086b0fe2" +checksum = "569d94157949b69d52fd80b77a33cffa083ebcd7abf6d60999b41cc977664a32" dependencies = [ "build-info-common", "build-info-proc", @@ -57,9 +291,9 @@ dependencies = [ [[package]] name = "build-info-build" -version = "0.0.20" +version = "0.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28813baefff56676114a7aa46eb32dd2c1eb077c08c517920a08b75b6c59632a" +checksum = "7975fa865b3562339bdec59ee877d2a8618c4f95dc0e4729f3d501818cae11b1" dependencies = [ "anyhow", "base64", @@ -78,21 +312,21 @@ dependencies = [ [[package]] name = "build-info-common" -version = "0.0.20" +version = "0.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57af7dfb8cd188925b45f04f44cd3545f82cb296f229cf23db3ae8a74bc2031a" +checksum = "9c3210a1a81517312746d7ecad5cde4ad9d3ec20640cb6e0c9900c9c6aa6dea1" dependencies = [ "chrono", "derive_more", - "semver 0.10.0", + "semver", "serde", ] [[package]] name = "build-info-proc" -version = "0.0.20" +version = "0.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25de6ce9c02af741ab76c4c02a7a3f117ebc7d6d96939eb250bbd816163bc936" +checksum = "22b0c07441c4c64c569cbf637e1e181b1c9c934764c7dcaac0ab7d8ffc8e1eba" dependencies = [ "anyhow", "base64", @@ -112,18 +346,45 @@ dependencies = [ ] [[package]] -name = "byteorder" -version = "1.3.4" +name = "bumpalo" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" +checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820" + +[[package]] +name = "cache-padded" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "631ae5198c9be5e753e5cc215e1bd73c2b466a3565173db433f52bb9d3e66dba" + +[[package]] +name = "camino" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4648c6d00a709aa069a236adcaae4f605a6241c72bf5bee79331a4b625921a9" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0226944a63d1bf35a3b5f948dd7c59e263db83695c9e8bffc4037de02e30f1d7" +dependencies = [ + "serde", +] [[package]] name = "cargo_metadata" -version = "0.11.4" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3a567c24b86754d629addc2db89e340ac9398d07b5875efcff837e3878e17ec" +checksum = "081e3f0755c1f380c2d010481b6fa2e02973586d5f2b24eebb7a2a1d98b143d8" dependencies = [ - "semver 0.10.0", + "camino", + "cargo-platform", + "semver", + "semver-parser", "serde", "serde_json", ] @@ -143,6 +404,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "chrono" version = "0.4.19" @@ -154,16 +421,88 @@ dependencies = [ "num-traits", "serde", "time", - "winapi", + "winapi 0.3.9", ] [[package]] -name = "cloudabi" -version = "0.0.3" +name = "concurrent-queue" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" dependencies = [ - "bitflags", + "cache-padded", +] + +[[package]] +name = "core-foundation" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25bfd746d203017f7d5cbd31ee5d8e17f94b6521c7af77ece6c9e4b2d4b16c67" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "065a5d7ffdcbc8fa145d6f0746f3555025b9097a9e9cda59f7467abae670c78d" +dependencies = [ + "libc", +] + +[[package]] +name = "coremidi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99f92de5534f182bad5f91cad85611ab222cb6e237a0555e06c65a24936c3173" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "coremidi-sys", + "time", +] + +[[package]] +name = "coremidi-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07f05827cebb30dcd539ff1ac9bf6764f574a15fa147f8572f99d7617142f95e" +dependencies = [ + "core-foundation-sys", +] + +[[package]] +name = "crossbeam-channel" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" +dependencies = [ + "crossbeam-utils 0.7.2", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if 0.1.10", + "lazy_static", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d" +dependencies = [ + "autocfg", + "cfg-if 1.0.0", + "lazy_static", ] [[package]] @@ -188,10 +527,31 @@ dependencies = [ ] [[package]] -name = "difference" -version = "2.0.0" +name = "diff" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" + +[[package]] +name = "either" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "event-listener" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7531096570974c3a9dcf9e4b8e1cede1ec26cf5046219fb3b9d897503b9be59" + +[[package]] +name = "fastrand" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5faf057445ce5c9d4329e382b2ce7ca38550ef3b73a5348362d5f24e0c7fe3" +dependencies = [ + "instant", +] [[package]] name = "filter_out_non_note" @@ -201,6 +561,12 @@ dependencies = [ "vst", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "form_urlencoded" version = "1.0.0" @@ -217,6 +583,86 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7aea5a5909a74969507051a3b17adc84737e31a5f910559892aedce026f4d53" +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "futures-channel" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b7109687aa4e177ef6fe84553af6280ef2778bdb7783ba44c9dc3399110fe64" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "847ce131b72ffb13b6109a221da9ad97a64cbe48feb1028356b836b47b8f1748" + +[[package]] +name = "futures-io" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611834ce18aaa1bd13c4b374f5d653e1027cf99b6b502584ff8c9a64413b30bb" + +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gimli" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" + [[package]] name = "git2" version = "0.13.12" @@ -238,14 +684,36 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "global_counter" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037cd38cf90be505d4f74eef395d027308d74b5dda77e328ad53244b30566a36" +checksum = "2a77c0a6d353621e0ba32c0eb64a5ed3832d900135712779e38bd2ea31d6509f" dependencies = [ "once_cell", "parking_lot", ] +[[package]] +name = "gloo-timers" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47204a46aaff920a1ea58b11d03dec6f704287d27561724a4631e450654a891f" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "hermit-abi" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" +dependencies = [ + "libc", +] + [[package]] name = "idna" version = "0.2.0" @@ -257,6 +725,52 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + +[[package]] +name = "ipc-channel" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9b32d360ae2d4662212f1d29bc8a277256f49029cea20fd6c182b89819aea7" +dependencies = [ + "bincode", + "crossbeam-channel", + "fnv", + "lazy_static", + "libc", + "mio", + "rand", + "serde", + "tempfile", + "uuid", + "winapi 0.3.9", +] + +[[package]] +name = "itertools" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.6" @@ -272,6 +786,34 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3d7383929f7c9c7c2d0fa596f325832df98c3704f2c60553080f7127a58175" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -303,7 +845,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2b111a074963af1d37a139918ac6d49ad1d0d5e47f72fd55388619691a7d753" dependencies = [ "cc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -314,61 +856,195 @@ checksum = "602113192b08db8f38796c4e85c39e960c145965140e918018bcde1952429655" dependencies = [ "cc", "libc", - "pkg-config", - "vcpkg", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "lock_api" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if 1.0.0", + "value-bag", +] + +[[package]] +name = "log-panics" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0136257df209261daa18d6c16394757c63e032e27aafd8b07788b051082bef" +dependencies = [ + "backtrace", + "log", +] + +[[package]] +name = "lzma-sys" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb4b7c3eddad11d3af9e86c487607d2d2442d185d848575365c4856ba96d619" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" + +[[package]] +name = "max_note_duration" +version = "0.1.0" +dependencies = [ + "build-info", + "build-info-build", + "util", + "vst", +] + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "memalloc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df39d232f5c40b0891c10216992c2f250c054105cb1e56f0fc9032db6203ecc1" + +[[package]] +name = "memchr" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" + +[[package]] +name = "midi_delay" +version = "0.1.0" +dependencies = [ + "build-info", + "build-info-build", + "log", + "util", + "vst", +] + +[[package]] +name = "midir" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25df1e7836ca65078a121efed77049084a434838562b9892d0401bdfb59244cc" +dependencies = [ + "alsa", + "bitflags", + "coremidi", + "js-sys", + "libc", + "memalloc", + "nix", + "wasm-bindgen", + "web-sys", + "winapi 0.3.9", ] [[package]] -name = "lock_api" -version = "0.3.4" +name = "miniz_oxide" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" dependencies = [ - "scopeguard", + "adler", + "autocfg", ] [[package]] -name = "log" -version = "0.4.11" +name = "mio" +version = "0.6.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow", + "net2", + "slab", + "winapi 0.2.8", ] [[package]] -name = "lzma-sys" -version = "0.1.17" +name = "miow" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb4b7c3eddad11d3af9e86c487607d2d2442d185d848575365c4856ba96d619" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" dependencies = [ - "cc", - "libc", - "pkg-config", + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", ] [[package]] -name = "matches" -version = "0.1.8" +name = "nb-connect" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +checksum = "8123a81538e457d44b933a02faf885d3fe8408806b23fa700e8f01c6c3a98998" +dependencies = [ + "libc", + "winapi 0.3.9", +] [[package]] -name = "max_note_duration" -version = "0.1.0" +name = "net2" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" dependencies = [ - "build-info", - "build-info-build", - "util", - "vst", + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", ] [[package]] -name = "midi_delay" -version = "0.1.0" +name = "nix" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2e0b4f3320ed72aaedb9a5ac838690a8047c7b275da22711fddff4f8a14229" dependencies = [ - "util", - "vst", + "bitflags", + "cc", + "cfg-if 0.1.10", + "libc", + "void", ] [[package]] @@ -393,15 +1069,16 @@ version = "0.1.0" dependencies = [ "build-info", "build-info-build", + "log", "util", "vst", ] [[package]] name = "num-bigint" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e9a41747ae4633fce5adffb4d2e81ffc5e89593cb19917f8fb2cc5ff76507bf" +checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512" dependencies = [ "autocfg", "num-integer", @@ -427,6 +1104,22 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" + [[package]] name = "once_cell" version = "1.5.2" @@ -439,31 +1132,38 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9" dependencies = [ - "winapi", + "winapi 0.3.9", ] +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + [[package]] name = "parking_lot" -version = "0.10.2" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" dependencies = [ + "instant", "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" -version = "0.7.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" +checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" dependencies = [ - "cfg-if", - "cloudabi", + "cfg-if 1.0.0", + "instant", "libc", - "redox_syscall", + "redox_syscall 0.2.8", "smallvec", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -472,21 +1172,61 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b063f57ec186e6140e2b8b6921e5f1bd89c7356dda5b33acc5401203ca6131c" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +[[package]] +name = "polling" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2a7bc6b2a29e632e45451c941832803a18cce6781db04de8a04696cdca8bde4" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "log", + "wepoll-sys", + "winapi 0.3.9", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" + [[package]] name = "pretty_assertions" -version = "0.6.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f81e1644e1b54f5a68959a29aa86cde704219254669da328ecfdf6a1f09d427" +checksum = "1cab0e7c02cf376875e9335e0ba1da535775beb5450d21e1dffca068818ed98b" dependencies = [ "ansi_term", "ctor", - "difference", + "diff", "output_vt100", ] @@ -522,9 +1262,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" -version = "1.0.24" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" dependencies = [ "unicode-xid", ] @@ -538,19 +1278,84 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + [[package]] name = "redox_syscall" version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" +[[package]] +name = "redox_syscall" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc" +dependencies = [ + "bitflags", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" + [[package]] name = "rustc_version" -version = "0.2.3" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" dependencies = [ - "semver 0.9.0", + "semver", ] [[package]] @@ -567,18 +1372,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "394cec28fa623e00903caf7ba4fa6fb9a0e260280bb8cdbbba029611108a0190" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" dependencies = [ "semver-parser", "serde", @@ -586,24 +1382,27 @@ dependencies = [ [[package]] name = "semver-parser" -version = "0.7.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" +dependencies = [ + "pest", +] [[package]] name = "serde" -version = "1.0.117" +version = "1.0.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.117" +version = "1.0.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" dependencies = [ "proc-macro2", "quote", @@ -621,23 +1420,63 @@ dependencies = [ "serde", ] +[[package]] +name = "simplelog" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59d0fe306a0ced1c88a58042dc22fc2ddd000982c26d75f6aa09a394547c41e0" +dependencies = [ + "chrono", + "log", + "termcolor", +] + +[[package]] +name = "slab" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" + [[package]] name = "smallvec" -version = "1.5.1" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae524f056d7d770e174287294f562e95044c68e88dec909a00d2094805db9d75" +checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" [[package]] name = "syn" -version = "1.0.51" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b4f34193997d92804d359ed09953e25d5138df6bcc055a71bf68ee89fdf9223" +checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" dependencies = [ "proc-macro2", "quote", "unicode-xid", ] +[[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "rand", + "redox_syscall 0.1.57", + "remove_dir_all", + "winapi 0.3.9", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + [[package]] name = "time" version = "0.1.44" @@ -645,8 +1484,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", - "wasi", - "winapi", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi 0.3.9", ] [[package]] @@ -664,6 +1503,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + [[package]] name = "unicode-bidi" version = "0.3.4" @@ -704,26 +1549,66 @@ dependencies = [ name = "util" version = "0.1.0" dependencies = [ + "async-channel", + "build-info", + "build-info-build", "global_counter", + "ipc-channel", + "log", + "log-panics", + "serde", + "simplelog", + "uuid", "vst", ] +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.3", +] + +[[package]] +name = "value-bag" +version = "1.0.0-alpha.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd320e1520f94261153e96f7534476ad869c14022aee1e59af7c778075d840ae" +dependencies = [ + "ctor", + "version_check", +] + [[package]] name = "vcpkg" version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" +[[package]] +name = "vec-arena" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eafc1b9b2dfc6f5529177b62cf806484db55b32dc7c9658a118e11bbeb33061d" + [[package]] name = "version_check" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + [[package]] name = "vst" version = "0.2.1" -source = "git+https://github.com/rustaudio/vst-rs#9eb1bef1826db1581b4162081de05c1090935afb" +source = "git+https://github.com/rustaudio/vst-rs#a398697b0214129720af2480e98f1abf7beecfce" dependencies = [ "bitflags", "libc", @@ -732,12 +1617,115 @@ dependencies = [ "num-traits", ] +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +[[package]] +name = "wasm-bindgen" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd364751395ca0f68cafb17666eee36b63077fb5ecd972bbcd74c90c4bf736e" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1114f89ab1f4106e5b55e688b828c0ab0ea593a1ea7c094b141b14cbaaec2d62" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fe9756085a84584ee9457a002b7cdfe0bfff169f45d2591d8be1345a6780e35" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7811dd7f9398f14cc76efd356f98f03aa30419dea46aa810d71e819fc97158" + +[[package]] +name = "web-sys" +version = "0.3.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222b1ef9334f92a21d3fb53dc3fd80f30836959a90f9274a626d7e06315ba3c3" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wepoll-sys" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcb14dea929042224824779fbc82d9fab8d2e6d3cbc0ac404de8edf489e77ff" +dependencies = [ + "cc", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -748,18 +1736,43 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + [[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi 0.3.9", +] + [[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 = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + [[package]] name = "xz2" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index 477f8eb..139403f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,5 +7,8 @@ members = [ "filter_out_non_note", "note_fan_out", "midi_delay", - "max_note_duration" + "max_note_duration", + "audio_data", + "arpegiator", + "arpegiator_pattern_receiver" ] diff --git a/arpegiator/Cargo.toml b/arpegiator/Cargo.toml new file mode 100644 index 0000000..f24d375 --- /dev/null +++ b/arpegiator/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "arpegiator" +version = "0.1.0" +authors = ["Vincent Alsteen "] +edition = "2018" + +[dependencies] +vst = { git = "https://github.com/rustaudio/vst-rs" } +util = { path = "../util" } +build-info = "0.0.23" +log = "0.4.14" +num-traits = "0.2.14" +itertools = "0.10.0" +bincode = "1.3.3" +midir = "0.7.0" +async-std = "1.9.0" +async-channel = "1.6.1" +futures-lite = "1.12.0" +ipc-channel = "0.15.0" +uuid = "0.8.2" + +[build-dependencies] +build-info-build = "0.0.23" + +[lib] +name = "arpegiator" +crate-type = ["cdylib", "lib"] + +[features] +default = ["pressure_as_channel_pressure", "forward_pattern_cc", "worker_debug", "midi_hack_transmission"] + +pressure_as_channel_pressure = [] +pressure_as_aftertouch = [] +pressure_as_cc7 = [] + +worker_debug = [] +forward_note_cc = [] +forward_pattern_cc = [] +device_debug = [] +midi_hack_transmission = [] + + +[target.'cfg(target_os = "macos")'.dependencies.coremidi] +version = "0.4.0" + +[target.'cfg(target_os = "macos")'.dependencies.mach] +version = "0.3" diff --git a/arpegiator/build.rs b/arpegiator/build.rs new file mode 100644 index 0000000..a49ab18 --- /dev/null +++ b/arpegiator/build.rs @@ -0,0 +1,5 @@ +fn main() { + // Calling `build_info_build::build_script` collects all data and makes it available to `build_info::build_info!` + // and `build_info::format!` in the main program. + build_info_build::build_script(); +} diff --git a/arpegiator/src/lib.rs b/arpegiator/src/lib.rs new file mode 100644 index 0000000..784fd95 --- /dev/null +++ b/arpegiator/src/lib.rs @@ -0,0 +1,708 @@ +#[allow(unused_imports)] +use { + log::{error, info}, + std::mem::take, +}; + +use std::cmp::Ordering; +use std::os::raw::c_void; +use std::sync::Arc; + +use itertools::Itertools; +use vst::api; +use vst::buffer::{AudioBuffer, SendEventBuffer}; +use vst::event::{Event, MidiEvent}; +use vst::plugin::{CanDo, Category, HostCallback, Info, Plugin}; + +use midi_messages::device::{Device, DeviceChange, Expression}; +use midi_messages::device_out::DeviceOut; +use util::logging::logging_setup; +#[cfg(feature = "pressure_as_aftertouch")] +use util::messages::AfterTouch; +#[cfg(feature = "pressure_as_channel_pressure")] +use util::messages::Pressure; +#[cfg(feature = "pressure_as_cc7")] +use util::messages::CC; +use util::messages::{PitchBend, Timbre}; +use util::midi_message_with_delta::MidiMessageWithDelta; +use util::raw_message::RawMessage; +#[cfg(not(feature = "midi_hack_transmission"))] +use workers::main_worker::{create_worker_thread, WorkerChannels, WorkerCommand}; + +#[cfg(not(feature = "midi_hack_transmission"))] +#[cfg(target_os = "macos")] +use mach::mach_time::mach_absolute_time; + +use crate::midi_messages::change::SourceChange; +use crate::midi_messages::pattern_device::{PatternDevice, PatternDeviceChange}; +use crate::midi_messages::timed_event::TimedEvent; +use crate::parameters::{ArpegiatorParameters, PitchBendValues, PARAMETER_COUNT, Parameter}; +use util::system::Uuid; +use util::parameters::ParameterConversion; + +#[cfg(not(feature = "midi_hack_transmission"))] +mod system; +#[cfg(not(feature = "midi_hack_transmission"))] +mod workers; + +mod midi_messages; +mod parameters; + +#[macro_use] +extern crate vst; + +plugin_main!(ArpegiatorPlugin); + + +struct PitchbendInProgress { + channel: u8, + increment_per_block: i32, + target: i32 +} + + +pub struct ArpegiatorPlugin { + events: Vec, + _host: HostCallback, + #[cfg(feature = "midi_hack_transmission")] + send_buffer: SendEventBuffer, + pattern_device_in: Device, + notes_device_in: Device, + pattern_device: PatternDevice, + current_time_in_samples: usize, + sample_rate: f32, + block_size: i64, + device_out: DeviceOut, + parameters: Arc, + #[cfg(not(feature = "midi_hack_transmission"))] + worker_channels: Option, + resumed: bool, + pitchbend_in_progress: Vec +} + +impl ArpegiatorPlugin { + #[cfg(not(feature = "midi_hack_transmission"))] + fn close_worker(&mut self, event_id: Uuid) { + if let Some(worker_channels) = take(&mut self.worker_channels) { + #[cfg(feature = "worker_debug")] + info!("[{}] stopping workers", event_id); + if let Err(e) = worker_channels.command_sender.try_send(WorkerCommand::Stop(event_id)) { + error!("[{}] Error while closing worker channel : {:?}", event_id, e) + } + if let Err(err) = worker_channels.worker.join() { + error!( + "[{}] Error while waiting for worker thread to finish {:?}", + event_id, err + ) + } + } + } +} + +impl Default for ArpegiatorPlugin { + fn default() -> Self { + ArpegiatorPlugin { + events: vec![], + _host: Default::default(), + #[cfg(feature = "midi_hack_transmission")] + send_buffer: Default::default(), + pattern_device_in: Device::new("Patterns".to_string()), + notes_device_in: Device::new("Notes".to_string()), + pattern_device: PatternDevice::default(), + current_time_in_samples: 0, + sample_rate: 44100.0, + block_size: 64, + device_out: DeviceOut::new("Out".to_string()), + parameters: Arc::new(ArpegiatorParameters::new()), + #[cfg(not(feature = "midi_hack_transmission"))] + worker_channels: None, + resumed: false, + pitchbend_in_progress: vec![] + } + } +} + +impl Plugin for ArpegiatorPlugin { + fn get_info(&self) -> Info { + Info { + name: "Arpegiator".to_string(), + vendor: "DJ Crontab".to_string(), + unique_id: 342111721, + parameters: PARAMETER_COUNT as i32, + category: Category::Synth, + // this device must start slightly later than the pattern receiver, so it receives patterns that + // apply to the same buffer + // one sample would be 0.2ms down to 0.05ms. increase amount of samples if the delay between plugins + // is greater than 0.05ms + initial_delay: 0, + version: 1, + inputs: 0, + outputs: 0, + midi_inputs: 1, + f64_precision: false, + presets: 1, + midi_outputs: 1, + preset_chunks: true, + silent_when_stopped: true, + } + } + + fn new(host: HostCallback) -> Self { + logging_setup(); + info!( + "{} \ + pressure_as_cc7: {} \ + pressure_as_aftertouch: {} \ + pressure_as_channel_pressure: {} \ + midi_hack_transmission {} \ + ", + build_info::format!("{{{} v{} built with {} at {}}} ", + $.crate_info.name, $.crate_info.version, $.compiler, $.timestamp), + cfg!(feature = "pressure_as_cc7"), + cfg!(feature = "pressure_as_aftertouch"), + cfg!(feature = "pressure_as_channel_pressure"), + cfg!(feature = "midi_hack_transmission"), + ); + + ArpegiatorPlugin { + events: vec![], + _host: host, + #[cfg(feature = "midi_hack_transmission")] + send_buffer: Default::default(), + pattern_device_in: Device::new("Pattern".to_string()), + notes_device_in: Device::new("Notes".to_string()), + pattern_device: Default::default(), + current_time_in_samples: 0, + sample_rate: 44100., + block_size: 64, + device_out: DeviceOut::new("Out".to_string()), + parameters: Arc::new(ArpegiatorParameters::new()), + #[cfg(not(feature = "midi_hack_transmission"))] + worker_channels: None, + resumed: false, + pitchbend_in_progress: vec![] + } + } + + fn set_sample_rate(&mut self, rate: f32) { + #[cfg(not(feature = "midi_hack_transmission"))] + if let Some(workers_channel) = &self.worker_channels { + workers_channel + .command_sender + .try_send(WorkerCommand::SetSampleRate(rate)) + .unwrap() + }; + self.sample_rate = rate + } + + fn set_block_size(&mut self, size: i64) { + self.block_size = size; + + #[cfg(not(feature = "midi_hack_transmission"))] + if let Some(workers_channel) = &self.worker_channels { + workers_channel + .command_sender + .try_send(WorkerCommand::SetBlockSize(size)) + .unwrap() + }; + } + + fn resume(&mut self) { + if self.resumed { + info!("Already resumed"); + return; + } + self.resumed = true; + + let event_id = Uuid::new_v4(); + + #[cfg(feature = "worker_debug")] + info!("[{}] resume: enter", event_id); + + self.current_time_in_samples = 0; + + #[cfg(not(feature = "midi_hack_transmission"))] + { + self.close_worker(event_id); + let worker_channels = create_worker_thread(); + worker_channels + .command_sender + .try_send(WorkerCommand::SetPort(self.parameters.get_port(), event_id)) + .unwrap(); + worker_channels + .command_sender + .try_send(WorkerCommand::SetSampleRate(self.sample_rate)) + .unwrap(); + + self.worker_channels = match self.parameters.worker_commands.lock() { + Ok(mut worker_commands) => { + *worker_commands = Some(worker_channels.command_sender.clone()); + Some(worker_channels) + } + Err(err) => { + error!("[{}] Could not get parameters lock: {}", event_id, err); + None + } + }; + } + + #[cfg(feature = "worker_debug")] + info!("[{}] resume: exit", event_id); + } + + fn suspend(&mut self) { + if !self.resumed { + info!("Already suspended"); + return; + } + let event_id = Uuid::new_v4(); + + self.resumed = false; + + #[cfg(not(feature = "midi_hack_transmission"))] + { + #[cfg(feature = "worker_debug")] + info!("[{}] suspend enter", event_id); + if let Ok(mut worker_commands) = self.parameters.worker_commands.lock() { + *worker_commands = None + } + + self.close_worker(event_id); + } + #[cfg(feature = "worker_debug")] + info!("[{}] suspend exit", event_id); + } + + fn vendor_specific(&mut self, index: i32, value: isize, ptr: *mut c_void, opt: f32) -> isize { + // according to MPE specifications a vendor specific call should occur in order to signal VST + // support ( page 15 ). As it seems all bitwig does is setting "MPE support" to true by default + // when CanDo replies to "MPE", and it's just sending pitchwheel/pressure. + // https://d30pueezughrda.cloudfront.net/campaigns/mpe/mpespec.pdf + info!("vendor_specific {:?} {:?} {:?} {:?}", index, value, ptr, opt); + 0 + } + + fn can_do(&self, can_do: CanDo) -> vst::api::Supported { + use vst::api::Supported::*; + use vst::plugin::CanDo::*; + + match can_do { + SendEvents + | SendMidiEvent + | ReceiveEvents + | ReceiveMidiEvent + | Offline + | MidiSingleNoteTuningChange + | MidiKeyBasedInstrumentControl => Yes, + Other(s) => { + if s == "MPE" { + Yes + } else { + info!("Cando : {}", s); + Maybe + } + } + _ => No, + } + } + + fn process(&mut self, buffer: &mut AudioBuffer) { + #[cfg(not(feature = "midi_hack_transmission"))] + let local_time = { + #[cfg(target_os = "macos")] + unsafe { + mach_absolute_time() + } + #[cfg(target_os = "linux")] + 0 + }; + + #[cfg(not(feature = "midi_hack_transmission"))] + let pattern_messages = { + match self.worker_channels.as_ref() { + None => vec![], + Some(socket_channels) => match socket_channels.pattern_receiver.try_recv() { + Ok(payload) => { + #[cfg(feature = "device_debug")] + info!( + "[{}] received patterns : {:02X?}", + self.current_time_in_samples, payload + ); + + payload.messages + } + Err(_) => vec![], + }, + } + }; + #[cfg(not(feature = "midi_hack_transmission"))] + let events = &self.events; + + // from here we cannot accurately tell when the buffer we're building will actually play + // so our best guest will be the earliest : + // current_time + (delta_frame + buffer_size) * sample_to_second + + // this avoids borrowing self + let current_time_in_samples = self.current_time_in_samples; + let pattern_device_in = &mut self.pattern_device_in; + let pattern_device = &mut self.pattern_device; + let notes_device_in = &mut self.notes_device_in; + + #[cfg(feature = "midi_hack_transmission")] + let (pattern_messages, notes): (Vec, Vec) = { + let (mut patterns, mut notes): (Vec, Vec) = + self.events.drain(..).partition(|item| item.data[0] < 0x80); + notes.sort_by_key(|x| [x.delta_frames, x.data[0] as i32, x.data[1] as i32]); + patterns.sort_by_key(|x| [x.delta_frames, x.data[0] as i32, x.data[1] as i32]); + let patterns = patterns + .iter() + .map(|event| MidiMessageWithDelta { + delta_frames: event.delta_frames as u16, + data: RawMessage::from([event.data[0] + 0x80, event.data[1], event.data[2]]), + }) + .collect_vec(); + (patterns, notes) + }; + + pattern_device_in.legato = self.parameters.get_bool_parameter(Parameter::PatternLegato); + let pattern_changes = pattern_device_in.process_buffer(pattern_messages, current_time_in_samples); + + let pattern_changes = pattern_changes.into_iter().map(|change| { + SourceChange::PatternChange(pattern_device.update(change)) + }); + + let notes_in_as_messages = notes.iter().map(|event| + MidiMessageWithDelta { + delta_frames: event.delta_frames as u16, + data: event.data.into(), + } + ).collect_vec(); + + let note_changes = notes_device_in.process_buffer(notes_in_as_messages, current_time_in_samples); + + let note_changes = note_changes.into_iter().map(|change| { + SourceChange::NoteChange(change) + }); + + // merge() gets a sorted output. If a note is triggered at the same time as a pattern, note comes first in order + // to set the pitch + for change in pattern_changes.sorted().merge(note_changes.sorted()) { + let delta_frames = (change.timestamp() - self.current_time_in_samples) as u16; + + match change { + SourceChange::NoteChange(change) => { + match change { + DeviceChange::AddNote { .. } => { + let pitch_bend_parameter_value = self.parameters.get_pitchbend(); + + match pitch_bend_parameter_value { + PitchBendValues::Off => {} + _ => { + // incoming note can move before other notes, so we have to recalculate pitches of + // all playing notes + for (position, note) in self + .notes_device_in + .notes + .values() + .sorted_by(|item1, item2| { + let pitch_cmp = item1.pitch.cmp(&item2.pitch); + match pitch_cmp { + Ordering::Equal => item1.channel.cmp(&item2.channel), + _ => pitch_cmp, + } + }) + .enumerate() + { + #[cfg(feature = "device_debug")] + info!("note will be applied to pattern {}", position); + for pattern in self.pattern_device.at(position as u8) { + if let Some(target_pitch) = pattern.transpose(note.pitch) { + #[cfg(feature = "device_debug")] + info!( + "Applying pitchbend to pattern {}, at position {}", + pattern.id, target_pitch + ); + // TODO this difference actually needs to apply relative to the + // pitchbend already in place. When the output pitchbend is met, + // we stop tweaking it + let note_out = self.device_out.find_by_note_id(pattern.id); + if note_out.is_none() { continue } + let note_out = note_out.unwrap(); + let difference = note_out.difference_in_millisemitones(target_pitch); + + let increment = match pitch_bend_parameter_value { + PitchBendValues::DurationToReachTarget(duration_in_seconds) => { + let blocks_per_second = self.sample_rate as f32 / self.block_size as f32; + let blocks_to_target = (blocks_per_second * duration_in_seconds) as i32; + let increment = difference / blocks_to_target; + self.pitchbend_in_progress.push(PitchbendInProgress { + channel: note_out.channel, + increment_per_block: increment, + target: difference + }); + self.device_out.update_pitch( + pattern.id, + increment, // "increment" is innacurate we need to + // start from the pitchbend applied now + // patterns can have octave difference, but for now + // the pitchbend we'd like is the same. it always + // make sense this way as long as pattern follow notes, + // but we may want a pitchbend modulation different for + // each octave. that's for another implementation, + // let's just make sure at that point that the 'target' + // pitchbend is reachable while modulation is applied + // *after*. Means, the output device might not be the + // right place to find that reference. + + /* + so : + - notes in can have its own pitch bend, so no + - patterns in can have their own pitch bend so no + - device out can already be consolidation of pitchbends + so no + - so it seems some intermediary after device note in + would be appropriate. it is only related to device + note in anyway. + */ + delta_frames, + current_time_in_samples, + ); + } + PitchBendValues::Immediate => { + self.device_out.update_pitch( + pattern.id, + difference, + delta_frames, + current_time_in_samples, + ); + } + _ => { panic!("'off' is not possible here") } + }; + } + } + } + } + } + } + DeviceChange::RemoveNote { .. } => {} + DeviceChange::NoteExpressionChange { .. } => {} + DeviceChange::ReplaceNote { .. } => {} + + DeviceChange::CCChange { cc: _cc, time: _time } => { + #[cfg(feature = "forward_note_cc")] + { + let message = MidiMessageWithDelta { + delta_frames, + data: Into::::into(_cc).into(), + }; + + let _ = self.device_out.update(message, current_time_in_samples, None); + } + } + DeviceChange::Ignored { .. } => {} + DeviceChange::NoteLegato { .. } => { + panic!("Legato not supported for notes in") + } + } + } + SourceChange::PatternChange(change) => { + match change { + PatternDeviceChange::AddPattern { pattern, .. } => { + // TODO "hold notes" logic + match self.notes_device_in.nth(pattern.index as usize) { + None => {} + Some(note) => self.device_out.push_note_on( + &pattern, + ¬e, + current_time_in_samples, + self.parameters.get_velocitysource()), + } + } + + PatternDeviceChange::PatternExpressionChange { + expression, pattern, .. + } => { + let raw_message: Option = match expression { + Expression::Timbre => Some( + Timbre { + channel: pattern.channel, + value: pattern.timbre, + } + .into(), + ), + Expression::PitchBend => { + Some( + PitchBend { + channel: pattern.channel, + millisemitones: pattern.pitchbend, + } + .into(), + ) + } + Expression::Pressure | Expression::AfterTouch => { + // TODO output pressure could be a combination of: + /* + - pattern pressure + - note pressure + - pattern velocity + - note velocity ( generalization of "pitchbend" note changes affect an ongoing + pattern ) + */ + #[cfg(feature = "pressure_as_channel_pressure")] + { + Some( + Pressure { + channel: pattern.channel, + value: pattern.pressure, + } + .into(), + ) + } + + #[cfg(any(feature = "pressure_as_aftertouch", feature = "pressure_as_cc7"))] + { + match self.notes_device_in.nth(pattern.index as usize) { + None => None, + Some(note) => { + if let Some(_pitch) = pattern.transpose(note.pitch) { + #[cfg(feature = "pressure_as_aftertouch")] + { + Some( + AfterTouch { + channel: pattern.channel, + pitch: _pitch, + value: pattern.pressure, + } + .into(), + ) + } + #[cfg(feature = "pressure_as_cc7")] + { + Some( + CC { + channel: pattern.channel, + cc: 7, + value: pattern.pressure, + } + .into(), + ) + } + } else { + None + } + } + } + } + } + }; + + if let Some(raw_message) = raw_message { + self.device_out.update( + MidiMessageWithDelta { + delta_frames, + data: raw_message, + }, + current_time_in_samples, + None, + ); + } + } + PatternDeviceChange::RemovePattern { pattern, .. } => { + self.device_out.push_note_off( + pattern.id, + pattern.velocity_off, + delta_frames, + current_time_in_samples, + ); + } + PatternDeviceChange::ReplacePattern { + old_pattern, + new_pattern, + .. + } => { + self.device_out.push_note_off( + old_pattern.id, + old_pattern.velocity_off, + delta_frames, + current_time_in_samples, + ); + + let note = match self + .notes_device_in + .notes + .values() + .sorted() + .nth(new_pattern.index as usize) + { + None => { + continue; + } + Some(note) => note, + }; + + self.device_out + .push_note_on( + &new_pattern, + note, + current_time_in_samples, + self.parameters.get_velocitysource() + ); + }, + PatternDeviceChange::Legato { old_pattern, new_pattern , .. } => { + self.device_out.legato(old_pattern.id, new_pattern.id); + } + PatternDeviceChange::CC { cc: _cc, time: _time } => { + #[cfg(feature = "forward_pattern_cc")] { + let message = MidiMessageWithDelta { + delta_frames, + data: _cc.into(), + }; + + let _ = self.device_out.update(message, current_time_in_samples, None); + } + } + PatternDeviceChange::None { .. } => {} + } + } + } + } + + #[cfg(not(feature = "midi_hack_transmission"))] + if let Some(worker_channels) = self.worker_channels.as_ref() { + self.device_out.flush_to(local_time, &worker_channels.command_sender) + } + + #[cfg(feature = "midi_hack_transmission")] + { + self.send_buffer + .send_events(take(&mut self.device_out.output_queue), &mut self._host); + } + + self.events.clear(); + + self.current_time_in_samples += buffer.samples() + } + + fn process_events(&mut self, events: &api::Events) { + for e in events.events() { + if let Event::Midi(e) = e { + #[cfg(feature = "device_debug")] + info!("Received {:2X?}", e.data); + self.events.push(e); + } + } + } + + fn get_parameter_object(&mut self) -> Arc { + Arc::clone(&self.parameters) as Arc + } +} + +#[cfg(not(feature = "midi_hack_transmission"))] +impl Drop for ArpegiatorPlugin { + fn drop(&mut self) { + let event_id = Uuid::new_v4(); + info!("[{}] Dropping plugin", event_id); + self.close_worker(event_id); + } +} diff --git a/arpegiator/src/midi_messages/change.rs b/arpegiator/src/midi_messages/change.rs new file mode 100644 index 0000000..f8649de --- /dev/null +++ b/arpegiator/src/midi_messages/change.rs @@ -0,0 +1,65 @@ +use crate::midi_messages::device::DeviceChange; +use crate::midi_messages::pattern_device::PatternDeviceChange; +use crate::midi_messages::timed_event::TimedEvent; +use core::cmp::{Ordering, PartialEq, PartialOrd}; +use core::option::Option; + +pub enum SourceChange { + NoteChange(DeviceChange), + PatternChange(PatternDeviceChange), +} + +impl PartialOrd for SourceChange { + fn partial_cmp(&self, other: &Self) -> Option { + // note come first, in order to start the pattern with the intended note + match self { + SourceChange::PatternChange(pattern) => match other { + SourceChange::PatternChange(other_pattern) => pattern.partial_cmp(other_pattern), + SourceChange::NoteChange(_) => Option::Some(Ordering::Greater), + }, + SourceChange::NoteChange(note) => match other { + SourceChange::PatternChange(_) => Option::Some(Ordering::Less), + SourceChange::NoteChange(other_note) => note.partial_cmp(other_note), + }, + } + } +} + +impl PartialEq for SourceChange { + fn eq(&self, other: &Self) -> bool { + match self { + SourceChange::PatternChange(pattern) => match other { + SourceChange::PatternChange(other_pattern) => pattern.eq(other_pattern), + SourceChange::NoteChange(_) => false, + }, + SourceChange::NoteChange(note) => match other { + SourceChange::PatternChange(_) => false, + SourceChange::NoteChange(other_note) => note.eq(other_note), + }, + } + } +} + +impl Ord for SourceChange { + fn cmp(&self, other: &Self) -> Ordering { + self.partial_cmp(other).unwrap() + } +} + +impl Eq for SourceChange {} + +impl TimedEvent for SourceChange { + fn timestamp(&self) -> usize { + match self { + SourceChange::NoteChange(note) => note.timestamp(), + SourceChange::PatternChange(pattern) => pattern.timestamp(), + } + } + + fn id(&self) -> usize { + match self { + SourceChange::NoteChange(note) => note.id(), + SourceChange::PatternChange(pattern) => pattern.id(), + } + } +} diff --git a/arpegiator/src/midi_messages/device.rs b/arpegiator/src/midi_messages/device.rs new file mode 100644 index 0000000..594cf3a --- /dev/null +++ b/arpegiator/src/midi_messages/device.rs @@ -0,0 +1,331 @@ +use log::info; +use std::cmp::Ordering; +use std::collections::HashMap; + +use util::constants::TIMBRECC; +use util::messages::CC; +use util::midi_message_type::MidiMessageType; +use util::midi_message_with_delta::MidiMessageWithDelta; + +use crate::midi_messages::note::{CCIndex, Note, NoteIndex}; +use crate::midi_messages::timed_event::TimedEvent; +use itertools::Itertools; + +pub struct Device { + pub _name: String, + pub notes: HashMap, + pub cc: HashMap, + pub channels: [Channel; 16], + pub note_index: usize, + pub legato: bool, +} + +impl Device { + pub fn new(name: String) -> Self { + Device { + _name: name, + notes: Default::default(), + cc: Default::default(), + channels: [Channel { + pressure: 0, + pitchbend: 0, + timbre: 0, + }; 16], + note_index: 0, + legato: false, + } + } + + #[inline] + pub fn nth(&self, n: usize) -> Option<&Note> { + self.notes.values().sorted().nth(n) + } +} + +#[derive(Copy, Clone, Debug)] +pub struct Channel { + pub pressure: u8, + // in millisemitones + pub pitchbend: i32, + pub timbre: u8, +} + +pub enum Expression { + Timbre, + Pressure, + PitchBend, + AfterTouch, +} + +pub enum DeviceChange { + AddNote { + time: usize, + note: Note, + }, + RemoveNote { + time: usize, + note: Note, + }, + NoteExpressionChange { + time: usize, + expression: Expression, + note: Note, + }, + // replacing happens when a note on is triggered for a note and channel that is already on + ReplaceNote { + time: usize, + old_note: Note, + new_note: Note, + }, + CCChange { + time: usize, + cc: CC, + }, + NoteLegato { + time: usize, + old_note: Note, + new_note: Note + }, + Ignored { + time: usize, + }, +} + +impl TimedEvent for DeviceChange { + fn timestamp(&self) -> usize { + *match self { + DeviceChange::AddNote { time, .. } => time, + DeviceChange::RemoveNote { time, .. } => time, + DeviceChange::NoteExpressionChange { time, .. } => time, + DeviceChange::ReplaceNote { time, .. } => time, + DeviceChange::CCChange { time, .. } => time, + DeviceChange::Ignored { time, .. } => time, + DeviceChange::NoteLegato { time, .. } => time + } + } + + fn id(&self) -> usize { + // used to order events that happen at the same time. Doesn't matter on CCs, in any case they'll be sorted + // by time already + match self { + DeviceChange::AddNote { note, .. } => note.id, + DeviceChange::RemoveNote { note, .. } => note.id, + DeviceChange::NoteExpressionChange { note, .. } => note.id, + DeviceChange::ReplaceNote { new_note: note, .. } => note.id, + DeviceChange::NoteLegato { new_note: note, .. } => note.id, + DeviceChange::CCChange { .. } => 0, + DeviceChange::Ignored { .. } => 0, + } + } +} + +impl PartialOrd for DeviceChange { + fn partial_cmp(&self, other: &Self) -> Option { + let timestamp_cmp = self.timestamp().cmp(&other.timestamp()); + if timestamp_cmp == Ordering::Equal { + Some(self.id().cmp(&other.id())) + } else { + Some(timestamp_cmp) + } + } +} + +impl PartialEq for DeviceChange { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} + + +impl Device { + pub fn push(&mut self, midi_message: MidiMessageWithDelta, current_time: usize, id: Option) -> DeviceChange { + #[cfg(feature = "device_debug")] + info!( + "[{}] Got event: {:?} {:?} {:02X?}", + self._name, id, current_time, midi_message + ); + + let time = current_time + midi_message.delta_frames as usize; + + match MidiMessageType::from(&midi_message.data.into()) { + MidiMessageType::NoteOnMessage(note) => { + let note_id = match id { + None => { + let note_id = self.note_index; + self.note_index += 1; + note_id + } + Some(id) => id, + }; + let index = NoteIndex { + channel: note.channel, + pitch: note.pitch, + }; + let new_note = Note { + id: note_id, + pressed_at: time, + released_at: 0, + channel: note.channel, + pitch: note.pitch, + velocity: note.velocity, + velocity_off: 0, + pressure: self.channels[note.channel as usize].pressure, + timbre: self.channels[note.channel as usize].timbre, + pitchbend: self.channels[note.channel as usize].pitchbend, + }; + + match self.notes.insert(index, new_note) { + None => DeviceChange::AddNote { time, note: new_note }, + Some(old_note) => DeviceChange::ReplaceNote { + time, + old_note, + new_note, + }, + } + } + MidiMessageType::NoteOffMessage(note) => { + let index = NoteIndex { + channel: note.channel, + pitch: note.pitch, + }; + + match self.notes.remove(&index) { + None => { + info!("Attempt to remove note, but it was not found {:02X?}", index); + DeviceChange::Ignored { time } + } + Some(mut old_note) => { + //info!("Removed note {:02X?}", index); + old_note.released_at = time; + old_note.velocity_off = note.velocity; + DeviceChange::RemoveNote { time, note: old_note } + } + } + } + MidiMessageType::CCMessage(cc) => { + self.cc.insert( + CCIndex { + channel: cc.channel, + index: cc.cc, + }, + cc.value, + ); + if cc.cc == TIMBRECC { + self.channels[cc.channel as usize].timbre = cc.value; + for (_, note) in self.notes.iter_mut() { + if note.channel == cc.channel { + note.timbre = cc.value; + // note: per design simplification, having several notes running on the same channel + // is not supported. only the first note found on the channel is updated + return DeviceChange::NoteExpressionChange { + time, + expression: Expression::Timbre, + note: *note, + }; + } + } + } + DeviceChange::CCChange { time, cc } + } + MidiMessageType::PressureMessage(message) => { + self.channels[message.channel as usize].pressure = message.value; + for (_, note) in self.notes.iter_mut() { + if note.channel == message.channel { + // note: per design simplification, having several notes running on the same channel + // is not supported. only the first note found on the channel is updated + note.pressure = message.value; + return DeviceChange::NoteExpressionChange { + time, + expression: Expression::Pressure, + note: *note, + }; + } + } + DeviceChange::Ignored { time } + } + MidiMessageType::AfterTouchMessage(message) => { + // redundant with pressure, but that's the message that bitwig will properly handle for by-note + // expressions + for (_, note) in self.notes.iter_mut() { + if note.channel == message.channel && note.pitch == message.pitch { + note.pressure = message.value; + // since aftertouch is assigned by pitch and channel, contrary to channel pressure + // we are sure it's only affecting one note + return DeviceChange::NoteExpressionChange { + time, + expression: Expression::Pressure, + note: *note, + }; + } + } + DeviceChange::Ignored { time } + } + MidiMessageType::PitchBendMessage(message) => { + self.channels[message.channel as usize].pitchbend = message.millisemitones; + for (_, note) in self.notes.iter_mut() { + if note.channel == message.channel { + // note: per design simplification, having several notes running on the same channel + // is not supported. only the first note found on the channel is updated + note.pitchbend = message.millisemitones; + return DeviceChange::NoteExpressionChange { + time, + expression: Expression::PitchBend, + note: *note, + }; + } + } + DeviceChange::Ignored { time } + } + MidiMessageType::UnsupportedChannelMessage(_) => DeviceChange::Ignored { time }, + MidiMessageType::Unsupported => DeviceChange::Ignored { time }, + } + } + + pub fn process_buffer(&mut self, messages: Vec, current_time: usize) -> Vec { + let mut output = vec![]; + + for message in messages { + output.push(self.push(message, current_time, None)) + } + + if self.legato { + // TODO pressure modulation at velocity change is a possibility + + let mut legato_output = vec![]; + while !output.is_empty() { + let change_1 = output.remove(0); + + // remove note is sorted as being before add note + if let DeviceChange::RemoveNote { time: time_1, note: note_1 } = change_1 { + if let Some(position) = output.iter().position(|change| { + matches!(change, &DeviceChange::AddNote { time: time_2, note: note_2 } if time_1 == time_2 && note_1.pitch == note_2.pitch && note_1.channel == note_2.channel) + }) { + let add_note = output.remove(position); + + if let Some(position) = output.iter().position(|change| { + matches!(change, &DeviceChange::NoteExpressionChange { time: time_2, expression: Expression::PitchBend, note: note_2 } if time_1 == time_2 && note_1.channel == note_2.channel) + }) { + output.remove(position); + } + + if let DeviceChange::AddNote { note: new_note, .. } = add_note { + output.push(DeviceChange::NoteLegato { time: time_1, old_note: note_1, new_note }); + } + + continue; + } + } + + if let DeviceChange::ReplaceNote { .. } = change_1 { + // ignore restarts in legato + continue; + } + + legato_output.push(change_1) + } + legato_output + } else { + output + } + } +} diff --git a/arpegiator/src/midi_messages/device_out.rs b/arpegiator/src/midi_messages/device_out.rs new file mode 100644 index 0000000..771f9fc --- /dev/null +++ b/arpegiator/src/midi_messages/device_out.rs @@ -0,0 +1,154 @@ +#[allow(unused_imports)] +use { + async_channel::Sender, + log::{error, info}, + std::mem::take, +}; + +use util::messages::{NoteOff, PitchBend}; +use util::midi_message_with_delta::MidiMessageWithDelta; +use util::raw_message::RawMessage; + +use crate::midi_messages::device::Device; +use crate::midi_messages::expressive_note::ExpressiveNote; +use crate::midi_messages::note::Note; +use crate::midi_messages::pattern::Pattern; +#[cfg(not(feature = "midi_hack_transmission"))] +use crate::workers::main_worker::WorkerCommand; +use crate::parameters::VelocitySource; + +pub(crate) struct DeviceOut { + device: Device, + pub output_queue: Vec, +} + +impl DeviceOut { + pub(crate) fn legato(&mut self, old_id: usize, new_id: usize) { + if let Some(note) = self.device.notes.values_mut().find(|note| note.id == old_id) { + note.id = new_id; + } + } + + pub fn new(name: String) -> Self { + Self { + device: Device::new(name), + output_queue: vec![], + } + } + + pub fn update(&mut self, midi_message: MidiMessageWithDelta, current_time: usize, id: Option) { + self.output_queue.push(midi_message); + self.device.push(midi_message, current_time, id); + if !self.device.notes.is_empty() { + #[cfg(feature = "device_debug")] + info!("Device out state after update: {:2X?}", self.device.notes) + } + } + + #[cfg(not(feature = "midi_hack_transmission"))] + pub fn flush_to(&mut self, reception_time: u64, midi_output_sender: &Sender) { + if self.output_queue.is_empty() { + return; + } + + { + midi_output_sender + .try_send(WorkerCommand::SendToMidiOutput { + reception_time, + messages: take(&mut self.output_queue), + }) + .unwrap_or_else(|err| error!("Could not send to the controller worker {}", err)); + } + } + + pub fn find_by_note_id(&self, note_id: usize) -> Option<&Note> { + self.device.notes.values().find(|note| note.id == note_id) + } + + pub fn update_pitch(&mut self, note_id: usize, increment: i32, delta_frames: u16, current_time: usize) { + match self.find_by_note_id(note_id) { + None => { + info!( + "Cannot find note to pitchbend. Required note_id: {}. Current notes: {:02X?}", + note_id, + self.device.notes.values() + ) + } + Some(note) => { + let raw_message: RawMessage = PitchBend { + channel: note.channel, + millisemitones: increment + }.into(); + + self.update( + MidiMessageWithDelta { + delta_frames, + data: raw_message, + }, + current_time, + None, + ); + } + } + } + + pub fn push_note_off(&mut self, note_id: usize, velocity_off: u8, delta_frames: u16, current_time: usize) { + let note = match self.device.notes.values().find(|note| note.id == note_id) { + None => { + #[cfg(feature = "device_debug")] + info!("Cannot find note to stop: {:02x?}", note_id); + return; + } + Some(note) => note, + }; + + let raw_message: RawMessage = NoteOff { + channel: note.channel, + pitch: note.pitch, + velocity: velocity_off, + } + .into(); + + self.update( + MidiMessageWithDelta { + delta_frames, + data: raw_message, + }, + current_time, + None, + ); + } + + pub fn push_note_on(&mut self, pattern: &Pattern, note: &Note, current_time: usize, velocity_source: VelocitySource) { + let pitch = match pattern.transpose(note.pitch) { + None => { + return; + } + Some(pitch) => pitch, + }; + + let velocity = match velocity_source { + VelocitySource::Pattern => pattern.velocity, + VelocitySource::Mixed(x) => (pattern.velocity as f32 * (1. - x) + note.velocity as f32 * x) as u8, + VelocitySource::Notes => note.velocity + }; + + for raw_message in (ExpressiveNote { + channel: pattern.channel, + pitch, + velocity, + pressure: pattern.pressure, + timbre: pattern.timbre, + pitchbend: pattern.pitchbend, + }).into_rawmessages() { + self.update( + MidiMessageWithDelta { + delta_frames: (pattern.pressed_at - current_time) as u16, + data: raw_message, + }, + current_time, + Some(pattern.id), + ); + } + } +} diff --git a/arpegiator/src/midi_messages/expressive_note.rs b/arpegiator/src/midi_messages/expressive_note.rs new file mode 100644 index 0000000..c9f6a32 --- /dev/null +++ b/arpegiator/src/midi_messages/expressive_note.rs @@ -0,0 +1,79 @@ +#[allow(unused_imports)] +use log::info; + +use util::messages::{NoteOn, PitchBend, Timbre}; +use util::raw_message::RawMessage; + +#[cfg(feature = "pressure_as_channel_pressure")] +use util::messages::Pressure; + +#[cfg(feature = "pressure_as_aftertouch")] +use util::messages::AfterTouch; + +#[cfg(feature = "pressure_as_cc7")] +use util::messages::CC; + +pub struct ExpressiveNote { + pub channel: u8, + pub pitch: u8, + pub velocity: u8, + pub pressure: u8, + pub timbre: u8, + pub pitchbend: i32, +} + +impl ExpressiveNote { + #[cfg(feature = "pressure_as_aftertouch")] + #[inline] + fn get_pressure_note(&self) -> RawMessage { + AfterTouch { + channel: self.channel, + pitch: self.pitch, + value: self.pressure, + } + .into() + } + + #[cfg(feature = "pressure_as_cc7")] + #[inline] + fn get_pressure_note(&self) -> RawMessage { + CC { + channel: self.channel, + value: self.pressure, + cc: 7, + } + .into() + } + + #[cfg(feature = "pressure_as_channel_pressure")] + #[inline] + fn get_pressure_note(&self) -> RawMessage { + Pressure { + channel: self.channel, + value: self.pressure, + } + .into() + } + + pub fn into_rawmessages(self) -> Vec { + vec![ + PitchBend { + channel: self.channel, + millisemitones: self.pitchbend, + } + .into(), + Timbre { + channel: self.channel, + value: self.timbre, + } + .into(), + self.get_pressure_note(), + NoteOn { + channel: self.channel, + pitch: self.pitch, + velocity: self.velocity, // todo mixing between pattern and note + } + .into(), + ] + } +} diff --git a/arpegiator/src/midi_messages/mod.rs b/arpegiator/src/midi_messages/mod.rs new file mode 100644 index 0000000..8aa1005 --- /dev/null +++ b/arpegiator/src/midi_messages/mod.rs @@ -0,0 +1,8 @@ +pub(crate) mod change; +pub(crate) mod device; +pub(crate) mod device_out; +pub(crate) mod expressive_note; +pub(crate) mod note; +pub(crate) mod pattern; +pub(crate) mod pattern_device; +pub(crate) mod timed_event; diff --git a/arpegiator/src/midi_messages/note.rs b/arpegiator/src/midi_messages/note.rs new file mode 100644 index 0000000..df5b8ca --- /dev/null +++ b/arpegiator/src/midi_messages/note.rs @@ -0,0 +1,75 @@ +use std::cmp::Ordering; + +#[derive(Clone, Copy, Debug)] +pub struct Note { + pub id: usize, + + pub pressed_at: usize, + pub released_at: usize, + + pub channel: u8, + pub pitch: u8, + pub velocity: u8, + pub velocity_off: u8, + + pub pressure: u8, + pub timbre: u8, + pub pitchbend: i32, // in millisemitones +} + +impl Note { + pub fn difference_in_millisemitones(&self, target_pitch: u8) -> i32 { + (target_pitch as i32 - self.pitch as i32) * 1000 + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct NoteIndex { + pub channel: u8, + pub pitch: u8, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct CCIndex { + pub channel: u8, + pub index: u8, +} + +impl PartialOrd for Note { + fn partial_cmp(&self, other: &Self) -> Option { + let cmp = self.pitch.cmp(&other.pitch); + + if cmp != Ordering::Equal { + Some(cmp) + } else { + // unlikely to produce any interesting result + Some(self.id.cmp(&other.id)) + } + } +} + +impl PartialOrd for NoteIndex { + fn partial_cmp(&self, other: &Self) -> Option { + let cmp = self.pitch.cmp(&other.pitch); + + if cmp != Ordering::Equal { + Some(cmp) + } else { + Some(self.channel.cmp(&other.channel)) + } + } +} + +impl PartialEq for Note { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Ord for Note { + fn cmp(&self, other: &Self) -> Ordering { + self.partial_cmp(other).unwrap() + } +} + +impl Eq for Note {} diff --git a/arpegiator/src/midi_messages/pattern.rs b/arpegiator/src/midi_messages/pattern.rs new file mode 100644 index 0000000..b37ab6c --- /dev/null +++ b/arpegiator/src/midi_messages/pattern.rs @@ -0,0 +1,70 @@ +use crate::midi_messages::note::Note; +use crate::midi_messages::timed_event::TimedEvent; + +pub const C3: u8 = 60; + +#[derive(Copy, Clone, Debug)] +pub struct Pattern { + pub id: usize, + + pub channel: u8, + pub index: u8, + pub octave: i8, + + pub pressed_at: usize, + pub released_at: usize, + + pub velocity: u8, + pub velocity_off: u8, + + pub pressure: u8, + pub timbre: u8, + pub pitchbend: i32, // in millisemitones +} + +impl Pattern { + pub fn transpose(&self, pitch: u8) -> Option { + let pitch = pitch as i16 + self.octave as i16 * 12; + if !(0..=127).contains(&pitch) { + // can't play. to fix if we want to change the pitch of a pattern that started + None + } else { + Some(pitch as u8) + } + } +} + +impl TimedEvent for Pattern { + fn timestamp(&self) -> usize { + if self.released_at > 0 { + self.released_at + } else { + self.pressed_at + } + } + + fn id(&self) -> usize { + self.id + } +} + +impl From for Pattern { + fn from(note: Note) -> Self { + let index = note.pitch % 12; + let octave = (((note.pitch - index) as i16 - C3 as i16) / 12) as i8; + + Pattern { + channel: note.channel, + id: note.id, + index, + velocity_off: 0, + pressure: 0, + timbre: 64, + octave, + pressed_at: note.pressed_at, + released_at: 0, + velocity: note.velocity, + pitchbend: 0, + } + } +} diff --git a/arpegiator/src/midi_messages/pattern_device.rs b/arpegiator/src/midi_messages/pattern_device.rs new file mode 100644 index 0000000..bf8b9da --- /dev/null +++ b/arpegiator/src/midi_messages/pattern_device.rs @@ -0,0 +1,180 @@ +use log::error; + +use core::iter::Filter; +use std::cmp::Ordering; +use std::collections::hash_map::Values; +use std::collections::HashMap; + +use crate::midi_messages::device::{DeviceChange, Expression}; +use crate::midi_messages::pattern::Pattern; +use crate::midi_messages::timed_event::TimedEvent; +use util::messages::CC; + +#[derive(Default)] +pub struct PatternDevice { + patterns: HashMap, +} + +pub enum PatternDeviceChange { + AddPattern { + time: usize, + pattern: Pattern, + }, + PatternExpressionChange { + time: usize, + expression: Expression, + pattern: Pattern, + }, + RemovePattern { + time: usize, + pattern: Pattern, + }, + ReplacePattern { + time: usize, + old_pattern: Pattern, + new_pattern: Pattern, + }, + Legato { + time: usize, + old_pattern: Pattern, + new_pattern: Pattern, + }, + CC { + cc: CC, + time: usize, + }, + None { + time: usize, + }, +} + +impl PartialOrd for PatternDeviceChange { + fn partial_cmp(&self, other: &Self) -> Option { + let timestamp_cmp = self.timestamp().cmp(&other.timestamp()); + if timestamp_cmp == Ordering::Equal { + Some(self.id().cmp(&other.id())) + } else { + Some(timestamp_cmp) + } + } +} + +impl PartialEq for PatternDeviceChange { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} + +impl TimedEvent for PatternDeviceChange { + fn timestamp(&self) -> usize { + *match self { + PatternDeviceChange::AddPattern { time, .. } => time, + PatternDeviceChange::PatternExpressionChange { time, .. } => time, + PatternDeviceChange::RemovePattern { time, .. } => time, + PatternDeviceChange::ReplacePattern { time, .. } => time, + PatternDeviceChange::None { time, .. } => time, + PatternDeviceChange::CC { time, .. } => time, + PatternDeviceChange::Legato { time, .. } => time + } + } + + fn id(&self) -> usize { + match self { + PatternDeviceChange::AddPattern { pattern, .. } => pattern.id, + PatternDeviceChange::PatternExpressionChange { pattern, .. } => pattern.id, + PatternDeviceChange::RemovePattern { pattern, .. } => pattern.id, + PatternDeviceChange::ReplacePattern { + new_pattern: pattern, .. + } => pattern.id, + PatternDeviceChange::CC { .. } => 0, + PatternDeviceChange::None { .. } => 0, + PatternDeviceChange::Legato { new_pattern, .. } => new_pattern.id + } + } +} + +impl PatternDevice { + pub fn update(&mut self, change: DeviceChange) -> PatternDeviceChange { + match change { + DeviceChange::AddNote { time, note } => { + let new_pattern = Pattern::from(note); + let pattern = self.patterns.insert(note.id, new_pattern); + + match pattern { + None => PatternDeviceChange::AddPattern { + time, + pattern: new_pattern, + }, + Some(old_pattern) => PatternDeviceChange::ReplacePattern { + time, + old_pattern, + new_pattern, + }, + } + } + DeviceChange::RemoveNote { time, note } => match self.patterns.remove(¬e.id) { + None => PatternDeviceChange::None { time }, + Some(mut pattern) => { + pattern.velocity_off = note.velocity_off; + pattern.released_at = note.released_at; + PatternDeviceChange::RemovePattern { time, pattern } + } + }, + DeviceChange::NoteExpressionChange { time, expression, note } => match self.patterns.get_mut(¬e.id) { + None => PatternDeviceChange::None { time }, + Some(pattern) => { + pattern.pressure = note.pressure; + pattern.timbre = note.timbre; + pattern.pitchbend = note.pitchbend; + PatternDeviceChange::PatternExpressionChange { + time, + expression, + pattern: *pattern, + } + } + }, + DeviceChange::ReplaceNote { + time, + old_note, + new_note, + } => { + let new_pattern = Pattern::from(new_note); + match self.patterns.insert(new_pattern.id, new_pattern) { + None => { + error!("Expected to replace a pattern matching, found nothing {:?}", old_note); + PatternDeviceChange::None { time } + } + Some(old_pattern) => PatternDeviceChange::ReplacePattern { + time, + old_pattern, + new_pattern, + }, + } + } + DeviceChange::NoteLegato { time, old_note, new_note } => { + let new_pattern = Pattern::from(new_note); + match self.patterns.insert(new_pattern.id, new_pattern) { + None => { + error!("Expected to replace a pattern matching, found nothing {:?}", old_note); + PatternDeviceChange::None { time } + } + Some(old_pattern) => PatternDeviceChange::Legato { + time, + old_pattern, + new_pattern, + }, + } + }, + DeviceChange::CCChange { time, cc } => PatternDeviceChange::CC { cc, time }, + DeviceChange::Ignored { time } => PatternDeviceChange::None { time }, + } + } + + pub fn at(&self, index: u8) -> Filter, PatternIteratorClosure> { + self.patterns + .values() + .filter(Box::new(move |pattern| pattern.index == index)) + } +} + +type PatternIteratorClosure = Box bool>; diff --git a/arpegiator/src/midi_messages/timed_event.rs b/arpegiator/src/midi_messages/timed_event.rs new file mode 100644 index 0000000..1b5d922 --- /dev/null +++ b/arpegiator/src/midi_messages/timed_event.rs @@ -0,0 +1,4 @@ +pub trait TimedEvent { + fn timestamp(&self) -> usize; + fn id(&self) -> usize; +} diff --git a/arpegiator/src/parameters.rs b/arpegiator/src/parameters.rs new file mode 100644 index 0000000..0363dfc --- /dev/null +++ b/arpegiator/src/parameters.rs @@ -0,0 +1,265 @@ +#[allow(unused_imports)] +use { + async_channel::Sender, + log::{error, info}, + std::sync::Mutex, + util::parameter_value_conversion::{f32_to_bool, f32_to_byte}, +}; + +use vst::plugin::PluginParameters; +use vst::util::ParameterTransfer; + +#[cfg(not(feature = "midi_hack_transmission"))] +use crate::workers::main_worker::WorkerCommand; +use util::duration_display; +use util::parameters::{ParameterConversion, get_exponential_scale_value}; +use util::system::Uuid; + +#[cfg(not(feature = "midi_hack_transmission"))] +pub const PARAMETER_COUNT: usize = 5; + +#[cfg(feature = "midi_hack_transmission")] +pub const PARAMETER_COUNT: usize = 4; + +#[cfg(not(feature = "midi_hack_transmission"))] +const BASE_PORT: u16 = 6000; + +pub(crate) enum PitchBendValues { + Off, // no pitchbend, means same pitch until pattern ends + DurationToReachTarget(f32), + Immediate, +} + +impl From for PitchBendValues { + fn from(x: f32) -> Self { + match x { + x if x <= 0. => PitchBendValues::Immediate, + x if x >= 1. => PitchBendValues::Off, + _ => { + let value = get_exponential_scale_value(x, 5., 80.); + PitchBendValues::DurationToReachTarget(value) + } + } + } +} + +// TODO trait to string, change parameter to string in parameters trait + +impl ToString for PitchBendValues { + fn to_string(&self) -> String { + match self { + PitchBendValues::Off => "Off".to_string(), + PitchBendValues::DurationToReachTarget(x) => duration_display(*x), + PitchBendValues::Immediate => "Immediate".to_string() + } + } +} + +pub(crate) enum VelocitySource { + Pattern, + Mixed(f32), + Notes +} + + +impl ToString for VelocitySource { + fn to_string(&self) -> String { + match self { + VelocitySource::Pattern => "Pattern".to_string(), + VelocitySource::Mixed(x) => format!("{}%", (x * 100.) as u8), + VelocitySource::Notes => "Notes".to_string() + } + } +} + +impl From for VelocitySource { + fn from(x: f32) -> Self { + match x { + x if x <= 0. => VelocitySource::Pattern, + x if x >= 1. => VelocitySource::Notes, + x => { + VelocitySource::Mixed(x) + } + } + } +} + + +pub(crate) struct ArpegiatorParameters { + pub transfer: ParameterTransfer, + #[cfg(not(feature = "midi_hack_transmission"))] + pub worker_commands: Mutex>>, +} + +impl ArpegiatorParameters { + #[cfg(not(feature = "midi_hack_transmission"))] + pub fn get_port(&self) -> u16 { + BASE_PORT + self.get_byte_parameter(Parameter::PortIndex) as u16 + } + + #[cfg(not(feature = "midi_hack_transmission"))] + pub fn update_port(&self, event_id: Uuid) { + let port = self.get_byte_parameter(Parameter::PortIndex) as u16 + BASE_PORT; + info!("Applying parameter change: port={}", port); + if let Err(error) = self + .worker_commands + .lock() + .unwrap() + .as_ref() + .unwrap() + .try_send(WorkerCommand::SetPort(port, event_id)) + { + info!( + "[{}] main worker is shutdown - ignoring port change ({})", + event_id, error + ); + } + } +} + +#[repr(i32)] +pub enum Parameter { + HoldNotes = 0, // a started pattern will find a note to play, even if no note is playing for that index + PatternLegato, // pattern is not restarted if start/end match, and note is thus held + Pitchbend, + VelocitySource, + #[cfg(not(feature = "midi_hack_transmission"))] + PortIndex, +} + +impl From for Parameter { + fn from(i: i32) -> Self { + match i { + 0 => Parameter::HoldNotes, + 1 => Parameter::PatternLegato, + 2 => Parameter::Pitchbend, + 3 => Parameter::VelocitySource, + #[cfg(not(feature = "midi_hack_transmission"))] + 4 => Parameter::PortIndex, + _ => panic!("no such parameter {}", i), + } + } +} + +impl From for i32 { + fn from(p: Parameter) -> Self { + p as i32 + } +} + +impl ParameterConversion for ArpegiatorParameters { + fn get_parameter_transfer(&self) -> &ParameterTransfer { + &self.transfer + } + + fn get_parameter_count() -> usize { + PARAMETER_COUNT + } +} + +impl ArpegiatorParameters { + pub fn new() -> Self { + let parameters = ArpegiatorParameters { + transfer: ParameterTransfer::new(PARAMETER_COUNT), + #[cfg(not(feature = "midi_hack_transmission"))] + worker_commands: Mutex::new(None), + }; + parameters.set_parameter(Parameter::PatternLegato.into(), 1.); + parameters + } + + pub fn get_pitchbend(&self) -> PitchBendValues { + PitchBendValues::from(self.get_parameter(Parameter::Pitchbend.into())) + } + + pub fn get_velocitysource(&self) -> VelocitySource { + VelocitySource::from(self.get_parameter(Parameter::VelocitySource.into())) + } +} + +impl PluginParameters for ArpegiatorParameters { + fn get_parameter_text(&self, index: i32) -> String { + let parameter = index.into(); + match parameter { + #[cfg(not(feature = "midi_hack_transmission"))] + Parameter::PortIndex => self.get_port().to_string(), + Parameter::HoldNotes | Parameter::PatternLegato => match self.get_bool_parameter(parameter) { + true => "On", + false => "Off", + } + .to_string(), + Parameter::Pitchbend => self.get_pitchbend().to_string(), + Parameter::VelocitySource => self.get_velocitysource().to_string() + } + } + + fn get_parameter_name(&self, index: i32) -> String { + match index.into() { + #[cfg(not(feature = "midi_hack_transmission"))] + Parameter::PortIndex => "Port", + Parameter::HoldNotes => "Hold notes", + Parameter::PatternLegato => "Pattern Legato", + Parameter::Pitchbend => "Use pitchbend", + Parameter::VelocitySource => "Velocity", + } + .to_string() + } + + fn get_parameter(&self, index: i32) -> f32 { + self.get_parameter_transfer().get_parameter(index as usize) + } + + fn set_parameter(&self, index: i32, value: f32) { + let parameter = index.into(); + match parameter { + #[cfg(not(feature = "midi_hack_transmission"))] + Parameter::PortIndex => { + let new_value = f32_to_byte(value); + let old_value = self.get_byte_parameter(Parameter::PortIndex); + if old_value != new_value { + let event_id = Uuid::new_v4(); + info!("[{}] set parameter port {}", event_id, BASE_PORT + new_value as u16); + self.transfer.set_parameter(index as usize, value); + self.update_port(event_id) + } + } + Parameter::HoldNotes | Parameter::PatternLegato => { + let new_value = f32_to_bool(value); + let old_value = self.get_bool_parameter(parameter); + if old_value != new_value { + self.transfer.set_parameter(index as usize, value); + } + } + Parameter::Pitchbend => self.transfer.set_parameter(index as usize, value), + Parameter::VelocitySource => self.transfer.set_parameter(index as usize, value), + } + } + + fn get_preset_data(&self) -> Vec { + self.serialize_state() + } + + fn get_bank_data(&self) -> Vec { + self.serialize_state() + } + + fn load_preset_data(&self, data: &[u8]) { + let event_id = Uuid::new_v4(); + info!("[{}] Load present data", event_id); + self.deserialize_state(data); + #[cfg(not(feature = "midi_hack_transmission"))] + { + self.update_port(event_id) + } + } + + fn load_bank_data(&self, data: &[u8]) { + let event_id = Uuid::new_v4(); + info!("[{}] Load bank data", event_id); + self.deserialize_state(data); + #[cfg(not(feature = "midi_hack_transmission"))] + { + self.update_port(event_id) + } + } +} diff --git a/arpegiator/src/system.rs b/arpegiator/src/system.rs new file mode 100644 index 0000000..c098d11 --- /dev/null +++ b/arpegiator/src/system.rs @@ -0,0 +1,10 @@ +#[cfg(target_os = "macos")] +#[inline] +pub fn second_to_mach_timebase() -> f64 { + let mut timebase_info = mach::mach_time::mach_timebase_info { numer: 0, denom: 0 }; + unsafe { + mach::mach_time::mach_timebase_info(&mut timebase_info); + } + + 10e9 * timebase_info.denom as f64 / timebase_info.numer as f64 +} diff --git a/arpegiator/src/workers/ipc_worker.rs b/arpegiator/src/workers/ipc_worker.rs new file mode 100644 index 0000000..3605254 --- /dev/null +++ b/arpegiator/src/workers/ipc_worker.rs @@ -0,0 +1,273 @@ +#[allow(unused_imports)] +use log::{error, info}; + +use async_channel::Sender; +use async_std::net::UdpSocket; +use async_std::task; +use ipc_channel::ipc::{IpcReceiver, IpcReceiverSet, IpcSelectionResult, IpcSender}; + +use util::ipc_payload::{BootstrapPayload, IPCCommand, PatternPayload}; + +use crate::workers::main_worker::WorkerCommand; +use std::mem::take; +use std::{error, thread}; +use util::system::Uuid; + +pub(crate) enum IPCWorkerCommand { + SocketReceive(IpcReceiver, Uuid), + Stop(Sender<()>, Uuid), + IPCDisconnect(Uuid), + PayloadReceived(PatternPayload, Uuid), +} + +fn bootstrap_ipc(ipc_server_name: String) -> Result, Box> { + let ipc_bootstrap_sender = IpcSender::connect(ipc_server_name)?; + let (ipc_sender, ipc_receiver) = ipc_channel::ipc::channel()?; + ipc_bootstrap_sender.send(BootstrapPayload::Channel(ipc_sender))?; + Ok(ipc_receiver) +} + +async fn udp_receive_worker(socket: UdpSocket, sender: Sender) { + let mut buf = vec![0u8; 1024]; + let mut exit_event_id = Uuid::default(); + + while let Ok(len) = socket.recv(&mut buf).await { + exit_event_id = Uuid::new_v4(); + + info!("[{}] Received {} bytes", exit_event_id, len); + + let ipc_worker_command = match bincode::deserialize::(&buf[..len]) { + Ok(ipc_server_name) => match bootstrap_ipc(ipc_server_name) { + Ok(ipc_receiver) => { + info!("[{}] Received IPC Receiver via UDP", exit_event_id); + IPCWorkerCommand::SocketReceive(ipc_receiver, exit_event_id) + } + Err(err) => { + info!("[{}] failed to get an IPC Receiver via UDP: {}", exit_event_id, err); + continue; + } + }, + Err(err) => { + error!( + "[{}] Ignoring invalid UDP payload ({}) - expected a name string", + exit_event_id, err + ); + continue; + } + }; + + if let Err(err) = sender.send(ipc_worker_command).await { + error!( + "[{}] UDP Worker: error while trying to send worker command {}, quitting UDP Worker", + exit_event_id, err + ); + break; + } + } + + #[cfg(feature = "worker_debug")] + info!("[{}] Leaving udp receiver worker", exit_event_id) +} + +fn spawn_ipc_receiver_thread( + ipc_receiver: IpcReceiver, + ipc_worker_sender: Sender, +) -> Result, Box> { + let mut set = IpcReceiverSet::new()?; + set.add(ipc_receiver)?; + + let (sender, receiver) = ipc_channel::ipc::channel::()?; + set.add(receiver)?; + + thread::spawn(|| { + task::block_on(async move { + let mut exit_event_id = Uuid::default(); + + #[cfg(feature = "worker_debug")] + info!("started ipc worker"); + 'mainloop: while let Ok(results) = set.select() { + for result in results { + let opaque_message = match result { + IpcSelectionResult::MessageReceived(_, opaque_message) => opaque_message, + IpcSelectionResult::ChannelClosed(_) => { + exit_event_id = Uuid::new_v4(); + error!("[{}] ipc channel closed", exit_event_id); + break 'mainloop; + } + }; + match opaque_message.to::().unwrap() { + IPCCommand::PatternPayload(payload) => { + let event_id = Uuid::new_v4(); + if let Err(err) = ipc_worker_sender + .send(IPCWorkerCommand::PayloadReceived(payload, event_id)) + .await + { + exit_event_id = event_id; + error!("[{}] could not send payload to ipc worker {}", exit_event_id, err); + break 'mainloop; + } + } + IPCCommand::Ping(sender) => { + info!("Received ping from peer"); + match sender.send(()) { + Ok(_) => { + info!("pong sent") + } + Err(err) => { + info!("error while sending pong: {}", err) + } + } + } + IPCCommand::Stop(ack_channel_sender, event_id) => { + exit_event_id = event_id; + #[cfg(feature = "worker_debug")] + info!("[{}] Stopping IPC worker thread", exit_event_id); + ack_channel_sender.send(()).unwrap_or_else(|err| { + error!("[{}] Could not signal ipc thread stop {}", err, exit_event_id); + }); + break 'mainloop; + } + } + } + } + ipc_worker_sender + .try_send(IPCWorkerCommand::IPCDisconnect(exit_event_id)) + .unwrap_or_else(|err| { + error!( + "[{}] Error {} while signaling IPC receiver quitting", + exit_event_id, err + ); + }); + }) + }); + + Ok(sender) +} + +pub(crate) fn spawn_ipc_worker( + port: u16, + pattern_sender: Sender, + worker_command_sender: Sender, +) -> Result, Box> { + let (ipc_worker_sender, ipc_worker_receiver) = async_channel::unbounded::(); + + let returned_ipc_worker_sender = ipc_worker_sender.clone(); + + let socket = task::block_on(UdpSocket::bind(format!("127.0.0.1:{}", port)))?; + + thread::spawn(move || { + task::block_on(async move { + let mut ipc_receiver_sender: Option> = None; + let udp_worker_handle = task::spawn(udp_receive_worker(socket, ipc_worker_sender.clone())); + + let mut exit_event_id: Uuid = Uuid::default(); + + while let Ok(command) = ipc_worker_receiver.recv().await { + match command { + IPCWorkerCommand::SocketReceive(ipc_receiver_from_socket, event_id) => { + if ipc_receiver_sender.is_some() { + if let Err(err) = + close_ipc_receiver_thread(take(&mut ipc_receiver_sender).unwrap(), event_id) + { + error!("[{}] Error while shutting down ipc receiver worker {}", event_id, err) + } + } + + let ipc_worker_sender = ipc_worker_sender.clone(); + + if let Ok(sender) = spawn_ipc_receiver_thread(ipc_receiver_from_socket, ipc_worker_sender) { + ipc_receiver_sender = Some(sender); + }; + } + IPCWorkerCommand::Stop(sender, event_id) => { + exit_event_id = event_id; + match sender.send(()).await { + Ok(_) => { + info!("[{}] Quitting socket port {}", exit_event_id, port); + } + Err(err) => { + info!("[{}] Error while quitting socket port {}: {}", exit_event_id, port, err); + } + } + break; + } + IPCWorkerCommand::IPCDisconnect(event_id) => { + ipc_receiver_sender = None; + error!("[{}] IPC Receiver disconnected", event_id) + } + IPCWorkerCommand::PayloadReceived(payload, event_id) => { + let _payload_time = payload.time; + if let Err(err) = pattern_sender.send(payload).await { + exit_event_id = event_id; + error!( + "[{}] IPC worker: notes sender channel error, quitting ({})", + exit_event_id, err + ); + break; + } + + #[cfg(feature = "device_debug")] + { + let local_time = unsafe { mach::mach_time::mach_absolute_time() }; + let diff_nanoseconds = local_time - _payload_time; + info!( + "Received time: {:?} current time: {:?} = {} nanoseconds", + payload_time, local_time, diff_nanoseconds + ); + } + } + } + } + + #[cfg(feature = "worker_debug")] + info!("[{}] stopping udp worker on port {}", exit_event_id, port); + udp_worker_handle.cancel().await; + #[cfg(feature = "worker_debug")] + info!("[{}] udp worker on port {} stopped", exit_event_id, port); + + if let Some(sender) = take(&mut ipc_receiver_sender) { + close_ipc_receiver_thread(sender, exit_event_id).unwrap_or_else(|err| { + info!( + "[{}] ipc receiver thread did not quit gracefully: {}", + exit_event_id, err + ); + }) + } + + if let Err(err) = worker_command_sender + .send(WorkerCommand::IPCWorkerStopped(exit_event_id, port)) + .await + { + info!("[{}] Could not signal main worker {}", err, exit_event_id); + } + + #[cfg(feature = "worker_debug")] + info!("[{}] exiting ipc worker ( port {} )", exit_event_id, port); + }) + }); + + Ok(returned_ipc_worker_sender) +} + +fn close_ipc_receiver_thread( + ipc_receiver_sender: IpcSender, + event_id: Uuid, +) -> Result<(), Box> { + #[cfg(feature = "worker_debug")] + info!("[{}] close_ipc_receiver_thread: enter", event_id); + let (ack_sender, ack_receiver) = ipc_channel::ipc::channel::<()>()?; + + #[cfg(feature = "worker_debug")] + info!("[{}] stopping ipc receiver", event_id); + ipc_receiver_sender.send(IPCCommand::Stop(ack_sender, event_id))?; + + #[cfg(feature = "worker_debug")] + info!("[{}] waiting for ack from ipc receiver", event_id); + ack_receiver + .try_recv() + .map_err(|x| format!("Error while receiving ack from ipc receiver {:?}", x))?; + + #[cfg(feature = "worker_debug")] + info!("[{}] stopped ipc receiver", event_id); + Ok(()) +} diff --git a/arpegiator/src/workers/main_worker.rs b/arpegiator/src/workers/main_worker.rs new file mode 100644 index 0000000..fc74eb5 --- /dev/null +++ b/arpegiator/src/workers/main_worker.rs @@ -0,0 +1,203 @@ +use std::thread; +use std::thread::JoinHandle; + +use async_channel::{unbounded, Receiver, Sender}; +use async_std::task; +use log::{error, info}; + +use util::ipc_payload::PatternPayload; +use util::midi_message_with_delta::MidiMessageWithDelta; + +use crate::workers::ipc_worker::{spawn_ipc_worker, IPCWorkerCommand}; +use crate::workers::midi_output_worker::{spawn_midi_output_worker, MidiOutputWorkerCommand}; +use std::mem::take; +use util::system::Uuid; + +#[derive(Debug)] +pub(crate) enum WorkerCommand { + Stop(Uuid), + SetPort(u16, Uuid), + SetSampleRate(f32), + SetBlockSize(i64), + SendToMidiOutput { + reception_time: u64, + messages: Vec, + }, + IPCWorkerStopped(Uuid, u16), +} + +pub(crate) struct WorkerChannels { + pub command_sender: Sender, + pub pattern_receiver: Receiver, + pub worker: JoinHandle<()>, +} + +pub(crate) fn create_worker_thread() -> WorkerChannels { + let (command_sender, command_receiver) = unbounded::(); + let (pattern_sender, pattern_receiver) = unbounded::(); + + let returned_command_sender = command_sender.clone(); + + let main = { + #[cfg(feature = "worker_debug")] + info!("starting workers"); + + async move { + let mut current_port: Option = None; + let mut midi_out_worker_sender: Option> = None; + let mut ipc_worker_sender: Option> = None; + let mut exit_event_id = Uuid::default(); + let mut exit_reason = ""; + + while let Ok(command) = command_receiver.recv().await { + match command { + WorkerCommand::SetPort(port, event_id) => { + if current_port.is_some() && current_port.unwrap() == port { + continue; + } + info!("[{}] Switching to port {}", event_id, port); + current_port = Some(port); + + close_workers(&mut midi_out_worker_sender, &mut ipc_worker_sender, event_id).await; + + { + let pattern_sender = pattern_sender.clone(); + let command_sender = command_sender.clone(); + + match spawn_ipc_worker(port, pattern_sender, command_sender) { + Ok(sender) => { + ipc_worker_sender = Some(sender); + } + Err(err) => { + exit_event_id = event_id; + exit_reason = "Cannot start ipc worker"; + + #[cfg(feature = "worker_debug")] + error!("[{}] Cannot start ipc worker: {}", exit_event_id, err); + break; + } + } + } + + match spawn_midi_output_worker(format!("Arpegiator {}", port)) { + Ok(sender) => { + midi_out_worker_sender = Some(sender); + } + Err(_) => { + exit_event_id = event_id; + exit_reason = "Could not spawn midi output worker"; + + #[cfg(feature = "worker_debug")] + error!("[{}] Could not spawn midi output worker", exit_event_id); + close_workers(&mut midi_out_worker_sender, &mut ipc_worker_sender, exit_event_id).await; + break; + } + } + } + + WorkerCommand::SendToMidiOutput { + reception_time, + messages, + } => { + if let Some(midi_out_worker_sender) = midi_out_worker_sender.as_ref() { + midi_out_worker_sender + .send(MidiOutputWorkerCommand::SendToController { + reception_time, + messages, + }) + .await + .unwrap(); + } + } + WorkerCommand::SetSampleRate(rate) => { + if let Some(midi_out_worker_sender) = midi_out_worker_sender.as_ref() { + midi_out_worker_sender + .send(MidiOutputWorkerCommand::SetSampleRate(rate)) + .await + .unwrap(); + } + } + WorkerCommand::SetBlockSize(size) => { + if let Some(midi_out_worker_sender) = midi_out_worker_sender.as_ref() { + midi_out_worker_sender + .send(MidiOutputWorkerCommand::SetBlockSize(size)) + .await + .unwrap(); + } + } + WorkerCommand::IPCWorkerStopped(event_id, ipc_worker_port) => { + if current_port.is_none() || current_port.unwrap() != ipc_worker_port { + continue; + } + exit_reason = "IPC worker stopped"; + exit_event_id = event_id; + close_workers(&mut midi_out_worker_sender, &mut ipc_worker_sender, event_id).await; + break; + } + WorkerCommand::Stop(event_id) => { + exit_reason = "Stop received"; + exit_event_id = event_id; + close_workers(&mut midi_out_worker_sender, &mut ipc_worker_sender, event_id).await; + break; + } + } + } + + #[cfg(feature = "worker_debug")] + info!("[{}] exiting main worker: {}", exit_event_id, exit_reason) + } + }; + + let worker = thread::spawn(move || task::block_on(main)); + + WorkerChannels { + command_sender: returned_command_sender, + pattern_receiver, + worker, + } +} + +async fn close_workers( + midi_out_worker_sender: &mut Option>, + ipc_worker_sender: &mut Option>, + event_id: Uuid, +) { + if let Some(ipc_worker_sender) = take(ipc_worker_sender) { + let (ack_sender, ack_receiver) = async_channel::bounded(1); + ipc_worker_sender + .send(IPCWorkerCommand::Stop(ack_sender, event_id)) + .await + .unwrap_or_else(|err| { + error!("[{}] Could not contact worker sender for shutdown : {}", event_id, err); + }); + match ack_receiver.recv().await { + Ok(_) => { + info!("[{}] ipc worker exit ack", event_id) + } + Err(err) => { + error!("[{}] ipc worker did not ack exit: {}", event_id, err) + } + } + }; + + if let Some(midi_out_worker_sender) = take(midi_out_worker_sender) { + let (ack_sender, ack_receiver) = async_channel::bounded(1); + midi_out_worker_sender + .send(MidiOutputWorkerCommand::Stop(ack_sender, event_id)) + .await + .unwrap_or_else(|err| { + error!( + "[{}] Could not contact midi output worker for shutdown : {}", + event_id, err + ); + }); + match ack_receiver.recv().await { + Ok(_) => { + info!("[{}] midi out worker exit ack", event_id) + } + Err(err) => { + error!("[{}] midi out did not ack exit: {}", event_id, err) + } + } + }; +} diff --git a/arpegiator/src/workers/midi_output_worker.rs b/arpegiator/src/workers/midi_output_worker.rs new file mode 100644 index 0000000..d2a280a --- /dev/null +++ b/arpegiator/src/workers/midi_output_worker.rs @@ -0,0 +1,151 @@ +#[allow(unused_imports)] +use log::{error, info}; + +use async_channel::{unbounded, Sender}; +use async_std::task; + +#[cfg(target_os = "macos")] +use coremidi::PacketBuffer; + +use midir::os::unix::VirtualInput; +use midir::MidiInput; +#[cfg(target_os = "linux")] +use {midir::os::unix::VirtualOutput, midir::MidiOutput}; + +use util::midi_message_with_delta::MidiMessageWithDelta; + +#[cfg(target_os = "macos")] +use crate::system::second_to_mach_timebase; +use std::error; +use util::system::Uuid; + +#[derive(Debug)] +pub(crate) enum MidiOutputWorkerCommand { + SendToController { + reception_time: u64, + messages: Vec, + }, + Stop(Sender<()>, Uuid), + SetSampleRate(f32), + SetBlockSize(i64), +} + +pub(crate) fn spawn_midi_output_worker( + name: String, +) -> Result, Box> { + #[cfg(target_os = "macos")] + let (second_to_mach, mut sample_to_mach, mut _block_size, mut block_duration): (f64, u64, u64, u64) = + { (second_to_mach_timebase(), 0, 0, 0) }; + + #[cfg(target_os = "linux")] + let mut midi_out_connection = { + info!("Creating midi device {}", name); + let midi_out = MidiOutput::new(&*name)?; + midi_out + .create_virtual(&*name) + .map_err(|e| format!("Cannot create midi out: {}", e))? + }; + + #[cfg(target_os = "macos")] + let (client, source) = { + let client = coremidi::Client::new(&*name).map_err(|x| format!("os error: {:?}", x))?; + let source = client + .virtual_source(&*name) + .map_err(|x| format!("os error: {:?}", x))?; + info!("Created virtual port {}", name); + (client, source) + }; + + let midi_in = MidiInput::new(&*name)?; + + // create input device just to ease setup. returned connection must not be dropped in order to keep the device alive + let midi_in_connection = midi_in + .create_virtual( + &*name, + |_time, _data, _| { + // noop for now + }, + (), + ) + .map_err(|x| format!("os error: {:?}", x))?; + + let (sender, receiver) = unbounded::(); + + task::spawn(async move { + // move the client inside the task, otherwise it will be dropped and the device won't appear + #[cfg(target_os = "macos")] + let _client = client; + + // move it here to keep the device visible. no use for now, just helps to have a consistent in/out setup + let _midi_in_connection = midi_in_connection; + + while let Ok(command) = receiver.recv().await { + match command { + #[allow(unused_mut)] + #[allow(unused_variables)] + MidiOutputWorkerCommand::SendToController { + reception_time, + mut messages, + } => { + #[cfg(target_os = "linux")] + for message in messages { + // TODO timing is lost, we should actually wait until buffer_start_time and wait for the + // time corresponding to delta frame for each message + if let Err(err) = midi_out_connection.send(message.data.get_bytes()) { + error!("Error while sending midi message: {}", err); + break; + } + } + + #[cfg(target_os = "macos")] + { + // we cannot accurately tell when those notes should be played, we just know when it was + // received and the earliest time being reception + time that represents a buffer + //let play_time = reception_time + block_duration; + let play_time = reception_time; + let message = messages.remove(0); + let mut buffer = PacketBuffer::new( + message.delta_frames as u64 * sample_to_mach + play_time, + message.data.get_bytes(), + ); + + for message in messages { + buffer.push_data( + message.delta_frames as u64 * sample_to_mach + play_time, + &message.data.get_bytes(), + ); + } + + source.received(&buffer).unwrap(); + } + } + MidiOutputWorkerCommand::Stop(sender, event_id) => { + match sender.send(()).await { + Ok(_) => { + info!("[{}] Stopping controller {}", event_id, name); + } + Err(err) => { + info!("[{}] Error while quitting midi out {}: {}", event_id, name, err); + } + } + return; + } + MidiOutputWorkerCommand::SetSampleRate(_rate) => + #[cfg(target_os = "macos")] + { + let sample_to_second = 1.0 / _rate as f64; + sample_to_mach = (sample_to_second * second_to_mach) as u64; + block_duration = sample_to_mach * _block_size; + } + MidiOutputWorkerCommand::SetBlockSize(_size) => + #[cfg(target_os = "macos")] + { + _block_size = _size as u64; + block_duration = sample_to_mach * _block_size; + } + } + } + }); + + Ok(sender) +} diff --git a/arpegiator/src/workers/mod.rs b/arpegiator/src/workers/mod.rs new file mode 100644 index 0000000..8392c7f --- /dev/null +++ b/arpegiator/src/workers/mod.rs @@ -0,0 +1,6 @@ +#[cfg(not(feature = "midi_hack_transmission"))] +pub mod ipc_worker; +#[cfg(not(feature = "midi_hack_transmission"))] +pub mod main_worker; +#[cfg(not(feature = "midi_hack_transmission"))] +mod midi_output_worker; diff --git a/arpegiator_pattern_receiver/Cargo.toml b/arpegiator_pattern_receiver/Cargo.toml new file mode 100644 index 0000000..8cacf4e --- /dev/null +++ b/arpegiator_pattern_receiver/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "arpegiator_pattern_receiver" +version = "0.1.0" +authors = ["Vincent Alsteen "] +edition = "2018" + +[dependencies] +vst = { git = "https://github.com/rustaudio/vst-rs" } +util = { path = "../util" } +build-info = "0.0.23" +log = "0.4.14" +serde = "1.0.126" +bincode = "1.3.3" +async-std = "1.9.0" +async-channel = "1.6.1" +futures-lite = "1.12.0" +ipc-channel = "0.15.0" +itertools = "0.10.0" + +[build-dependencies] +build-info-build = "0.0.23" + +[lib] +name = "arpegiator_pattern_receiver" +crate-type = ["cdylib", "lib"] + +[target.'cfg(target_os = "macos")'.dependencies.mach] +version = "0.3.2" + +[features] +# this transmits midi via the vst output instead of using an IPC, setting the highest bit to 0 so we transmit +# midi messages in the range 0x00-0x7F and it does not interfer with any other message. Somehow bitwig just passes +# those messages while sysex are being blocked. +default = ["midi_hack_transmission"] +midi_hack_transmission = [] diff --git a/arpegiator_pattern_receiver/build.rs b/arpegiator_pattern_receiver/build.rs new file mode 100644 index 0000000..a49ab18 --- /dev/null +++ b/arpegiator_pattern_receiver/build.rs @@ -0,0 +1,5 @@ +fn main() { + // Calling `build_info_build::build_script` collects all data and makes it available to `build_info::build_info!` + // and `build_info::format!` in the main program. + build_info_build::build_script(); +} diff --git a/arpegiator_pattern_receiver/src/ipc_worker.rs b/arpegiator_pattern_receiver/src/ipc_worker.rs new file mode 100644 index 0000000..f8de8ed --- /dev/null +++ b/arpegiator_pattern_receiver/src/ipc_worker.rs @@ -0,0 +1,134 @@ +use { + async_channel::{Receiver, Sender}, + async_std::net::UdpSocket, + async_std::task, + ipc_channel::ipc::IpcSender, + log::{error, info}, + std::net::ToSocketAddrs, + std::time::Duration, + std::{error, thread}, + util::ipc_payload::{BootstrapPayload, IPCCommand, PatternPayload}, +}; + +pub(crate) enum IPCWorkerCommand { + Stop, + SetPort(u16), + Send(PatternPayload), + TryConnect, +} + +async fn try_udp_send_receiver(port: u16) -> Result, Box> { + let socket = UdpSocket::bind("127.0.0.1:0").await?; + let to = format!("127.0.0.1:{}", port) + .to_socket_addrs()? + .next() + .ok_or("empty list")?; + + let (one_shot, name) = ipc_channel::ipc::IpcOneShotServer::new()?; + let serialized_name = bincode::serialize(&name)?; + socket.send_to(&*serialized_name, to).await?; + + let (bootstrap_result_sender, bootstrap_result_receiver) = async_channel::unbounded(); + + thread::spawn(move || { + let (_, result) = one_shot.accept().unwrap(); + match result { + BootstrapPayload::Channel(ipc_sender) => { + info!("Returning ipc_sender"); + bootstrap_result_sender.try_send(ipc_sender).unwrap() + } + BootstrapPayload::Timeout => { + error!("timed out while waiting for a connection"); + } + } + }); + + task::sleep(Duration::new(1, 0)).await; + + let ipc_sender = bootstrap_result_receiver.try_recv().or_else(|e| { + IpcSender::::connect(name) + .or(Err("Cannot connect to signal timeout on ipc bootstrap"))? + .send(BootstrapPayload::Timeout) + .or(Err("Cannot send timeout to ipc bootstrapper"))?; + Err(format!("Connection timeout {}", e)) + })?; + + let (ping_sender, ping_receiver) = ipc_channel::ipc::channel::<()>()?; + info!("sending ping"); + ipc_sender.send(IPCCommand::Ping(ping_sender))?; + task::sleep(Duration::new(1, 0)).await; + + ping_receiver.try_recv().or(Err("Pong not received"))?; + info!("Ping received"); + Ok(ipc_sender) +} + +async fn ipc_worker(ipc_worker_sender: Sender, ipc_worker_receiver: Receiver) { + let mut port = None; + let mut ipc_sender: Option> = None; + let mut retry_scheduled = false; + + while let Ok(command) = ipc_worker_receiver.recv().await { + match command { + IPCWorkerCommand::Stop => { + break; + } + IPCWorkerCommand::TryConnect => { + retry_scheduled = false; + if ipc_sender.is_some() || port.is_none() { + // already connected or not configured + continue; + } + ipc_sender = match try_udp_send_receiver(port.unwrap()).await { + Ok(ipc_sender) => Some(ipc_sender), + Err(err) => { + error!( + "Error while connecting to arpegiator on port {} : {}", + port.unwrap(), + err + ); + task::sleep(Duration::new(1, 0)).await; + retry_scheduled = true; + ipc_worker_sender.send(IPCWorkerCommand::TryConnect).await.unwrap(); + None + } + }; + } + IPCWorkerCommand::SetPort(new_port) => { + ipc_sender = None; + port = Some(new_port); + if !retry_scheduled { + ipc_worker_sender.send(IPCWorkerCommand::TryConnect).await.unwrap(); + retry_scheduled = true + } + } + IPCWorkerCommand::Send(payload) => { + let ipc_sender_ref = match ipc_sender.as_ref() { + None => { + error!("IPC not ready, ignoring {:?}", payload); + continue; + } + Some(ipc_sender) => ipc_sender, + }; + + if let Err(err) = ipc_sender_ref.send(IPCCommand::PatternPayload(payload)) { + error!("IPC failed ; will attempt to reconnect ({})", err); + ipc_sender = None; + if !retry_scheduled { + ipc_worker_sender.send(IPCWorkerCommand::TryConnect).await.unwrap(); + retry_scheduled = true + } + }; + } + } + } +} + +pub(crate) fn spawn_ipc_worker() -> Sender { + let (worker_sender, worker_receiver) = async_channel::unbounded(); + { + let worker_sender = worker_sender.clone(); + thread::spawn(move || task::block_on(ipc_worker(worker_sender, worker_receiver))) + }; + worker_sender +} diff --git a/arpegiator_pattern_receiver/src/lib.rs b/arpegiator_pattern_receiver/src/lib.rs new file mode 100644 index 0000000..f29bb28 --- /dev/null +++ b/arpegiator_pattern_receiver/src/lib.rs @@ -0,0 +1,253 @@ +use std::sync::Arc; +#[allow(unused_imports)] +use { + async_channel::Sender, + log::{error, info}, + std::mem::take, + vst::api::MidiEvent, + vst::buffer::SendEventBuffer, + vst::event::Event, +}; + +use vst::api; +use vst::buffer::AudioBuffer; +use vst::plugin::{CanDo, Category, HostCallback, Info, Plugin}; + +use util::logging::logging_setup; + +#[cfg(not(feature = "midi_hack_transmission"))] +use { + crate::ipc_worker::{spawn_ipc_worker, IPCWorkerCommand}, + util::ipc_payload::PatternPayload, + util::midi_message_with_delta::MidiMessageWithDelta, +}; + +use crate::parameters::{ArpegiatorPatternReceiverParameters, PARAMETER_COUNT}; + +#[cfg(not(feature = "midi_hack_transmission"))] +mod ipc_worker; +mod parameters; + +#[macro_use] +extern crate vst; + +plugin_main!(ArpegiatorPatternReceiver); + +struct ArpegiatorPatternReceiver { + #[allow(dead_code)] + host: HostCallback, + #[cfg(feature = "midi_hack_transmission")] + send_buffer: SendEventBuffer, + #[cfg(not(feature = "midi_hack_transmission"))] + ipc_worker_sender: Option>, + #[cfg(not(feature = "midi_hack_transmission"))] + messages: Vec, + #[cfg(feature = "midi_hack_transmission")] + messages: Vec, + current_time: usize, + resumed: bool, + parameters: Arc, +} + +impl Default for ArpegiatorPatternReceiver { + fn default() -> Self { + ArpegiatorPatternReceiver { + host: Default::default(), + #[cfg(not(feature = "midi_hack_transmission"))] + ipc_worker_sender: None, + messages: vec![], + current_time: 0, + resumed: false, + parameters: Arc::new(ArpegiatorPatternReceiverParameters::new()), + #[cfg(feature = "midi_hack_transmission")] + send_buffer: Default::default(), + } + } +} + +impl ArpegiatorPatternReceiver { + #[cfg(not(feature = "midi_hack_transmission"))] + fn stop_worker(&mut self) { + if let Some(sender) = take(&mut self.ipc_worker_sender) { + sender + .try_send(IPCWorkerCommand::Stop) + .unwrap_or_else(|err| error!("Error while closing sender channel : {}", err)); + sender.close(); + } + } +} + +impl Plugin for ArpegiatorPatternReceiver { + fn get_info(&self) -> Info { + Info { + name: "Arpegiator Pattern Receiver".to_string(), + vendor: "DJ Crontab".to_string(), + unique_id: 342112720, + parameters: PARAMETER_COUNT as i32, + category: Category::Synth, + initial_delay: 0, + version: 2, + inputs: 0, + outputs: 0, + midi_inputs: 1, + f64_precision: false, + presets: 1, + midi_outputs: 1, + preset_chunks: true, + silent_when_stopped: true, + } + } + + fn new(host: HostCallback) -> Self { + logging_setup(); + info!( + "{} midi_hack_transmission={}", + build_info::format!("{{{} v{} built with {} at {}}}", $.crate_info.name, + $.crate_info.version, + $.compiler, $.timestamp), + cfg!(feature = "midi_hack_transmission") + ); + + ArpegiatorPatternReceiver { + host, + #[cfg(not(feature = "midi_hack_transmission"))] + ipc_worker_sender: None, + messages: vec![], + current_time: 0, + resumed: false, + parameters: Arc::new(ArpegiatorPatternReceiverParameters::new()), + #[cfg(feature = "midi_hack_transmission")] + send_buffer: Default::default(), + } + } + + fn resume(&mut self) { + if self.resumed { + return; + } + self.resumed = true; + + self.current_time = 0; + + #[cfg(not(feature = "midi_hack_transmission"))] + { + self.stop_worker(); + + let sender = spawn_ipc_worker(); + + self.ipc_worker_sender = Some(sender.clone()); + sender + .try_send(IPCWorkerCommand::SetPort(self.parameters.get_port())) + .unwrap(); + + if let Ok(mut socket_command) = self.parameters.ipc_worker_sender.lock() { + *socket_command = Some(sender); + } + } + } + + fn suspend(&mut self) { + if !self.resumed { + return; + } + self.resumed = false; + + #[cfg(not(feature = "midi_hack_transmission"))] + { + self.stop_worker() + } + } + + fn can_do(&self, can_do: CanDo) -> vst::api::Supported { + use vst::api::Supported::*; + use vst::plugin::CanDo::*; + + match can_do { + SendEvents | SendMidiEvent | ReceiveEvents | ReceiveMidiEvent | Offline => Yes, + Other(s) => { + if s == "MPE" { + Yes + } else { + Maybe + } + } + _ => No, + } + } + + fn process(&mut self, buffer: &mut AudioBuffer) { + if !self.messages.is_empty() { + #[cfg(not(feature = "midi_hack_transmission"))] + if let Some(ipc_worker_sender) = &self.ipc_worker_sender { + let payload = PatternPayload { + time: { + #[cfg(target_os = "macos")] + unsafe { + mach::mach_time::mach_absolute_time() + } + #[cfg(target_os = "linux")] + 0 + }, + messages: take(&mut self.messages), + }; + ipc_worker_sender.try_send(IPCWorkerCommand::Send(payload)).unwrap() + } else { + self.messages.clear(); + } + + #[cfg(feature = "midi_hack_transmission")] + { + self.send_buffer.send_events(&self.messages, &mut self.host); + self.messages.clear() + } + } + + self.current_time += buffer.samples() + } + + fn process_events(&mut self, events: &api::Events) { + #[cfg(not(feature = "midi_hack_transmission"))] + if self.ipc_worker_sender.is_none() { + return; + } + + self.messages.extend( + events + .events() + .map(|event| match event { + #[allow(unused_mut)] + Event::Midi(mut event) => { + #[cfg(not(feature = "midi_hack_transmission"))] + { + Ok(MidiMessageWithDelta { + delta_frames: event.delta_frames as u16, + data: event.data.into(), + }) + } + #[cfg(feature = "midi_hack_transmission")] + { + event.data[0] -= 0x80; + Ok(event) + } + } + Event::SysEx(_) => Err(()), + Event::Deprecated(_) => Err(()), + }) + .filter(|item| item.is_ok()) + .map(|item| item.unwrap()), + ); + } + + fn get_parameter_object(&mut self) -> Arc { + Arc::clone(&self.parameters) as Arc + } +} + +impl Drop for ArpegiatorPatternReceiver { + fn drop(&mut self) { + #[cfg(not(feature = "midi_hack_transmission"))] + { + self.stop_worker(); + } + } +} diff --git a/arpegiator_pattern_receiver/src/parameters.rs b/arpegiator_pattern_receiver/src/parameters.rs new file mode 100644 index 0000000..b2905db --- /dev/null +++ b/arpegiator_pattern_receiver/src/parameters.rs @@ -0,0 +1,147 @@ +#[allow(unused_imports)] +use { + log::{error, info}, + std::error, + util::parameter_value_conversion::f32_to_byte, +}; + +use vst::plugin::PluginParameters; +use vst::util::ParameterTransfer; + +use util::parameters::ParameterConversion; +#[cfg(not(feature = "midi_hack_transmission"))] +use {crate::ipc_worker::IPCWorkerCommand, async_channel::Sender, std::sync::Mutex}; + +#[cfg(feature = "midi_hack_transmission")] +pub const PARAMETER_COUNT: usize = 0; +#[cfg(not(feature = "midi_hack_transmission"))] +pub const PARAMETER_COUNT: usize = 1; + +const BASE_PORT: u16 = 6000; + +pub(crate) struct ArpegiatorPatternReceiverParameters { + pub transfer: ParameterTransfer, + #[cfg(not(feature = "midi_hack_transmission"))] + pub ipc_worker_sender: Mutex>>, +} + +impl ArpegiatorPatternReceiverParameters { + pub fn get_port(&self) -> u16 { + BASE_PORT + self.get_byte_parameter(Parameter::PortIndex) as u16 + } + + #[cfg(not(feature = "midi_hack_transmission"))] + fn update_port(&self) -> Result<(), Box> { + let port = self.get_byte_parameter(Parameter::PortIndex); + self.ipc_worker_sender + .lock()? + .as_ref() + .ok_or("cannot unlock ipc worker sender mutex")? + .try_send(IPCWorkerCommand::SetPort(BASE_PORT + port as u16))?; + Ok(()) + } +} + +#[repr(i32)] +pub enum Parameter { + PortIndex = 0, +} + +impl From for Parameter { + fn from(i: i32) -> Self { + match i { + 0 => Parameter::PortIndex, + _ => panic!("no such parameter {}", i), + } + } +} + +impl From for i32 { + fn from(p: Parameter) -> Self { + p as i32 + } +} + +impl ParameterConversion for ArpegiatorPatternReceiverParameters { + fn get_parameter_transfer(&self) -> &ParameterTransfer { + &self.transfer + } + + fn get_parameter_count() -> usize { + PARAMETER_COUNT + } +} + +impl ArpegiatorPatternReceiverParameters { + pub fn new() -> Self { + ArpegiatorPatternReceiverParameters { + transfer: ParameterTransfer::new(PARAMETER_COUNT), + #[cfg(not(feature = "midi_hack_transmission"))] + ipc_worker_sender: Mutex::new(None), + } + } +} + +impl PluginParameters for ArpegiatorPatternReceiverParameters { + fn get_parameter_text(&self, index: i32) -> String { + match index.into() { + Parameter::PortIndex => self.get_port().to_string(), + } + } + + fn get_parameter_name(&self, index: i32) -> String { + match index.into() { + Parameter::PortIndex => "Port", + } + .to_string() + } + + fn get_parameter(&self, index: i32) -> f32 { + self.get_parameter_transfer().get_parameter(index as usize) + } + + fn set_parameter(&self, index: i32, value: f32) { + match index.into() { + Parameter::PortIndex => { + #[cfg(not(feature = "midi_hack_transmission"))] { + let new_value = f32_to_byte(value); + let old_value = self.get_byte_parameter(Parameter::PortIndex); + if old_value != new_value { + self.transfer.set_parameter(index as usize, value); + self.update_port().unwrap_or_else(|err| { + error!("Could not update port: {}", err); + }); + } + } + } + } + } + + fn get_preset_data(&self) -> Vec { + self.serialize_state() + } + + fn get_bank_data(&self) -> Vec { + self.serialize_state() + } + + fn load_preset_data(&self, data: &[u8]) { + self.deserialize_state(data); + #[cfg(not(feature = "midi_hack_transmission"))] + { + self.update_port().unwrap_or_else(|err| { + error!("Could not update port: {}", err); + }); + } + } + + fn load_bank_data(&self, data: &[u8]) { + self.deserialize_state(data); + #[cfg(not(feature = "midi_hack_transmission"))] + { + self.update_port().unwrap_or_else(|err| { + error!("Could not update port: {}", err); + }); + } + } +} diff --git a/audio_data/Cargo.toml b/audio_data/Cargo.toml new file mode 100644 index 0000000..1405439 --- /dev/null +++ b/audio_data/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "audio_data" +version = "0.1.0" +authors = ["Vincent Alsteen "] +edition = "2018" + +[dependencies] +vst = { git = "https://github.com/rustaudio/vst-rs" } +util = { path = "../util" } +build-info = "0.0.23" +log = "0.4.14" + +[build-dependencies] +build-info-build = "0.0.23" + +[lib] +name = "audio_data" +crate-type = ["cdylib", "lib"] diff --git a/audio_data/build.rs b/audio_data/build.rs new file mode 100644 index 0000000..a49ab18 --- /dev/null +++ b/audio_data/build.rs @@ -0,0 +1,5 @@ +fn main() { + // Calling `build_info_build::build_script` collects all data and makes it available to `build_info::build_info!` + // and `build_info::format!` in the main program. + build_info_build::build_script(); +} diff --git a/audio_data/src/lib.rs b/audio_data/src/lib.rs new file mode 100644 index 0000000..affabfc --- /dev/null +++ b/audio_data/src/lib.rs @@ -0,0 +1,120 @@ +#[macro_use] +extern crate vst; + +use log::info; + +use std::time::SystemTime; +use util::logging::logging_setup; +use util::transmute_buffer::{transmute_raw_buffer, transmute_raw_buffer_mut}; +use vst::api; +use vst::buffer::{AudioBuffer, SendEventBuffer}; +use vst::event::{Event, MidiEvent}; +use vst::plugin::{CanDo, Category, HostCallback, Info, Plugin}; + +plugin_main!(AudioData); + +pub struct AudioData { + events: Vec, + send_buffer: SendEventBuffer, + host: HostCallback, + last_was: u128, + last_now: u128, +} + +impl Default for AudioData { + fn default() -> Self { + AudioData { + events: vec![], + send_buffer: Default::default(), + host: Default::default(), + last_was: 0, + last_now: 0, + } + } +} + +impl AudioData { + fn send_midi(&mut self) { + self.send_buffer.send_events(&self.events, &mut self.host); + self.events.clear(); + } +} + +impl Plugin for AudioData { + fn get_info(&self) -> Info { + Info { + name: "Audio data".to_string(), + vendor: "DJ Crontab".to_string(), + unique_id: 342131710, + parameters: 0, + category: Category::Synth, + initial_delay: 0, + version: 1, + inputs: 2, + outputs: 2, + midi_inputs: 1, + f64_precision: false, + presets: 1, + midi_outputs: 1, + preset_chunks: true, + silent_when_stopped: true, + } + } + + fn new(host: HostCallback) -> Self { + logging_setup(); + AudioData { + events: vec![], + send_buffer: Default::default(), + host, + last_was: 0, + last_now: 0, + } + } + + fn can_do(&self, can_do: CanDo) -> vst::api::Supported { + use vst::api::Supported::*; + use vst::plugin::CanDo::*; + + match can_do { + SendEvents | SendMidiEvent | ReceiveEvents | ReceiveMidiEvent => Yes, + _ => No, + } + } + + fn process(&mut self, buffer: &mut AudioBuffer) { + self.send_midi(); + let (inputs, mut outputs) = buffer.split(); + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_micros(); + + let input_buffer = transmute_raw_buffer(inputs.get(0)); + let was = input_buffer[0]; + + if self.last_was != was { + self.last_was = was; + info!( + "difference = {} microseconds. Since last check: diff={} now={} was={}", + now - was, + now - self.last_now, + now, + was + ); + } + + self.last_now = now; + + let output_buffer = transmute_raw_buffer_mut(outputs.get_mut(0)); + output_buffer[0] = now + } + + fn process_events(&mut self, events: &api::Events) { + for e in events.events() { + if let Event::Midi(e) = e { + self.events.push(e); + } + } + } +} diff --git a/max_note_duration/Cargo.toml b/max_note_duration/Cargo.toml index c558771..9a9efc0 100644 --- a/max_note_duration/Cargo.toml +++ b/max_note_duration/Cargo.toml @@ -7,10 +7,10 @@ edition = "2018" [dependencies] vst = { git = "https://github.com/rustaudio/vst-rs" } util = { path = "../util" } -build-info = "0.0.20" +build-info = "0.0.23" [build-dependencies] -build-info-build = "0.0.20" +build-info-build = "0.0.23" [lib] name = "max_note_duration" diff --git a/max_note_duration/src/lib.rs b/max_note_duration/src/lib.rs index a0d9436..ce12862 100644 --- a/max_note_duration/src/lib.rs +++ b/max_note_duration/src/lib.rs @@ -5,17 +5,17 @@ extern crate vst; use std::sync::Arc; +use vst::api::Events; use vst::buffer::{AudioBuffer, SendEventBuffer}; -use vst::plugin::{CanDo, Category, HostCallback, Info, Plugin}; use vst::event::{Event, MidiEvent}; -use vst::api::Events; +use vst::plugin::{CanDo, Category, HostCallback, Info, Plugin}; use parameters::MaxNoteDurationPluginParameters; use parameters::Parameter; -use util::midi_message_type::MidiMessageType; -use util::parameters::ParameterConversion; use std::collections::HashSet; use std::hash::{Hash, Hasher}; +use util::midi_message_type::MidiMessageType; +use util::parameters::ParameterConversion; plugin_main!(MaxNoteDurationPlugin); @@ -32,7 +32,6 @@ impl PartialEq for PlayingNote { } } - impl Hash for PlayingNote { fn hash(&self, state: &mut H) { self.channel.hash(state); @@ -40,7 +39,6 @@ impl Hash for PlayingNote { } } - pub struct MaxNoteDurationPlugin { current_time_in_samples: usize, parameters: Arc, @@ -67,7 +65,6 @@ impl Default for MaxNoteDurationPlugin { } } - impl MaxNoteDurationPlugin { #[allow(dead_code)] fn seconds_per_sample(&self) -> f32 { @@ -84,7 +81,6 @@ impl MaxNoteDurationPlugin { } } - impl Plugin for MaxNoteDurationPlugin { fn get_info(&self) -> Info { Info { @@ -134,13 +130,18 @@ impl Plugin for MaxNoteDurationPlugin { use vst::plugin::CanDo::*; match can_do { - SendEvents | SendMidiEvent | ReceiveEvents | ReceiveMidiEvent | Offline | ReceiveTimeInfo | MidiKeyBasedInstrumentControl | Bypass => Yes, + SendEvents + | SendMidiEvent + | ReceiveEvents + | ReceiveMidiEvent + | Offline + | ReceiveTimeInfo + | MidiKeyBasedInstrumentControl + | Bypass => Yes, MidiProgramNames => No, ReceiveSysExEvent => Yes, MidiSingleNoteTuningChange => No, - Other(_) => { - Maybe - } + Other(_) => Maybe, } } @@ -173,8 +174,11 @@ impl Plugin for MaxNoteDurationPlugin { } fn process_events(&mut self, events: &Events) { - let maximum_duration = self.seconds_to_samples(self.parameters.get_exponential_scale_parameter(Parameter::MaxDuration, - 10., 20.)); + let maximum_duration = self.seconds_to_samples(self.parameters.get_exponential_scale_parameter( + Parameter::MaxDuration, + 10., + 20., + )); for event in events.events() { let midi_event = if let Event::Midi(midi_event) = event { @@ -188,7 +192,7 @@ impl Plugin for MaxNoteDurationPlugin { self.current_playing_notes.remove(&PlayingNote { channel: note.channel, pitch: note.pitch, - deadline: 0, // ignored for comparisons + deadline: 0, // ignored for comparisons }); } MidiMessageType::NoteOnMessage(note) => { diff --git a/max_note_duration/src/parameters.rs b/max_note_duration/src/parameters.rs index 0fd0fb5..cc99df9 100644 --- a/max_note_duration/src/parameters.rs +++ b/max_note_duration/src/parameters.rs @@ -1,10 +1,10 @@ use std::sync::Mutex; -use vst::plugin::HostCallback ; +use vst::plugin::HostCallback; use vst::util::ParameterTransfer; -use util::{HostCallbackLock, duration_display}; use util::parameters::ParameterConversion; +use util::{duration_display, HostCallbackLock}; const PARAMETER_COUNT: usize = 1; @@ -27,14 +27,12 @@ impl From for Parameter { } } - -impl Into for Parameter { - fn into(self) -> i32 { - self as i32 +impl From for i32 { + fn from(p: Parameter) -> Self { + p as i32 } } - impl ParameterConversion for MaxNoteDurationPluginParameters { fn get_parameter_transfer(&self) -> &ParameterTransfer { &self.transfer @@ -45,7 +43,6 @@ impl ParameterConversion for MaxNoteDurationPluginParameters { } } - impl MaxNoteDurationPluginParameters { pub fn new(host: HostCallback) -> Self { MaxNoteDurationPluginParameters { @@ -55,7 +52,6 @@ impl MaxNoteDurationPluginParameters { } } - impl Default for MaxNoteDurationPluginParameters { fn default() -> Self { MaxNoteDurationPluginParameters { @@ -65,7 +61,6 @@ impl Default for MaxNoteDurationPluginParameters { } } - impl vst::plugin::PluginParameters for MaxNoteDurationPluginParameters { fn get_parameter_text(&self, index: i32) -> String { match index.into() { diff --git a/midi_delay/Cargo.toml b/midi_delay/Cargo.toml index 28e2e3f..9add6d6 100644 --- a/midi_delay/Cargo.toml +++ b/midi_delay/Cargo.toml @@ -7,7 +7,12 @@ edition = "2018" [dependencies] vst = { git = "https://github.com/rustaudio/vst-rs" } util = { path = "../util" } +log = "0.4.11" +build-info = "0.0.23" [lib] name = "midi_delay" crate-type = ["cdylib", "lib"] + +[build-dependencies] +build-info-build = "0.0.23" diff --git a/midi_delay/build.rs b/midi_delay/build.rs new file mode 100644 index 0000000..a49ab18 --- /dev/null +++ b/midi_delay/build.rs @@ -0,0 +1,5 @@ +fn main() { + // Calling `build_info_build::build_script` collects all data and makes it available to `build_info::build_info!` + // and `build_info::format!` in the main program. + build_info_build::build_script(); +} diff --git a/midi_delay/src/lib.rs b/midi_delay/src/lib.rs index 35d4f4a..a697047 100644 --- a/midi_delay/src/lib.rs +++ b/midi_delay/src/lib.rs @@ -1,10 +1,13 @@ mod parameters; +#[allow(unused_imports)] +use log::{error, info}; + #[macro_use] extern crate vst; -use std::sync::Arc; use std::cell::RefCell; +use std::sync::Arc; use vst::api::Events; use vst::buffer::{AudioBuffer, SendEventBuffer}; use vst::event::Event; @@ -12,24 +15,23 @@ use vst::plugin::{CanDo, Category, HostCallback, Info, Plugin, PluginParameters} use parameters::{MidiDelayParameters, Parameter}; use util::absolute_time_midi_message_vector::AbsoluteTimeMidiMessageVector; -use util::delayed_message_consumer::{process_scheduled_events, MessageReason}; -use util::midi_message_type::MidiMessageType; +use util::delayed_message_consumer::raw_process_scheduled_events; use util::parameters::ParameterConversion; - - +use vst::host::Host; +use util::logging::logging_setup; +use util::SyncDuration; plugin_main!(MidiDelay); - pub struct MidiDelay { current_time_in_samples: usize, message_queue: AbsoluteTimeMidiMessageVector, parameters: Arc, sample_rate: f32, + bpm: f64, send_buffer: RefCell, } - impl Default for MidiDelay { fn default() -> Self { MidiDelay { @@ -37,12 +39,12 @@ impl Default for MidiDelay { message_queue: Default::default(), parameters: Arc::new(Default::default()), sample_rate: 44100.0, + bpm: 0.0, send_buffer: Default::default(), } } } - impl MidiDelay { fn increase_time_in_samples(&mut self, samples: usize) { let new_time_in_samples = self.current_time_in_samples + samples; @@ -50,7 +52,7 @@ impl MidiDelay { } #[allow(dead_code)] - fn seconds_per_sample(&self) -> f32 { + fn samples_to_seconds(&self) -> f32 { 1.0 / self.sample_rate } @@ -60,22 +62,34 @@ impl MidiDelay { fn send_events(&mut self, samples: usize) { if let Ok(mut host_callback_lock) = self.parameters.host.lock() { - let (next_message_queue, events) - = process_scheduled_events( + let (next_message_queue, events) = raw_process_scheduled_events( samples, self.current_time_in_samples, &self.message_queue, - 0, - false, - self.parameters.get_parameter(Parameter::Delay.into()) > 0.0 - ); + ); self.message_queue = next_message_queue; - self.send_buffer.borrow_mut().send_events(events, &mut host_callback_lock.host); + self.send_buffer + .borrow_mut() + .send_events(events, &mut host_callback_lock.host); } } -} + fn update_bpm(&mut self) { + use vst::api::TimeInfoFlags; + if let Ok(host_callback_lock) = self.parameters.host.lock() { + match host_callback_lock.host.get_time_info(TimeInfoFlags::TEMPO_VALID.bits()) { + None => (), + Some(ti) => { + if ti.flags & TimeInfoFlags::TEMPO_VALID.bits() != 0 { + self.bpm = ti.tempo; + info!("{}", self.bpm); + } + } + } + } + } +} impl Plugin for MidiDelay { fn get_info(&self) -> Info { @@ -83,7 +97,7 @@ impl Plugin for MidiDelay { name: "Midi Delay".to_string(), vendor: "DJ Crontab".to_string(), unique_id: 133498, - parameters: 1, + parameters: 2, category: Category::Effect, initial_delay: 0, version: 2, @@ -99,6 +113,8 @@ impl Plugin for MidiDelay { } fn new(host: HostCallback) -> Self { + logging_setup(); + info!("{}", build_info::format!("{{{} v{} built with {} at {}}}", $.crate_info.name, $.crate_info.version, $.compiler, $.timestamp)); let parameters = MidiDelayParameters::new(host); MidiDelay { @@ -106,6 +122,7 @@ impl Plugin for MidiDelay { message_queue: Default::default(), parameters: Arc::new(parameters), sample_rate: 44100.0, + bpm: 0.0, send_buffer: Default::default(), } } @@ -118,7 +135,7 @@ impl Plugin for MidiDelay { SendEvents | SendMidiEvent | ReceiveEvents | ReceiveMidiEvent | Offline | Bypass => Yes, MidiProgramNames | ReceiveSysExEvent | MidiSingleNoteTuningChange => No, Other(_) => Maybe, - _ => Maybe + _ => Maybe, } } @@ -136,43 +153,30 @@ impl Plugin for MidiDelay { } fn process_events(&mut self, events: &Events) { - let midi_delay = self.seconds_to_samples( - self.parameters.get_exponential_scale_parameter(Parameter::Delay, 1., 80.) - ); - + self.update_bpm(); + let mut midi_delay = self.seconds_to_samples(self.parameters.get_exponential_scale_parameter( + Parameter::Delay, + 1., + 80., + )); + let sync_delay = SyncDuration::from(self.parameters.get_parameter(Parameter::SyncDelay.into())); + match sync_delay.delay_to_samples(self.bpm, self.sample_rate) { + None => {} + Some(delay) => { + midi_delay += delay + } + } for event in events.events() { let midi_event = if let Event::Midi(midi_event) = event { midi_event } else { - continue + continue; }; - if let MidiMessageType::NoteOffMessage(_) = MidiMessageType::from(&midi_event.data) { - // TODO because of changes in process_scheduled_events specific to delay note off plugin, - // note offs need a special handling ; not all usages of process_scheduled_events should have to care - // about that - - self.message_queue.insert_message( - midi_event.data, - midi_event.delta_frames as usize + self.current_time_in_samples, - MessageReason::Live, - ); - - if midi_delay > 0 { - self.message_queue.insert_message( - midi_event.data, - midi_delay + midi_event.delta_frames as usize + self.current_time_in_samples, - MessageReason::Delayed, - ); - } - } else { - self.message_queue.insert_message( - midi_event.data, - midi_delay + midi_event.delta_frames as usize + self.current_time_in_samples, MessageReason::Live - ); - } - - + self.message_queue.raw_insert( + midi_event.data, + midi_event.delta_frames as usize + self.current_time_in_samples + midi_delay, + ); } } } diff --git a/midi_delay/src/parameters.rs b/midi_delay/src/parameters.rs index f7fbb74..4e1a20e 100644 --- a/midi_delay/src/parameters.rs +++ b/midi_delay/src/parameters.rs @@ -1,8 +1,8 @@ use std::sync::Mutex; -use util::{HostCallbackLock, duration_display}; +use util::parameters::ParameterConversion; +use util::{duration_display, HostCallbackLock, SyncDuration}; use vst::plugin::{HostCallback, PluginParameters}; use vst::util::ParameterTransfer; -use util::parameters::ParameterConversion; pub struct MidiDelayParameters { pub host: Mutex, @@ -12,32 +12,32 @@ pub struct MidiDelayParameters { #[repr(i32)] pub enum Parameter { Delay = 0, + SyncDelay = 1, } - impl From for Parameter { fn from(i: i32) -> Self { match i { 0 => Parameter::Delay, - _ => panic!(format!("No such Parameter {}", i)), + 1 => Parameter::SyncDelay, + _ => panic!("No such Parameter {}", i), } } } -impl Into for Parameter { - fn into(self) -> i32 { - self as i32 +impl From for i32 { + fn from(p: Parameter) -> Self { + p as i32 } } - impl ParameterConversion for MidiDelayParameters { fn get_parameter_transfer(&self) -> &ParameterTransfer { &self.transfer } fn get_parameter_count() -> usize { - 1 + 2 } } @@ -45,12 +45,11 @@ impl MidiDelayParameters { pub fn new(host: HostCallback) -> Self { MidiDelayParameters { host: Mutex::new(HostCallbackLock { host }), - transfer: ParameterTransfer::new(1), + transfer: ParameterTransfer::new(2), } } } - impl PluginParameters for MidiDelayParameters { fn get_parameter_text(&self, index: i32) -> String { match index.into() { @@ -62,13 +61,20 @@ impl PluginParameters for MidiDelayParameters { "Off".to_string() } } + Parameter::SyncDelay => { + let value = self.get_parameter(Parameter::SyncDelay.into()); + let tempo_delay = SyncDuration::from(value); + tempo_delay.to_string() + } } } fn get_parameter_name(&self, index: i32) -> String { match Parameter::from(index as i32) { - Parameter::Delay => "Delay" - }.to_string() + Parameter::Delay => "Delay", + Parameter::SyncDelay => "Sync Delay" + } + .to_string() } fn get_parameter(&self, index: i32) -> f32 { @@ -77,9 +83,9 @@ impl PluginParameters for MidiDelayParameters { fn set_parameter(&self, index: i32, value: f32) { match index.into() { - Parameter::Delay => { + Parameter::Delay | Parameter::SyncDelay => { let old_value = self.get_parameter(index); - if (value - old_value).abs() > 0.00001 { + if (value - old_value).abs() > 0.0001 || (value == 0.0 && old_value != 0.0) { self.transfer.set_parameter(index as usize, value) } } diff --git a/note_fan_out/src/lib.rs b/note_fan_out/src/lib.rs index 9f363cb..9f0e595 100644 --- a/note_fan_out/src/lib.rs +++ b/note_fan_out/src/lib.rs @@ -4,7 +4,7 @@ mod parameters; extern crate vst; use std::collections::HashSet; -use std::hash::{Hasher, Hash}; +use std::hash::{Hash, Hasher}; use std::sync::Arc; use vst::api; use vst::buffer::{AudioBuffer, SendEventBuffer}; @@ -20,7 +20,6 @@ use util::raw_message::RawMessage; plugin_main!(NoteFanOut); - #[derive(Default)] pub struct NoteFanOut { events: Vec, @@ -28,25 +27,23 @@ pub struct NoteFanOut { send_buffer: SendEventBuffer, parameters: Arc, current_step: u8, - notes_counter: usize + notes_counter: usize, } - impl NoteFanOut { fn send_midi(&mut self) { if let Ok(mut host_callback_lock) = self.parameters.host.lock() { - self.send_buffer - .send_events(&self.events, &mut host_callback_lock.host); + self.send_buffer.send_events(&self.events, &mut host_callback_lock.host); } self.events.clear(); } } -#[derive(Eq,Clone)] +#[derive(Eq, Clone)] struct PlayingNote { channel: u8, pitch: u8, - mapped_channel: u8 + mapped_channel: u8, } impl PartialEq for PlayingNote { @@ -98,7 +95,7 @@ impl Plugin for NoteFanOut { SendEvents | SendMidiEvent | ReceiveEvents | ReceiveMidiEvent | Offline | Bypass => Yes, MidiProgramNames | ReceiveSysExEvent | MidiSingleNoteTuningChange => No, Other(_) => Maybe, - _ => Maybe + _ => Maybe, } } @@ -116,23 +113,23 @@ impl Plugin for NoteFanOut { match midi_message { MidiMessageType::NoteOnMessage(midi_message) => { - let target_channel = match self.parameters.get_channel_distribution(Parameter::ChannelDistribute) { - ChannelDistribution::Channels(distribution) => { - let target_channel = (self.notes_counter % (distribution as usize)) as u8 + 1; - self.notes_counter += 1; - target_channel - } - ChannelDistribution::Off => { - GenericChannelMessage::from(&e.data).get_channel() - } - }; + let target_channel = + match self.parameters.get_channel_distribution(Parameter::ChannelDistribute) { + ChannelDistribution::Channels(distribution) => { + let target_channel = (self.notes_counter % (distribution as usize)) as u8 + 1; + self.notes_counter += 1; + target_channel + } + ChannelDistribution::Off => GenericChannelMessage::from(&e.data).get_channel(), + }; if steps == 0 || selection == self.current_step { - let raw_message : RawMessage = NoteOn { + let raw_message: RawMessage = NoteOn { channel: target_channel, pitch: midi_message.pitch, - velocity: midi_message.velocity - }.into(); + velocity: midi_message.velocity, + } + .into(); self.events.push(MidiEvent { data: raw_message.into(), @@ -141,34 +138,35 @@ impl Plugin for NoteFanOut { note_length: e.note_length, note_offset: e.note_offset, detune: e.detune, - note_off_velocity: e.note_off_velocity + note_off_velocity: e.note_off_velocity, }); self.current_playing_notes.insert(PlayingNote { channel: midi_message.get_channel(), pitch: midi_message.get_pitch(), - mapped_channel: target_channel + mapped_channel: target_channel, }); } if steps > 0 { - self.current_step = (self.current_step + 1) % steps ; + self.current_step = (self.current_step + 1) % steps; } } MidiMessageType::NoteOffMessage(midi_message) => { let lookup = PlayingNote { channel: midi_message.get_channel(), pitch: midi_message.get_pitch(), - mapped_channel: midi_message.get_channel() + mapped_channel: midi_message.get_channel(), }; match self.current_playing_notes.take(&lookup) { Some(note) => { - let raw_message : RawMessage = NoteOff { + let raw_message: RawMessage = NoteOff { channel: note.mapped_channel, pitch: midi_message.pitch, - velocity: midi_message.velocity - }.into(); + velocity: midi_message.velocity, + } + .into(); self.events.push(MidiEvent { data: raw_message.into(), @@ -177,7 +175,7 @@ impl Plugin for NoteFanOut { note_length: e.note_length, note_offset: e.note_offset, detune: e.detune, - note_off_velocity: e.note_off_velocity + note_off_velocity: e.note_off_velocity, }); } None => { diff --git a/note_fan_out/src/parameters.rs b/note_fan_out/src/parameters.rs index c246a6a..f08a83d 100644 --- a/note_fan_out/src/parameters.rs +++ b/note_fan_out/src/parameters.rs @@ -1,9 +1,9 @@ use std::sync::Mutex; use util::parameter_value_conversion::f32_to_byte; +use util::parameters::ParameterConversion; use util::HostCallbackLock; use vst::plugin::{HostCallback, PluginParameters}; use vst::util::ParameterTransfer; -use util::parameters::ParameterConversion; pub struct NoteFanoutParameters { pub host: Mutex, @@ -18,7 +18,6 @@ pub enum Parameter { ChannelDistribute, } - impl Clone for Parameter { fn clone(&self) -> Self { let value = *self as i32; @@ -37,14 +36,14 @@ impl From for Parameter { 0 => Parameter::Steps, 1 => Parameter::Selection, 2 => Parameter::ChannelDistribute, - _ => panic!(format!("No such Parameter {}", i)), + _ => panic!("No such Parameter {}", i), } } } -impl Into for Parameter { - fn into(self) -> i32 { - self as i32 +impl From for i32 { + fn from(p: Parameter) -> Self { + p as i32 } } @@ -75,7 +74,7 @@ impl NoteFanoutParameters { pub enum ChannelDistribution { Channels(u8), - Off + Off, } impl From for ChannelDistribution { @@ -85,27 +84,26 @@ impl From for ChannelDistribution { if channels_value < 2 { ChannelDistribution::Off } else { - ChannelDistribution::Channels(channels_value) // channel 0 ( displayed as 1 ) is reserved for MPE + ChannelDistribution::Channels(channels_value) // channel 0 ( displayed as 1 ) is reserved for MPE } } } - -impl Into for ChannelDistribution { - fn into(self) -> f32 { - match self { +impl From for f32 { + fn from(cd: ChannelDistribution) -> f32 { + match cd { ChannelDistribution::Channels(value) => { // normalize over 15 values, first range is off - ((value as f32 - 2.) / 13.) * 14. / 15. + 1./15. + ((value as f32 - 2.) / 13.) * 14. / 15. + 1. / 15. } - ChannelDistribution::Off => 0.0 + ChannelDistribution::Off => 0.0, } } } impl PluginParameters for NoteFanoutParameters { fn get_parameter_text(&self, index: i32) -> String { - match Parameter::from(index ) { + match Parameter::from(index) { Parameter::Steps => { let value = self.get_byte_parameter(Parameter::Steps) / 8; if value == 0 { @@ -117,12 +115,10 @@ impl PluginParameters for NoteFanoutParameters { Parameter::Selection => { format!("{}", self.get_byte_parameter(Parameter::Selection) / 8) } - Parameter::ChannelDistribute => { - match self.get_channel_distribution(Parameter::ChannelDistribute) { - ChannelDistribution::Channels(c) => format!("{}", c), - ChannelDistribution::Off => "Off".to_string() - } - } + Parameter::ChannelDistribute => match self.get_channel_distribution(Parameter::ChannelDistribute) { + ChannelDistribution::Channels(c) => format!("{}", c), + ChannelDistribution::Off => "Off".to_string(), + }, } } @@ -130,7 +126,7 @@ impl PluginParameters for NoteFanoutParameters { match Parameter::from(index as i32) { Parameter::Steps => "Steps", Parameter::Selection => "Selection", - Parameter::ChannelDistribute => "Channel distribution" + Parameter::ChannelDistribute => "Channel distribution", } .to_string() } diff --git a/note_generator/src/bin/host.rs b/note_generator/src/bin/host.rs index 8e428cc..b455c90 100644 --- a/note_generator/src/bin/host.rs +++ b/note_generator/src/bin/host.rs @@ -33,7 +33,7 @@ fn main() { // Load the plugin let mut loader = PluginLoader::load(path, Arc::clone(&host)) - .unwrap_or_else(|e| panic!(format!("Failed to load plugin: {} {}", e, arg))); + .unwrap_or_else(|e| panic!("Failed to load plugin: {} {}", e, arg)); // Create an instance of the plugin let mut instance = loader.instance().unwrap(); @@ -49,23 +49,14 @@ fn main() { VST ID: {}\n\t\ Version: {}\n\t\ Initial Delay: {} samples", - info.name, - info.vendor, - info.presets, - info.parameters, - info.unique_id, - info.version, - info.initial_delay + info.name, info.vendor, info.presets, info.parameters, info.unique_id, info.version, info.initial_delay ); // Initialize the instance instance.init(); println!("{}", instance.can_do(CanDo::Offline) == Supported::No); - println!( - "{}", - instance.can_do(CanDo::ReceiveEvents) == Supported::Yes - ); + println!("{}", instance.can_do(CanDo::ReceiveEvents) == Supported::Yes); let mut host_buffer: HostBuffer = HostBuffer::new(2, 2); let inputs = vec![vec![0.0; 1000]; 2]; let mut outputs = vec![vec![0.0; 1000]; 2]; diff --git a/note_generator/src/lib.rs b/note_generator/src/lib.rs index 3e32aaf..19e837f 100644 --- a/note_generator/src/lib.rs +++ b/note_generator/src/lib.rs @@ -69,14 +69,7 @@ impl NoteGeneratorPlugin { let pitchbend_value = self.parameters.get_u14_parameter(Parameter::PitchBend); let msb = pitchbend_value >> 7; let lsb = pitchbend_value & 0x7F; - make_midi_message( - [ - channel + PITCHBEND, - lsb as u8, - msb as u8, - ], - delta, - ) + make_midi_message([channel + PITCHBEND, lsb as u8, msb as u8], delta) } fn get_current_pressure(&self, delta: i32) -> MidiEvent { @@ -134,8 +127,7 @@ impl NoteGeneratorPlugin { } if let Ok(mut host_callback_lock) = self.parameters.host.lock() { - self.send_buffer - .send_events(&self.events, &mut host_callback_lock.host); + self.send_buffer.send_events(&self.events, &mut host_callback_lock.host); } self.events.clear(); } diff --git a/note_generator/src/parameters.rs b/note_generator/src/parameters.rs index ced0982..11691e4 100644 --- a/note_generator/src/parameters.rs +++ b/note_generator/src/parameters.rs @@ -1,10 +1,10 @@ use std::sync::Mutex; use util::constants::{C0, NOTE_NAMES}; use util::parameter_value_conversion::{f32_to_bool, f32_to_byte, f32_to_u14}; +use util::parameters::ParameterConversion; use util::HostCallbackLock; use vst::plugin::{HostCallback, PluginParameters}; use vst::util::ParameterTransfer; -use util::parameters::ParameterConversion; pub struct NoteGeneratorPluginParameters { pub host: Mutex, @@ -36,14 +36,14 @@ impl From for Parameter { 6 => Parameter::Trigger, 7 => Parameter::TriggeredPitch, 8 => Parameter::TriggeredChannel, - _ => panic!(format!("No such Parameter {}", i)), + _ => panic!("No such Parameter {}", i), } } } -impl Into for Parameter { - fn into(self) -> i32 { - self as i32 +impl From for i32 { + fn from(parameter: Parameter) -> Self { + parameter as i32 } } @@ -90,7 +90,10 @@ impl NoteGeneratorPluginParameters { #[inline] fn get_pitchbend_label(&self) -> String { let semitones = self.get_u14_parameter(Parameter::PitchBend); - format!("{:.2} semitones", ((semitones as i32 * 96000 / 16383) - 48000) as f32 / 1000.) + format!( + "{:.2} semitones", + ((semitones as i32 * 96000 / 16384) - 48000) as f32 / 1000. + ) } #[inline] @@ -184,39 +187,35 @@ impl PluginParameters for NoteGeneratorPluginParameters { } Err(_) => false, }, - Parameter::Velocity | Parameter::NoteOffVelocity | Parameter::Pressure => { - match text.parse::() { - Ok(n) => { - if n < 128 { - self.set_byte_parameter(Parameter::Velocity, n); - true - } else { - false - } + Parameter::Velocity | Parameter::NoteOffVelocity | Parameter::Pressure => match text.parse::() { + Ok(n) => { + if n < 128 { + self.set_byte_parameter(Parameter::Velocity, n); + true + } else { + false } - Err(_) => false, } - } + Err(_) => false, + }, Parameter::Pitch => match NOTE_NAMES.iter().position(|&s| text.starts_with(s)) { None => false, - Some(position) => { - match text[NOTE_NAMES[position].len()..text.len()].parse::() { - Ok(octave) => { - if octave >= -2 && octave <= 8 { - let pitch = octave as i16 * 12 + C0 as i16 + position as i16; - if pitch < 128 { - self.set_byte_parameter(Parameter::Pitch, pitch as u8); - true - } else { - false - } + Some(position) => match text[NOTE_NAMES[position].len()..text.len()].parse::() { + Ok(octave) => { + if octave >= -2 && octave <= 8 { + let pitch = octave as i16 * 12 + C0 as i16 + position as i16; + if pitch < 128 { + self.set_byte_parameter(Parameter::Pitch, pitch as u8); + true } else { false } + } else { + false } - Err(_) => false, } - } + Err(_) => false, + }, }, Parameter::Trigger => match text.to_ascii_lowercase().as_ref() { "0" | "off" | "" => { diff --git a/note_off_delay/Cargo.toml b/note_off_delay/Cargo.toml index 944cedc..6ff9fe4 100644 --- a/note_off_delay/Cargo.toml +++ b/note_off_delay/Cargo.toml @@ -7,10 +7,11 @@ edition = "2018" [dependencies] vst = { git = "https://github.com/rustaudio/vst-rs" } util = { path = "../util" } -build-info = "0.0.20" +build-info = "0.0.23" +log = "0.4.14" [build-dependencies] -build-info-build = "0.0.20" +build-info-build = "0.0.23" [lib] name = "note_off_delay" diff --git a/note_off_delay/src/bin/note_off_host.rs b/note_off_delay/src/bin/note_off_host.rs index 1f31701..9b43311 100644 --- a/note_off_delay/src/bin/note_off_host.rs +++ b/note_off_delay/src/bin/note_off_host.rs @@ -4,7 +4,7 @@ use std::env; use std::path::Path; use std::sync::{Arc, Mutex}; -use vst::api::Supported ; +use vst::api::Supported; use vst::host::{Host, HostBuffer, PluginLoader}; use vst::plugin::CanDo; use vst::plugin::Plugin; @@ -33,7 +33,7 @@ fn main() { // Load the plugin let mut loader = PluginLoader::load(path, Arc::clone(&host)) - .unwrap_or_else(|e| panic!(format!("Failed to load plugin: {} {}", e, arg))); + .unwrap_or_else(|e| panic!("Failed to load plugin: {} {}", e, arg)); // Create an instance of the plugin let mut instance = loader.instance().unwrap(); @@ -49,23 +49,14 @@ fn main() { VST ID: {}\n\t\ Version: {}\n\t\ Initial Delay: {} samples", - info.name, - info.vendor, - info.presets, - info.parameters, - info.unique_id, - info.version, - info.initial_delay + info.name, info.vendor, info.presets, info.parameters, info.unique_id, info.version, info.initial_delay ); // Initialize the instance instance.init(); println!("{}", instance.can_do(CanDo::Offline) == Supported::No); - println!( - "{}", - instance.can_do(CanDo::ReceiveEvents) == Supported::Yes - ); + println!("{}", instance.can_do(CanDo::ReceiveEvents) == Supported::Yes); let mut host_buffer: HostBuffer = HostBuffer::new(2, 2); let inputs = vec![vec![0.0; 1000]; 2]; let mut outputs = vec![vec![0.0; 1000]; 2]; @@ -126,9 +117,7 @@ fn main() { let v = parameters.get_preset_data(); parameters.load_preset_data(&v); - instance.process(&mut audio_buffer); - println!("Closing instance..."); } diff --git a/note_off_delay/src/lib.rs b/note_off_delay/src/lib.rs index 8bcccc6..fb906ca 100644 --- a/note_off_delay/src/lib.rs +++ b/note_off_delay/src/lib.rs @@ -1,24 +1,28 @@ mod parameters; +mod tests; #[macro_use] extern crate vst; +use log::info; use std::cell::RefCell; use std::sync::Arc; -use vst::buffer::{AudioBuffer, SendEventBuffer}; -use vst::plugin::{CanDo, Category, HostCallback, Info, Plugin, PluginParameters}; -use vst::event::Event; use vst::api::Events; +use vst::buffer::{AudioBuffer, SendEventBuffer}; +use vst::event::{Event, MidiEvent}; +use vst::plugin::{CanDo, Category, HostCallback, Info, Plugin}; +use crate::parameters::PARAMETER_COUNT; use parameters::NoteOffDelayPluginParameters; use parameters::Parameter; use util::absolute_time_midi_message_vector::AbsoluteTimeMidiMessageVector; -use util::debug::DebugSocket; use util::delayed_message_consumer::{process_scheduled_events, MessageReason}; +use util::logging::logging_setup; use util::messages::format_event; use util::midi_message_type::MidiMessageType; use util::parameters::ParameterConversion; +use util::absolute_time_midi_message::AbsoluteTimeMidiMessage; plugin_main!(NoteOffDelayPlugin); @@ -37,25 +41,32 @@ impl Default for NoteOffDelayPlugin { parameters: Arc::new(Default::default()), sample_rate: 44100.0, current_time_in_samples: 0, - message_queue: Default::default() + message_queue: Default::default(), } } } impl NoteOffDelayPlugin { + fn process_scheduled_events(&self, samples: usize) -> (AbsoluteTimeMidiMessageVector, Vec) { + process_scheduled_events( + samples, + self.parameters.get_delay().is_active(), + self.parameters.get_max_notes(), + self.parameters + .get_bool_parameter(Parameter::MaxNotesAppliesToDelayedNotesOnly), + self.current_time_in_samples, + &self.message_queue, + ) + } + fn send_events(&mut self, samples: usize) { + let (queued_messages, events) = self.process_scheduled_events(samples); + self.message_queue = queued_messages; + if let Ok(mut host_callback_lock) = self.parameters.host_mutex.lock() { - let (next_message_queue, events) = process_scheduled_events( - samples, - self.current_time_in_samples, - &self.message_queue, - self.parameters.get_max_notes(), - self.parameters.get_bool_parameter(Parameter::MaxNotesAppliesToDelayedNotesOnly), - self.parameters.get_parameter(Parameter::Delay.into()) > 0.0 - ); - - self.message_queue = next_message_queue; - self.send_buffer.borrow_mut().send_events(events, &mut host_callback_lock.host); + self.send_buffer + .borrow_mut() + .send_events(events, &mut host_callback_lock.host); } } @@ -64,22 +75,19 @@ impl NoteOffDelayPlugin { 1.0 / self.sample_rate } + #[allow(dead_code)] fn seconds_to_samples(&self, seconds: f32) -> usize { (seconds * self.sample_rate) as usize } fn debug_events_in(&mut self, events: &Events) { for e in events.events() { - DebugSocket::send( - &*(format_event(&e) - + &*format!(" current time={}", self.current_time_in_samples)), - ); + info!("{} current time={}", format_event(&e), self.current_time_in_samples); } } fn increase_time_in_samples(&mut self, samples: usize) { - let new_time_in_samples = self.current_time_in_samples + samples; - self.current_time_in_samples = new_time_in_samples; + self.current_time_in_samples += samples; } } @@ -89,7 +97,7 @@ impl Plugin for NoteOffDelayPlugin { name: "Note Off Delay".to_string(), vendor: "DJ Crontab".to_string(), unique_id: 234213173, - parameters: 3, + parameters: PARAMETER_COUNT as i32, category: Category::Effect, initial_delay: 0, version: 1, @@ -105,10 +113,12 @@ impl Plugin for NoteOffDelayPlugin { } fn new(host: HostCallback) -> Self { - let parameters = NoteOffDelayPluginParameters::new(host); - DebugSocket::send( - build_info::format!("{{{} v{} built with {} at {}}}", $.crate_info.name, $.crate_info.version, $.compiler, $.timestamp), + logging_setup(); + info!( + "{}", + build_info::format!("{{{} v{} built with {} at {}}}", $.crate_info.name, $.crate_info.version, $.compiler, $.timestamp) ); + let parameters = NoteOffDelayPluginParameters::new(host); NoteOffDelayPlugin { current_time_in_samples: 0, message_queue: Default::default(), @@ -146,7 +156,7 @@ impl Plugin for NoteOffDelayPlugin { // if s == "MPE" { // Yes // } else { - // DebugSocket::send(&*s); + // info!("{}", s) ; // No // } Maybe @@ -162,34 +172,40 @@ impl Plugin for NoteOffDelayPlugin { fn process_events(&mut self, events: &Events) { self.debug_events_in(events); - let note_off_delay = self.seconds_to_samples(self.parameters - .get_exponential_scale_parameter(Parameter::Delay, 10., 20.)); - - for event in events.events() { - let midi_event = if let Event::Midi(midi_event) = event { - midi_event + let midi_events = events.events().filter_map( + |event| if let Event::Midi(midi_event) = event { + Some(midi_event) } else { - continue - }; + None + } + ); + for midi_event in midi_events { // TODO: minimum time, maximum time ( with delay ) match MidiMessageType::from(&midi_event.data) { - MidiMessageType::NoteOffMessage(_) => { - self.message_queue.insert_message( - midi_event.data, - midi_event.delta_frames as usize + self.current_time_in_samples, MessageReason::Live, - ); - - if note_off_delay > 0 { - // send two times the note off, the live one will be only used to mark the note on as delayed - self.message_queue.insert_message( - midi_event.data, - note_off_delay + midi_event.delta_frames as usize + self.current_time_in_samples, - MessageReason::Delayed, - ); + MidiMessageType::NoteOffMessage(note_off) => { + let note_off_play_time = midi_event.delta_frames as usize + self.current_time_in_samples; + + self.message_queue + .insert_message(midi_event.data, note_off_play_time, MessageReason::Live); + + let delay = self.parameters.get_delay(); + + if delay.is_active() { + let matching_note_on = self.message_queue.get_matching_note_on(note_off.channel, note_off.pitch); + if let Some(&AbsoluteTimeMidiMessage { play_time_in_samples,.. }) = matching_note_on { + let duration = note_off_play_time - play_time_in_samples; + let new_duration = delay.apply(duration, self.sample_rate).expect("delay is supposed to be active"); + + // send two times the note off, the live one will be only used to mark the note on as delayed + self.message_queue.insert_message( + midi_event.data, + play_time_in_samples + new_duration, + MessageReason::Delayed, + ); + } } - } MidiMessageType::Unsupported => { continue; @@ -197,7 +213,8 @@ impl Plugin for NoteOffDelayPlugin { _ => { self.message_queue.insert_message( midi_event.data, - midi_event.delta_frames as usize + self.current_time_in_samples, MessageReason::Live, + midi_event.delta_frames as usize + self.current_time_in_samples, + MessageReason::Live, ); } }; diff --git a/note_off_delay/src/parameters.rs b/note_off_delay/src/parameters.rs index f77bb64..5a27e04 100644 --- a/note_off_delay/src/parameters.rs +++ b/note_off_delay/src/parameters.rs @@ -1,14 +1,16 @@ use std::sync::Mutex; -use vst::plugin::HostCallback ; +use vst::plugin::{HostCallback, PluginParameters}; use vst::util::ParameterTransfer; -use util::debug::DebugSocket; -use util::parameter_value_conversion::{f32_to_byte, f32_to_bool}; -use util::{HostCallbackLock, duration_display}; -use util::parameters::ParameterConversion; +use std::fmt; +use std::fmt::{Display, Formatter}; +use util::delayed_message_consumer::MaxNotesParameter; +use util::parameter_value_conversion::{f32_to_bool, f32_to_byte}; +use util::parameters::{get_exponential_scale_value, ParameterConversion}; +use util::{Duration, HostCallbackLock}; -const PARAMETER_COUNT: usize = 3; +pub const PARAMETER_COUNT: usize = 5; pub struct NoteOffDelayPluginParameters { pub host_mutex: Mutex, @@ -17,30 +19,32 @@ pub struct NoteOffDelayPluginParameters { #[repr(i32)] pub enum Parameter { - Delay = 0, + DelayOffset = 0, MaxNotes, - MaxNotesAppliesToDelayedNotesOnly + MaxNotesAppliesToDelayedNotesOnly, + MultiplyLength, + Threshold, } impl From for Parameter { fn from(i: i32) -> Self { match i { - 0 => Parameter::Delay, + 0 => Parameter::DelayOffset, 1 => Parameter::MaxNotes, 2 => Parameter::MaxNotesAppliesToDelayedNotesOnly, + 3 => Parameter::MultiplyLength, + 4 => Parameter::Threshold, _ => panic!("no such parameter {}", i), } } } - -impl Into for Parameter { - fn into(self) -> i32 { - self as i32 +impl From for i32 { + fn from(p: Parameter) -> Self { + p as i32 } } - impl ParameterConversion for NoteOffDelayPluginParameters { fn get_parameter_transfer(&self) -> &ParameterTransfer { &self.transfer @@ -51,7 +55,6 @@ impl ParameterConversion for NoteOffDelayPluginParameters { } } - impl NoteOffDelayPluginParameters { pub fn new(host: HostCallback) -> Self { NoteOffDelayPluginParameters { @@ -60,16 +63,22 @@ impl NoteOffDelayPluginParameters { } } - pub fn get_max_notes(&self) -> u8 { - self.get_byte_parameter(Parameter::MaxNotes) / 4 + pub fn get_max_notes(&self) -> MaxNotesParameter { + match self.get_byte_parameter(Parameter::MaxNotes) / 4 { + 0 => MaxNotesParameter::Infinite, + i => MaxNotesParameter::Limited(i), + } } - pub fn set_max_notes(&self, value: u8) { - self.set_byte_parameter(Parameter::MaxNotes, value * 4) + pub fn get_delay(&self) -> Delay { + Delay { + offset: Duration::from(self.get_parameter(Parameter::DelayOffset.into())), + multiplier: DelayMultiplier::from(self.get_parameter(Parameter::MultiplyLength.into())), + threshold: Duration::from(self.get_parameter(Parameter::Threshold.into())), + } } } - impl Default for NoteOffDelayPluginParameters { fn default() -> Self { NoteOffDelayPluginParameters { @@ -79,21 +88,75 @@ impl Default for NoteOffDelayPluginParameters { } } +pub struct Delay { + pub offset: Duration, + pub multiplier: DelayMultiplier, + pub threshold: Duration, +} + +pub enum DelayMultiplier { + Off, + Multiplier(f32), +} + +impl Delay { + pub fn is_active(&self) -> bool { + !matches!((&self.offset, &self.multiplier), (Duration::Off, DelayMultiplier::Off)) + } + + pub fn apply(&self, duration_in_samples: usize, sample_rate: f32) -> Option { + if self.is_active() { + if let Duration::Duration(threshold) = self.threshold { + let threshold_in_samples = (threshold * sample_rate) as usize; + if duration_in_samples < threshold_in_samples { + return Some(duration_in_samples); + } + } + + let duration_in_samples = match self.multiplier { + DelayMultiplier::Off => duration_in_samples as f32, + DelayMultiplier::Multiplier(x) => x * duration_in_samples as f32, + }; + + Some(match self.offset { + Duration::Off => duration_in_samples as usize, + Duration::Duration(x) => (duration_in_samples + x * sample_rate).round() as usize, + }) + } else { + None + } + } +} + +impl Display for DelayMultiplier { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + DelayMultiplier::Off => "off".to_string(), + DelayMultiplier::Multiplier(multiplier) => { + format!("{:.3}x", multiplier) + } + } + .fmt(f) + } +} + +impl From for DelayMultiplier { + fn from(parameter_value: f32) -> Self { + match get_exponential_scale_value(parameter_value, 19., 20.) { + x if x == 0.0 => DelayMultiplier::Off, + value => DelayMultiplier::Multiplier(1.0 + value), + } + } +} impl vst::plugin::PluginParameters for NoteOffDelayPluginParameters { fn get_parameter_text(&self, index: i32) -> String { + let value = self.get_parameter(index); match index.into() { - Parameter::Delay => { - let value = self.get_exponential_scale_parameter(Parameter::Delay, 10., 20.); + Parameter::DelayOffset | Parameter::Threshold => Duration::from(value).to_string(), - if value > 0.0 { - duration_display(value) - } else { - "Off".to_string() - } - } Parameter::MaxNotes => { - if self.get_parameter(Parameter::MaxNotes as i32) == 0.0 { + if value == 0.0 { "Off".to_string() } else { format!("{}", self.get_max_notes()) @@ -105,16 +168,20 @@ impl vst::plugin::PluginParameters for NoteOffDelayPluginParameters { "On" } else { "Off" - }.to_string() + } + .to_string() } + Parameter::MultiplyLength => DelayMultiplier::from(value).to_string(), } } fn get_parameter_name(&self, index: i32) -> String { match index.into() { - Parameter::Delay => "Delay", + Parameter::DelayOffset => "Delay", Parameter::MaxNotes => "Max Notes", Parameter::MaxNotesAppliesToDelayedNotesOnly => "Apply max notes to delayed notes only", + Parameter::MultiplyLength => "Length multiplier", + Parameter::Threshold => "Threshold", } .to_string() } @@ -125,23 +192,25 @@ impl vst::plugin::PluginParameters for NoteOffDelayPluginParameters { fn set_parameter(&self, index: i32, value: f32) { match index.into() { - Parameter::Delay => { - DebugSocket::send(&*format!("Parameter {} set to {}", index, value)); + Parameter::DelayOffset | Parameter::MultiplyLength | Parameter::Threshold => { let old_value = self.get_parameter(index); - if (value - old_value).abs() > 0.0001 { + if (value - old_value).abs() > 0.0001 || (value == 0.0 && old_value != 0.0) { self.transfer.set_parameter(index as usize, value) } } Parameter::MaxNotes => { let old_value = self.get_max_notes(); - let max_notes = f32_to_byte(value) / 4; + let byte_value = f32_to_byte(value); + let max_notes = match byte_value / 4 { + 0 => MaxNotesParameter::Infinite, + i => MaxNotesParameter::Limited(i), + }; if max_notes != old_value { - self.set_max_notes(max_notes) + self.set_byte_parameter(Parameter::MaxNotes, byte_value); } } Parameter::MaxNotesAppliesToDelayedNotesOnly => { - self.set_bool_parameter(Parameter::MaxNotesAppliesToDelayedNotesOnly, - f32_to_bool(value)) + self.set_bool_parameter(Parameter::MaxNotesAppliesToDelayedNotesOnly, f32_to_bool(value)) } } } diff --git a/note_off_delay/src/tests.rs b/note_off_delay/src/tests.rs new file mode 100644 index 0000000..7cf92b6 --- /dev/null +++ b/note_off_delay/src/tests.rs @@ -0,0 +1,99 @@ +#[cfg(test)] +mod test { + use crate::parameters::Parameter; + use crate::NoteOffDelayPlugin; + use core::mem; + use std::convert::TryFrom; + use util::parameters::get_reverse_exponential_scale_value; + use vst::api::{Event, EventType, Events, MidiEvent}; + use vst::plugin::{Plugin, PluginParameters}; + + fn make_event(message: [u8; 3], delta_frames: i32) -> Event { + let midi_event: MidiEvent = MidiEvent { + event_type: EventType::Midi, + byte_size: mem::size_of::() as i32, + delta_frames, + flags: 0, + note_length: 0, + note_offset: 0, + midi_data: [message[0], message[1], message[2]], + _midi_reserved: 0, + detune: 0, + note_off_velocity: 0, + _reserved1: 0, + _reserved2: 0, + }; + let mut event: Event = unsafe { std::mem::transmute(midi_event) }; + event.event_type = EventType::Midi; + + event + } + + #[test] + fn test_process_scheduled_events() { + let original = vec![([0x90, 0x60, 0x60], 10), ([0x80, 0x60, 0x60], 20)]; + let expected = vec![([0x90, 0x60, 0x60], 10), ([0x80, 0x60, 0x60], 120)]; + + assert_process_scheduled_events(original, expected, 100) + } + + #[test] + fn test_process_scheduled_events_feature_off() { + let original = vec![([0x90, 0x60, 0x60], 10), ([0x80, 0x60, 0x60], 20)]; + let expected = vec![([0x90, 0x60, 0x60], 10), ([0x80, 0x60, 0x60], 20)]; + + assert_process_scheduled_events(original, expected, 0) + } + + #[test] + fn test_process_scheduled_events_next() { + let original = vec![([0x90, 0x60, 0x60], 10), ([0x80, 0x60, 0x60], 20)]; + let expected = vec![([0x90, 0x60, 0x60], 10)]; + + assert_process_scheduled_events(original, expected, 1004) + } + + fn compare(events: Vec, expected: Vec<([u8; 3], i32)>) { + assert_eq!(events.len(), expected.len()); + for ((test_data, test_delta_frame), (expected_data, expected_delta_frame)) in events + .iter() + .map(|event| (event.data, event.delta_frames)) + .zip(expected.iter()) + { + assert_eq!(test_data, *expected_data); + assert_eq!(test_delta_frame, *expected_delta_frame); + } + } + + fn assert_process_scheduled_events( + original: Vec<([u8; 3], i32)>, + expected: Vec<([u8; 3], i32)>, + delay_parameter_value_samples: usize, + ) { + let mut plugin = NoteOffDelayPlugin::default(); + + let delay_parameter_value_seconds = delay_parameter_value_samples as f32 / 44100.; + let delay_parameter_value = get_reverse_exponential_scale_value(delay_parameter_value_seconds, 10., 20.); + + plugin + .parameters + .set_parameter(i32::from(Parameter::DelayOffset), delay_parameter_value); + + let mut original_as_events: Vec = original + .iter() + .map(|(raw_message, delta_frame)| make_event(*raw_message, *delta_frame)) + .collect(); + + let events: Vec<*mut Event> = original_as_events.iter_mut().map(|x| x as *mut Event).collect(); + let events = Events { + num_events: events.len() as i32, + _reserved: 0, + events: <[*mut Event; 2]>::try_from(events).unwrap(), + }; + + plugin.process_events(&events); + let (_, events) = plugin.process_scheduled_events(1024); + + compare(events, expected); + } +} diff --git a/osx_vst_bundler.sh b/osx_vst_bundler.sh index 69cbb15..c10f040 100755 --- a/osx_vst_bundler.sh +++ b/osx_vst_bundler.sh @@ -2,9 +2,11 @@ ARTEFACT_DIRECTORY=artefact LIB_PATH_PREFIX=target/release/ +#LIB_PATH_PREFIX=target/debug/ for BUNDLE_LIB in "NoteGenerator note_generator" "NoteOffDelay note_off_delay" "FilterOutNonNote filter_out_non_note" \ - " NoteFanOut note_fan_out" "MidiDelay midi_delay" "MaxNoteDuration max_note_duration"; do + " NoteFanOut note_fan_out" "MidiDelay midi_delay" "MaxNoteDuration max_note_duration" "AudioData audio_data" \ + " ArpegiatorPatternReceiver arpegiator_pattern_receiver" "Arpegiator arpegiator" ; do BUNDLE_NAME=$(echo $BUNDLE_LIB | cut -f1 -d" ") LIBNAME=$(echo $BUNDLE_LIB | cut -f2 -d" ") LIB_PATH=${LIB_PATH_PREFIX}lib${LIBNAME}.dylib diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..866c756 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 120 \ No newline at end of file diff --git a/util/Cargo.toml b/util/Cargo.toml index 1e27629..c2ef3d9 100644 --- a/util/Cargo.toml +++ b/util/Cargo.toml @@ -6,4 +6,22 @@ edition = "2018" [dependencies] vst = { git = "https://github.com/rustaudio/vst-rs" } -global_counter = "0.2.1" \ No newline at end of file +global_counter = "0.2.2" +log = "0.4.14" +simplelog = "0.10.0" +build-info = "0.0.23" +serde = "1.0.126" +ipc-channel = "0.15.0" +async-channel = "1.6.1" +uuid = "0.8.2" + +[dependencies.log-panics] +version = "2.0.0" +features = ["with-backtrace"] + +[build-dependencies] +build-info-build = "0.0.23" + +[features] +default = ["enable_logging"] +enable_logging = [] diff --git a/util/build.rs b/util/build.rs new file mode 100644 index 0000000..a49ab18 --- /dev/null +++ b/util/build.rs @@ -0,0 +1,5 @@ +fn main() { + // Calling `build_info_build::build_script` collects all data and makes it available to `build_info::build_info!` + // and `build_info::format!` in the main program. + build_info_build::build_script(); +} diff --git a/util/src/absolute_time_midi_message.rs b/util/src/absolute_time_midi_message.rs index 36795ff..1a6fe38 100644 --- a/util/src/absolute_time_midi_message.rs +++ b/util/src/absolute_time_midi_message.rs @@ -1,13 +1,13 @@ use core::clone::Clone; -use core::fmt::Display; use core::fmt; +use core::fmt::Display; //use core::option::Option::{Some, None}; //use vst::event::{Event, MidiEvent}; use super::raw_message::RawMessage; -use vst::event::MidiEvent; -use std::cmp::max; use crate::delayed_message_consumer::MessageReason; +use std::cmp::max; +use vst::event::MidiEvent; #[derive(Copy)] pub struct AbsoluteTimeMidiMessage { @@ -27,7 +27,7 @@ impl AbsoluteTimeMidiMessage { note_length: None, note_offset: None, detune: 0, - note_off_velocity: 0 + note_off_velocity: 0, } } @@ -42,14 +42,13 @@ impl AbsoluteTimeMidiMessage { } } - impl Clone for AbsoluteTimeMidiMessage { fn clone(&self) -> Self { AbsoluteTimeMidiMessage { id: self.id, data: self.data, play_time_in_samples: self.play_time_in_samples, - reason: self.reason + reason: self.reason, } } @@ -64,8 +63,8 @@ impl Clone for AbsoluteTimeMidiMessage { impl Display for AbsoluteTimeMidiMessage { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.write_str(&*format!( - "{} {} [{:#04X} {:#04X} {:#04X}]", self.play_time_in_samples, - self.id, self.data[0], self.data[1], self.data[2] + "{} {} [{:#04X} {:#04X} {:#04X}]", + self.play_time_in_samples, self.id, self.data[0], self.data[1], self.data[2] )) } } diff --git a/util/src/absolute_time_midi_message_vector.rs b/util/src/absolute_time_midi_message_vector.rs index f3998b3..fc347b3 100644 --- a/util/src/absolute_time_midi_message_vector.rs +++ b/util/src/absolute_time_midi_message_vector.rs @@ -3,7 +3,6 @@ use std::ops::{Deref, DerefMut}; use global_counter::primitive::exact::CounterUsize; use super::absolute_time_midi_message::AbsoluteTimeMidiMessage; -use super::debug::DebugSocket; use super::delayed_message_consumer::MessageReason; use super::midi_message_type::MidiMessageType; use super::raw_message::RawMessage; @@ -30,7 +29,6 @@ impl DerefMut for AbsoluteTimeMidiMessageVector { } } - // plot twist: this vector is for all events, not just note on/note off. // would still be OK to add that metadata to any event after all ? no // as a weak reference should be ok @@ -39,67 +37,58 @@ impl DerefMut for AbsoluteTimeMidiMessageVector { static NOTE_SEQUENCE_ID: CounterUsize = CounterUsize::new(0); impl AbsoluteTimeMidiMessageVector { - pub fn insert_message(&mut self, data: [u8; 3], play_time_in_samples: usize, reason: MessageReason) { - // we generate unique identifier per event. this is in order to match note on/note off pairs - let channel_pitch_lookup = match MidiMessageType::from(RawMessage::from(data)) { - MidiMessageType::NoteOffMessage(midi_message) => { - Some((midi_message.channel, midi_message.pitch)) + pub fn get_matching_note_on(&self, channel: u8, pitch: u8) -> Option<&AbsoluteTimeMidiMessage> { + self.iter().filter(|message| + match MidiMessageType::from(**message) { + MidiMessageType::NoteOnMessage(midi_message) => channel == midi_message.channel && pitch == midi_message.pitch, + _ => false } - _ => { - None - } - }; + ).last() + } - let mut last_note_on_match = None ; + pub fn raw_insert(&mut self, data: [u8; 3], play_time_in_samples: usize) { + // insert in vector, disregarding matching note on/off + let message = AbsoluteTimeMidiMessage { + data: data.into(), + id: NOTE_SEQUENCE_ID.inc(), + play_time_in_samples, + reason: MessageReason::PlayUnprocessed + }; - let insert_point = self.iter().position(|message_at_position| { - if let Some((channel, pitch)) = channel_pitch_lookup { - if let MidiMessageType::NoteOnMessage(midi_message) = MidiMessageType::from(*message_at_position) { - if channel == midi_message.channel && pitch == midi_message.pitch { - last_note_on_match = Some(message_at_position); - } - } - } + self.ordered_insert(message); + } - // we use '<' and not '<=', because this method is called in the same order as events - // come, and find their position starting from the beginning of the vector. By moving - // past equally timed elements, we keep the original order. - play_time_in_samples < message_at_position.play_time_in_samples - }); + pub fn insert_message(&mut self, data: [u8; 3], play_time_in_samples: usize, reason: MessageReason) { + let raw_message = RawMessage::from(data) ; - let id = if let Some(note_message) = last_note_on_match { - note_message.id - } else { - NOTE_SEQUENCE_ID.inc() + let id = match MidiMessageType::from(raw_message) { + MidiMessageType::NoteOffMessage(midi_message) => { + match self.get_matching_note_on(midi_message.channel, midi_message.pitch) { + Some(note_on_message) => note_on_message.id, + None => NOTE_SEQUENCE_ID.inc() + } + }, + _ => NOTE_SEQUENCE_ID.inc(), }; let message = AbsoluteTimeMidiMessage { - data: data.into(), + data: raw_message, id, play_time_in_samples, - reason + reason, }; - DebugSocket::send(&*format!("Inserting {}", message)); - if let Some(insert_point) = insert_point { - self.insert(insert_point, message); - } else { - self.push(message); - } + self.ordered_insert(message); } pub fn ordered_insert(&mut self, message: AbsoluteTimeMidiMessage) { - let position = self.iter().position(|message_at_position| { - message.play_time_in_samples < message_at_position.play_time_in_samples - }); + let position = self + .iter() + .position(|message_at_position| message.play_time_in_samples < message_at_position.play_time_in_samples); match position { - Some(position) => { - self.insert(position, message) - } - None => { - self.push(message) - } + Some(position) => self.insert(position, message), + None => self.push(message), } } } diff --git a/util/src/constants.rs b/util/src/constants.rs index ed8b366..6a2e63e 100644 --- a/util/src/constants.rs +++ b/util/src/constants.rs @@ -1,13 +1,11 @@ -pub const PRESSURE: u8 = 0xD0; -pub const PITCHBEND: u8 = 0xE0; pub const ZEROVALUE: u8 = 0x40; -pub const CC: u8 = 0xB0; -pub const TIMBRECC: u8 = 0x4A; +pub const TIMBRECC: u8 = 0x4A; // 74 pub const NOTE_OFF: u8 = 0x80; pub const NOTE_ON: u8 = 0x90; -pub const AFTERTOUCH: u8 = 0x90; +pub const AFTERTOUCH: u8 = 0xA0; +pub const CC: u8 = 0xB0; +pub const PRESSURE: u8 = 0xD0; +pub const PITCHBEND: u8 = 0xE0; pub const C0: i8 = 0x18; -pub static NOTE_NAMES: &[&str; 12] = &[ - "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B", -]; +pub static NOTE_NAMES: &[&str; 12] = &["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; diff --git a/util/src/debug.rs b/util/src/debug.rs deleted file mode 100644 index 2411873..0000000 --- a/util/src/debug.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::net::{SocketAddr, UdpSocket}; - -pub struct DebugSocket { - socket: UdpSocket, - to: SocketAddr, -} - -impl DebugSocket { - fn create() { - let socket_result = (10000..20000) - .map(|port| UdpSocket::bind(format!("127.0.0.1:{}", port))) - .filter_map(Result::ok) - .next(); - - let socket = socket_result.unwrap(); - socket.set_nonblocking(true).unwrap(); - - unsafe { - DEBUG_SOCKET = Some(DebugSocket { - socket, - to: "127.0.0.1:5555".parse().unwrap(), - }) - } - } - - pub fn send(debug_str: &str) { - if debug_str.is_empty() { - return - } - let debug_string = debug_str.to_owned() + "\n"; - unsafe { - if DEBUG_SOCKET.is_none() { - Self::create() - } - - if let Some(debug_socket) = &DEBUG_SOCKET { - debug_socket - .socket - .send_to(debug_string.as_bytes(), debug_socket.to) - .unwrap(); - } - } - } -} - -static mut DEBUG_SOCKET: Option = None; diff --git a/util/src/delayed_message_consumer.rs b/util/src/delayed_message_consumer.rs index 9f30d02..11d34af 100644 --- a/util/src/delayed_message_consumer.rs +++ b/util/src/delayed_message_consumer.rs @@ -1,14 +1,15 @@ use core::iter::Iterator; -use std::collections::HashMap; use std::collections::hash_map::RandomState; -use std::ops::{DerefMut, Deref}; +use std::collections::HashMap; +use std::ops::{Deref, DerefMut}; use vst::event::MidiEvent; use super::absolute_time_midi_message::AbsoluteTimeMidiMessage; use super::absolute_time_midi_message_vector::AbsoluteTimeMidiMessageVector; -use super::midi_message_type::MidiMessageType; use super::messages::NoteOff; - +use super::midi_message_type::MidiMessageType; +use std::fmt; +use std::fmt::{Display, Formatter}; #[derive(Hash, Clone, Copy, PartialEq, Eq)] struct PlayingNoteIndex { @@ -16,12 +17,38 @@ struct PlayingNoteIndex { pitch: u8, } +#[derive(Eq, PartialEq, Clone)] +pub enum MaxNotesParameter { + Infinite, + Limited(u8), +} + +impl Display for MaxNotesParameter { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + MaxNotesParameter::Infinite => "Infinite".to_string(), + MaxNotesParameter::Limited(x) => x.to_string(), + } + .fmt(f) + } +} + +impl MaxNotesParameter { + pub fn should_limit(&self, currently_playing: usize) -> bool { + match self { + MaxNotesParameter::Infinite => false, + MaxNotesParameter::Limited(limit) => currently_playing >= *limit as usize, + } + } +} + #[derive(Clone, Copy, PartialEq)] pub enum MessageReason { Live, - Delayed, // the same event will exist live and delayed + Delayed, // the same event will exist live and delayed MaxNotes, - Retrigger + Retrigger, + PlayUnprocessed, } #[derive(Default)] @@ -43,152 +70,244 @@ impl DerefMut for PlayingNotes { impl PlayingNotes { fn oldest_playing_note(&self, delayed_only: bool) -> Option<&AbsoluteTimeMidiMessage> { - self.iter().fold( - None, |prev, (_, message)| { - if prev.is_none() - || (!(delayed_only && message.reason == MessageReason::Live) && prev.unwrap().id > message.id) { - Some(message) - } else { - prev - } + self.iter().fold(None, |prev, (_, message)| { + if prev.is_none() + || (!(delayed_only && message.reason == MessageReason::Live) && prev.unwrap().id > message.id) + { + Some(message) + } else { + prev } - ) + }) } } +struct ScheduledEventsHelper { + playing_notes: PlayingNotes, + queued_messages: AbsoluteTimeMidiMessageVector, + notes_on_to_requeue: HashMap, + events: Vec, + buffer_duration_in_samples: usize, + delay_is_active: bool, + max_notes: MaxNotesParameter, + apply_max_notes_to_delayed_notes_only: bool, + current_time_in_samples: usize, +} -pub fn process_scheduled_events(samples: usize, current_time_in_samples: usize, - messages: &AbsoluteTimeMidiMessageVector, max_notes: u8, - apply_max_notes_to_delayed_notes_only: bool, delay_is_active: bool +pub fn process_scheduled_events( + buffer_duration_in_samples: usize, + delay_is_active: bool, + max_notes: MaxNotesParameter, + apply_max_notes_to_delayed_notes_only: bool, + current_time_in_samples: usize, + messages: &AbsoluteTimeMidiMessageVector, ) -> (AbsoluteTimeMidiMessageVector, Vec) { - let mut playing_notes: PlayingNotes = PlayingNotes::default(); - let mut queued_messages = AbsoluteTimeMidiMessageVector::default(); - let mut notes_on_to_requeue : HashMap = HashMap::new(); - let mut events: Vec = vec![]; + let helper = ScheduledEventsHelper::new( + buffer_duration_in_samples, + delay_is_active, + max_notes, + apply_max_notes_to_delayed_notes_only, + current_time_in_samples, + ); + helper.process_scheduled_events(messages) +} - let mut add_event = |event: AbsoluteTimeMidiMessage, playing_notes: &mut PlayingNotes| { - // note: playing_notes cannot be captured by closure because the method also uses it, causing the borrow checker - // to complain +impl ScheduledEventsHelper { + fn new( + buffer_duration_in_samples: usize, + delay_is_active: bool, + max_notes: MaxNotesParameter, + apply_max_notes_to_delayed_notes_only: bool, + current_time_in_samples: usize, + ) -> Self { + Self { + playing_notes: Default::default(), + queued_messages: Default::default(), + notes_on_to_requeue: Default::default(), + events: vec![], + buffer_duration_in_samples, + delay_is_active, + max_notes, + apply_max_notes_to_delayed_notes_only, + current_time_in_samples, + } + } - if event.play_time_in_samples < current_time_in_samples + samples { + fn add_event(&mut self, event: AbsoluteTimeMidiMessage) { + if event.play_time_in_samples < self.current_time_in_samples + self.buffer_duration_in_samples { // test if it belongs to that time window, as we don't want to replay notes on we put // back in the scheduled queue - if event.play_time_in_samples >= current_time_in_samples { + if event.play_time_in_samples >= self.current_time_in_samples { if let MidiMessageType::NoteOffMessage(_) = MidiMessageType::from(event) { - let note_on = notes_on_to_requeue.get_mut( & event.id); - if note_on.is_none() { return } // no such note running, skip - let note_on = note_on.unwrap(); + let note_off_event = event; - if event.reason == MessageReason::Live && delay_is_active { + let note_on = match self.notes_on_to_requeue.get_mut(¬e_off_event.id) { + None => { + // no such note running, skip + return; + } + Some(note_on) => note_on, + }; + + if note_off_event.reason == MessageReason::Live + && self.delay_is_active + && !self.max_notes.should_limit(self.playing_notes.len() - 1) + { // mark the note on as delayed from now on, but don't sent the note off note_on.reason = MessageReason::Delayed; - return + return; } - if apply_max_notes_to_delayed_notes_only && - event.reason == MessageReason::MaxNotes && - note_on.reason == MessageReason::Live { + if self.apply_max_notes_to_delayed_notes_only + && note_off_event.reason == MessageReason::MaxNotes + && note_on.reason == MessageReason::Live + { // should be redundant, as MaxNotes messages are not generated for Live notes. // keeping the logic to facilitate potential refactoring - return + return; } // stop this note, don't requeue - playing_notes.remove(&PlayingNoteIndex { pitch: event.get_pitch(), channel: event.get_channel() }); - notes_on_to_requeue.remove(&event.id); + self.playing_notes.remove(&PlayingNoteIndex { + pitch: note_off_event.get_pitch(), + channel: note_off_event.get_channel(), + }); + self.notes_on_to_requeue.remove(¬e_off_event.id); } - events.push(event.new_midi_event(current_time_in_samples)); + self.events.push(event.new_midi_event(self.current_time_in_samples)); } if let MidiMessageType::NoteOnMessage(_) = MidiMessageType::from(event) { - playing_notes.insert(PlayingNoteIndex { pitch: event.get_pitch(), channel: event.get_channel() }, - event); - notes_on_to_requeue.insert(event.id, event); + self.playing_notes.insert( + PlayingNoteIndex { + pitch: event.get_pitch(), + channel: event.get_channel(), + }, + event, + ); + self.notes_on_to_requeue.insert(event.id, event); } } else { - queued_messages.push(event); + self.queued_messages.push(event); } - }; + } - for mut message in messages.iter().copied() { - if message.play_time_in_samples < current_time_in_samples { - match MidiMessageType::from(message) { - MidiMessageType::NoteOnMessage(_) => {} - _ => { panic!("Only pending note on are expected to be found in the past") } - } - }; - - match MidiMessageType::from(message) { - MidiMessageType::NoteOnMessage(note_on) => { - // if still playing : generate note off at current sample, put note on with - // delta + 1 in the queue - let index = PlayingNoteIndex { channel: note_on.channel, pitch: note_on.pitch }; - - if let Some(already_playing_note) = playing_notes.get(&index) { - // we were still playing that note. generate a note off first. - add_event(AbsoluteTimeMidiMessage { - data: NoteOff { - channel: note_on.channel, - pitch: note_on.pitch, - velocity: 0, - }.into(), - id: already_playing_note.id, - reason: MessageReason::Retrigger, - play_time_in_samples: message.play_time_in_samples, - }, &mut playing_notes); - - // move the note on to the next sample or the daw might be confused - message.play_time_in_samples += 1; - } else if max_notes > 0 && playing_notes.len() >= max_notes as usize { - if let Some(oldest_playing_note) - = playing_notes.oldest_playing_note(apply_max_notes_to_delayed_notes_only) { - - let oldest_playing_note = *oldest_playing_note; // drop the borrow - - add_event(AbsoluteTimeMidiMessage { + fn process_scheduled_events( + mut self, + messages: &AbsoluteTimeMidiMessageVector, + ) -> (AbsoluteTimeMidiMessageVector, Vec) { + for mut message in messages.iter().copied() { + let midi_message_type = MidiMessageType::from(message); + + if message.play_time_in_samples < self.current_time_in_samples { + match midi_message_type { + MidiMessageType::NoteOnMessage(_) => {} + _ => { + panic!("Only pending note on are expected to be found in the past") + } + } + }; + + match midi_message_type { + MidiMessageType::NoteOnMessage(note_on) => { + // if still playing : generate note off at current sample, put note on with + // delta + 1 in the queue + let index = PlayingNoteIndex { + channel: note_on.channel, + pitch: note_on.pitch, + }; + + if let Some(&AbsoluteTimeMidiMessage { id, .. }) = self.playing_notes.get(&index) { + // we were still playing that note. generate a note off first. + self.add_event(AbsoluteTimeMidiMessage { data: NoteOff { - channel: oldest_playing_note.get_channel(), - pitch: oldest_playing_note.get_pitch(), + channel: note_on.channel, + pitch: note_on.pitch, velocity: 0, - }.into(), - id: oldest_playing_note.id, + } + .into(), + id, + reason: MessageReason::Retrigger, play_time_in_samples: message.play_time_in_samples, - reason: MessageReason::MaxNotes, - }, &mut playing_notes); - }; - } + }); - add_event(message, &mut playing_notes); - } + // move the note on to the next sample or the daw might be confused + message.play_time_in_samples += 1; + } else if self.max_notes.should_limit(self.playing_notes.len()) { + if let Some(&oldest_playing_note) = self + .playing_notes + .oldest_playing_note(self.apply_max_notes_to_delayed_notes_only) + { + self.add_event(AbsoluteTimeMidiMessage { + data: NoteOff { + channel: oldest_playing_note.get_channel(), + pitch: oldest_playing_note.get_pitch(), + velocity: 0, + } + .into(), + id: oldest_playing_note.id, + play_time_in_samples: message.play_time_in_samples, + reason: MessageReason::MaxNotes, + }); + }; + } - MidiMessageType::NoteOffMessage(note_off) => { - let playing_note = PlayingNoteIndex { channel: note_off.channel, pitch: note_off.pitch }; - match playing_notes.get(&playing_note) { - Some(currently_playing_note) => { - if currently_playing_note.id == message.id { - add_event(message, &mut playing_notes); - continue; - } else { - // this note was interrupted earlier already, don't send that - // note off or we may interrupt a new note with that delayed note - // off + self.add_event(message); + } + + MidiMessageType::NoteOffMessage(note_off) => { + let playing_note = PlayingNoteIndex { + channel: note_off.channel, + pitch: note_off.pitch, + }; + match self.playing_notes.get(&playing_note) { + Some(currently_playing_note) => { + if currently_playing_note.id == message.id { + self.add_event(message); + continue; + } else { + // this note was interrupted earlier already, don't send that + // note off or we may interrupt a new note with that delayed note + // off + continue; + } + } + None => { + // was not playing at all, skip continue; } - } - None => { - // was not playing at all, skip - continue; - } - }; - } - _ => { - add_event(message, &mut playing_notes); + }; + } + _ => { + self.add_event(message); + } } } + + for (_, event) in self.notes_on_to_requeue.drain() { + self.queued_messages.ordered_insert(event) + } + + (self.queued_messages, self.events) } +} - for (_, event) in notes_on_to_requeue { - queued_messages.ordered_insert(event) +pub fn raw_process_scheduled_events( + samples: usize, + current_time_in_samples: usize, + messages: &AbsoluteTimeMidiMessageVector, +) -> (AbsoluteTimeMidiMessageVector, Vec) { + let mut queued_messages = AbsoluteTimeMidiMessageVector::default(); + let mut events: Vec = vec![]; + + for message in messages.iter().copied() { + if message.play_time_in_samples < current_time_in_samples + samples { + if message.play_time_in_samples >= current_time_in_samples { + events.push(message.new_midi_event(current_time_in_samples)); + } + } else { + queued_messages.push(message); + } } (queued_messages, events) diff --git a/util/src/ipc_payload.rs b/util/src/ipc_payload.rs new file mode 100644 index 0000000..aabfc52 --- /dev/null +++ b/util/src/ipc_payload.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; + +use crate::midi_message_with_delta::MidiMessageWithDelta; +use crate::system::Uuid; +use ipc_channel::ipc::IpcSender; + +#[derive(Debug, Serialize, Deserialize)] +pub struct PatternPayload { + pub time: u64, + pub messages: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum IPCCommand { + PatternPayload(PatternPayload), + // stop is only used locally, but send over an IPC channel so the worker can listen both on the remote IPC + // for new patterns, and on local IPC for stopping the worker + Stop(IpcSender<()>, Uuid), + Ping(IpcSender<()>), +} + +#[derive(Serialize, Deserialize)] +pub enum BootstrapPayload { + Channel(IpcSender), + Timeout, +} diff --git a/util/src/lib.rs b/util/src/lib.rs index 54353a6..41b4dec 100644 --- a/util/src/lib.rs +++ b/util/src/lib.rs @@ -2,17 +2,24 @@ extern crate global_counter; use vst::event::MidiEvent; use vst::plugin::HostCallback; +use std::fmt::{Display, Formatter}; +use std::fmt; +use crate::parameters::get_exponential_scale_value; +pub mod absolute_time_midi_message; +pub mod absolute_time_midi_message_vector; pub mod constants; -pub mod debug; -pub mod parameter_value_conversion; -pub mod parameters; +pub mod delayed_message_consumer; +pub mod ipc_payload; +pub mod logging; pub mod messages; -pub mod absolute_time_midi_message; pub mod midi_message_type; +pub mod midi_message_with_delta; +pub mod parameter_value_conversion; +pub mod parameters; pub mod raw_message; -pub mod absolute_time_midi_message_vector; -pub mod delayed_message_consumer; +pub mod system; +pub mod transmute_buffer; #[derive(Default)] pub struct HostCallbackLock { @@ -31,15 +38,94 @@ pub fn make_midi_message(bytes: [u8; 3], delta_frames: i32) -> MidiEvent { } } -pub fn duration_display(value: f32) -> String { +pub fn duration_display(seconds: f32) -> String { let mut out = String::new(); - let mut _value = value; + let mut _value = seconds; if _value >= 1.0 { - out += &*format!("{:.0}s ", value); - _value -= value.trunc(); + out += &*format!("{:.0}s ", seconds); + _value -= seconds.trunc(); } if _value > 0.0 { - out += &*format!("{:3.0}ms", value * 1000.0); + out += &*format!("{:3.0}ms", seconds * 1000.0); } out } + +impl Display for Duration { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Duration::Off => "off".to_string(), + Duration::Duration(seconds) => { + duration_display(*seconds) + } + }.fmt(f) + } +} + +pub enum Duration { + Off, + Duration(f32), +} + + +impl From for Duration { + fn from(parameter_value: f32) -> Self { + match get_exponential_scale_value(parameter_value, 10., 20.) { + x if x == 0.0 => Duration::Off, + value => Duration::Duration(value) + } + } +} + +pub enum SyncDuration { + Off, + Subdivision(u8, u8), +} + + +impl From for SyncDuration { + fn from(value: f32) -> Self { + let value = (value * 10.0) as u8; + + match value { + 0 => SyncDuration::Off, + 1 => SyncDuration::Subdivision(1, 128), + 2 => SyncDuration::Subdivision(1, 64), + 3 => SyncDuration::Subdivision(1, 32), + 4 => SyncDuration::Subdivision(1, 16), + 5 => SyncDuration::Subdivision(1, 8), + 6 => SyncDuration::Subdivision(3, 16), + 7 => SyncDuration::Subdivision(1, 4), + 8 => SyncDuration::Subdivision(1, 2), + 9 => SyncDuration::Subdivision(3, 4), + _ => SyncDuration::Subdivision(1, 1), + } + } +} + +impl Display for SyncDuration { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + SyncDuration::Off => "off".to_string(), + SyncDuration::Subdivision(numerator, denominator) => { + match (numerator, denominator) { + (1, 1) => "1".to_string(), + (n, d) => format!("{}/{}", n, d) + } + } + }.fmt(f) + } +} + + +impl SyncDuration { + // this considers a 4/4 signature ( 4 beats per bar ) + + pub fn delay_to_samples(&self, bpm: f64, sample_rate: f32) -> Option { + match self { + SyncDuration::Off => None, + SyncDuration::Subdivision(numerator, denominator) => + Some((((*numerator as f64 / *denominator as f64 * 4.0) / bpm * 60.) as f32 * sample_rate) as usize), + } + } +} \ No newline at end of file diff --git a/util/src/logging.rs b/util/src/logging.rs new file mode 100644 index 0000000..f6121db --- /dev/null +++ b/util/src/logging.rs @@ -0,0 +1,32 @@ +#[cfg(not(feature = "enable_logging"))] +pub fn logging_setup() {} + +#[cfg(feature = "enable_logging")] +pub fn logging_setup() { + use log::info; + use simplelog::*; + use std::fs::OpenOptions; + + unsafe { + if LOGGING_SETUP { + return; + } + } + + if let Ok(file) = OpenOptions::new().append(true).create(true).open("/tmp/plugin.log") { + let mut config = ConfigBuilder::new(); + config.set_time_format("%+".to_string()); + WriteLogger::init(LevelFilter::Info, config.build(), file).unwrap(); + info!( + "{}", + build_info::format!("{{{} v{} built with {} at {}}}", $.crate_info.name, $.crate_info.version, $ + .compiler, $.timestamp) + ) + } + + log_panics::init(); + + unsafe { LOGGING_SETUP = true } +} + +static mut LOGGING_SETUP: bool = false; diff --git a/util/src/messages.rs b/util/src/messages.rs index eb9dca1..dbc5bbb 100644 --- a/util/src/messages.rs +++ b/util/src/messages.rs @@ -1,8 +1,12 @@ +#[allow(unused_imports)] +use log::{error, info}; + use vst::event::Event::Midi; use vst::event::{Event, MidiEvent}; -use super::constants::{NOTE_ON, NOTE_OFF, PRESSURE, PITCHBEND}; +use super::constants::{NOTE_OFF, NOTE_ON, PITCHBEND, PRESSURE}; use super::raw_message::RawMessage; +use crate::constants::{AFTERTOUCH, TIMBRECC}; pub fn format_midi_event(e: &MidiEvent) -> String { format!( @@ -42,18 +46,18 @@ pub fn format_event(e: &Event) -> String { */ pub trait ChannelMessage { - fn get_channel(&self) -> u8 ; + fn get_channel(&self) -> u8; } pub struct NoteOn { pub channel: u8, pub pitch: u8, - pub velocity: u8 + pub velocity: u8, } -impl Into for NoteOn { - fn into(self) -> RawMessage { - [NOTE_ON + self.channel, self.pitch, self.velocity].into() +impl From for RawMessage { + fn from(note_on: NoteOn) -> Self { + RawMessage([NOTE_ON + note_on.channel, note_on.pitch, note_on.velocity]) } } @@ -67,24 +71,22 @@ impl NoteMessage for NoteOn { } } - impl From for NoteOn { fn from(data: RawMessage) -> Self { NoteOn { channel: data[0] & 0x0F, pitch: data[1], - velocity: data[2] + velocity: data[2], } } } - impl From for NoteOff { fn from(data: RawMessage) -> Self { NoteOff { channel: data[0] & 0x0F, pitch: data[1], - velocity: data[2] + velocity: data[2], } } } @@ -95,30 +97,33 @@ impl ChannelMessage for NoteOn { } } -pub trait NoteMessage where Self: ChannelMessage { +pub trait NoteMessage +where + Self: ChannelMessage, +{ fn get_pitch(&self) -> u8; - fn get_velocity(&self) -> u8 ; + fn get_velocity(&self) -> u8; } pub struct NoteOff { pub channel: u8, pub pitch: u8, - pub velocity: u8 + pub velocity: u8, } impl From for NoteOff { fn from(m: NoteOn) -> Self { - NoteOff{ + NoteOff { channel: m.channel, pitch: m.pitch, - velocity: 0 + velocity: 0, } } } -impl Into for NoteOff { - fn into(self) -> RawMessage { - [NOTE_OFF + self.channel, self.pitch, self.velocity].into() +impl From for RawMessage { + fn from(note_off: NoteOff) -> Self { + RawMessage([NOTE_OFF + note_off.channel, note_off.pitch, note_off.velocity]) } } @@ -139,13 +144,13 @@ impl NoteMessage for NoteOff { } pub struct Pressure { - channel: u8, - value: u8 + pub channel: u8, + pub value: u8, } -impl Into for Pressure { - fn into(self) -> RawMessage { - [PRESSURE + self.channel, self.value, 0].into() +impl From for RawMessage { + fn from(pressure: Pressure) -> Self { + RawMessage([PRESSURE + pressure.channel, pressure.value, 0]) } } @@ -153,7 +158,7 @@ impl From for Pressure { fn from(data: RawMessage) -> Self { Pressure { channel: data[0] & 0x0F, - value: data[1] + value: data[1], } } } @@ -165,9 +170,8 @@ impl ChannelMessage for Pressure { } pub struct PitchBend { - channel: u8, - semitones: u8, - millisemitones: u8 + pub channel: u8, + pub millisemitones: i32, } impl ChannelMessage for PitchBend { @@ -176,42 +180,69 @@ impl ChannelMessage for PitchBend { } } -impl Into for PitchBend { - fn into(self) -> RawMessage { +impl From for RawMessage { + fn from(pitch_bend: PitchBend) -> Self { // 96000 millisemitones are expressed over the possible values of 14 bits ( 16384 ) // which never gets us an exact integer amount of semitones - let millisemitones = (self.semitones as i32 * 1000) + self.millisemitones as i32 ; - let value = ((millisemitones + 48000) * 16383) / 96000; + let value = ((pitch_bend.millisemitones + 48000) * 16384) / 96000; let msb = value >> 7; let lsb = value & 0x7F; - [self.channel + PITCHBEND, lsb as u8, msb as u8].into() + RawMessage([pitch_bend.channel + PITCHBEND, lsb as u8, msb as u8]) } } impl From for PitchBend { fn from(data: RawMessage) -> Self { - let lsb : i32 = data[1] as i32; - let msb : i32 = data[2] as i32; + let lsb: i32 = data[1] as i32; + let msb: i32 = data[2] as i32; let value = lsb + (msb << 7); - let millisemitones = (value * 96000 / 16383) - 48000; + let millisemitones = (value * 96000 / 16384) - 48000; PitchBend { channel: data[0] & 0x0F, - semitones: (millisemitones / 1000) as u8, - millisemitones: (millisemitones % 1000) as u8 + millisemitones, + } + } +} + +#[derive(Debug)] +pub struct AfterTouch { + pub channel: u8, + pub pitch: u8, + pub value: u8, +} + +impl ChannelMessage for AfterTouch { + fn get_channel(&self) -> u8 { + self.channel + } +} + +impl From for RawMessage { + fn from(aftertouch: AfterTouch) -> Self { + RawMessage([aftertouch.channel + AFTERTOUCH, aftertouch.pitch, aftertouch.value]) + } +} + +impl From for AfterTouch { + fn from(data: RawMessage) -> Self { + AfterTouch { + channel: data[0] & 0x0F, + pitch: data[1], + value: data[2], } } } pub struct CC { - channel: u8, - cc: u8, - value: u8 + pub channel: u8, + pub cc: u8, + pub value: u8, } -impl Into for CC { - fn into(self) -> RawMessage { - [self.channel, self.cc, self.value].into() +impl From for RawMessage { + fn from(cc: CC) -> Self { + RawMessage([0xB0 + cc.channel, cc.cc, cc.value]) } } @@ -220,7 +251,7 @@ impl From for CC { CC { channel: data[0] & 0x0F, cc: data[1], - value: data[2] + value: data[2], } } } @@ -245,9 +276,23 @@ impl From for GenericChannelMessage { } } - impl From<&[u8; 3]> for GenericChannelMessage { fn from(data: &[u8; 3]) -> Self { GenericChannelMessage(RawMessage::from(*data)) } } + +pub struct Timbre { + pub channel: u8, + pub value: u8, +} + +impl From for RawMessage { + fn from(timbre: Timbre) -> Self { + RawMessage::from(CC { + channel: timbre.channel, + cc: TIMBRECC, + value: timbre.value, + }) + } +} diff --git a/util/src/midi_message_type.rs b/util/src/midi_message_type.rs index ccce44e..b2a3ddf 100644 --- a/util/src/midi_message_type.rs +++ b/util/src/midi_message_type.rs @@ -1,8 +1,8 @@ -use core::convert::From; -use super::raw_message::RawMessage; use super::absolute_time_midi_message::AbsoluteTimeMidiMessage; -use super::messages::{NoteOn, NoteOff, CC, Pressure, PitchBend, GenericChannelMessage}; - +use super::messages::{GenericChannelMessage, NoteOff, NoteOn, PitchBend, Pressure, CC}; +use super::raw_message::RawMessage; +use crate::messages::AfterTouch; +use core::convert::From; pub enum MidiMessageType { NoteOnMessage(NoteOn), @@ -10,8 +10,9 @@ pub enum MidiMessageType { CCMessage(CC), PressureMessage(Pressure), PitchBendMessage(PitchBend), + AfterTouchMessage(AfterTouch), UnsupportedChannelMessage(GenericChannelMessage), - Unsupported + Unsupported, } impl MidiMessageType { @@ -19,20 +20,19 @@ impl MidiMessageType { let (channel, pitch) = match self { MidiMessageType::NoteOnMessage(m) => (m.channel, m.pitch), MidiMessageType::NoteOffMessage(m) => (m.channel, m.pitch), - _ => return false + _ => return false, }; let (channel2, pitch2) = match other { MidiMessageType::NoteOnMessage(m) => (m.channel, m.pitch), MidiMessageType::NoteOffMessage(m) => (m.channel, m.pitch), - _ => return false + _ => return false, }; channel == channel2 && pitch == pitch2 } } - impl From for MidiMessageType { fn from(data: RawMessage) -> Self { match data[0] & 0xF0 { @@ -42,12 +42,11 @@ impl From for MidiMessageType { 0xD0 => MidiMessageType::PressureMessage(Pressure::from(data)), 0xE0 => MidiMessageType::PitchBendMessage(PitchBend::from(data)), 0xA0 | 0xC0 | 0xF0 => MidiMessageType::UnsupportedChannelMessage(GenericChannelMessage::from(data)), - _ => MidiMessageType::Unsupported + _ => MidiMessageType::Unsupported, } } } - impl From<&[u8; 3]> for MidiMessageType { fn from(data: &[u8; 3]) -> Self { Self::from(RawMessage::from(*data)) diff --git a/util/src/midi_message_with_delta.rs b/util/src/midi_message_with_delta.rs new file mode 100644 index 0000000..0ad24ce --- /dev/null +++ b/util/src/midi_message_with_delta.rs @@ -0,0 +1,39 @@ +use crate::raw_message::RawMessage; +use serde::{Deserialize, Serialize}; +use vst::buffer::PlaceholderEvent; +use vst::event::MidiEvent; + +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +pub struct MidiMessageWithDelta { + pub delta_frames: u16, + pub data: RawMessage, +} + +impl vst::buffer::WriteIntoPlaceholder for MidiMessageWithDelta { + fn write_into(&self, out: &mut PlaceholderEvent) { + MidiEvent { + data: self.data.into(), + delta_frames: self.delta_frames as i32, + live: false, + note_length: None, + note_offset: None, + detune: 0, + note_off_velocity: 0, + } + .write_into(out) + } +} + +impl MidiMessageWithDelta { + pub fn new_midi_event(&self) -> MidiEvent { + MidiEvent { + data: self.data.into(), + delta_frames: self.delta_frames as i32, + live: true, + note_length: None, + note_offset: None, + detune: 0, + note_off_velocity: 0, + } + } +} diff --git a/util/src/parameter_value_conversion.rs b/util/src/parameter_value_conversion.rs index e660416..dc4f13f 100644 --- a/util/src/parameter_value_conversion.rs +++ b/util/src/parameter_value_conversion.rs @@ -25,15 +25,14 @@ pub fn f32_to_byte(value: f32) -> u8 { #[inline] pub fn u14_to_f32(value: u16) -> f32 { // pitchbend goes from 0x0000 ( -48 semitones ) to 0x3FFF ( +48 semitones ) - value as f32 / (0x3FFF as usize) as f32 + value as f32 / (0x3FFF_usize) as f32 } #[inline] pub fn f32_to_u14(value: f32) -> u16 { - (value * (0x3FFF as usize) as f32) as u16 + (value * (0x3FFF_usize) as f32) as u16 } - // TODO better try to find a type that fits in 32 bits and store it as binary into the f32, // disregarding what f32 is suppose to contain diff --git a/util/src/parameters.rs b/util/src/parameters.rs index d132edb..6ba4f93 100644 --- a/util/src/parameters.rs +++ b/util/src/parameters.rs @@ -1,12 +1,25 @@ use vst::plugin::PluginParameters; use vst::util::ParameterTransfer; -use super::parameter_value_conversion::{f32_to_byte, byte_to_f32, f32_to_bool, bool_to_f32, u14_to_f32, f32_to_u14}; +use super::parameter_value_conversion::{bool_to_f32, byte_to_f32, f32_to_bool, f32_to_byte, f32_to_u14, u14_to_f32}; + + +#[inline] +pub fn get_exponential_scale_value(value: f32, max: f32, factor: f32) -> f32 { + (factor.powf(value) - 1.) * max / (factor - 1.0) +} + +#[inline] +pub fn get_reverse_exponential_scale_value(value: f32, max: f32, factor: f32) -> f32 { + ((value * (factor - 1.0) / max) + 1.).log(factor) +} + // TODO can Parameter implement just from/into i32, and provide a default implementation for usize ? pub trait ParameterConversion - where ParameterType: Into + From, - Self: PluginParameters +where + ParameterType: Into + From, + Self: PluginParameters, { #[inline] fn get_byte_parameter(&self, index: ParameterType) -> u8 { @@ -33,7 +46,7 @@ pub trait ParameterConversion #[inline] fn get_exponential_scale_parameter(&self, index: ParameterType, max: f32, factor: f32) -> f32 { let x = self.get_parameter_transfer().get_parameter(index.into() as usize); - (factor.powf(x) - 1.) * max / (factor - 1.0) + get_exponential_scale_value(x, max, factor) } #[inline] @@ -58,19 +71,22 @@ pub trait ParameterConversion // self.get_parameter_transfer().get_parameter(index as usize) // } - fn get_parameter_transfer(&self) -> &ParameterTransfer ; - + fn get_parameter_transfer(&self) -> &ParameterTransfer; - fn get_parameter_count() -> usize ; + fn get_parameter_count() -> usize; fn serialize_state(&self) -> Vec { (0..Self::get_parameter_count()) - .map(|i | self.get_byte_parameter((i as i32).into())) + .map(|i| self.get_byte_parameter((i as i32).into())) .collect() } fn deserialize_state(&self, data: &[u8]) { - for (i, item) in data.iter().enumerate() { + for (i, item) in data + .iter() + .enumerate() + .take_while(|(i, _)| *i < Self::get_parameter_count()) + { self.set_byte_parameter((i as i32).into(), *item); } } diff --git a/util/src/raw_message.rs b/util/src/raw_message.rs index 4f2a9ca..8e5a7eb 100644 --- a/util/src/raw_message.rs +++ b/util/src/raw_message.rs @@ -1,10 +1,29 @@ +use super::messages::ChannelMessage; +use crate::constants::PRESSURE; use core::clone::Clone; -use core::convert::{From, Into}; +use core::convert::From; use core::ops::Index; -use super::messages::ChannelMessage; +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; + +#[derive(Copy, Debug, Serialize, Deserialize)] +pub struct RawMessage(pub [u8; 3]); + + +impl RawMessage { + #[inline] + pub fn get_bytes(&self) -> &[u8] { + // RawMessage is 3 bytes long to keep moving around a known-size message, but some actually are 2 bytes long + &self.0 -#[derive(Copy)] -pub struct RawMessage([u8; 3]); + // bitwig crashes when receiving pressure messages from a VST ; commented out as it might help, still unlikely + // if self.0[0] & 0xF0 == PRESSURE { + // &self.0[..2] + // } else { + // &self.0 + // } + } +} impl ChannelMessage for RawMessage { fn get_channel(&self) -> u8 { @@ -18,15 +37,20 @@ impl Clone for RawMessage { } } -impl From<[u8;3]> for RawMessage { +impl From<[u8; 3]> for RawMessage { fn from(e: [u8; 3]) -> Self { RawMessage(e) } } -impl Into<[u8;3]> for RawMessage { - fn into(self) -> [u8; 3] { - self.0 + +impl From for [u8; 3] { + fn from(raw_message: RawMessage) -> Self { + if raw_message.0[0] & 0xF0 == PRESSURE { + [raw_message.0[0],raw_message.0[1],0] + } else { + raw_message.0 + } } } @@ -37,3 +61,31 @@ impl Index for RawMessage { &self.0[index] } } + + +impl PartialOrd for RawMessage { + fn partial_cmp(&self, other: &Self) -> Option { + let cmp_0 = self.0[0].cmp(&other.0[0]); + match cmp_0 { + Ordering::Equal => { + Some(self.0[1].cmp(&other.0[1])) + } + _ => Some(cmp_0) + } + } +} + +impl PartialEq for RawMessage { + fn eq(&self, other: &Self) -> bool { + self.0[0] == other.0[0] && self.0[1] == other.0[1] + } +} + +impl Ord for RawMessage { + fn cmp(&self, other: &Self) -> Ordering { + self.partial_cmp(other).unwrap() + } +} + + +impl Eq for RawMessage {} diff --git a/util/src/system.rs b/util/src/system.rs new file mode 100644 index 0000000..0b41642 --- /dev/null +++ b/util/src/system.rs @@ -0,0 +1,30 @@ +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::fmt::{Debug, Display}; + +// generate unique IDs on top of Uuid, which does not implement Serialize/Deserialize + +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +pub struct Uuid(u128); + +impl Default for Uuid { + fn default() -> Self { + Self { + 0: uuid::Uuid::default().to_u128_le(), + } + } +} + +impl Uuid { + pub fn new_v4() -> Self { + Self { + 0: uuid::Uuid::new_v4().to_u128_le(), + } + } +} + +impl Display for Uuid { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + std::fmt::Display::fmt(&uuid::Uuid::from_u128_le(self.0), f) + } +} diff --git a/util/src/transmute_buffer.rs b/util/src/transmute_buffer.rs new file mode 100644 index 0000000..62ba223 --- /dev/null +++ b/util/src/transmute_buffer.rs @@ -0,0 +1,22 @@ +pub fn transmute_raw_buffer_mut(buffer: &mut [F]) -> &mut [T] { + use std::mem::size_of; + use std::slice; + unsafe { + slice::from_raw_parts_mut( + buffer.as_mut_ptr() as *mut T, + buffer.len() * size_of::() / size_of::(), + ) + } +} + +pub fn transmute_raw_buffer(buffer: &[F]) -> &[T] { + use std::mem::size_of; + use std::slice; + + unsafe { + slice::from_raw_parts_mut( + buffer.as_ptr() as *mut T, + buffer.len() * size_of::() / size_of::(), + ) + } +}