Compare commits

..
Author SHA1 Message Date
Dylan M. Taylor 5b2cac81ed build: pin craft-update to craft-libs main (495e597)
Picks up the update prompt fix that draws its close mark and version pill
without glyphs a font may lack, so the prompt no longer takes the app down
when a bundled face has no ✕ or →.

Part of tracking issue storytold/craft-libs#319

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
2026-10-25 16:51:50 -04:00
27 changed files with 805 additions and 980 deletions
Generated
+208 -139
View File
@@ -4,18 +4,18 @@ version = 4
[[package]]
name = "accesskit"
version = "0.25.2"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e883d1d28c5fd5568c79ed2d2bd48d9f2fd3945c8974ed0f53690ceddb051be"
checksum = "ad442f58ee04714aaa0ba0a2768c1ea1935b29507bb22ddece8cbbc76db02932"
dependencies = [
"uuid",
]
[[package]]
name = "accesskit_atspi_common"
version = "0.21.1"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24f5baf62e34a4a6c30e3d697e4782f821a7337049c44c3b40576a27dfbb580d"
checksum = "52c182f9c282ac9c5638d876d551d15e5f7d397ec263349a0c6a2b61595dd5e4"
dependencies = [
"accesskit",
"accesskit_consumer",
@@ -27,9 +27,9 @@ dependencies = [
[[package]]
name = "accesskit_consumer"
version = "0.39.2"
version = "0.39.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3da8f27636015191b2264ee1df681503170a632cafd832fdc1edfdd4e3a2fd3"
checksum = "8a45e2bb300bea6d5f33a82c3e051c3ddc6f41c209750f68a0acbe991a284344"
dependencies = [
"accesskit",
"hashbrown 0.17.1",
@@ -37,9 +37,9 @@ dependencies = [
[[package]]
name = "accesskit_ios"
version = "0.2.2"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5280724673bbfa8b30a838c013500b719392989be3b97c5a9964062c252c232"
checksum = "28e539d64b27fd4c14de8f78ada8f7c944c092f52f7e49fb63316507a57f6937"
dependencies = [
"accesskit",
"accesskit_consumer",
@@ -51,9 +51,9 @@ dependencies = [
[[package]]
name = "accesskit_macos"
version = "0.27.2"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "123828ceeec755b05255e3976cb0852993202de98a0626c97dd8d34246991f84"
checksum = "5701624bf6a11efb6d4c8e922bf44126ed96250e5faa9a7a0468c979635839a4"
dependencies = [
"accesskit",
"accesskit_consumer",
@@ -65,9 +65,9 @@ dependencies = [
[[package]]
name = "accesskit_unix"
version = "0.24.1"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d5192871228c54f1b0fb4a85cef000e11a3804f25d2165f14afa0daa67f3f6"
checksum = "202f24df034a7476d07b7f74284de84f6d62aabd858dbe7ee9cad3b7ad6f8f9d"
dependencies = [
"accesskit",
"accesskit_atspi_common",
@@ -83,9 +83,9 @@ dependencies = [
[[package]]
name = "accesskit_windows"
version = "0.35.2"
version = "0.35.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "498668b90da9995302a0055aaecad4756932333da2ddef6baee1c00844c430fc"
checksum = "ce63f35d6bdcf59f26b76b3379063f738e6412cef46999cc772d46aa3de35adb"
dependencies = [
"accesskit",
"accesskit_consumer",
@@ -97,9 +97,9 @@ dependencies = [
[[package]]
name = "accesskit_winit"
version = "0.34.2"
version = "0.34.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd4ab4437b520ff03088fbe48676a11e7d44a929535e64b31432c1073e48dbcc"
checksum = "ee96dd150375c05912d8a473f98c2fb155fb3234fac88292d396548a2460fa77"
dependencies = [
"accesskit",
"accesskit_ios",
@@ -208,9 +208,9 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
[[package]]
name = "arboard"
version = "3.7.0"
version = "3.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aeb9553e62312c8756a5979254fc6d7da8bbeb5986a1e17037fab932f7037917"
checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
dependencies = [
"clipboard-win",
"image",
@@ -222,7 +222,7 @@ dependencies = [
"objc2-foundation 0.3.2",
"parking_lot",
"percent-encoding",
"windows-sys 0.61.2",
"windows-sys 0.60.2",
"wl-clipboard-rs",
"x11rb",
]
@@ -354,7 +354,7 @@ checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
@@ -389,7 +389,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
@@ -574,7 +574,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
@@ -600,7 +600,7 @@ checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
@@ -667,9 +667,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.10.0"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0"
checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -746,9 +746,9 @@ checksum = "a76abbdb2907f6fd97fb6bc0b7be96b77d328f2dd9669d1075cc03369ed22154"
[[package]]
name = "clipboard-win"
version = "5.4.3"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "052af7878466feccda12ba7a6a67d019d712f296809559b47c12c8cc196d3a9a"
checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
dependencies = [
"error-code",
]
@@ -791,9 +791,9 @@ checksum = "5ce183b975b8fc736a20919c4002717255fbd867df575c792aeefa9a56a73ad0"
[[package]]
name = "combine"
version = "4.6.9"
version = "4.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92"
checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e"
dependencies = [
"bytes",
"memchr",
@@ -984,9 +984,9 @@ dependencies = [
[[package]]
name = "crc32fast"
version = "1.5.3"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055"
checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78"
dependencies = [
"cfg-if",
]
@@ -1108,7 +1108,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
@@ -1362,9 +1362,9 @@ source = "git+https://github.com/storytold/egui.git?rev=6d518fc799681571963842f9
[[package]]
name = "equivalent"
version = "1.0.3"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
@@ -1456,9 +1456,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
version = "0.1.15"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48"
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
[[package]]
name = "fixedbitset"
@@ -1497,9 +1497,9 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "font-types"
version = "0.12.7"
version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "835ae9d781464a79026cde387f9cbc650c9a4909d7cf7e658b127e3c35d2f35d"
checksum = "6c92d9081c7ab3bd83ef1eecdfad090f3d966e3ce74106779664114cfe1a6274"
dependencies = [
"bytemuck",
]
@@ -1522,7 +1522,7 @@ checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
@@ -1582,7 +1582,7 @@ checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
@@ -1663,11 +1663,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi 6.0.0",
"rand_core",
"wasm-bindgen",
]
[[package]]
@@ -1887,9 +1885,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "icu_collections"
version = "2.4.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd"
checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
dependencies = [
"displaydoc",
"potential_utf",
@@ -1901,9 +1899,9 @@ dependencies = [
[[package]]
name = "icu_locale_core"
version = "2.4.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0"
checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
dependencies = [
"displaydoc",
"litemap",
@@ -1914,9 +1912,9 @@ dependencies = [
[[package]]
name = "icu_normalizer"
version = "2.4.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599"
checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
dependencies = [
"icu_collections",
"icu_normalizer_data",
@@ -1928,15 +1926,15 @@ dependencies = [
[[package]]
name = "icu_normalizer_data"
version = "2.4.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc"
checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
[[package]]
name = "icu_properties"
version = "2.4.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f"
checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
dependencies = [
"displaydoc",
"icu_collections",
@@ -1949,15 +1947,15 @@ dependencies = [
[[package]]
name = "icu_properties_data"
version = "2.4.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf"
checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
[[package]]
name = "icu_provider"
version = "2.4.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514"
checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
dependencies = [
"displaydoc",
"icu_locale_core",
@@ -2194,9 +2192,9 @@ checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
[[package]]
name = "libc"
version = "0.2.191"
version = "0.2.190"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78"
[[package]]
name = "libloading"
@@ -2233,7 +2231,7 @@ dependencies = [
"bitflags 2.13.2",
"libc",
"plain",
"redox_syscall 0.9.5",
"redox_syscall 0.9.4",
]
[[package]]
@@ -2262,9 +2260,9 @@ checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
[[package]]
name = "litrs"
version = "1.0.1"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4744e383959f0db86ede514b809b1c53251889093803c05267acc7d4e7030d70"
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
[[package]]
name = "lock_api"
@@ -2295,9 +2293,9 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
[[package]]
name = "memmap2"
version = "0.9.12"
version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b0ca1155a9ecc25aa12e8313a119f32edf0255fea2f44839c4ac0a12b2ac157"
checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0"
dependencies = [
"libc",
]
@@ -2908,9 +2906,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "open"
version = "5.4.5"
version = "5.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d8c8b8e725ad26404d6896eb2f064294a0a7b4f1d9120210d85544684c4ca95"
checksum = "aa576c76302b7b808eecc68061e67336c47833ef9d22caa74dda10fa9675eebc"
dependencies = [
"is-wsl",
"libc",
@@ -2934,9 +2932,9 @@ dependencies = [
[[package]]
name = "ordered-float"
version = "5.5.1"
version = "5.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f59006ce1e204c30956638535b74d317bca99477b9356dfaec54c37f6f600ab"
checksum = "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0"
dependencies = [
"num-traits",
]
@@ -3429,9 +3427,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.9.5"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "580c875845b41ecb1564ad1ae4703044cef774145e3d7a94dc2b3fa7c97868fd"
checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e"
dependencies = [
"bitflags 2.13.2",
]
@@ -3773,7 +3771,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
@@ -3797,14 +3795,14 @@ checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
name = "serde_spanned"
version = "1.1.2"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c"
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [
"serde_core",
]
@@ -3897,9 +3895,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.16.4"
version = "1.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8"
checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
[[package]]
name = "smithay-client-toolkit"
@@ -3990,7 +3988,6 @@ dependencies = [
"soundcraft-engine",
"soundcraft-playback",
"soundcraft-ui-egui",
"winit",
"winresource",
]
@@ -4467,9 +4464,9 @@ dependencies = [
[[package]]
name = "syn"
version = "3.0.9"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
dependencies = [
"proc-macro2",
"quote",
@@ -4484,7 +4481,7 @@ checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
@@ -4556,7 +4553,7 @@ checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
@@ -4605,9 +4602,9 @@ dependencies = [
[[package]]
name = "tinystr"
version = "0.8.5"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a"
checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643"
dependencies = [
"displaydoc",
"zerovec",
@@ -4615,18 +4612,18 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.54.1"
version = "1.53.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3"
checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044"
dependencies = [
"pin-project-lite",
]
[[package]]
name = "toml"
version = "1.1.8+spec-1.1.0"
version = "1.1.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20489e00e4d8741d6be680764cc12e270655e375a20d1011e844a9c3379e678d"
checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a"
dependencies = [
"indexmap",
"serde_core",
@@ -4639,18 +4636,18 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "1.1.2+spec-1.1.0"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053"
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.25.17+spec-1.1.0"
version = "0.25.15+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c"
checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614"
dependencies = [
"indexmap",
"toml_datetime",
@@ -4660,18 +4657,18 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.1.5+spec-1.1.0"
version = "1.1.3+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c"
checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.1.3+spec-1.1.0"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770"
checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2"
[[package]]
name = "tracing"
@@ -4737,9 +4734,9 @@ dependencies = [
[[package]]
name = "typed-path"
version = "0.13.0"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce898823d8cd0f028662d2f349694b1af0a5cc9be83f1d213a00d3147445e1e"
checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
[[package]]
name = "typenum"
@@ -4778,9 +4775,9 @@ checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
[[package]]
name = "unicode-ident"
version = "1.0.27"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]
name = "unicode-properties"
@@ -4861,9 +4858,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.29.0"
version = "1.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514"
checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a"
dependencies = [
"js-sys",
"serde_core",
@@ -4999,7 +4996,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
"wasm-bindgen-shared",
]
@@ -5169,9 +5166,9 @@ dependencies = [
[[package]]
name = "webbrowser"
version = "1.2.5"
version = "1.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2c8ddfbbf66e501434657400268cefe51b95140a0266236415cda7627732df7"
checksum = "62c35be770821a214dbc362fc26908c853e776c0004294d0b10b8a6bad582f94"
dependencies = [
"jni",
"log",
@@ -5497,7 +5494,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
@@ -5506,7 +5503,16 @@ version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.5",
]
[[package]]
@@ -5524,14 +5530,31 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
"windows_i686_gnullvm 0.53.1",
"windows_i686_msvc 0.53.1",
"windows_x86_64_gnu 0.53.1",
"windows_x86_64_gnullvm 0.53.1",
"windows_x86_64_msvc 0.53.1",
]
[[package]]
@@ -5549,48 +5572,96 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winit"
version = "0.30.13"
@@ -5778,9 +5849,9 @@ dependencies = [
[[package]]
name = "yoke"
version = "0.8.4"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3"
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
dependencies = [
"stable_deref_trait",
"yoke-derive",
@@ -5795,7 +5866,7 @@ checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
"synstructure",
]
@@ -5867,7 +5938,7 @@ dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
"zbus_names",
"zvariant",
"zvariant_utils",
@@ -5875,9 +5946,9 @@ dependencies = [
[[package]]
name = "zbus_names"
version = "4.3.5"
version = "4.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812a12be4ead7a59e18ea8f047d08add0066b73bc62fc1467066a2472ae088dd"
checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e"
dependencies = [
"serde",
"winnow",
@@ -5907,18 +5978,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.63"
version = "0.8.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8"
checksum = "6400fc4bb7426f6faac3fb07566122bb18622e3022bd2c36426a92ed489b1dbe"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.63"
version = "0.8.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f"
checksum = "99c27cc7525bad4df59a45e073c45b2c2aa820efb588234cc23a547ec69a34db"
dependencies = [
"proc-macro2",
"quote",
@@ -5942,7 +6013,7 @@ checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
"synstructure",
]
@@ -5965,9 +6036,9 @@ dependencies = [
[[package]]
name = "zerovec"
version = "0.11.9"
version = "0.11.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40"
checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8"
dependencies = [
"yoke",
"zerofrom",
@@ -5976,29 +6047,27 @@ dependencies = [
[[package]]
name = "zerovec-derive"
version = "0.11.7"
version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a"
checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
]
[[package]]
name = "zip"
version = "9.1.1"
version = "9.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "518b847fd864440c379748ad684c449f293af68978a485a4ff16f7c901c1f7c4"
checksum = "6e04976da629d3b04204ba8105b396a193af8ad2583b2c79bfb3ab8af580a11e"
dependencies = [
"crc32fast",
"flate2",
"getrandom 0.4.3",
"indexmap",
"memchr",
"nix",
"typed-path",
"windows-sys 0.61.2",
"zopfli",
]
@@ -6043,9 +6112,9 @@ dependencies = [
[[package]]
name = "zvariant"
version = "5.15.1"
version = "5.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80bf6d6cd6a4b601acb64cfb8284cdc643fcfa75ef26ae3c46ec8aa4e8a1fbb7"
checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147"
dependencies = [
"endi",
"enumflags2",
@@ -6058,14 +6127,14 @@ dependencies = [
[[package]]
name = "zvariant_derive"
version = "5.15.1"
version = "5.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef2672456ba1cb54b5eaec136e63135b3c7495fb938e53b28a62e633496d486d"
checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 3.0.9",
"syn 3.0.6",
"zvariant_utils",
]
@@ -6078,7 +6147,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 3.0.9",
"syn 3.0.6",
"winnow",
]
-3
View File
@@ -24,9 +24,6 @@ serde_json.workspace = true
log.workspace = true
chrono.workspace = true
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
winit = { version = "0.30", default-features = false, features = ["x11"] }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
rfd.workspace = true
-10
View File
@@ -274,16 +274,6 @@ fn main() -> eframe::Result {
// Before eframe creates the wgpu instance: default Windows to DirectX 12 with FXC (see graphics.rs).
#[cfg(target_os = "windows")]
graphics::configure(&mut options, eframe::wgpu::Backends::from_env(), eframe::wgpu::Dx12Compiler::from_env());
#[cfg(all(unix, not(target_os = "macos")))]
// winit has no file drag-and-drop on Wayland (only on X11), so dropping files from the file
// manager showed a "no" cursor. Run through XWayland when it's there; SOUNDCRAFT_WAYLAND=1
// keeps the native Wayland backend.
if std::env::var_os("DISPLAY").is_some_and(|d| !d.is_empty()) && std::env::var_os("SOUNDCRAFT_WAYLAND").is_none() {
options.event_loop_builder = Some(Box::new(|b| {
use winit::platform::x11::EventLoopBuilderExtX11;
b.with_x11();
}));
}
eframe::run_native(
"SoundCraft",
options,
+16 -37
View File
@@ -456,39 +456,6 @@ fn info_of(reader: &dyn FormatReader, detected: FileFormat) -> Result<(u32, Audi
))
}
/// A demux error ends the decode; it is only an error when nothing was decoded yet.
fn demux_failure(frames: u64, e: SymError) -> Result<()> {
if frames == 0 {
return Err(map_err(e));
}
log::warn!("stopping decode after demux error: {e}");
Ok(())
}
/// Note a corrupt packet (the `errors`-th); a stream that never decodes is malformed.
fn corrupt_packet(errors: usize, frames: u64, msg: &str) -> Result<()> {
log::debug!("skipping corrupt packet: {msg}");
if errors > 1000 && frames == 0 {
return Err(AudioError::Malformed(format!("no decodable packets: {msg}")));
}
Ok(())
}
/// The decoded stream is authoritative for the channel count: record it and allocate the planes.
fn init_planar(info: &mut AudioInfo, ch: usize) -> Result<Vec<Vec<f32>>> {
info.channels = u16::try_from(ch).map_err(|_| AudioError::Unsupported("too many channels".into()))?;
Ok((0..ch).map(|_| Vec::new()).collect())
}
/// Append one interleaved block to the planes, replacing non-finite samples with silence.
fn append_interleaved(planar: &mut [Vec<f32>], interleaved: &[f32], ch: usize) {
for frame in interleaved.chunks_exact(ch) {
for (dst, &s) in planar.iter_mut().zip(frame) {
dst.push(if s.is_finite() { s } else { 0.0 });
}
}
}
/// Decode the whole first audio track. When `count_only`, samples are counted but not stored.
fn run(bytes: &[u8], detected: FileFormat, count_only: bool) -> Result<(AudioInfo, AudioBuffer)> {
let mut reader = match open(bytes, detected)? {
@@ -524,7 +491,10 @@ fn run(bytes: &[u8], detected: FileFormat, count_only: bool) -> Result<(AudioInf
Err(SymError::IoError(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
Err(SymError::ResetRequired) => break,
Err(e) => {
demux_failure(frames, e)?;
if frames == 0 {
return Err(map_err(e));
}
log::warn!("stopping decode after demux error: {e}");
break;
}
};
@@ -535,7 +505,10 @@ fn run(bytes: &[u8], detected: FileFormat, count_only: bool) -> Result<(AudioInf
Ok(d) => d,
Err(SymError::DecodeError(m)) => {
errors += 1;
corrupt_packet(errors, frames, m)?;
log::debug!("skipping corrupt packet: {m}");
if errors > 1000 && frames == 0 {
return Err(AudioError::Malformed(format!("no decodable packets: {m}")));
}
continue;
}
Err(SymError::IoError(_)) => break,
@@ -548,7 +521,9 @@ fn run(bytes: &[u8], detected: FileFormat, count_only: bool) -> Result<(AudioInf
continue;
}
if planar.is_empty() {
planar = init_planar(&mut info, ch)?;
// The decoded stream is authoritative for the channel count.
info.channels = u16::try_from(ch).map_err(|_| AudioError::Unsupported("too many channels".into()))?;
planar = (0..ch).map(|_| Vec::new()).collect();
}
let n = decoded.frames() as u64;
frames = frames.saturating_add(n);
@@ -567,7 +542,11 @@ fn run(bytes: &[u8], detected: FileFormat, count_only: bool) -> Result<(AudioInf
interleaved.resize(needed, 0.0);
}
decoded.copy_to_slice_interleaved(&mut interleaved[..needed]);
append_interleaved(&mut planar, &interleaved[..needed], ch);
for frame in interleaved[..needed].chunks_exact(ch) {
for (dst, &s) in planar.iter_mut().zip(frame) {
dst.push(if s.is_finite() { s } else { 0.0 });
}
}
}
if info.channels == 0 {
return Err(AudioError::Malformed("stream contains no audio".into()));
+21 -41
View File
@@ -178,45 +178,6 @@ fn only_chunks_follow(b: &[u8], from: usize) -> bool {
false
}
/// A `data` chunk header as found in the file, for [`data_chunk_len`].
struct DataChunk {
body: usize,
size32: u32,
is_rf64: bool,
remaining: usize,
declared: usize,
}
/// Bytes of audio a `data` chunk holds: leniently, whatever is present when the size is wrong or
/// the recording was never finalised.
fn data_chunk_len(b: &[u8], c: DataChunk) -> usize {
let finished = u64::from(le_u32(b, 4).unwrap_or(0)) + 8 == b.len() as u64;
let empty = c.size32 == 0 && !c.is_rf64 && finished && only_chunks_follow(b, c.body);
if c.size32 == 0 && !c.is_rf64 && !empty { c.remaining } else { c.declared.min(c.remaining) }
}
/// The sample layout of a `fmt` chunk, `None` for a format we do not read natively.
fn pcm_layout(fmt: &Fmt) -> Result<Option<(PcmKind, SampleFormat)>> {
Ok(Some(match (fmt.tag, fmt.bits) {
(WAVE_FORMAT_PCM, bits @ 1..=32) => {
let bytes = usize::from(bits.div_ceil(8));
let sf = match fmt.valid_bits.min(bits) {
0..=8 => SampleFormat::Int8,
9..=16 => SampleFormat::Int16,
17..=24 => SampleFormat::Int24,
_ => SampleFormat::Int32,
};
(PcmKind::Int { bytes, big_endian: false, unsigned: bytes == 1 }, sf)
}
(WAVE_FORMAT_IEEE_FLOAT, 32) => (PcmKind::Float { bytes: 4, big_endian: false }, SampleFormat::Float32),
(WAVE_FORMAT_IEEE_FLOAT, 64) => (PcmKind::Float { bytes: 8, big_endian: false }, SampleFormat::Float64),
(WAVE_FORMAT_PCM | WAVE_FORMAT_IEEE_FLOAT, bits) => {
return Err(AudioError::Unsupported(format!("WAV format {} with {bits} bits per sample", fmt.tag)));
}
_ => return Ok(None),
}))
}
pub(crate) fn parse(b: &[u8]) -> Result<WavParse> {
if !is_wav(b) {
return Err(AudioError::Malformed("not a RIFF/RF64 WAVE file".into()));
@@ -248,7 +209,9 @@ pub(crate) fn parse(b: &[u8]) -> Result<WavParse> {
// A size of 0 in a file whose RIFF size is right and that has only chunks after it is an
// empty data chunk followed by metadata (LIST, bext, …); otherwise it is a recording
// that was never finalised, and what follows is audio.
let len = data_chunk_len(b, DataChunk { body, size32, is_rf64, remaining, declared });
let finished = u64::from(le_u32(b, 4).unwrap_or(0)) + 8 == b.len() as u64;
let empty = size32 == 0 && !is_rf64 && finished && only_chunks_follow(b, body);
let len = if size32 == 0 && !is_rf64 && !empty { remaining } else { declared.min(remaining) };
if data.is_none() {
data = Some((body, len));
}
@@ -283,7 +246,24 @@ pub(crate) fn parse(b: &[u8]) -> Result<WavParse> {
return Err(AudioError::Malformed("WAV declares a zero sample rate".into()));
}
let channels = usize::from(fmt.channels);
let Some((kind, sample_format)) = pcm_layout(&fmt)? else { return Ok(WavParse::Foreign { bwf }) };
let (kind, sample_format) = match (fmt.tag, fmt.bits) {
(WAVE_FORMAT_PCM, bits @ 1..=32) => {
let bytes = usize::from(bits.div_ceil(8));
let sf = match fmt.valid_bits.min(bits) {
0..=8 => SampleFormat::Int8,
9..=16 => SampleFormat::Int16,
17..=24 => SampleFormat::Int24,
_ => SampleFormat::Int32,
};
(PcmKind::Int { bytes, big_endian: false, unsigned: bytes == 1 }, sf)
}
(WAVE_FORMAT_IEEE_FLOAT, 32) => (PcmKind::Float { bytes: 4, big_endian: false }, SampleFormat::Float32),
(WAVE_FORMAT_IEEE_FLOAT, 64) => (PcmKind::Float { bytes: 8, big_endian: false }, SampleFormat::Float64),
(WAVE_FORMAT_PCM | WAVE_FORMAT_IEEE_FLOAT, bits) => {
return Err(AudioError::Unsupported(format!("WAV format {} with {bits} bits per sample", fmt.tag)));
}
_ => return Ok(WavParse::Foreign { bwf }),
};
let block = kind.bytes().checked_mul(channels).ok_or_else(|| AudioError::Malformed("block size overflow".into()))?;
if usize::from(fmt.block_align) != block {
log::debug!("WAV block_align {} disagrees with computed {block}; using computed", fmt.block_align);
+19 -31
View File
@@ -102,35 +102,6 @@ fn note_mode(p: Option<NotePort>) -> NoteMode {
}
}
/// Copy `n` frames from `start` of the host buffers into the plugin's input `port` (mono sources
/// fill every port channel).
fn feed_input(inputs: &mut PortBuffers, port: usize, io: &[Vec<f32>], ch: usize, (start, n): (usize, usize)) {
for c in 0..inputs.channels(port) {
let src = io.get(c.min(ch - 1)).and_then(|s| s.get(start..start + n));
if let (Some(src), Some(dst)) = (src, inputs.channel_mut(port, c).and_then(|d| d.get_mut(..n))) {
dst.copy_from_slice(src);
}
}
}
/// Copy `n` frames of the plugin's output `port` into the host buffers from `start`; a mono host
/// buffer takes the average of a stereo port.
fn read_output(outputs: &PortBuffers, port: usize, io: &mut [Vec<f32>], ch: usize, (start, n): (usize, usize)) {
let oc = outputs.channels(port);
for (c, dst) in io.iter_mut().take(ch).enumerate() {
let Some(dst) = dst.get_mut(start..start + n) else { continue };
if ch == 1 && oc >= 2 {
if let (Some(l), Some(r)) = (outputs.channel(port, 0), outputs.channel(port, 1)) {
for ((d, a), b) in dst.iter_mut().zip(l).zip(r) {
*d = 0.5 * (a + b);
}
}
} else if let Some(src) = outputs.channel(port, c.min(oc.saturating_sub(1))).and_then(|s| s.get(..n)) {
dst.copy_from_slice(src);
}
}
}
impl Plugin for ClapPlugin {
fn info(&self) -> &'static PluginInfo {
self.info
@@ -226,7 +197,12 @@ impl Plugin for ClapPlugin {
let end = u32::try_from(start + n).unwrap_or(u32::MAX);
// Feed the main input port (mono sources fill every port channel).
if let Some(p) = self.in_main {
feed_input(&mut self.inputs, p, io, ch, (start, n));
for c in 0..self.inputs.channels(p) {
let src = io.get(c.min(ch - 1)).and_then(|s| s.get(start..start + n));
if let (Some(src), Some(dst)) = (src, self.inputs.channel_mut(p, c).and_then(|d| d.get_mut(..n))) {
dst.copy_from_slice(src);
}
}
}
let k = self.queue.iter().position(|e| e.time() >= end).unwrap_or(self.queue.len());
let offset = u32::try_from(start).unwrap_or(u32::MAX);
@@ -236,7 +212,19 @@ impl Plugin for ClapPlugin {
self.steady = self.steady.saturating_add(i64::try_from(n).unwrap_or(0));
match (r, self.out_main) {
(Ok(_), Some(p)) => {
read_output(&self.outputs, p, io, ch, (start, n));
let oc = self.outputs.channels(p);
for (c, dst) in io.iter_mut().take(ch).enumerate() {
let Some(dst) = dst.get_mut(start..start + n) else { continue };
if ch == 1 && oc >= 2 {
if let (Some(l), Some(r)) = (self.outputs.channel(p, 0), self.outputs.channel(p, 1)) {
for ((d, a), b) in dst.iter_mut().zip(l).zip(r) {
*d = 0.5 * (a + b);
}
}
} else if let Some(src) = self.outputs.channel(p, c.min(oc.saturating_sub(1))).and_then(|s| s.get(..n)) {
dst.copy_from_slice(src);
}
}
}
(Err(e), _) => log::debug!("{}: {e}", self.info.id),
_ => {}
@@ -15,11 +15,6 @@ fn block(p: &mut Box<dyn Plugin>) -> Vec<Vec<f32>> {
}
fn main() {
// nextest lists custom-harness tests with `--list`; each binary here is one test.
if std::env::args().any(|a| a == "--list") {
println!("editor_main_thread: test");
return;
}
common::fixture_dir();
let mut p = soundcraft_clap_host::create(GAIN).expect("fixture");
p.prepare(48_000.0, 64, 2);
+47 -57
View File
@@ -189,61 +189,6 @@ fn apply(f: &mut Fx, h: &clap_event_header) {
}
}
/// Fill every output channel with 1.0 when all ports have the wide layout, else -1.0.
unsafe fn fill_wide(pr: &clap_process, ports: u32, channels: u32, n: usize) {
let fits = |bufs: *const clap_audio_buffer, n: u32| {
n == ports && (0..n as usize).all(|i| unsafe { &*bufs.add(i) }.channel_count == channels)
};
let ok = fits(pr.audio_inputs, pr.audio_inputs_count) && fits(pr.audio_outputs, pr.audio_outputs_count);
if pr.audio_outputs_count > 0 {
let out = unsafe { &*pr.audio_outputs };
for c in 0..out.channel_count as usize {
let o = unsafe { *out.data32.add(c) };
for i in 0..n {
unsafe { *o.add(i) = if ok { 1.0 } else { -1.0 } };
}
}
}
}
/// Push the host-visible parameter edit and an ignorable note event onto the output queue.
unsafe fn push_gui_edit(pr: &clap_process) {
let e = clap_event_param_value {
header: clap_event_header {
size: std::mem::size_of::<clap_event_param_value>() as u32,
time: 0,
space_id: CLAP_CORE_EVENT_SPACE_ID,
type_: CLAP_EVENT_PARAM_VALUE,
flags: 0,
},
param_id: 7,
cookie: std::ptr::null_mut(),
note_id: -1,
port_index: -1,
channel: -1,
key: -1,
value: 0.25,
};
let out = unsafe { &*pr.out_events };
assert!(unsafe { (out.try_push.unwrap())(pr.out_events, &e.header) });
// Other event types are accepted and ignored by the host.
let note = clap_event_note {
header: clap_event_header {
size: std::mem::size_of::<clap_event_note>() as u32,
time: 0,
space_id: CLAP_CORE_EVENT_SPACE_ID,
type_: CLAP_EVENT_NOTE_END,
flags: 0,
},
note_id: -1,
port_index: 0,
channel: 0,
key: 60,
velocity: 0.0,
};
assert!(unsafe { (out.try_push.unwrap())(pr.out_events, &note.header) });
}
unsafe extern "C" fn process(p: *const clap_plugin, pr: *const clap_process) -> clap_process_status {
let f = unsafe { fx(p) };
let pr = unsafe { &*pr };
@@ -252,7 +197,19 @@ unsafe extern "C" fn process(p: *const clap_plugin, pr: *const clap_process) ->
let ev = unsafe { &*pr.in_events };
let count = unsafe { (ev.size.unwrap())(pr.in_events) };
if let Some((ports, channels)) = f.wide {
unsafe { fill_wide(pr, ports, channels, n) };
let fits = |bufs: *const clap_audio_buffer, n: u32| {
n == ports && (0..n as usize).all(|i| unsafe { &*bufs.add(i) }.channel_count == channels)
};
let ok = fits(pr.audio_inputs, pr.audio_inputs_count) && fits(pr.audio_outputs, pr.audio_outputs_count);
if pr.audio_outputs_count > 0 {
let out = unsafe { &*pr.audio_outputs };
for c in 0..out.channel_count as usize {
let o = unsafe { *out.data32.add(c) };
for i in 0..n {
unsafe { *o.add(i) = if ok { 1.0 } else { -1.0 } };
}
}
}
return CLAP_PROCESS_CONTINUE;
}
if pr.audio_outputs_count != 1 {
@@ -273,7 +230,40 @@ unsafe extern "C" fn process(p: *const clap_plugin, pr: *const clap_process) ->
if f.gui_edit_pending {
f.gui_edit_pending = false;
f.gain = 0.25;
unsafe { push_gui_edit(pr) };
let e = clap_event_param_value {
header: clap_event_header {
size: std::mem::size_of::<clap_event_param_value>() as u32,
time: 0,
space_id: CLAP_CORE_EVENT_SPACE_ID,
type_: CLAP_EVENT_PARAM_VALUE,
flags: 0,
},
param_id: 7,
cookie: std::ptr::null_mut(),
note_id: -1,
port_index: -1,
channel: -1,
key: -1,
value: 0.25,
};
let out = unsafe { &*pr.out_events };
assert!(unsafe { (out.try_push.unwrap())(pr.out_events, &e.header) });
// Other event types are accepted and ignored by the host.
let note = clap_event_note {
header: clap_event_header {
size: std::mem::size_of::<clap_event_note>() as u32,
time: 0,
space_id: CLAP_CORE_EVENT_SPACE_ID,
type_: CLAP_EVENT_NOTE_END,
flags: 0,
},
note_id: -1,
port_index: 0,
channel: 0,
key: 60,
velocity: 0.0,
};
assert!(unsafe { (out.try_push.unwrap())(pr.out_events, &note.header) });
}
let mut next = 0u32;
let mut last_time = 0u32;
+63 -74
View File
@@ -322,77 +322,6 @@ pub fn resample(ch: &[Vec<f32>], from: u32, to: u32) -> Vec<Vec<f32>> {
// ---------------------------------------------------------------------------------------------
// Time stretch (WSOLA) and pitch shift
/// Start position in the guide signal whose `hop` samples best match the natural continuation at
/// `natural`, searched within `tol` of `nominal` (coarse stride 4, then refined +-3).
fn best_start(g: &impl Fn(i64) -> f32, nominal: i64, natural: i64, tol: i64, hop: usize) -> i64 {
// Similarity of candidate start `p` with the natural continuation (stride 2).
let score = |p: i64| -> f32 {
let (mut xy, mut yy) = (0.0f32, 1e-9f32);
let mut i = 0i64;
while i < hop as i64 {
let a = g(natural + i);
let b = g(p + i);
xy += a * b;
yy += b * b;
i += 2;
}
xy / yy.sqrt()
};
let lo = (nominal - tol).max(0);
let hi = nominal + tol;
let mut best = nominal.max(0);
let mut best_s = f32::NEG_INFINITY;
let mut p = lo;
while p <= hi {
let s = score(p);
if s > best_s {
best_s = s;
best = p;
}
p += 4;
}
let (rlo, rhi) = ((best - 3).max(lo), (best + 3).min(hi));
for p in rlo..=rhi {
let s = score(p);
if s > best_s {
best_s = s;
best = p;
}
}
best
}
/// Add one windowed frame of every channel, read from `at.0`, into `out` at `at.1`, and the window
/// into `wsum`.
fn overlap_add(ch: &[Vec<f32>], out: &mut [Vec<f32>], wsum: &mut [f32], window: &[f32], at: (usize, usize)) {
let (in_pos, out_pos) = at;
for (c, o) in ch.iter().zip(out.iter_mut()) {
for (i, w) in window.iter().enumerate() {
let v = c.get(in_pos + i).copied().unwrap_or(0.0);
if let Some(d) = o.get_mut(out_pos + i) {
*d += v * w;
}
}
}
for (i, w) in window.iter().enumerate() {
if let Some(d) = wsum.get_mut(out_pos + i) {
*d += w;
}
}
}
/// Divide out the accumulated window weights and cut each channel to `out_len`.
fn normalise(out: &mut [Vec<f32>], wsum: &[f32], out_len: usize) {
for o in out.iter_mut() {
for (s, w) in o.iter_mut().zip(wsum) {
if *w > 1e-3 {
*s /= w;
}
}
o.truncate(out_len);
}
}
/// Pitch-preserving time stretch by WSOLA (waveform-similarity overlap-add).
/// `ratio` = output length / input length, clamped to 0.1 ..= 10. All channels share the same
/// segment choices so the stereo image stays coherent.
@@ -425,12 +354,72 @@ pub fn time_stretch(ch: &[Vec<f32>], ratio: f64, sample_rate: f32) -> Vec<Vec<f3
if out_pos >= out_len {
break;
}
let in_pos = if k == 0 { 0 } else { best_start(&g, (out_pos as f64 / ratio).round() as i64, prev_in + hop as i64, tol, hop) };
overlap_add(ch, &mut out, &mut wsum, &window, (in_pos as usize, out_pos));
let in_pos = if k == 0 {
0
} else {
let nominal = (out_pos as f64 / ratio).round() as i64;
let natural = prev_in + hop as i64;
// Similarity of candidate start `p` with the natural continuation (stride 2).
let score = |p: i64| -> f32 {
let (mut xy, mut yy) = (0.0f32, 1e-9f32);
let mut i = 0i64;
while i < hop as i64 {
let a = g(natural + i);
let b = g(p + i);
xy += a * b;
yy += b * b;
i += 2;
}
xy / yy.sqrt()
};
let lo = (nominal - tol).max(0);
let hi = nominal + tol;
let mut best = nominal.max(0);
let mut best_s = f32::NEG_INFINITY;
let mut p = lo;
while p <= hi {
let s = score(p);
if s > best_s {
best_s = s;
best = p;
}
p += 4;
}
let (rlo, rhi) = ((best - 3).max(lo), (best + 3).min(hi));
for p in rlo..=rhi {
let s = score(p);
if s > best_s {
best_s = s;
best = p;
}
}
best
};
for (c, o) in ch.iter().zip(out.iter_mut()) {
for (i, w) in window.iter().enumerate() {
let src = in_pos as usize + i;
let v = c.get(src).copied().unwrap_or(0.0);
if let Some(d) = o.get_mut(out_pos + i) {
*d += v * w;
}
}
}
for (i, w) in window.iter().enumerate() {
if let Some(d) = wsum.get_mut(out_pos + i) {
*d += w;
}
}
prev_in = in_pos;
k += 1;
}
normalise(&mut out, &wsum, out_len);
for o in out.iter_mut() {
for (s, w) in o.iter_mut().zip(&wsum) {
if *w > 1e-3 {
*s /= w;
}
}
o.truncate(out_len);
}
out
}
+61 -86
View File
@@ -94,29 +94,6 @@ fn duration_bars(duration: usize) -> Option<i64> {
}
}
/// Writes one frame: a constant-power pan for stereo and wider, plain gain for mono. Channels past
/// the second only get the output gain.
fn write_frame(io: &mut [Vec<f32>], n: usize, ch: usize, left: f32, right: f32, position: f32, out: f32) {
if ch >= 2 {
// Constant power: sin² + cos² = 1 across the field, and both gains are 1 at the center.
let theta = (position.clamp(-1.0, 1.0) + 1.0) * FRAC_PI_4;
let (l, r) = (theta.cos().max(0.0), theta.sin().max(0.0));
if let Some(x) = io.get_mut(0).and_then(|b| b.get_mut(n)) {
*x = left * l * SQRT_2 * out;
}
if let Some(x) = io.get_mut(1).and_then(|b| b.get_mut(n)) {
*x = right * r * SQRT_2 * out;
}
for c in 2..ch {
if let Some(x) = io.get_mut(c).and_then(|b| b.get_mut(n)) {
*x = if x.is_finite() { *x * out } else { 0.0 };
}
}
} else if let Some(x) = io.get_mut(0).and_then(|b| b.get_mut(n)) {
*x = left * out;
}
}
/// Moves the balance of the first two channels with a constant-power law: as the pan position
/// moves right the left channel's gain falls and the right's rises, and each channel stays on its
/// own side. The pan position is `manual · (1 - width) + modulator · width`, where the modulator is
@@ -253,63 +230,6 @@ impl AutoPan {
let ratio = hz * dur * 60.0 / t.tempo;
Some((into * ratio).rem_euclid(1.0))
}
/// Advances the envelope follower by one sample. `level` is the louder channel's magnitude.
fn follow_envelope(&mut self, level: f32, attack: f32, release: f32) {
let k = if level > self.env { attack } else { release };
self.env = level + (self.env - level) * k;
if self.env < 1.0e-12 {
self.env = 0.0;
}
}
/// Arms the Envelope trigger on a rising threshold crossing and re-arms it after a Release of silence.
fn envelope_trigger(&mut self, threshold: f32, release_samples: u64) {
if self.armed && self.env >= threshold {
self.armed = false;
self.phase = 0.0;
self.holding = false;
self.below = 0;
} else if !self.armed {
if self.env < threshold {
self.below = self.below.saturating_add(1);
self.armed = self.below >= release_samples;
} else {
self.below = 0;
}
}
}
/// The LFO phase for sample `n` of the block. Advances the phase unless a held or Beat Clock position sets it.
fn lfo_phase(&mut self, n: usize, trigger: usize, hz: f64, inc: f64) -> f64 {
match (trigger == TRIGGER_BEAT_CLOCK).then(|| self.beat_clock_phase(n, hz)).flatten() {
Some(phase) => phase,
None if self.holding => self.phase,
None => {
let current = self.phase;
self.phase += inc;
if self.phase >= 1.0 {
if matches!(trigger, TRIGGER_FREE | TRIGGER_BEAT_CLOCK) {
self.phase -= 1.0;
} else {
self.phase = 1.0;
self.holding = true;
}
}
current
}
}
}
/// The modulator in -1..=1: the envelope level above the threshold, or the LFO shape.
fn modulator(&self, envelope_source: bool, threshold_db: f32, waveform: usize, lfo_phase: f64) -> f32 {
if envelope_source {
let above = (gain_to_db(self.env) - threshold_db) / -threshold_db;
2.0 * above.clamp(0.0, 1.0) - 1.0
} else {
wave_at(waveform, lfo_phase as f32)
}
}
}
impl Plugin for AutoPan {
@@ -363,12 +283,50 @@ impl Plugin for AutoPan {
let left = sample(io, 0);
let right = if ch >= 2 { sample(io, 1) } else { left };
// The louder channel drives the follower, so anti-phase material still has a level.
self.follow_envelope(left.abs().max(right.abs()), attack, release);
if trigger == TRIGGER_ENVELOPE {
self.envelope_trigger(threshold, release_samples);
let level = left.abs().max(right.abs());
let k = if level > self.env { attack } else { release };
self.env = level + (self.env - level) * k;
if self.env < 1.0e-12 {
self.env = 0.0;
}
let lfo_phase = self.lfo_phase(n, trigger, hz, inc);
let modulator = self.modulator(envelope_source, threshold_db, waveform, lfo_phase);
if trigger == TRIGGER_ENVELOPE {
if self.armed && self.env >= threshold {
self.armed = false;
self.phase = 0.0;
self.holding = false;
self.below = 0;
} else if !self.armed {
if self.env < threshold {
self.below = self.below.saturating_add(1);
self.armed = self.below >= release_samples;
} else {
self.below = 0;
}
}
}
let lfo_phase = match (trigger == TRIGGER_BEAT_CLOCK).then(|| self.beat_clock_phase(n, hz)).flatten() {
Some(phase) => phase,
None if self.holding => self.phase,
None => {
let current = self.phase;
self.phase += inc;
if self.phase >= 1.0 {
if matches!(trigger, TRIGGER_FREE | TRIGGER_BEAT_CLOCK) {
self.phase -= 1.0;
} else {
self.phase = 1.0;
self.holding = true;
}
}
current
}
};
let modulator = if envelope_source {
let above = (gain_to_db(self.env) - threshold_db) / -threshold_db;
2.0 * above.clamp(0.0, 1.0) - 1.0
} else {
wave_at(waveform, lfo_phase as f32)
};
let width = self.width.next();
let manual = self.manual.next();
self.position.set(manual * (1.0 - width) + sign * modulator * width);
@@ -378,7 +336,24 @@ impl Plugin for AutoPan {
}
let position = self.position.next();
let out = self.output.next();
write_frame(io, n, ch, left, right, position, out);
if ch >= 2 {
// Constant power: sin² + cos² = 1 across the field, and both gains are 1 at the center.
let theta = (position.clamp(-1.0, 1.0) + 1.0) * FRAC_PI_4;
let (l, r) = (theta.cos().max(0.0), theta.sin().max(0.0));
if let Some(x) = io.get_mut(0).and_then(|b| b.get_mut(n)) {
*x = left * l * SQRT_2 * out;
}
if let Some(x) = io.get_mut(1).and_then(|b| b.get_mut(n)) {
*x = right * r * SQRT_2 * out;
}
for c in 2..ch {
if let Some(x) = io.get_mut(c).and_then(|b| b.get_mut(n)) {
*x = if x.is_finite() { *x * out } else { 0.0 };
}
}
} else if let Some(x) = io.get_mut(0).and_then(|b| b.get_mut(n)) {
*x = left * out;
}
}
self.since_transport = self.since_transport.saturating_add(frames as u64);
}
+55 -60
View File
@@ -135,65 +135,6 @@ impl Default for DrumSynth {
}
}
/// One output sample of a sounding voice of `kind`, advancing its envelopes.
fn voice_sample(v: &mut DrumVoice, kind: Kind, sr: f32, clap_gap: usize) -> f32 {
match kind {
Kind::Kick | Kind::Tom(_) => {
v.pitch_env *= v.pitch_coef;
let f = v.f_end + (v.f_start - v.f_end) * v.pitch_env;
v.phase += f / sr;
v.phase -= v.phase.floor();
let tone = (v.phase * TAU).sin() * v.amp;
let click = v.filt.process_sample(v.rng.bipolar()) * v.noise_amp;
v.amp *= v.amp_coef;
v.noise_amp *= v.noise_coef;
tone + click
}
Kind::Snare => {
v.pitch_env *= v.pitch_coef;
let f = v.f_end + (v.f_start - v.f_end) * v.pitch_env;
v.phase += f / sr;
v.phase -= v.phase.floor();
let tone = (v.phase * TAU).sin() * v.amp * 0.6;
let n = v.filt2.process_sample(v.filt.process_sample(v.rng.bipolar())) * v.noise_amp * 0.7;
v.amp *= v.amp_coef;
v.noise_amp *= v.noise_coef;
tone + n
}
Kind::Clap => {
// Three quick bursts then a diffuse tail.
let burst = if v.t < clap_gap * 3 {
if v.t.is_multiple_of(clap_gap) {
v.amp = 1.0;
}
v.amp
} else {
0.0
};
let env = burst.max(if v.t >= clap_gap * 2 { v.noise_amp * 0.6 } else { 0.0 });
let y = v.filt.process_sample(v.rng.bipolar()) * env * 2.0;
v.amp *= v.amp_coef;
if v.t >= clap_gap * 2 {
v.noise_amp *= v.noise_coef;
}
y
}
Kind::ClosedHat | Kind::OpenHat | Kind::Crash | Kind::Ride => {
let scale = if matches!(kind, Kind::Crash | Kind::Ride) { 1.0 } else { 1.9 };
let mut metal = 0.0;
for (m, f) in v.metal.iter_mut().zip(METAL) {
*m += f * scale / sr;
*m -= m.floor();
metal += if *m < 0.5 { 1.0 } else { -1.0 };
}
let raw = metal / 6.0 * (1.0 - v.noise_amp * 0.5) + v.rng.bipolar() * v.noise_amp;
let y = v.filt2.process_sample(v.filt.process_sample(raw)) * v.amp;
v.amp *= v.amp_coef;
y
}
}
}
impl DrumSynth {
pub fn new() -> Self {
let mut voices = [DrumVoice::new(1); VOICES];
@@ -305,7 +246,61 @@ impl DrumSynth {
let mut out = 0.0;
for v in &mut self.voices {
let Some(kind) = v.kind else { continue };
let y = voice_sample(v, kind, sr, clap_gap);
let y = match kind {
Kind::Kick | Kind::Tom(_) => {
v.pitch_env *= v.pitch_coef;
let f = v.f_end + (v.f_start - v.f_end) * v.pitch_env;
v.phase += f / sr;
v.phase -= v.phase.floor();
let tone = (v.phase * TAU).sin() * v.amp;
let click = v.filt.process_sample(v.rng.bipolar()) * v.noise_amp;
v.amp *= v.amp_coef;
v.noise_amp *= v.noise_coef;
tone + click
}
Kind::Snare => {
v.pitch_env *= v.pitch_coef;
let f = v.f_end + (v.f_start - v.f_end) * v.pitch_env;
v.phase += f / sr;
v.phase -= v.phase.floor();
let tone = (v.phase * TAU).sin() * v.amp * 0.6;
let n = v.filt2.process_sample(v.filt.process_sample(v.rng.bipolar())) * v.noise_amp * 0.7;
v.amp *= v.amp_coef;
v.noise_amp *= v.noise_coef;
tone + n
}
Kind::Clap => {
// Three quick bursts then a diffuse tail.
let burst = if v.t < clap_gap * 3 {
if v.t % clap_gap == 0 {
v.amp = 1.0;
}
v.amp
} else {
0.0
};
let env = burst.max(if v.t >= clap_gap * 2 { v.noise_amp * 0.6 } else { 0.0 });
let y = v.filt.process_sample(v.rng.bipolar()) * env * 2.0;
v.amp *= v.amp_coef;
if v.t >= clap_gap * 2 {
v.noise_amp *= v.noise_coef;
}
y
}
Kind::ClosedHat | Kind::OpenHat | Kind::Crash | Kind::Ride => {
let scale = if matches!(kind, Kind::Crash | Kind::Ride) { 1.0 } else { 1.9 };
let mut metal = 0.0;
for (m, f) in v.metal.iter_mut().zip(METAL) {
*m += f * scale / sr;
*m -= m.floor();
metal += if *m < 0.5 { 1.0 } else { -1.0 };
}
let raw = metal / 6.0 * (1.0 - v.noise_amp * 0.5) + v.rng.bipolar() * v.noise_amp;
let y = v.filt2.process_sample(v.filt.process_sample(raw)) * v.amp;
v.amp *= v.amp_coef;
y
}
};
v.t += 1;
let alive = match kind {
Kind::Clap => v.t < clap_gap * 3 || v.noise_amp > OFF,
+15 -27
View File
@@ -201,31 +201,6 @@ impl Reverb {
}
}
/// Input sample `n` of the left and right channels (a mono input feeds both); non-finite reads as 0.
fn input_pair(io: &[Vec<f32>], ch: usize, n: usize) -> (f32, f32) {
let xl = io.first().and_then(|b| b.get(n)).copied().filter(|v| v.is_finite()).unwrap_or(0.0);
let xr = if ch > 1 { io.get(1).and_then(|b| b.get(n)).copied().filter(|v| v.is_finite()).unwrap_or(0.0) } else { xl };
(xl, xr)
}
/// Blend the wet signal `[mid, left, right]` into sample `n` of the first `ch` channels at mix `m`.
fn mix_out(io: &mut [Vec<f32>], ch: usize, n: usize, wet: [f32; 3], m: f32) {
let [mid, wl, wr] = wet;
for (c, b) in io.iter_mut().take(ch).enumerate() {
let wet = if ch == 1 {
mid
} else if c % 2 == 0 {
wl
} else {
wr
};
if let Some(x) = b.get_mut(n) {
let dry = if x.is_finite() { *x } else { 0.0 };
*x = dry * (1.0 - m) + wet * m;
}
}
}
impl Plugin for Reverb {
fn info(&self) -> &'static PluginInfo {
self.info
@@ -304,7 +279,8 @@ impl Plugin for Reverb {
let size = self.size.value();
let base: [f32; LINES] = self.cfg.delays_ms.map(|ms| ms_to_samples(ms * size, sr));
for n in 0..frames {
let (xl, xr) = input_pair(io, ch, n);
let xl = io.first().and_then(|b| b.get(n)).copied().filter(|v| v.is_finite()).unwrap_or(0.0);
let xr = if ch > 1 { io.get(1).and_then(|b| b.get(n)).copied().filter(|v| v.is_finite()).unwrap_or(0.0) } else { xl };
let pre = ms_to_samples(self.pre_ms.next(), sr);
let mut ins = [xl, xr];
for (side, x) in ins.iter_mut().enumerate() {
@@ -343,7 +319,19 @@ impl Plugin for Reverb {
let side = 0.5 * (wl - wr) * w;
let (wl, wr) = (mid + side, mid - side);
let m = self.mix.next();
mix_out(io, ch, n, [mid, wl, wr], m);
for (c, b) in io.iter_mut().take(ch).enumerate() {
let wet = if ch == 1 {
mid
} else if c % 2 == 0 {
wl
} else {
wr
};
if let Some(x) = b.get_mut(n) {
let dry = if x.is_finite() { *x } else { 0.0 };
*x = dry * (1.0 - m) + wet * m;
}
}
}
}
+9 -13
View File
@@ -20,18 +20,6 @@ pub fn specs() -> Vec<CommandSpec> {
)]
}
/// The most frames a process can take in: these processes cut their output at MAX_OFFLINE_SAMPLES frames.
fn frame_limit(name: &str, ratio: f64, semitones: f32, sr: f64, to: u32, selection: SpeedRampSelection) -> usize {
let longest = |grows: f64| (soundcraft_dsp::offline::MAX_OFFLINE_SAMPLES as f64 / grows) as usize;
match name {
"time_stretch" => longest(ratio),
"pitch_shift" => longest(2f64.powf(f64::from(semitones) / 12.0)),
"varispeed" => longest(f64::from(to) / sr),
"speed_ramp" => longest(if selection == SpeedRampSelection::Extend { 1.5 } else { 1.0 }),
_ => usize::MAX,
}
}
/// Process each target clip's audio and replace the clip with one backed by a new source.
fn process(e: &mut Engine, p: &Value) -> Result<Value> {
let name = str_param(p, "process").or_else(|| str_param(p, "plugin")).ok_or_else(|| bad("audiosuite.process", "`process` required"))?.to_string();
@@ -75,7 +63,15 @@ fn process(e: &mut Engine, p: &Value) -> Result<Value> {
let change = if get("change", 0.0) >= 0.5 { SpeedRampChange::SpeedUp } else { SpeedRampChange::SlowDown };
let selection = if get("selection", 0.0) >= 0.5 { SpeedRampSelection::Extend } else { SpeedRampSelection::FitTo };
let fades = get("fades", 1.0) >= 0.5;
let most = frame_limit(&name, ratio, semitones, sr.as_f64(), to, selection);
// These processes cut their output at MAX_OFFLINE_SAMPLES frames.
let longest = |grows: f64| (soundcraft_dsp::offline::MAX_OFFLINE_SAMPLES as f64 / grows) as usize;
let most = match name.as_str() {
"time_stretch" => longest(ratio),
"pitch_shift" => longest(2f64.powf(f64::from(semitones) / 12.0)),
"varispeed" => longest(f64::from(to) / sr.as_f64()),
"speed_ramp" => longest(if selection == SpeedRampSelection::Extend { 1.5 } else { 1.0 }),
_ => usize::MAX,
};
let mut done = 0;
for id in ids {
let Some((track, clip)) = e.session().find_clip(id).map(|(t, c)| (t, c.clone())) else { continue };
+25 -34
View File
@@ -475,38 +475,6 @@ fn select_all(e: &mut Engine, _: &Value) -> Result<Value> {
Ok(json!({"tracks": s.edit.selected_tracks.len(), "end": end}))
}
/// Adds the members of every active edit group that touches `t`, then puts `t` in track order.
fn add_group_members(s: &Session, t: &mut Vec<TrackId>) {
let extra: Vec<TrackId> =
s.groups.iter().filter(|g| g.active && g.edit && g.members.iter().any(|m| t.contains(m))).flat_map(|g| g.members.clone()).collect();
for m in extra {
if !t.contains(&m) {
t.push(m);
}
}
let order: Vec<TrackId> = s.tracks.iter().map(|x| x.id).collect();
t.sort_by_key(|x| order.iter().position(|o| o == x).unwrap_or(usize::MAX));
}
/// Selecting clips also selects their range and tracks (Grabber click).
fn select_clip_ids(s: &Session, ed: &mut soundcraft_model::EditState, c: Vec<ClipId>) {
let mut range: Option<soundcraft_time::Range> = None;
let mut trs = Vec::new();
for id in &c {
if let Some((tid, clip)) = s.find_clip(*id) {
range = Some(range.map_or(clip.range(), |r| soundcraft_time::Range::new(r.start.min(clip.start), r.end.max(clip.end()))));
if !trs.contains(&tid) {
trs.push(tid);
}
}
}
if let Some(r) = range {
ed.selection = r;
ed.selected_tracks = trs;
}
ed.selected_clips = c;
}
fn select(e: &mut Engine, p: &Value) -> Result<Value> {
let tracks = if p.get("tracks").is_some() || p.get("track").is_some() { Some(tracks_param(e, "edit.select", p)?) } else { None };
let st = position_param(e, "edit.select", p, "start")?;
@@ -522,7 +490,15 @@ fn select(e: &mut Engine, p: &Value) -> Result<Value> {
if let Some(mut t) = tracks {
// Active edit groups select all their members together.
if !exact {
add_group_members(s, &mut t);
let extra: Vec<soundcraft_model::TrackId> =
s.groups.iter().filter(|g| g.active && g.edit && g.members.iter().any(|m| t.contains(m))).flat_map(|g| g.members.clone()).collect();
for m in extra {
if !t.contains(&m) {
t.push(m);
}
}
let order: Vec<soundcraft_model::TrackId> = s.tracks.iter().map(|x| x.id).collect();
t.sort_by_key(|x| order.iter().position(|o| o == x).unwrap_or(usize::MAX));
}
ed.selected_tracks = t;
}
@@ -532,7 +508,22 @@ fn select(e: &mut Engine, p: &Value) -> Result<Value> {
_ => {}
}
if let Some(c) = clips {
select_clip_ids(s, &mut ed, c);
// Selecting clips also selects their range and tracks (Grabber click).
let mut range: Option<soundcraft_time::Range> = None;
let mut trs = Vec::new();
for id in &c {
if let Some((tid, clip)) = s.find_clip(*id) {
range = Some(range.map_or(clip.range(), |r| soundcraft_time::Range::new(r.start.min(clip.start), r.end.max(clip.end()))));
if !trs.contains(&tid) {
trs.push(tid);
}
}
}
if let Some(r) = range {
ed.selection = r;
ed.selected_tracks = trs;
}
ed.selected_clips = c;
} else if list_changes {
ed.selected_clips.clear();
}
+43 -53
View File
@@ -857,57 +857,6 @@ fn coalesce_clip_gain_to_volume(e: &mut Engine, p: &Value) -> Result<Value> {
Ok(json!({"clips": n}))
}
/// The automation parameters a trim/glide over a range touches on `tr`.
fn trim_params(tr: &soundcraft_model::Track, pan_only: bool) -> Vec<AutoParam> {
let mut params: Vec<AutoParam> = tr
.automation
.iter()
.filter(|l| l.enabled && !matches!(l.param, AutoParam::Mute | AutoParam::SendMute(_)))
.filter(|l| !pan_only || matches!(l.param, AutoParam::Pan(_)))
.map(|l| l.param.clone())
.collect();
if pan_only {
for i in 0..tr.mixer.pan.len().min(2) {
let pp = AutoParam::Pan(i as u8);
if !params.contains(&pp) {
params.push(pp);
}
}
}
params
}
/// Trim (shift the points inside `r` by the change at its end) or glide (ramp across `r`) one lane.
fn trim_lane(tr: &mut soundcraft_model::Track, param: &AutoParam, r: Range, glide: bool) {
let cur = super::mix::current_value(tr, param);
let (lo, hi, _) = param.range();
let l = tr.lane_mut(param);
let before = l.value_at(r.start - 1, cur);
let after = l.value_at(r.end, cur);
let from = l.value_at(r.start, cur);
if glide {
l.clear_range(r.start, r.end.saturating_add(2));
if r.start > 0 {
l.set_point(r.start - 1, before);
}
l.set_point(r.start, from);
l.set_point(r.end, cur);
l.set_point(r.end.saturating_add(1), after);
} else {
let delta = cur - from;
let last = l.value_at(r.end - 1, cur);
if r.start > 0 {
l.set_point(r.start - 1, before);
}
l.set_point(r.start, from);
l.set_point(r.end - 1, last);
l.set_point(r.end, after);
for pt in l.points.iter_mut().filter(|pt| pt.at >= r.start && pt.at < r.end) {
pt.value = (pt.value + delta).clamp(lo, hi);
}
}
}
fn trim_glide(e: &mut Engine, p: &Value, glide: bool, pan_only: bool, id: &str) -> Result<Value> {
let tracks = tracks_required(e, id, p)?;
let r = nonempty_range(e, p, id)?;
@@ -915,8 +864,49 @@ fn trim_glide(e: &mut Engine, p: &Value, glide: bool, pan_only: bool, id: &str)
let mut lanes = 0;
for t in &tracks {
let Some(tr) = s.track_mut(*t) else { continue };
for param in trim_params(tr, pan_only) {
trim_lane(tr, &param, r, glide);
let mut params: Vec<AutoParam> = tr
.automation
.iter()
.filter(|l| l.enabled && !matches!(l.param, AutoParam::Mute | AutoParam::SendMute(_)))
.filter(|l| !pan_only || matches!(l.param, AutoParam::Pan(_)))
.map(|l| l.param.clone())
.collect();
if pan_only {
for i in 0..tr.mixer.pan.len().min(2) {
let pp = AutoParam::Pan(i as u8);
if !params.contains(&pp) {
params.push(pp);
}
}
}
for param in params {
let cur = super::mix::current_value(tr, &param);
let (lo, hi, _) = param.range();
let l = tr.lane_mut(&param);
let before = l.value_at(r.start - 1, cur);
let after = l.value_at(r.end, cur);
let from = l.value_at(r.start, cur);
if glide {
l.clear_range(r.start, r.end.saturating_add(2));
if r.start > 0 {
l.set_point(r.start - 1, before);
}
l.set_point(r.start, from);
l.set_point(r.end, cur);
l.set_point(r.end.saturating_add(1), after);
} else {
let delta = cur - from;
let last = l.value_at(r.end - 1, cur);
if r.start > 0 {
l.set_point(r.start - 1, before);
}
l.set_point(r.start, from);
l.set_point(r.end - 1, last);
l.set_point(r.end, after);
for pt in l.points.iter_mut().filter(|pt| pt.at >= r.start && pt.at < r.end) {
pt.value = (pt.value + delta).clamp(lo, hi);
}
}
lanes += 1;
}
}
+22 -35
View File
@@ -564,11 +564,18 @@ pub fn chord_name(weights: &[f64; 12], bass: usize) -> Option<String> {
const MAX_CHORD_BARS: usize = 100_000;
const MAX_CHORD_NOTES: usize = 10_000_000;
/// Absolute `(start, end, pitch)` tick notes from every MIDI clip on `tracks`, cut to their clip
/// and sorted by start.
fn midi_tick_notes(s: &Session, tracks: &[TrackId]) -> Vec<(i64, i64, u8)> {
fn extract_chords(e: &mut Engine, p: &Value) -> Result<Value> {
let id = "event.extract_chords";
let (_, t0, t1) = selection_ticks(e, p, id)?;
let s = e.session();
let picked = tracks_param(e, id, p)?;
let mut tracks: Vec<TrackId> = picked.into_iter().filter(|t| s.track(*t).is_some_and(|x| x.kind.is_midi())).collect();
if tracks.is_empty() {
tracks = s.tracks.iter().filter(|t| t.kind.is_midi()).map(|t| t.id).collect();
}
// Absolute (tick) notes from every MIDI clip on those tracks, cut to their clip.
let mut notes: Vec<(i64, i64, u8)> = Vec::new();
for t in tracks {
for t in &tracks {
for c in s.track(*t).map(|tr| tr.clips()).unwrap_or(&[]) {
let ClipContent::Midi { sequence } = &c.content else { continue };
let base = s.tempo.samples_to_ticks(c.start, s.sample_rate);
@@ -583,36 +590,6 @@ fn midi_tick_notes(s: &Session, tracks: &[TrackId]) -> Vec<(i64, i64, u8)> {
}
}
notes.sort_unstable_by_key(|n| n.0);
notes
}
/// Duration-weighted pitch classes of the notes sounding in `a..b`, and the lowest pitch among them.
fn pitch_class_weights(sounding: &[(i64, i64, u8)], a: i64, b: i64) -> ([f64; 12], Option<u8>) {
let mut w = [0.0f64; 12];
let mut bass: Option<u8> = None;
for (na, nb, pitch) in sounding {
let ov = (*nb).min(b) - (*na).max(a);
if ov > 0 {
if let Some(x) = w.get_mut(usize::from(*pitch) % 12) {
*x += ov as f64;
}
bass = Some(bass.map_or(*pitch, |bp| bp.min(*pitch)));
}
}
(w, bass)
}
fn extract_chords(e: &mut Engine, p: &Value) -> Result<Value> {
let id = "event.extract_chords";
let (_, t0, t1) = selection_ticks(e, p, id)?;
let s = e.session();
let picked = tracks_param(e, id, p)?;
let mut tracks: Vec<TrackId> = picked.into_iter().filter(|t| s.track(*t).is_some_and(|x| x.kind.is_midi())).collect();
if tracks.is_empty() {
tracks = s.tracks.iter().filter(|t| t.kind.is_midi()).map(|t| t.id).collect();
}
// Absolute (tick) notes from every MIDI clip on those tracks, cut to their clip.
let notes = midi_tick_notes(s, &tracks);
let mut bar = s.tempo.bar_beat_at_tick(t0).bar;
let mut sounding: Vec<(i64, i64, u8)> = Vec::new();
let (mut next, mut bars, mut heard) = (0, 0, 0);
@@ -639,7 +616,17 @@ fn extract_chords(e: &mut Engine, p: &Value) -> Result<Value> {
}
bars += 1;
heard += sounding.len();
let (w, bass) = pitch_class_weights(&sounding, a, b);
let mut w = [0.0f64; 12];
let mut bass: Option<u8> = None;
for (na, nb, pitch) in &sounding {
let ov = (*nb).min(b) - (*na).max(a);
if ov > 0 {
if let Some(x) = w.get_mut(usize::from(*pitch) % 12) {
*x += ov as f64;
}
bass = Some(bass.map_or(*pitch, |bp| bp.min(*pitch)));
}
}
if let Some(name) = bass.and_then(|bp| chord_name(&w, usize::from(bp) % 12)) {
let at = s.tempo.tick_to_samples(a, s.sample_rate);
chords.push(json!({"bar": bar, "chord": name, "at": at}));
+62 -72
View File
@@ -612,76 +612,6 @@ fn env_db(env: &[(i64, f32)], at: i64) -> f32 {
prev.map_or(0.0, |(_, v)| v)
}
/// Fold the volume `lane` into the gain envelope of clip `c` over the selection `r`.
fn bake_volume_into_clip(c: &mut soundcraft_model::Clip, lane: &soundcraft_model::AutomationLane, base: f32, r: soundcraft_time::Range) {
let delta = |at: i64| lane.value_at(at, base) - base;
let sel0 = r.start.max(c.start);
let sel1 = r.end.min(c.end());
if sel1 <= sel0 {
return;
}
let rel = |at: i64| at - c.start;
let rel0 = rel(sel0);
let rel1 = rel(sel1);
// Later writes win, so the selection anchors replace any point they land on.
let mut pts: Vec<(i64, f32)> =
c.gain_env.iter().copied().filter(|&(at, _)| at < rel0.saturating_sub(1) || (sel1 < c.end() && at > rel1)).collect();
if rel0 > 0 {
pts.push((rel0 - 1, env_db(&c.gain_env, rel0 - 1)));
}
pts.push((rel0, env_db(&c.gain_env, rel0) + delta(sel0)));
for pt in lane.points.iter().filter(|pt| pt.at > sel0 && pt.at < sel1) {
let at = rel(pt.at);
pts.push((at, env_db(&c.gain_env, at) + delta(pt.at)));
}
// The clip's own gain points inside the selection keep their shape, offset by the lane.
for &(at, _) in c.gain_env.iter().filter(|&&(at, _)| at > rel0 && at < rel1) {
pts.push((at, env_db(&c.gain_env, at) + delta(c.start.saturating_add(at))));
}
if sel1 - 1 > sel0 {
let at = rel(sel1 - 1);
pts.push((at, env_db(&c.gain_env, at) + delta(sel1 - 1)));
}
if sel1 < c.end() {
pts.push((rel1, env_db(&c.gain_env, rel1)));
}
pts.sort_by_key(|p| p.0);
let mut env = Vec::new();
for p in pts {
if env.last().is_some_and(|q: &(i64, f32)| q.0 == p.0) {
env.pop();
}
env.push(p);
}
c.gain_env = env;
}
/// Clear the volume lane over `r` when the clips all sit inside it, else hold the fader there.
fn reset_volume_lane(tr: &mut soundcraft_model::Track, lane: &soundcraft_model::AutomationLane, base: f32, r: soundcraft_time::Range) {
// Clearing the lane is only safe when no audio plays outside the selection; otherwise the
// held edge values outside would fall back to the fader level.
let clips_inside = tr.clips().iter().all(|c| c.start >= r.start && c.end() <= r.end);
let covers_all = clips_inside && lane.points.iter().all(|pt| pt.at >= r.start && pt.at < r.end);
let lane_m = tr.lane_mut(&AutoParam::Volume);
if covers_all {
lane_m.clear_range(r.start, r.end);
} else {
// Hold the fader across the selection and anchor the samples on either side, so the
// curve outside does not interpolate through the gap or keep the old level inside.
let before = (r.start > 0).then(|| lane.value_at(r.start - 1, base));
let after = lane.value_at(r.end, base);
lane_m.clear_range(r.start, r.end);
if let Some(v) = before {
lane_m.set_point(r.start - 1, v);
}
lane_m.set_point(r.start, base);
if r.end - 1 > r.start {
lane_m.set_point(r.end - 1, base);
}
lane_m.set_point(r.end, after);
}
}
fn vol_to_clip_gain(e: &mut Engine, p: &Value) -> Result<Value> {
let tracks = tracks_required(e, "automation.volume_to_clip_gain", p)?;
let r = range_param(e, "automation.volume_to_clip_gain", p)?;
@@ -690,12 +620,72 @@ fn vol_to_clip_gain(e: &mut Engine, p: &Value) -> Result<Value> {
let Some(tr) = s.track_mut(*t) else { continue };
let Some(lane) = tr.lane(&AutoParam::Volume).cloned().filter(|l| !l.points.is_empty()) else { continue };
let base = tr.mixer.volume_db;
let delta = |at: i64| lane.value_at(at, base) - base;
if let Some(pl) = tr.playlist_mut() {
for c in pl.clips.iter_mut().filter(|c| c.range().overlaps(&r)) {
bake_volume_into_clip(c, &lane, base, r);
let sel0 = r.start.max(c.start);
let sel1 = r.end.min(c.end());
if sel1 <= sel0 {
continue;
}
let rel = |at: i64| at - c.start;
let rel0 = rel(sel0);
let rel1 = rel(sel1);
// Later writes win, so the selection anchors replace any point they land on.
let mut pts: Vec<(i64, f32)> =
c.gain_env.iter().copied().filter(|&(at, _)| at < rel0.saturating_sub(1) || (sel1 < c.end() && at > rel1)).collect();
if rel0 > 0 {
pts.push((rel0 - 1, env_db(&c.gain_env, rel0 - 1)));
}
pts.push((rel0, env_db(&c.gain_env, rel0) + delta(sel0)));
for pt in lane.points.iter().filter(|pt| pt.at > sel0 && pt.at < sel1) {
let at = rel(pt.at);
pts.push((at, env_db(&c.gain_env, at) + delta(pt.at)));
}
// The clip's own gain points inside the selection keep their shape, offset by the lane.
for &(at, _) in c.gain_env.iter().filter(|&&(at, _)| at > rel0 && at < rel1) {
pts.push((at, env_db(&c.gain_env, at) + delta(c.start.saturating_add(at))));
}
if sel1 - 1 > sel0 {
let at = rel(sel1 - 1);
pts.push((at, env_db(&c.gain_env, at) + delta(sel1 - 1)));
}
if sel1 < c.end() {
pts.push((rel1, env_db(&c.gain_env, rel1)));
}
pts.sort_by_key(|p| p.0);
let mut env = Vec::new();
for p in pts {
if env.last().is_some_and(|q: &(i64, f32)| q.0 == p.0) {
env.pop();
}
env.push(p);
}
c.gain_env = env;
}
}
reset_volume_lane(tr, &lane, base, r);
// Clearing the lane is only safe when no audio plays outside the selection; otherwise the
// held edge values outside would fall back to the fader level.
let clips_inside = tr.clips().iter().all(|c| c.start >= r.start && c.end() <= r.end);
let covers_all = clips_inside && lane.points.iter().all(|pt| pt.at >= r.start && pt.at < r.end);
let lane_m = tr.lane_mut(&AutoParam::Volume);
if covers_all {
lane_m.clear_range(r.start, r.end);
} else {
// Hold the fader across the selection and anchor the samples on either side, so the
// curve outside does not interpolate through the gap or keep the old level inside.
let before = (r.start > 0).then(|| lane.value_at(r.start - 1, base));
let after = lane.value_at(r.end, base);
lane_m.clear_range(r.start, r.end);
if let Some(v) = before {
lane_m.set_point(r.start - 1, v);
}
lane_m.set_point(r.start, base);
if r.end - 1 > r.start {
lane_m.set_point(r.end - 1, base);
}
lane_m.set_point(r.end, after);
}
}
Ok(json!({}))
}
+13 -18
View File
@@ -221,23 +221,6 @@ fn io_state(s: &Session) -> Value {
json!({"busses": s.busses, "outputs": s.outputs})
}
/// Disconnect every track input, output and send that used bus `bid`.
fn unroute_bus(s: &mut Session, bid: BusId) {
for t in &mut s.tracks {
if t.mixer.input == Route::Bus(bid) {
t.mixer.input = Route::None;
}
if t.mixer.output == Route::Bus(bid) {
t.mixer.output = Route::None;
}
for slot in &mut t.mixer.sends {
if slot.as_ref().is_some_and(|x| x.target == Route::Bus(bid)) {
*slot = None;
}
}
}
}
fn io_setup(e: &mut Engine, p: &Value) -> Result<Value> {
let id = "setup.io";
let action = str_param(p, "action").unwrap_or("list");
@@ -269,7 +252,19 @@ fn io_setup(e: &mut Engine, p: &Value) -> Result<Value> {
"delete_bus" => {
let bid: BusId = s.bus_by_name(&name).map(|b| b.id).ok_or_else(|| bad(id, format!("no bus `{name}`")))?;
s.busses.retain(|b| b.id != bid);
unroute_bus(s, bid);
for t in &mut s.tracks {
if t.mixer.input == Route::Bus(bid) {
t.mixer.input = Route::None;
}
if t.mixer.output == Route::Bus(bid) {
t.mixer.output = Route::None;
}
for slot in &mut t.mixer.sends {
if slot.as_ref().is_some_and(|x| x.target == Route::Bus(bid)) {
*slot = None;
}
}
}
}
"create_output" => {
if s.outputs.iter().any(|o| o.name.eq_ignore_ascii_case(&name)) {
+27 -32
View File
@@ -671,35 +671,6 @@ fn change_width(e: &mut Engine, p: &Value) -> Result<Value> {
Ok(json!({"format": f}))
}
/// The nearest ancestor folder of `folder` that is not itself being deleted (walks at most 64 levels).
fn surviving_parent(s: &Session, deleted: &[TrackId], folder: TrackId) -> Option<TrackId> {
let mut parent = s.track(folder).and_then(|folder| folder.folder);
for _ in 0..64 {
match parent {
Some(candidate) if deleted.contains(&candidate) => parent = s.track(candidate).and_then(|folder| folder.folder),
_ => break,
}
}
parent
}
/// Clear inputs and sends that used the removed bus; outputs that used it move to `replacement`.
fn reroute_deleted_bus(s: &mut Session, bus: soundcraft_model::BusId, replacement: &Route) {
for track in &mut s.tracks {
if track.mixer.input == Route::Bus(bus) {
track.mixer.input = Route::None;
}
if track.mixer.output == Route::Bus(bus) {
track.mixer.output = replacement.clone();
}
for send in &mut track.mixer.sends {
if send.as_ref().is_some_and(|send| send.target == Route::Bus(bus)) {
*send = None;
}
}
}
}
fn delete(e: &mut Engine, p: &Value) -> Result<Value> {
let tracks = tracks_required(e, "track.delete", p)?;
let s = e.session_mut();
@@ -707,7 +678,13 @@ fn delete(e: &mut Engine, p: &Value) -> Result<Value> {
.iter()
.filter_map(|folder| {
let bus = owned_folder_bus(s, *folder)?;
let parent = surviving_parent(s, &tracks, *folder);
let mut parent = s.track(*folder).and_then(|folder| folder.folder);
for _ in 0..64 {
match parent {
Some(candidate) if tracks.contains(&candidate) => parent = s.track(candidate).and_then(|folder| folder.folder),
_ => break,
}
}
Some((bus, parent.and_then(|parent| folder_bus(s, parent)).unwrap_or(Route::Main)))
})
.collect();
@@ -719,7 +696,13 @@ fn delete(e: &mut Engine, p: &Value) -> Result<Value> {
continue;
}
let Some(folder) = child.folder.filter(|folder| tracks.contains(folder)) else { continue };
let parent = surviving_parent(s, &tracks, folder);
let mut parent = s.track(folder).and_then(|folder| folder.folder);
for _ in 0..64 {
match parent {
Some(candidate) if tracks.contains(&candidate) => parent = s.track(candidate).and_then(|folder| folder.folder),
_ => break,
}
}
promoted.push((child.id, folder_bus(s, folder), parent, parent.and_then(|parent| folder_bus(s, parent))));
}
for (child, old_bus, parent, parent_bus) in promoted {
@@ -737,7 +720,19 @@ fn delete(e: &mut Engine, p: &Value) -> Result<Value> {
}
for (bus, replacement) in removed_busses {
s.busses.retain(|candidate| candidate.id != bus);
reroute_deleted_bus(s, bus, &replacement);
for track in &mut s.tracks {
if track.mixer.input == Route::Bus(bus) {
track.mixer.input = Route::None;
}
if track.mixer.output == Route::Bus(bus) {
track.mixer.output = replacement.clone();
}
for send in &mut track.mixer.sends {
if send.as_ref().is_some_and(|send| send.target == Route::Bus(bus)) {
*send = None;
}
}
}
}
Ok(json!({"deleted": tracks.len()}))
}
+55 -66
View File
@@ -439,70 +439,6 @@ fn write_track(out: &mut Vec<u8>, mut events: Vec<WEvent>) -> Result<(), MidiErr
Ok(())
}
/// The conductor track: tempos, meters, key signatures and markers.
fn conductor_events(smf: &Smf) -> Result<Vec<WEvent>, MidiError> {
let mut ev = Vec::new();
for &(t, bpm) in &smf.tempos {
if !bpm.is_finite() || bpm <= 0.0 {
return Err(MidiError::Write(format!("invalid tempo {bpm} bpm")));
}
let us = (60_000_000.0 / bpm).round().clamp(1.0, f64::from(0x00FF_FFFFu32)) as u32;
let [_, a, b, c] = us.to_be_bytes();
ev.push(WEvent { tick: clamp_tick(t), order: 0, bytes: meta(0x51, &[a, b, c])? });
}
for &(t, num, den) in &smf.meters {
if den == 0 || !den.is_power_of_two() {
return Err(MidiError::Write(format!("meter denominator {den} is not a power of two")));
}
let dd = den.trailing_zeros() as u8;
ev.push(WEvent { tick: clamp_tick(t), order: 1, bytes: meta(0x58, &[num, dd, 24, 8])? });
}
for &(t, sf, minor) in &smf.key_sigs {
let sf = sf.clamp(-7, 7);
ev.push(WEvent { tick: clamp_tick(t), order: 2, bytes: meta(0x59, &[sf as u8, u8::from(minor)])? });
}
for (t, text) in &smf.markers {
ev.push(WEvent { tick: clamp_tick(*t), order: 3, bytes: meta(0x06, text.as_bytes())? });
}
Ok(ev)
}
/// Trim notes still sounding when the next same-pitch note starts, and drop duplicates.
fn trim_overlaps(notes: &[Note], ends: &mut [u64], dropped: &mut [bool]) {
// The reader pairs offs with ons first-in first-out, so a note still sounding when the next
// same-pitch note starts would swap ends with it: trim it to that start. When two same-pitch
// notes start on the same tick they cannot be told apart, so only the longer one is written.
let mut order: Vec<usize> = (0..notes.len()).collect();
order.sort_by_key(|&i| {
notes.get(i).map_or((0, 0, 0, std::cmp::Reverse(0)), |n| {
(n.channel & 0x0F, n.pitch.min(127), clamp_tick(n.start), std::cmp::Reverse(ends.get(i).copied().unwrap_or(0)))
})
});
let mut prev: Option<usize> = None;
for &b in &order {
let Some(nb) = notes.get(b) else { continue };
if let Some(a) = prev
&& let Some(na) = notes.get(a)
&& na.channel & 0x0F == nb.channel & 0x0F
&& na.pitch.min(127) == nb.pitch.min(127)
{
let (sa, sb) = (clamp_tick(na.start), clamp_tick(nb.start));
if sa == sb {
if let Some(d) = dropped.get_mut(b) {
*d = true;
}
continue;
}
if let Some(ea) = ends.get_mut(a)
&& sb < *ea
{
*ea = sb;
}
}
prev = Some(b);
}
}
/// Writes a format-1 Standard MIDI File at [`PPQ`] (960) ticks per quarter note.
///
/// Track 1 is a conductor track holding tempos, meters, key signatures and markers; every
@@ -527,7 +463,29 @@ pub fn write_smf(smf: &Smf) -> Result<Vec<u8>, MidiError> {
out.extend_from_slice(&u16::try_from(PPQ).unwrap_or(960).to_be_bytes());
// Conductor track.
let ev = conductor_events(smf)?;
let mut ev = Vec::new();
for &(t, bpm) in &smf.tempos {
if !bpm.is_finite() || bpm <= 0.0 {
return Err(MidiError::Write(format!("invalid tempo {bpm} bpm")));
}
let us = (60_000_000.0 / bpm).round().clamp(1.0, f64::from(0x00FF_FFFFu32)) as u32;
let [_, a, b, c] = us.to_be_bytes();
ev.push(WEvent { tick: clamp_tick(t), order: 0, bytes: meta(0x51, &[a, b, c])? });
}
for &(t, num, den) in &smf.meters {
if den == 0 || !den.is_power_of_two() {
return Err(MidiError::Write(format!("meter denominator {den} is not a power of two")));
}
let dd = den.trailing_zeros() as u8;
ev.push(WEvent { tick: clamp_tick(t), order: 1, bytes: meta(0x58, &[num, dd, 24, 8])? });
}
for &(t, sf, minor) in &smf.key_sigs {
let sf = sf.clamp(-7, 7);
ev.push(WEvent { tick: clamp_tick(t), order: 2, bytes: meta(0x59, &[sf as u8, u8::from(minor)])? });
}
for (t, text) in &smf.markers {
ev.push(WEvent { tick: clamp_tick(*t), order: 3, bytes: meta(0x06, text.as_bytes())? });
}
if has_conductor {
write_track(&mut out, ev)?;
}
@@ -540,7 +498,38 @@ pub fn write_smf(smf: &Smf) -> Result<Vec<u8>, MidiError> {
let notes = &track.sequence.notes;
let mut ends: Vec<u64> = notes.iter().map(|n| clamp_tick(n.end()).max(clamp_tick(n.start).saturating_add(1))).collect();
let mut dropped = vec![false; notes.len()];
trim_overlaps(notes, &mut ends, &mut dropped);
// The reader pairs offs with ons first-in first-out, so a note still sounding when the next
// same-pitch note starts would swap ends with it: trim it to that start. When two same-pitch
// notes start on the same tick they cannot be told apart, so only the longer one is written.
let mut order: Vec<usize> = (0..notes.len()).collect();
order.sort_by_key(|&i| {
notes.get(i).map_or((0, 0, 0, std::cmp::Reverse(0)), |n| {
(n.channel & 0x0F, n.pitch.min(127), clamp_tick(n.start), std::cmp::Reverse(ends.get(i).copied().unwrap_or(0)))
})
});
let mut prev: Option<usize> = None;
for &b in &order {
let Some(nb) = notes.get(b) else { continue };
if let Some(a) = prev
&& let Some(na) = notes.get(a)
&& na.channel & 0x0F == nb.channel & 0x0F
&& na.pitch.min(127) == nb.pitch.min(127)
{
let (sa, sb) = (clamp_tick(na.start), clamp_tick(nb.start));
if sa == sb {
if let Some(d) = dropped.get_mut(b) {
*d = true;
}
continue;
}
if let Some(ea) = ends.get_mut(a)
&& sb < *ea
{
*ea = sb;
}
}
prev = Some(b);
}
for (i, n) in notes.iter().enumerate() {
if dropped.get(i).copied().unwrap_or(false) {
continue;
+25 -31
View File
@@ -439,40 +439,34 @@ impl AudioState {
let b = if i0 + 1 < self.pending_len { ch.and_then(|v| v.get(i0 + 1)).copied().unwrap_or(a) } else { a };
a * (1.0 - fr) + b * fr
};
write_frame(frame, channels, nch, sounding, &self.fold, sample);
self.read += step;
}
}
}
/// Write one device frame from `nch` rendered channels: fold surround to stereo (a mono device
/// averages the two sides), or copy the channels straight through. `sample(c)` is channel `c`.
fn write_frame(frame: &mut [f32], channels: usize, nch: usize, sounding: bool, fold: &[(f32, f32)], sample: impl Fn(usize) -> f32) {
if channels < nch {
// Fold surround to stereo first; a mono device averages the two sides.
let (mut l, mut r) = (0.0f32, 0.0f32);
if sounding {
if nch == 2 {
(l, r) = (sample(0), sample(1));
if channels < nch {
// Fold surround to stereo first; a mono device averages the two sides.
let (mut l, mut r) = (0.0f32, 0.0f32);
if sounding {
if nch == 2 {
(l, r) = (sample(0), sample(1));
} else {
for (c, (gl, gr)) in self.fold.iter().enumerate().take(nch) {
let v = sample(c);
l += v * gl;
r += v * gr;
}
}
}
for (c, o) in frame.iter_mut().enumerate() {
*o = match c {
0 if channels == 1 => out_sample(0.5 * l + 0.5 * r),
0 => out_sample(l),
1 => out_sample(r),
_ => 0.0,
};
}
} else {
for (c, (gl, gr)) in fold.iter().enumerate().take(nch) {
let v = sample(c);
l += v * gl;
r += v * gr;
for (c, o) in frame.iter_mut().enumerate() {
*o = if c < nch.max(1) && sounding { out_sample(sample(c)) } else { 0.0 };
}
}
}
for (c, o) in frame.iter_mut().enumerate() {
*o = match c {
0 if channels == 1 => out_sample(0.5 * l + 0.5 * r),
0 => out_sample(l),
1 => out_sample(r),
_ => 0.0,
};
}
} else {
for (c, o) in frame.iter_mut().enumerate() {
*o = if c < nch.max(1) && sounding { out_sample(sample(c)) } else { 0.0 };
self.read += step;
}
}
}
+19 -31
View File
@@ -194,35 +194,6 @@ fn keep_releases(notes: &mut Vec<NoteEvent>) {
}
}
/// Copy `n` frames from `start` of the host buffers into the plugin's input `port` (mono sources
/// fill every port channel).
fn feed_input(inputs: &mut BusBuffers, port: usize, io: &[Vec<f32>], ch: usize, (start, n): (usize, usize)) {
for c in 0..inputs.channels(port) {
let src = io.get(c.min(ch - 1)).and_then(|s| s.get(start..start + n));
if let (Some(src), Some(dst)) = (src, inputs.channel_mut(port, c).and_then(|d| d.get_mut(..n))) {
dst.copy_from_slice(src);
}
}
}
/// Copy `n` frames of the plugin's output `port` into the host buffers from `start`; a mono host
/// buffer takes the average of a stereo port.
fn read_output(outputs: &BusBuffers, port: usize, io: &mut [Vec<f32>], ch: usize, (start, n): (usize, usize)) {
let oc = outputs.channels(port);
for (c, dst) in io.iter_mut().take(ch).enumerate() {
let Some(dst) = dst.get_mut(start..start + n) else { continue };
if ch == 1 && oc >= 2 {
if let (Some(l), Some(r)) = (outputs.channel(port, 0), outputs.channel(port, 1)) {
for ((d, a), b) in dst.iter_mut().zip(l).zip(r) {
*d = 0.5 * (a + b);
}
}
} else if let Some(src) = outputs.channel(port, c.min(oc.saturating_sub(1))).and_then(|s| s.get(..n)) {
dst.copy_from_slice(src);
}
}
}
impl Plugin for Vst3Plugin {
fn info(&self) -> &'static PluginInfo {
self.info
@@ -319,7 +290,12 @@ impl Plugin for Vst3Plugin {
let end = u32::try_from(start + n).unwrap_or(u32::MAX);
// Feed the main input bus (mono sources fill every bus channel).
if self.has_input {
feed_input(&mut self.inputs, 0, io, ch, (start, n));
for c in 0..self.inputs.channels(0) {
let src = io.get(c.min(ch - 1)).and_then(|s| s.get(start..start + n));
if let (Some(src), Some(dst)) = (src, self.inputs.channel_mut(0, c).and_then(|d| d.get_mut(..n))) {
dst.copy_from_slice(src);
}
}
}
let k = self.notes.iter().position(|e| e.time() >= end).unwrap_or(self.notes.len());
let offset = u32::try_from(start).unwrap_or(u32::MAX);
@@ -340,7 +316,19 @@ impl Plugin for Vst3Plugin {
self.position = self.position.saturating_add(i64::try_from(n).unwrap_or(0));
match r {
Ok(()) if self.has_output => {
read_output(&self.outputs, 0, io, ch, (start, n));
let oc = self.outputs.channels(0);
for (c, dst) in io.iter_mut().take(ch).enumerate() {
let Some(dst) = dst.get_mut(start..start + n) else { continue };
if ch == 1 && oc >= 2 {
if let (Some(l), Some(r)) = (self.outputs.channel(0, 0), self.outputs.channel(0, 1)) {
for ((d, a), b) in dst.iter_mut().zip(l).zip(r) {
*d = 0.5 * (a + b);
}
}
} else if let Some(src) = self.outputs.channel(0, c.min(oc.saturating_sub(1))).and_then(|s| s.get(..n)) {
dst.copy_from_slice(src);
}
}
}
Err(e) => log::debug!("{}: {e}", self.info.id),
_ => {}
@@ -6,11 +6,6 @@
mod common;
fn main() {
// nextest lists custom-harness tests with `--list`; each binary here is one test.
if std::env::args().any(|a| a == "--list") {
println!("editor_main_thread: test");
return;
}
let dir = common::fixture_dir();
let gain = soundcraft_vst3_host::scan_paths(&[dir.to_path_buf()]).into_iter().find(|d| d.name == "Fixture Gain").expect("fixture").id;
let mut p = soundcraft_vst3_host::instantiate_plugin(&gain).expect("instantiate");
-5
View File
@@ -6,11 +6,6 @@
mod common;
fn main() {
// nextest lists custom-harness tests with `--list`; each binary here is one test.
if std::env::args().any(|a| a == "--list") {
println!("module_exit: test");
return;
}
let dir = common::fixture_dir();
let gain = soundcraft_vst3_host::scan_paths(&[dir.to_path_buf()]).into_iter().find(|d| d.name == "Fixture Gain").expect("fixture").id;
let p = soundcraft_vst3_host::instantiate_plugin(&gain).expect("instantiate");
@@ -5,11 +5,6 @@
mod common;
fn main() {
// nextest lists custom-harness tests with `--list`; each binary here is one test.
if std::env::args().any(|a| a == "--list") {
println!("module_exit_live_instance: test");
return;
}
let dir = common::fixture_dir();
let gain = soundcraft_vst3_host::scan_paths(&[dir.to_path_buf()]).into_iter().find(|d| d.name == "Fixture Gain").expect("fixture").id;
let p = soundcraft_vst3_host::instantiate_plugin(&gain).expect("instantiate");
@@ -29,11 +29,6 @@ fn run(case: &str) -> Output {
}
fn main() {
// nextest lists custom-harness tests with `--list`; each binary here is one test.
if std::env::args().any(|a| a == "--list") {
println!("module_exit_shutdown: test");
return;
}
match std::env::args().nth(1).as_deref() {
Some("shutdown") => return child(false),
Some("exit-handler") => return child(true),
@@ -6,11 +6,6 @@
mod common;
fn main() {
// nextest lists custom-harness tests with `--list`; each binary here is one test.
if std::env::args().any(|a| a == "--list") {
println!("module_exit_worker_thread: test");
return;
}
std::thread::spawn(|| {
let dir = common::fixture_dir();
let gain = soundcraft_vst3_host::scan_paths(&[dir.to_path_buf()]).into_iter().find(|d| d.name == "Fixture Gain").expect("fixture").id;