diff --git a/.DS_Store b/.DS_Store index eb3eed6f..f9d43b46 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/Cargo.lock b/Cargo.lock index 146318c0..2b18978d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,6 +9,7 @@ dependencies = [ "axum 0.8.6", "console-subscriber", "pmoapp", + "pmoaudio-ext", "pmoaudiocache", "pmoconfig", "pmocovers", @@ -32,9 +33,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -80,7 +81,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -107,7 +108,7 @@ checksum = "f548ad2c4031f2902e3edc1f29c29e835829437de49562d8eb5dc5584d3a1043" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -140,7 +141,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -151,7 +152,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -179,14 +180,14 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "av1-grain" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" dependencies = [ "anyhow", "arrayvec", "log", - "nom", + "nom 8.0.0", "num-rational", "v_frame", ] @@ -343,22 +344,22 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bevy_macro_utils" -version = "0.17.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61e64e7431f7349b5caa1ea4ff2efd94afd73480fac2f44a2b94b1642b4a02a" +checksum = "17dbc3f8948da58b3c17767d20fd3cd35fe4721ed19a9a3204a6f1d6c9951bdd" dependencies = [ "parking_lot", "proc-macro2", "quote", - "syn 2.0.106", - "toml_edit 0.23.6", + "syn 2.0.108", + "toml_edit 0.23.7", ] [[package]] name = "bevy_platform" -version = "0.17.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239c3f54e9e25a01e4d0f54a3a066e1d8d6d5509b071a78c729c1e0143db7f4b" +checksum = "10cf8cda162688c95250e74cffaa1c3a04597f105d4ca35554106f107308ea57" dependencies = [ "critical-section", "foldhash 0.2.0", @@ -375,15 +376,15 @@ dependencies = [ [[package]] name = "bevy_ptr" -version = "0.17.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b3533a9a9c2158bc704c18ba26f0f0692d0423ff0a10b656b8db46a9b953620" +checksum = "28ab4074e7b781bab84e9b0a41ede245d673d1f75646ce0db27643aedcfb3a85" [[package]] name = "bevy_reflect" -version = "0.17.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18f363a1e984a5b9e908e9b98181e396d4a93ee8209a698c0d0e38227f464e45" +checksum = "333df3f5947b7e62728eb5c0b51d679716b16c7c5283118fed4563f13230954e" dependencies = [ "assert_type_match", "bevy_platform", @@ -408,23 +409,23 @@ dependencies = [ [[package]] name = "bevy_reflect_derive" -version = "0.17.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65b08cc2fb06b872143c506ee7e14decb3ade19bf25af9ddd39dfc1799ccc85" +checksum = "0205dce9c5a4d8d041b263bcfd96e9d9d6f3d49416e12db347ab5778b3071fe1" dependencies = [ "bevy_macro_utils", - "indexmap 2.11.4", + "indexmap 2.12.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", "uuid", ] [[package]] name = "bevy_utils" -version = "0.17.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6927fae6678c8ca2984b97d1a77bb244cfe095f5fd28ed2f88e0d66cfab938fa" +checksum = "080254083c74d5f6eb0649d7cd6181bda277e8fe3c509ec68990a5d56ec23f24" dependencies = [ "bevy_platform", "disqualified", @@ -436,16 +437,16 @@ version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -462,11 +463,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -513,7 +514,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -542,9 +543,9 @@ checksum = "4964518bd3b4a8190e832886cdc0da9794f12e8e6c1613a9e90ff331c4c8724b" [[package]] name = "cc" -version = "1.2.39" +version = "1.2.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" +checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3" dependencies = [ "find-msvc-tools", "jobserver", @@ -558,7 +559,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom", + "nom 7.1.3", ] [[package]] @@ -573,9 +574,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" @@ -588,7 +589,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -840,9 +841,9 @@ checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" [[package]] name = "deranged" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] @@ -855,7 +856,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -875,7 +876,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -906,7 +907,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -917,7 +918,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -928,9 +929,9 @@ checksum = "c9c272297e804878a2a4b707cfcfc6d2328b5bb936944613b4fdf2b9269afdfd" [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -973,7 +974,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -1000,7 +1001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -1080,7 +1081,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -1098,7 +1099,7 @@ version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d658424d233cbd993a972dd73a66ca733acd12a494c68995c9ac32ae1fe65b40" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "ffmpeg-sys-next", "libc", ] @@ -1119,9 +1120,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" +checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" [[package]] name = "flacenc" @@ -1143,9 +1144,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "libz-rs-sys", @@ -1196,9 +1197,9 @@ dependencies = [ [[package]] name = "fs-err" -version = "3.1.2" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f150ffc8782f35521cec2b23727707cb4045706ba3c854e86bef66b3a8cdbd" +checksum = "6ad492b2cf1d89d568a43508ab24f98501fe03f2f31c01e1d0fe7366a71745d2" dependencies = [ "autocfg", "tokio", @@ -1260,7 +1261,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -1301,9 +1302,9 @@ checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" [[package]] name = "generic-array" -version = "0.14.7" +version = "0.14.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", @@ -1339,19 +1340,19 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "wasip2", ] [[package]] @@ -1366,9 +1367,9 @@ dependencies = [ [[package]] name = "glam" -version = "0.30.8" +version = "0.30.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12d847aeb25f41be4c0ec9587d624e9cd631bc007a8fd7ce3f5851e064c6460" +checksum = "bd47b05dddf0005d850e5644cae7f2b14ac3df487979dbfff3b56f20b1a6ae46" dependencies = [ "serde_core", ] @@ -1391,7 +1392,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.11.4", + "indexmap 2.12.0", "slab", "tokio", "tokio-util", @@ -1400,12 +1401,13 @@ dependencies = [ [[package]] name = "half" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", + "zerocopy", ] [[package]] @@ -1460,7 +1462,7 @@ dependencies = [ "base64 0.21.7", "byteorder", "flate2", - "nom", + "nom 7.1.3", "num-traits", ] @@ -1643,7 +1645,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2 0.6.1", "system-configuration", "tokio", "tower-service", @@ -1663,7 +1665,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.1", + "windows-core 0.62.2", ] [[package]] @@ -1677,9 +1679,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -1690,9 +1692,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -1703,11 +1705,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -1718,42 +1719,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -1834,9 +1831,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown 0.16.0", @@ -1852,7 +1849,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -1889,6 +1886,24 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -1901,15 +1916,15 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -1940,9 +1955,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.176" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libflac-sys" @@ -1971,7 +1986,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -1980,7 +1995,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "libc", ] @@ -2031,23 +2046,22 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "litrs" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -2074,7 +2088,7 @@ checksum = "ed9983e64b2358522f745c1251924e3ab7252d55637e80f6a0a3de642d6a9efc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -2202,13 +2216,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -2258,9 +2272,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.6" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cc7d85f3d741164e8972ad355e26ac6e51b20fcae5f911c7da8f2d8bbbb3f33" +checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6" dependencies = [ "num-traits", "pxfm", @@ -2313,6 +2327,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "noop_proc_macro" version = "0.3.0" @@ -2330,11 +2353,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.50.1" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2372,7 +2395,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -2440,11 +2463,11 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "openssl" -version = "0.10.73" +version = "0.10.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "foreign-types", "libc", @@ -2461,7 +2484,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -2472,9 +2495,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.109" +version = "0.9.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" dependencies = [ "cc", "libc", @@ -2518,9 +2541,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -2528,15 +2551,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-link 0.2.1", ] [[package]] @@ -2568,7 +2591,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -2596,7 +2619,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", - "indexmap 2.11.4", + "indexmap 2.12.0", "quick-xml 0.38.3", "serde", "time", @@ -2626,9 +2649,25 @@ dependencies = [ "tokio", "tokio-test", "tokio-util", + "tracing", "wiremock", ] +[[package]] +name = "pmoaudio-ext" +version = "0.1.0" +dependencies = [ + "async-trait", + "pmoaudio", + "pmoaudiocache", + "pmoflac", + "pmometadata", + "pmoplaylist", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "pmoaudiocache" version = "0.1.0" @@ -2641,7 +2680,6 @@ dependencies = [ "futures-util", "lofty", "paste", - "pmoaudio", "pmocache", "pmoconfig", "pmodidl", @@ -2850,6 +2888,7 @@ dependencies = [ "pmocache", "pmoconfig", "pmodidl", + "pmometadata", "pmoupnp", "rusqlite", "serde", @@ -2985,7 +3024,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "crc32fast", "fdeflate", "flate2", @@ -3009,9 +3048,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -3033,9 +3072,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -3056,7 +3095,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -3076,10 +3115,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools", + "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -3109,9 +3148,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde" +checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" dependencies = [ "num-traits", ] @@ -3222,7 +3261,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] @@ -3239,7 +3278,7 @@ dependencies = [ "built", "cfg-if", "interpolate_name", - "itertools", + "itertools 0.12.1", "libc", "libfuzzer-sys", "log", @@ -3297,11 +3336,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -3317,9 +3356,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.3" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -3329,9 +3368,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -3340,15 +3379,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.23" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64 0.22.1", "bytes", @@ -3416,7 +3455,7 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -3426,9 +3465,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.7.2" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" +checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -3437,22 +3476,22 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.7.2" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" +checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.106", + "syn 2.0.108", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.7.2" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" +checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475" dependencies = [ "sha2", "walkdir", @@ -3479,18 +3518,18 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.32" +version = "0.23.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7" dependencies = [ "once_cell", "rustls-pki-types", @@ -3501,18 +3540,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.7" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", @@ -3546,7 +3585,7 @@ version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -3561,7 +3600,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation", "core-foundation-sys", "libc", @@ -3617,7 +3656,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -3671,7 +3710,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "itoa", "ryu", "serde", @@ -3789,12 +3828,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3803,7 +3842,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3ca6bc65602daad89c3217255ff89c4abbe489b9dd3e56c66aa16d1addac979" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytemuck", "libsoxr-sys", ] @@ -3819,9 +3858,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "subtle" @@ -4037,9 +4076,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" dependencies = [ "proc-macro2", "quote", @@ -4063,7 +4102,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -4087,7 +4126,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation", "system-configuration-sys", ] @@ -4134,10 +4173,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -4166,7 +4205,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -4177,7 +4216,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -4236,9 +4275,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -4271,10 +4310,10 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.0", + "socket2 0.6.1", "tokio-macros", "tracing", - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -4285,7 +4324,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -4334,9 +4373,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -4368,9 +4407,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] @@ -4381,7 +4420,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "serde", "serde_spanned", "toml_datetime 0.6.11", @@ -4390,21 +4429,21 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.6" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ - "indexmap 2.11.4", - "toml_datetime 0.7.2", + "indexmap 2.12.0", + "toml_datetime 0.7.3", "toml_parser", "winnow", ] [[package]] name = "toml_parser" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow", ] @@ -4481,7 +4520,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytes", "futures-util", "http", @@ -4525,7 +4564,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -4581,9 +4620,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicase" @@ -4593,9 +4632,9 @@ checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unsafe-libyaml" @@ -4643,7 +4682,7 @@ version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "serde", "serde_json", "utoipa-gen", @@ -4658,7 +4697,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -4692,7 +4731,7 @@ version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "js-sys", "serde", "wasm-bindgen", @@ -4723,7 +4762,7 @@ checksum = "41b6d82be61465f97d42bd1d15bf20f3b0a3a0905018f38f9d6f6962055b0b5c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -4734,9 +4773,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version-compare" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" [[package]] name = "version_check" @@ -4769,15 +4808,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -4789,9 +4819,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", @@ -4800,25 +4830,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ "cfg-if", "js-sys", @@ -4829,9 +4845,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4839,22 +4855,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 2.0.108", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] @@ -4874,9 +4890,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -4904,7 +4920,7 @@ version = "26.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eca7a8d8af57c18f57d393601a1fb159ace8b2328f1b6b5f80893f7d672c9ae2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytemuck", "js-sys", "log", @@ -4941,7 +4957,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.1", + "windows-sys 0.61.2", ] [[package]] @@ -4957,7 +4973,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ "windows-core 0.52.0", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -4966,42 +4982,42 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] name = "windows-core" -version = "0.62.1" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.0", - "windows-result 0.4.0", - "windows-strings 0.5.0", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] name = "windows-implement" -version = "0.60.1" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] name = "windows-interface" -version = "0.59.2" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -5012,9 +5028,9 @@ checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-registry" @@ -5038,11 +5054,11 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -5056,11 +5072,11 @@ dependencies = [ [[package]] name = "windows-strings" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -5069,7 +5085,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]] @@ -5078,16 +5094,25 @@ 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.61.1" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-link 0.2.0", + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", ] [[package]] @@ -5096,14 +5121,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 0.2.1", + "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]] @@ -5112,48 +5154,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 = "winnow" version = "0.7.13" @@ -5194,15 +5284,15 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "xml-rs" -version = "0.8.27" +version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" [[package]] name = "xmltree" @@ -5215,11 +5305,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -5227,13 +5316,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", "synstructure", ] @@ -5254,7 +5343,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -5274,7 +5363,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", "synstructure", ] @@ -5286,9 +5375,9 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -5297,9 +5386,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -5308,13 +5397,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.108", ] [[package]] @@ -5326,7 +5415,7 @@ dependencies = [ "arbitrary", "crc32fast", "flate2", - "indexmap 2.11.4", + "indexmap 2.12.0", "memchr", "zopfli", ] @@ -5339,9 +5428,9 @@ checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" [[package]] name = "zopfli" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" dependencies = [ "bumpalo", "crc32fast", diff --git a/PMOMusic/Cargo.toml b/PMOMusic/Cargo.toml index cda6d03a..d31b3de9 100644 --- a/PMOMusic/Cargo.toml +++ b/PMOMusic/Cargo.toml @@ -12,6 +12,7 @@ pmosource = { path = "../pmosource", features = ["server"] } pmoserver = { path = "../pmoserver" } pmocovers = { path = "../pmocovers", features = ["pmoserver"] } pmoaudiocache = { path = "../pmoaudiocache", features = ["pmoserver"]} +pmoaudio-ext = { path = "../pmoaudio-ext", features = ["all"] } pmoapp = { path = "../pmoapp", features = ["pmoserver"] } tokio = { version = "1.35", features = ["rt-multi-thread", "macros", "sync", "time","signal"] } diff --git a/pmoapp/src/lib.rs b/pmoapp/src/lib.rs index fcfe1263..b527636f 100644 --- a/pmoapp/src/lib.rs +++ b/pmoapp/src/lib.rs @@ -238,8 +238,6 @@ //! - [Vite Documentation](https://vitejs.dev/) use rust_embed::RustEmbed; -use std::future::Future; -use std::pin::Pin; /// Structure représentant l'application web embarquée. /// diff --git a/pmoaudio-ext/Cargo.toml b/pmoaudio-ext/Cargo.toml new file mode 100644 index 00000000..3cd35584 --- /dev/null +++ b/pmoaudio-ext/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "pmoaudio-ext" +version = "0.1.0" +edition = "2021" + +[dependencies] +# Core audio types +pmoaudio = { path = "../pmoaudio" } + +# Optional dependencies for cache-sink feature +pmoaudiocache = { path = "../pmoaudiocache", optional = true } +pmoflac = { path = "../pmoflac", optional = true } +pmometadata = { path = "../pmometadata", optional = true } + +# Optional dependency for playlist integration +pmoplaylist = { path = "../pmoplaylist", optional = true } + +# Async runtime +tokio = { version = "1.0", features = ["full"] } +tokio-util = { version = "0.7" } +async-trait = "0.1" + +# Utilities +tracing = "0.1" + +[features] +default = [] +cache-sink = ["dep:pmoaudiocache", "dep:pmoflac", "dep:pmometadata"] +playlist = ["dep:pmoplaylist"] +all = ["cache-sink", "playlist"] diff --git a/pmoaudio-ext/src/lib.rs b/pmoaudio-ext/src/lib.rs new file mode 100644 index 00000000..a99a5c2a --- /dev/null +++ b/pmoaudio-ext/src/lib.rs @@ -0,0 +1,30 @@ +//! Extensions pour pmoaudio +//! +//! Cette crate fournit des nodes d'extension pour pmoaudio qui dépendent +//! d'autres crates du projet. Elle permet d'éviter les dépendances cycliques +//! en plaçant ces extensions en "bout de chaîne" de dépendances. +//! +//! # Features +//! +//! - `cache-sink` : Active le `FlacCacheSink` qui encode l'audio en FLAC et le stocke dans pmoaudiocache +//! - `playlist` : Active l'intégration avec pmoplaylist pour les sinks +//! - `all` : Active toutes les features d'un coup +//! +//! # Architecture +//! +//! Cette crate dépend de : +//! - `pmoaudio` : Types de base (AudioSegment, AudioError, etc.) +//! - `pmoaudiocache` (optionnel) : Cache audio pour le stockage FLAC +//! - `pmoflac` (optionnel) : Encodage FLAC +//! - `pmometadata` (optionnel) : Gestion des métadonnées +//! - `pmoplaylist` (optionnel) : Intégration playlist +//! +//! Aucune des crates ci-dessus ne dépend de `pmoaudio-ext`, évitant ainsi +//! tout cycle de dépendances. + +#[cfg(feature = "cache-sink")] +pub mod sinks; + +// Re-exports pour faciliter l'utilisation +#[cfg(feature = "cache-sink")] +pub use sinks::*; diff --git a/pmoaudiocache/src/nodes/flac_cache_sink.rs b/pmoaudio-ext/src/sinks/flac_cache_sink.rs similarity index 84% rename from pmoaudiocache/src/nodes/flac_cache_sink.rs rename to pmoaudio-ext/src/sinks/flac_cache_sink.rs index f4b50fb0..2ebab15c 100644 --- a/pmoaudiocache/src/nodes/flac_cache_sink.rs +++ b/pmoaudio-ext/src/sinks/flac_cache_sink.rs @@ -1,11 +1,11 @@ //! Sink qui encode les AudioSegment au format FLAC et les stocke dans le cache audio -use crate::metadata_ext::AudioTrackMetadataExt; use pmoaudio::{ nodes::{AudioError, TypedAudioNode, DEFAULT_CHANNEL_SIZE}, type_constraints::TypeRequirement, - AudioChunk, AudioSegment, SyncMarker, _AudioSegment, + AudioChunk, AudioPipelineNode, AudioSegment, SyncMarker, _AudioSegment, }; +use pmoaudiocache::AudioTrackMetadataExt; use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat}; use std::{ collections::VecDeque, @@ -18,6 +18,7 @@ use tokio::{ io::{self, AsyncRead, ReadBuf}, sync::{mpsc, RwLock}, }; +use tokio_util::sync::CancellationToken; /// Sink qui encode les `AudioSegment` reçus au format FLAC et les stocke dans le cache audio. /// @@ -26,13 +27,17 @@ use tokio::{ /// - Crée une nouvelle entrée de cache pour chaque TrackBoundary rencontré /// - Adapte automatiquement l'encodage FLAC selon la profondeur de bit du chunk (8/16/24/32-bit) /// - Copie les métadonnées du TrackBoundary dans le cache après ingestion +/// - Peut optionnellement ajouter les tracks à une playlist via `register_playlist()` /// - Termine l'encodage proprement quand il reçoit EndOfStream pub struct FlacCacheSink { + tx: mpsc::Sender>, rx: mpsc::Receiver>, - cache: Arc, + cache: Arc, collection: Option, encoder_options: EncoderOptions, pcm_buffer_capacity: usize, + #[cfg(feature = "playlist")] + playlist_handle: Option>, } impl FlacCacheSink { @@ -41,7 +46,7 @@ impl FlacCacheSink { /// # Arguments /// /// * `cache` - Arc vers le cache audio où stocker les fichiers FLAC encodés - pub fn new(cache: Arc) -> (Self, mpsc::Sender>) { + pub fn new(cache: Arc) -> Self { Self::with_channel_size(cache, DEFAULT_CHANNEL_SIZE) } @@ -51,10 +56,7 @@ impl FlacCacheSink { /// /// * `cache` - Arc vers le cache audio /// * `channel_size` - Taille du buffer MPSC (nombre de segments en attente avant backpressure) - pub fn with_channel_size( - cache: Arc, - channel_size: usize, - ) -> (Self, mpsc::Sender>) { + pub fn with_channel_size(cache: Arc, channel_size: usize) -> Self { Self::with_config(cache, channel_size, EncoderOptions::default(), None) } @@ -67,33 +69,51 @@ impl FlacCacheSink { /// * `encoder_options` - Options d'encodage FLAC (compression, etc.) /// * `collection` - Collection optionnelle à laquelle appartiennent les fichiers pub fn with_config( - cache: Arc, + cache: Arc, channel_size: usize, encoder_options: EncoderOptions, collection: Option, - ) -> (Self, mpsc::Sender>) { + ) -> Self { let (tx, rx) = mpsc::channel(channel_size); - let sink = Self { + Self { + tx, rx, cache, collection, encoder_options, pcm_buffer_capacity: 8, - }; - (sink, tx) + #[cfg(feature = "playlist")] + playlist_handle: None, + } } - /// Lance l'encodage et l'ingestion dans le cache. + /// Enregistre une playlist pour recevoir automatiquement les tracks sauvées dans le cache. + /// + /// # Arguments + /// + /// * `handle` - WriteHandle de la playlist qui recevra les pk des tracks + #[cfg(feature = "playlist")] + pub fn register_playlist(&mut self, handle: pmoplaylist::WriteHandle) { + self.playlist_handle = Some(Arc::new(handle)); + } + + /// Lance l'encodage et l'ingestion dans le cache (version interne). /// /// Cette méthode crée une nouvelle entrée de cache pour chaque TrackBoundary rencontré. /// Les métadonnées du TrackBoundary sont copiées dans le cache après l'ingestion. - pub async fn run(self) -> Result { + async fn run_internal( + self, + stop_token: CancellationToken, + ) -> Result { let FlacCacheSink { + tx: _, mut rx, cache, collection, encoder_options, pcm_buffer_capacity, + #[cfg(feature = "playlist")] + playlist_handle, } = self; let mut all_tracks = Vec::new(); @@ -102,7 +122,7 @@ impl FlacCacheSink { loop { // Attendre le premier chunk audio pour cette track, en capturant les métadonnées du TrackBoundary let (first_segment, track_metadata) = - match wait_for_first_audio_chunk_with_metadata(&mut rx).await { + match wait_for_first_audio_chunk_with_metadata(&mut rx, &stop_token).await { Ok(result) => result, Err(_) => { // Plus d'audio disponible @@ -157,6 +177,7 @@ impl FlacCacheSink { pcm_tx, bits_per_sample, sample_rate, + &stop_token, ); let copy_future = async { tokio::io::copy(&mut flac_stream, &mut flac_buffer) @@ -200,6 +221,15 @@ impl FlacCacheSink { })?; } + // Ajouter à la playlist si enregistrée + #[cfg(feature = "playlist")] + if let Some(ref playlist_handle) = playlist_handle { + playlist_handle.push(pk.clone()).await + .map_err(|e| AudioError::ProcessingError( + format!("Failed to add to playlist: {}", e) + ))?; + } + // Ajouter les stats de cette track all_tracks.push(TrackStats { pk, @@ -238,6 +268,7 @@ enum StopReason { /// Retourne une erreur si EndOfStream est reçu avant tout audio. async fn wait_for_first_audio_chunk_with_metadata( rx: &mut mpsc::Receiver>, + stop_token: &CancellationToken, ) -> Result< ( Arc, @@ -248,10 +279,14 @@ async fn wait_for_first_audio_chunk_with_metadata( let mut track_metadata: Option>> = None; loop { - let segment = rx - .recv() - .await - .ok_or_else(|| AudioError::ProcessingError("No audio data received".into()))?; + let segment = tokio::select! { + result = rx.recv() => { + result.ok_or_else(|| AudioError::ProcessingError("No audio data received".into()))? + } + _ = stop_token.cancelled() => { + return Err(AudioError::ProcessingError("Cancelled".into())); + } + }; match &segment.segment { _AudioSegment::Chunk(chunk) => { @@ -260,8 +295,8 @@ async fn wait_for_first_audio_chunk_with_metadata( } return Ok((segment, track_metadata)); } - _AudioSegment::Sync(marker) => match **marker { - SyncMarker::TrackBoundary { ref metadata, .. } => { + _AudioSegment::Sync(marker) => match &**marker { + SyncMarker::TrackBoundary { metadata, .. } => { // Capturer les métadonnées du TrackBoundary track_metadata = Some(metadata.clone()); continue; @@ -287,6 +322,7 @@ async fn pump_track_segments( pcm_tx: mpsc::Sender>, bits_per_sample: u8, expected_rate: u32, + stop_token: &CancellationToken, ) -> Result<(u64, u64, f64, StopReason), AudioError> { let mut chunks = 0u64; let mut samples = 0u64; @@ -308,9 +344,17 @@ async fn pump_track_segments( // Boucle sur les segments suivants loop { - let segment = match rx.recv().await { - Some(seg) => seg, - None => { + let segment = tokio::select! { + result = rx.recv() => { + match result { + Some(seg) => seg, + None => { + drop(pcm_tx); // Fermer le channel PCM + return Ok((chunks, samples, duration_sec, StopReason::ChannelClosed)); + } + } + } + _ = stop_token.cancelled() => { drop(pcm_tx); // Fermer le channel PCM return Ok((chunks, samples, duration_sec, StopReason::ChannelClosed)); } @@ -327,7 +371,7 @@ async fn pump_track_segments( ))); } - let pcm_bytes = chunk_to_pcm_bytes(chunk, bits_per_sample)?; + let pcm_bytes = chunk_to_pcm_bytes(&chunk, bits_per_sample)?; if pcm_bytes.is_empty() { continue; } @@ -546,6 +590,25 @@ pub struct FlacCacheSinkStats { pub tracks: Vec, } +#[async_trait::async_trait] +impl AudioPipelineNode for FlacCacheSink { + fn get_tx(&self) -> Option>> { + Some(self.tx.clone()) + } + + fn register(&mut self, _child: Box) { + panic!("FlacCacheSink is a terminal sink and cannot have children"); + } + + async fn run( + self: Box, + stop_token: CancellationToken, + ) -> Result<(), AudioError> { + self.run_internal(stop_token).await?; + Ok(()) + } +} + impl TypedAudioNode for FlacCacheSink { fn input_type(&self) -> Option { // FlacCacheSink accepte n'importe quel type entier (I16, I24, I32) diff --git a/pmoaudio-ext/src/sinks/mod.rs b/pmoaudio-ext/src/sinks/mod.rs new file mode 100644 index 00000000..9cb71261 --- /dev/null +++ b/pmoaudio-ext/src/sinks/mod.rs @@ -0,0 +1,11 @@ +//! Sinks d'extension pour pmoaudio +//! +//! Ce module contient des sinks qui dépendent de multiples crates +//! et ne peuvent pas être placés directement dans pmoaudio sans créer +//! de dépendances cycliques. + +#[cfg(feature = "cache-sink")] +mod flac_cache_sink; + +#[cfg(feature = "cache-sink")] +pub use flac_cache_sink::{FlacCacheSink, FlacCacheSinkStats, TrackStats}; diff --git a/pmoaudio/Cargo.toml b/pmoaudio/Cargo.toml index 02f186df..fa4c29b0 100644 --- a/pmoaudio/Cargo.toml +++ b/pmoaudio/Cargo.toml @@ -18,6 +18,7 @@ paste = "1" soxr = "0.6.0" bytemuck = "1.24.0" reqwest = { version = "0.12", features = ["stream"] } +tracing = "0.1" [dev-dependencies] tokio-test = "0.4" diff --git a/pmoaudio/examples/file_nodes_test.rs b/pmoaudio/examples/file_nodes_test.rs index ac5909dd..e40f1d5e 100644 --- a/pmoaudio/examples/file_nodes_test.rs +++ b/pmoaudio/examples/file_nodes_test.rs @@ -1,14 +1,20 @@ -//! Test d'intégration pour FileSource et FlacFileSink +//! Test d'intégration pour FileSource et FlacFileSink avec la nouvelle architecture AudioPipelineNode //! //! Ce programme teste la chaîne complète : //! 1. Lecture d'un fichier audio avec FileSource //! 2. Écriture vers FLAC avec FlacFileSink //! +//! La nouvelle architecture permet de : +//! - Construire le pipeline en enregistrant des enfants avec register() +//! - Lancer tout le pipeline avec un seul appel à run() sur la racine +//! - Arrêter proprement tout le pipeline avec un CancellationToken +//! //! Usage: //! cargo run --example file_nodes_test -- -use pmoaudio::{FileSource, FlacFileSink}; +use pmoaudio::{AudioPipelineNode, FileSource, FlacFileSink}; use std::env; +use tokio_util::sync::CancellationToken; #[tokio::main] async fn main() -> Result<(), Box> { @@ -28,48 +34,35 @@ async fn main() -> Result<(), Box> { println!(); // Créer le pipeline: FileSource → FlacFileSink - let mut source = FileSource::new(input_path); // Calcul automatique de la taille des chunks (~50ms) - let (sink, tx) = FlacFileSink::new(output_path); // Utilise le buffer par défaut (16 segments) + let mut source = FileSource::new(input_path); + let sink = FlacFileSink::new(output_path); - source.add_subscriber(tx); + // Enregistrer le sink comme enfant de la source + source.register(Box::new(sink)); - // Lancer le sink dans une tâche séparée - let sink_handle = tokio::spawn(async move { - println!("FlacFileSink started"); - let result = sink.run().await; - println!("FlacFileSink finished"); - result - }); + // Créer un token d'arrêt pour contrôle manuel si besoin + let stop_token = CancellationToken::new(); - // Lancer le source - println!("FileSource started"); - let source_result = source.run().await; - println!("FileSource finished"); + // Lancer tout le pipeline - run() spawne automatiquement tous les enfants + println!("Pipeline started"); + println!(" FileSource: reading from {}", input_path); + println!(" FlacFileSink: writing to {}", output_path); - // Vérifier les résultats - match source_result { - Ok(()) => println!("✓ FileSource completed successfully"), + let result = Box::new(source).run(stop_token).await; + + // Vérifier le résultat + match result { + Ok(()) => { + println!(); + println!("✓ Pipeline completed successfully"); + println!(" Output file: {}", output_path); + } Err(e) => { - eprintln!("✗ FileSource error: {}", e); + eprintln!(); + eprintln!("✗ Pipeline error: {}", e); return Err(e.into()); } } - let stats = sink_handle.await??; - println!("✓ FlacFileSink completed successfully"); - println!(); - println!("Statistics:"); - println!(" Tracks written: {}", stats.tracks.len()); - for (i, track) in stats.tracks.iter().enumerate() { - println!(" Track {}:", i); - println!(" Output file: {:?}", track.path); - println!(" Chunks received: {}", track.chunks_received); - println!(" Total samples: {}", track.total_samples); - println!( - " Duration: {:.2} seconds", - track.total_duration_sec - ); - } - Ok(()) } diff --git a/pmoaudio/src/lib.rs b/pmoaudio/src/lib.rs index 1d2004c8..ce9eaee1 100644 --- a/pmoaudio/src/lib.rs +++ b/pmoaudio/src/lib.rs @@ -85,6 +85,7 @@ mod audio_segment; pub mod conversions; pub mod events; pub mod nodes; +pub mod pipeline; mod sample_types; mod sync_marker; pub mod type_constraints; @@ -112,13 +113,16 @@ pub use events::{ VolumeChangeEvent, }; +// Export du trait de pipeline +pub use pipeline::AudioPipelineNode; + // Exports publics des nodes pub use nodes::{ converter_nodes::{ToF32Node, ToF64Node, ToI16Node, ToI24Node, ToI32Node}, file_source::FileSource, flac_file_sink::{FlacFileSink, FlacFileSinkStats}, http_source::HttpSource, - AudioError, AudioNode, MultiSubscriberNode, SingleSubscriberNode, TypedAudioNode, + AudioError, AudioNode, TypedAudioNode, }; // Nodes temporairement désactivés diff --git a/pmoaudio/src/nodes/converter_nodes.rs b/pmoaudio/src/nodes/converter_nodes.rs index bf06dc88..96bfa8f8 100644 --- a/pmoaudio/src/nodes/converter_nodes.rs +++ b/pmoaudio/src/nodes/converter_nodes.rs @@ -8,376 +8,174 @@ //! les incompatibilités de type entre producers et consumers. use crate::{ - nodes::{AudioError, MultiSubscriberNode, TypedAudioNode}, + nodes::{AudioError, TypedAudioNode}, type_constraints::{SampleType, TypeRequirement}, - AudioSegment, + AudioPipelineNode, AudioSegment, }; use std::sync::Arc; use tokio::sync::mpsc; +use tokio_util::sync::CancellationToken; -/// Node de conversion vers I16 -/// -/// Convertit n'importe quel type de chunk audio vers I16 (16-bit signed integer). -/// Utilise les conversions DSP SIMD optimisées. -pub struct ToI16Node { - rx: mpsc::Receiver>, - subscribers: MultiSubscriberNode, -} - -impl ToI16Node { - /// Crée un nouveau node de conversion vers I16 - pub fn new() -> (Self, mpsc::Sender>) { - Self::with_channel_size(16) - } - - /// Crée un nouveau node avec une taille de buffer spécifique - pub fn with_channel_size(channel_size: usize) -> (Self, mpsc::Sender>) { - let (tx, rx) = mpsc::channel(channel_size); - let node = Self { - rx, - subscribers: MultiSubscriberNode::new(), - }; - (node, tx) - } - - /// Ajoute un abonné qui recevra les segments audio convertis - pub fn add_subscriber(&mut self, tx: mpsc::Sender>) { - self.subscribers.add_subscriber(tx); - } - - /// Lance le traitement de conversion - pub async fn run(mut self) -> Result<(), AudioError> { - while let Some(segment) = self.rx.recv().await { - // Si c'est un syncmarker, passer directement - if !segment.is_audio_chunk() { - self.subscribers.push(segment).await?; - continue; - } - - // Convertir le chunk audio vers I16 - let converted_segment = if let Some(chunk) = segment.as_chunk() { - let converted_chunk = chunk.to_i16(); - Arc::new(AudioSegment { - order: segment.order, - timestamp_sec: segment.timestamp_sec, - segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)), - }) - } else { - segment - }; - - self.subscribers.push(converted_segment).await?; +// Macro pour générer les converter nodes avec la nouvelle architecture AudioPipelineNode +macro_rules! converter_node { + ($node_name:ident, $convert_method:ident, $output_type:expr, $doc:expr) => { + #[doc = $doc] + pub struct $node_name { + tx: mpsc::Sender>, + rx: mpsc::Receiver>, + child_txs: Vec>>, + children: Vec>, } - Ok(()) - } -} - -impl TypedAudioNode for ToI16Node { - fn input_type(&self) -> Option { - // Accepte n'importe quel type - Some(TypeRequirement::any()) - } - - fn output_type(&self) -> Option { - // Produit uniquement I16 - Some(TypeRequirement::specific(SampleType::I16)) - } -} - -impl Default for ToI16Node { - fn default() -> Self { - Self::new().0 - } -} - -/// Node de conversion vers I24 -/// -/// Convertit n'importe quel type de chunk audio vers I24 (24-bit signed integer). -/// Utilise les conversions DSP SIMD optimisées. -pub struct ToI24Node { - rx: mpsc::Receiver>, - subscribers: MultiSubscriberNode, -} - -impl ToI24Node { - /// Crée un nouveau node de conversion vers I24 - pub fn new() -> (Self, mpsc::Sender>) { - Self::with_channel_size(16) - } - - /// Crée un nouveau node avec une taille de buffer spécifique - pub fn with_channel_size(channel_size: usize) -> (Self, mpsc::Sender>) { - let (tx, rx) = mpsc::channel(channel_size); - let node = Self { - rx, - subscribers: MultiSubscriberNode::new(), - }; - (node, tx) - } - - /// Ajoute un abonné qui recevra les segments audio convertis - pub fn add_subscriber(&mut self, tx: mpsc::Sender>) { - self.subscribers.add_subscriber(tx); - } - - /// Lance le traitement de conversion - pub async fn run(mut self) -> Result<(), AudioError> { - while let Some(segment) = self.rx.recv().await { - if !segment.is_audio_chunk() { - self.subscribers.push(segment).await?; - continue; + impl $node_name { + /// Crée un nouveau node de conversion + pub fn new() -> Self { + Self::with_channel_size(16) } - let converted_segment = if let Some(chunk) = segment.as_chunk() { - let converted_chunk = chunk.to_i24(); - Arc::new(AudioSegment { - order: segment.order, - timestamp_sec: segment.timestamp_sec, - segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)), - }) - } else { - segment - }; - - self.subscribers.push(converted_segment).await?; + /// Crée un nouveau node avec une taille de buffer spécifique + pub fn with_channel_size(channel_size: usize) -> Self { + let (tx, rx) = mpsc::channel(channel_size); + Self { + tx, + rx, + child_txs: Vec::new(), + children: Vec::new(), + } + } } - Ok(()) - } -} - -impl TypedAudioNode for ToI24Node { - fn input_type(&self) -> Option { - Some(TypeRequirement::any()) - } - - fn output_type(&self) -> Option { - Some(TypeRequirement::specific(SampleType::I24)) - } -} - -impl Default for ToI24Node { - fn default() -> Self { - Self::new().0 - } -} - -/// Node de conversion vers I32 -/// -/// Convertit n'importe quel type de chunk audio vers I32 (32-bit signed integer). -/// Utilise les conversions DSP SIMD optimisées. -pub struct ToI32Node { - rx: mpsc::Receiver>, - subscribers: MultiSubscriberNode, -} - -impl ToI32Node { - /// Crée un nouveau node de conversion vers I32 - pub fn new() -> (Self, mpsc::Sender>) { - Self::with_channel_size(16) - } - - /// Crée un nouveau node avec une taille de buffer spécifique - pub fn with_channel_size(channel_size: usize) -> (Self, mpsc::Sender>) { - let (tx, rx) = mpsc::channel(channel_size); - let node = Self { - rx, - subscribers: MultiSubscriberNode::new(), - }; - (node, tx) - } - - /// Ajoute un abonné qui recevra les segments audio convertis - pub fn add_subscriber(&mut self, tx: mpsc::Sender>) { - self.subscribers.add_subscriber(tx); - } - - /// Lance le traitement de conversion - pub async fn run(mut self) -> Result<(), AudioError> { - while let Some(segment) = self.rx.recv().await { - if !segment.is_audio_chunk() { - self.subscribers.push(segment).await?; - continue; + #[async_trait::async_trait] + impl AudioPipelineNode for $node_name { + fn get_tx(&self) -> Option>> { + Some(self.tx.clone()) } - let converted_segment = if let Some(chunk) = segment.as_chunk() { - let converted_chunk = chunk.to_i32(); - Arc::new(AudioSegment { - order: segment.order, - timestamp_sec: segment.timestamp_sec, - segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)), - }) - } else { - segment - }; - - self.subscribers.push(converted_segment).await?; - } - - Ok(()) - } -} - -impl TypedAudioNode for ToI32Node { - fn input_type(&self) -> Option { - Some(TypeRequirement::any()) - } - - fn output_type(&self) -> Option { - Some(TypeRequirement::specific(SampleType::I32)) - } -} - -impl Default for ToI32Node { - fn default() -> Self { - Self::new().0 - } -} - -/// Node de conversion vers F32 -/// -/// Convertit n'importe quel type de chunk audio vers F32 (32-bit floating point). -/// Utilise les conversions DSP SIMD optimisées. -pub struct ToF32Node { - rx: mpsc::Receiver>, - subscribers: MultiSubscriberNode, -} - -impl ToF32Node { - /// Crée un nouveau node de conversion vers F32 - pub fn new() -> (Self, mpsc::Sender>) { - Self::with_channel_size(16) - } - - /// Crée un nouveau node avec une taille de buffer spécifique - pub fn with_channel_size(channel_size: usize) -> (Self, mpsc::Sender>) { - let (tx, rx) = mpsc::channel(channel_size); - let node = Self { - rx, - subscribers: MultiSubscriberNode::new(), - }; - (node, tx) - } - - /// Ajoute un abonné qui recevra les segments audio convertis - pub fn add_subscriber(&mut self, tx: mpsc::Sender>) { - self.subscribers.add_subscriber(tx); - } - - /// Lance le traitement de conversion - pub async fn run(mut self) -> Result<(), AudioError> { - while let Some(segment) = self.rx.recv().await { - if !segment.is_audio_chunk() { - self.subscribers.push(segment).await?; - continue; + fn register(&mut self, child: Box) { + if let Some(tx) = child.get_tx() { + self.child_txs.push(tx); + } + self.children.push(child); } - let converted_segment = if let Some(chunk) = segment.as_chunk() { - let converted_chunk = chunk.to_f32(); - Arc::new(AudioSegment { - order: segment.order, - timestamp_sec: segment.timestamp_sec, - segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)), - }) - } else { - segment - }; + async fn run( + mut self: Box, + stop_token: CancellationToken, + ) -> Result<(), AudioError> { + // Spawner tous les enfants + let mut child_handles = Vec::new(); + for child in self.children { + let child_token = stop_token.child_token(); + let handle = tokio::spawn(async move { child.run(child_token).await }); + child_handles.push(handle); + } - self.subscribers.push(converted_segment).await?; + // Boucle de traitement + loop { + let segment = tokio::select! { + result = self.rx.recv() => { + match result { + Some(seg) => seg, + None => break, + } + } + _ = stop_token.cancelled() => { + break; + } + }; + + // Convertir si c'est un chunk audio, sinon passer tel quel + let output_segment = if segment.is_audio_chunk() { + if let Some(chunk) = segment.as_chunk() { + let converted_chunk = chunk.$convert_method(); + Arc::new(AudioSegment { + order: segment.order, + timestamp_sec: segment.timestamp_sec, + segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)), + }) + } else { + segment + } + } else { + segment + }; + + // Envoyer à tous les enfants + for tx in &self.child_txs { + if tx.send(output_segment.clone()).await.is_err() { + // Un enfant est mort, arrêter + break; + } + } + } + + // Attendre que tous les enfants se terminent + for handle in child_handles { + match handle.await { + Ok(Ok(())) => {} + Ok(Err(e)) => return Err(e), + Err(e) => { + return Err(AudioError::ProcessingError(format!( + "Child task panicked: {}", + e + ))) + } + } + } + + Ok(()) + } } - Ok(()) - } -} - -impl TypedAudioNode for ToF32Node { - fn input_type(&self) -> Option { - Some(TypeRequirement::any()) - } - - fn output_type(&self) -> Option { - Some(TypeRequirement::specific(SampleType::F32)) - } -} - -impl Default for ToF32Node { - fn default() -> Self { - Self::new().0 - } -} - -/// Node de conversion vers F64 -/// -/// Convertit n'importe quel type de chunk audio vers F64 (64-bit floating point). -/// Utilise les conversions DSP SIMD optimisées. -pub struct ToF64Node { - rx: mpsc::Receiver>, - subscribers: MultiSubscriberNode, -} - -impl ToF64Node { - /// Crée un nouveau node de conversion vers F64 - pub fn new() -> (Self, mpsc::Sender>) { - Self::with_channel_size(16) - } - - /// Crée un nouveau node avec une taille de buffer spécifique - pub fn with_channel_size(channel_size: usize) -> (Self, mpsc::Sender>) { - let (tx, rx) = mpsc::channel(channel_size); - let node = Self { - rx, - subscribers: MultiSubscriberNode::new(), - }; - (node, tx) - } - - /// Ajoute un abonné qui recevra les segments audio convertis - pub fn add_subscriber(&mut self, tx: mpsc::Sender>) { - self.subscribers.add_subscriber(tx); - } - - /// Lance le traitement de conversion - pub async fn run(mut self) -> Result<(), AudioError> { - while let Some(segment) = self.rx.recv().await { - if !segment.is_audio_chunk() { - self.subscribers.push(segment).await?; - continue; + impl TypedAudioNode for $node_name { + fn input_type(&self) -> Option { + Some(TypeRequirement::any()) } - let converted_segment = if let Some(chunk) = segment.as_chunk() { - let converted_chunk = chunk.to_f64(); - Arc::new(AudioSegment { - order: segment.order, - timestamp_sec: segment.timestamp_sec, - segment: crate::_AudioSegment::Chunk(Arc::new(converted_chunk)), - }) - } else { - segment - }; - - self.subscribers.push(converted_segment).await?; + fn output_type(&self) -> Option { + Some(TypeRequirement::specific($output_type)) + } } - Ok(()) - } + impl Default for $node_name { + fn default() -> Self { + Self::new() + } + } + }; } -impl TypedAudioNode for ToF64Node { - fn input_type(&self) -> Option { - Some(TypeRequirement::any()) - } - - fn output_type(&self) -> Option { - Some(TypeRequirement::specific(SampleType::F64)) - } -} - -impl Default for ToF64Node { - fn default() -> Self { - Self::new().0 - } -} +// Générer les 5 converter nodes +converter_node!( + ToI16Node, + to_i16, + SampleType::I16, + "Node de conversion vers I16 (16-bit signed integer)" +); +converter_node!( + ToI24Node, + to_i24, + SampleType::I24, + "Node de conversion vers I24 (24-bit signed integer)" +); +converter_node!( + ToI32Node, + to_i32, + SampleType::I32, + "Node de conversion vers I32 (32-bit signed integer)" +); +converter_node!( + ToF32Node, + to_f32, + SampleType::F32, + "Node de conversion vers F32 (32-bit floating point)" +); +converter_node!( + ToF64Node, + to_f64, + SampleType::F64, + "Node de conversion vers F64 (64-bit floating point)" +); #[cfg(test)] mod tests { @@ -386,7 +184,7 @@ mod tests { #[tokio::test] async fn test_to_f32_node_type_requirements() { - let (node, _tx) = ToF32Node::new(); + let node = ToF32Node::new(); // Vérifier les types d'entrée/sortie assert_eq!( @@ -405,14 +203,60 @@ mod tests { ); } + // Nœud de test simple qui collecte les segments + struct TestCollectorNode { + input_tx: mpsc::Sender>, + input_rx: mpsc::Receiver>, + output_tx: mpsc::Sender>, + } + + impl TestCollectorNode { + fn new(output_tx: mpsc::Sender>) -> Self { + let (input_tx, input_rx) = mpsc::channel(16); + Self { + input_tx, + input_rx, + output_tx, + } + } + } + + #[async_trait::async_trait] + impl AudioPipelineNode for TestCollectorNode { + fn get_tx(&self) -> Option>> { + Some(self.input_tx.clone()) + } + + fn register(&mut self, _child: Box) { + panic!("TestCollectorNode is a sink"); + } + + async fn run( + mut self: Box, + _stop_token: CancellationToken, + ) -> Result<(), AudioError> { + while let Some(segment) = self.input_rx.recv().await { + if self.output_tx.send(segment).await.is_err() { + break; + } + } + Ok(()) + } + } + #[tokio::test] async fn test_to_i16_node_converts_from_i32() { - let (mut node, tx) = ToI16Node::new(); + let mut node = ToI16Node::new(); let (out_tx, mut out_rx) = mpsc::channel(16); - node.add_subscriber(out_tx); + let collector = TestCollectorNode::new(out_tx); + node.register(Box::new(collector)); + + // Récupérer le tx du node + let tx = node.get_tx().unwrap(); // Lancer le node dans une tâche - let handle = tokio::spawn(async move { node.run().await }); + let stop_token = CancellationToken::new(); + let handle = tokio::spawn(async move { Box::new(node).run(stop_token).await }); // Créer et envoyer un chunk I32 let stereo = vec![[1_000_000i32 << 16, -500_000i32 << 16]; 100]; @@ -451,12 +295,16 @@ mod tests { #[tokio::test] async fn test_syncmarkers_passthrough() { - let (mut node, tx) = ToI16Node::new(); + let mut node = ToI16Node::new(); let (out_tx, mut out_rx) = mpsc::channel(16); - node.add_subscriber(out_tx); + let collector = TestCollectorNode::new(out_tx); + node.register(Box::new(collector)); + + let tx = node.get_tx().unwrap(); + let stop_token = CancellationToken::new(); tokio::spawn(async move { - node.run().await.unwrap(); + Box::new(node).run(stop_token).await.unwrap(); }); // Envoyer un syncmarker diff --git a/pmoaudio/src/nodes/file_source.rs b/pmoaudio/src/nodes/file_source.rs index 510c5d0a..0f92e204 100644 --- a/pmoaudio/src/nodes/file_source.rs +++ b/pmoaudio/src/nodes/file_source.rs @@ -1,5 +1,6 @@ use crate::{ - nodes::{AudioError, MultiSubscriberNode, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS}, + nodes::{AudioError, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS}, + pipeline::AudioPipelineNode, type_constraints::TypeRequirement, AudioChunk, AudioChunkData, AudioSegment, I24, }; @@ -7,6 +8,8 @@ use pmoflac::{decode_audio_stream, AudioFileMetadata, StreamInfo}; use pmometadata::{MemoryTrackMetadata, TrackMetadata}; use std::{path::PathBuf, sync::Arc, time::Duration}; use tokio::{fs::File, io::AsyncReadExt, sync::mpsc}; +use tokio_util::sync::CancellationToken; +use tracing; /// FileSource - Lit un fichier audio et publie des `AudioSegment` /// @@ -21,7 +24,8 @@ use tokio::{fs::File, io::AsyncReadExt, sync::mpsc}; pub struct FileSource { path: PathBuf, chunk_frames: usize, - subscribers: MultiSubscriberNode, + child_txs: Vec>>, + children: Vec>, } impl FileSource { @@ -43,156 +47,10 @@ impl FileSource { Self { path: path.into(), chunk_frames, - subscribers: MultiSubscriberNode::new(), + child_txs: Vec::new(), + children: Vec::new(), } } - - /// Ajoute un abonné qui recevra les segments audio. - pub fn add_subscriber(&mut self, tx: mpsc::Sender>) { - self.subscribers.add_subscriber(tx); - } - - /// Lance la lecture du fichier et diffuse les segments audio. - pub async fn run(self) -> Result<(), AudioError> { - // Ouvrir le fichier - let file = File::open(&self.path).await.map_err(|e| { - AudioError::ProcessingError(format!("Failed to open {:?}: {}", self.path, e)) - })?; - - // Décoder le flux audio - let mut stream = decode_audio_stream(file) - .await - .map_err(|e| AudioError::ProcessingError(format!("Decode error: {}", e)))?; - let stream_info = stream.info().clone(); - - validate_stream(&stream_info)?; - - // Calculer la taille des chunks si non spécifiée (0 = auto) - let chunk_frames = if self.chunk_frames == 0 { - // Calculer pour obtenir DEFAULT_CHUNK_DURATION_MS millisecondes - let frames = - (stream_info.sample_rate as f64 * DEFAULT_CHUNK_DURATION_MS / 1000.0) as usize; - // Arrondir à la puissance de 2 la plus proche pour optimiser les buffers - frames.next_power_of_two().max(256) - } else { - self.chunk_frames.max(1) - }; - - // Émettre TopZeroSync - let top_zero = AudioSegment::new_top_zero_sync(); - self.subscribers.push(top_zero).await?; - - // Extraire et émettre les métadonnées du fichier - match AudioFileMetadata::from_file(&self.path) { - Ok(file_metadata) => { - let mut metadata = MemoryTrackMetadata::new(); - - // Convertir AudioFileMetadata vers MemoryTrackMetadata - if let Some(title) = file_metadata.title { - let _ = metadata.set_title(Some(title)).await; - } - if let Some(artist) = file_metadata.artist { - let _ = metadata.set_artist(Some(artist)).await; - } - if let Some(album) = file_metadata.album { - let _ = metadata.set_album(Some(album)).await; - } - if let Some(year) = file_metadata.year { - let _ = metadata.set_year(Some(year)).await; - } - if let Some(duration_secs) = file_metadata.duration_secs { - let _ = metadata - .set_duration(Some(Duration::from_secs(duration_secs))) - .await; - } - - // Émettre TrackBoundary - let track_boundary = AudioSegment::new_track_boundary(0, 0.0, Arc::new(tokio::sync::RwLock::new(metadata))); - self.subscribers.push(track_boundary).await?; - } - Err(e) => { - eprintln!( - "Warning: Failed to extract metadata from {:?}: {}", - self.path, e - ); - // Continuer sans métadonnées - } - } - - // Préparer la lecture des chunks audio - let frame_bytes = stream_info.bytes_per_sample() * stream_info.channels as usize; - let chunk_byte_len = chunk_frames * frame_bytes; - let mut pending = Vec::new(); - let mut read_buf = vec![0u8; frame_bytes * 512.max(chunk_frames)]; - let mut chunk_index = 0u64; - let mut total_frames = 0u64; - - // Lire et émettre les chunks audio - loop { - // Remplir le buffer - if pending.len() < chunk_byte_len { - let read = stream.read(&mut read_buf).await.map_err(|e| { - AudioError::ProcessingError(format!("I/O error while decoding: {}", e)) - })?; - if read == 0 { - break; - } - pending.extend_from_slice(&read_buf[..read]); - } - - if pending.is_empty() { - break; - } - - // Extraire un chunk - let frames_in_pending = pending.len() / frame_bytes; - let frames_to_emit = frames_in_pending.min(chunk_frames); - let take_bytes = frames_to_emit * frame_bytes; - let chunk_bytes = pending.drain(..take_bytes).collect::>(); - - // Calculer le timestamp - let timestamp_sec = total_frames as f64 / stream_info.sample_rate as f64; - - // Créer le segment audio - let segment = bytes_to_segment( - &chunk_bytes, - &stream_info, - frames_to_emit, - chunk_index, - timestamp_sec, - )?; - self.subscribers.push(segment).await?; - - chunk_index += 1; - total_frames += frames_to_emit as u64; - } - - // Traiter le reste éventuel (moins qu'un chunk complet) - if !pending.is_empty() { - let frames = pending.len() / frame_bytes; - if frames > 0 { - let timestamp_sec = total_frames as f64 / stream_info.sample_rate as f64; - let segment = - bytes_to_segment(&pending, &stream_info, frames, chunk_index, timestamp_sec)?; - self.subscribers.push(segment).await?; - total_frames += frames as u64; - chunk_index += 1; - } - } - - // Émettre EndOfStream - let final_timestamp = total_frames as f64 / stream_info.sample_rate as f64; - let eos = AudioSegment::new_end_of_stream(chunk_index, final_timestamp); - self.subscribers.push(eos).await?; - - // Attendre la fin du décodage - stream - .wait() - .await - .map_err(|e| AudioError::ProcessingError(format!("Decode task failed: {}", e)))?; - - Ok(()) - } } fn validate_stream(info: &StreamInfo) -> Result<(), AudioError> { @@ -330,6 +188,230 @@ fn bytes_to_segment( })) } +#[async_trait::async_trait] +impl AudioPipelineNode for FileSource { + fn get_tx(&self) -> Option>> { + // FileSource est une source, elle n'a pas d'input + None + } + + fn register(&mut self, child: Box) { + // Extraire le tx du child avant de le stocker + if let Some(tx) = child.get_tx() { + self.child_txs.push(tx.clone()); + } + self.children.push(child); + } + + async fn run( + self: Box, + stop_token: CancellationToken, + ) -> Result<(), AudioError> { + let Self { + path, + chunk_frames, + child_txs, + children, + } = *self; + + // 1. Spawner tous les enfants AVANT de commencer à lire + let mut child_handles = Vec::new(); + for child in children { + let child_token = stop_token.child_token(); + let handle = tokio::spawn(async move { + child.run(child_token).await + }); + child_handles.push(handle); + } + + // 2. Faire le travail de lecture du fichier + let work_result: Result<(), AudioError> = async { + // Macro helper pour envoyer à tous les enfants + macro_rules! send_to_children { + ($segment:expr) => { + for tx in &child_txs { + if tx.send($segment.clone()).await.is_err() { + tracing::warn!("FileSource: child died during send"); + return Err(AudioError::SendError); + } + } + }; + } + + // Ouvrir le fichier + let file = File::open(&path).await.map_err(|e| { + AudioError::ProcessingError(format!("Failed to open {:?}: {}", path, e)) + })?; + + // Décoder le flux audio + let mut stream = decode_audio_stream(file) + .await + .map_err(|e| AudioError::ProcessingError(format!("Decode error: {}", e)))?; + let stream_info = stream.info().clone(); + + validate_stream(&stream_info)?; + + // Calculer la taille des chunks si non spécifiée (0 = auto) + let chunk_frames = if chunk_frames == 0 { + let frames = + (stream_info.sample_rate as f64 * DEFAULT_CHUNK_DURATION_MS / 1000.0) as usize; + frames.next_power_of_two().max(256) + } else { + chunk_frames.max(1) + }; + + // Émettre TopZeroSync + let top_zero = AudioSegment::new_top_zero_sync(); + send_to_children!(top_zero); + + // Extraire et émettre les métadonnées du fichier + if let Ok(file_metadata) = AudioFileMetadata::from_file(&path) { + let mut metadata = MemoryTrackMetadata::new(); + if let Some(title) = file_metadata.title { + let _ = metadata.set_title(Some(title)).await; + } + if let Some(artist) = file_metadata.artist { + let _ = metadata.set_artist(Some(artist)).await; + } + if let Some(album) = file_metadata.album { + let _ = metadata.set_album(Some(album)).await; + } + if let Some(year) = file_metadata.year { + let _ = metadata.set_year(Some(year)).await; + } + if let Some(duration_secs) = file_metadata.duration_secs { + let _ = metadata + .set_duration(Some(Duration::from_secs(duration_secs))) + .await; + } + + let track_boundary = AudioSegment::new_track_boundary( + 0, + 0.0, + Arc::new(tokio::sync::RwLock::new(metadata)), + ); + send_to_children!(track_boundary); + } + + // Préparer la lecture des chunks audio + let frame_bytes = stream_info.bytes_per_sample() * stream_info.channels as usize; + let chunk_byte_len = chunk_frames * frame_bytes; + let mut pending = Vec::new(); + let mut read_buf = vec![0u8; frame_bytes * 512.max(chunk_frames)]; + let mut chunk_index = 0u64; + let mut total_frames = 0u64; + + // Lire et émettre les chunks audio + loop { + tokio::select! { + _ = stop_token.cancelled() => { + tracing::info!("FileSource: stop requested"); + break; + } + + read_result = stream.read(&mut read_buf) => { + // Remplir le buffer + if pending.len() < chunk_byte_len { + let read = read_result.map_err(|e| { + AudioError::ProcessingError(format!("I/O error while decoding: {}", e)) + })?; + if read == 0 && pending.is_empty() { + break; + } + if read > 0 { + pending.extend_from_slice(&read_buf[..read]); + } + } + + if pending.is_empty() { + break; + } + + // Extraire un chunk + let frames_in_pending = pending.len() / frame_bytes; + let frames_to_emit = frames_in_pending.min(chunk_frames); + if frames_to_emit == 0 { + break; + } + let take_bytes = frames_to_emit * frame_bytes; + let chunk_bytes = pending.drain(..take_bytes).collect::>(); + + // Calculer le timestamp + let timestamp_sec = total_frames as f64 / stream_info.sample_rate as f64; + + // Créer et envoyer le segment audio + let segment = bytes_to_segment( + &chunk_bytes, + &stream_info, + frames_to_emit, + chunk_index, + timestamp_sec, + )?; + send_to_children!(segment); + + chunk_index += 1; + total_frames += frames_to_emit as u64; + } + } + } + + // Traiter le reste éventuel (moins qu'un chunk complet) + if !pending.is_empty() { + let frames = pending.len() / frame_bytes; + if frames > 0 { + let timestamp_sec = total_frames as f64 / stream_info.sample_rate as f64; + let segment = + bytes_to_segment(&pending, &stream_info, frames, chunk_index, timestamp_sec)?; + send_to_children!(segment); + total_frames += frames as u64; + chunk_index += 1; + } + } + + // Émettre EndOfStream + let final_timestamp = total_frames as f64 / stream_info.sample_rate as f64; + let eos = AudioSegment::new_end_of_stream(chunk_index, final_timestamp); + send_to_children!(eos); + + // Attendre la fin du décodage + stream + .wait() + .await + .map_err(|e| AudioError::ProcessingError(format!("Decode task failed: {}", e)))?; + + Ok(()) + }.await; + + // 3. Arrêter les enfants qui tournent encore (descendant uniquement) + stop_token.cancel(); + + // 4. Fermer les channels pour signaler EOF + drop(child_txs); + + // 5. Attendre que TOUS les enfants se terminent + for handle in child_handles { + match handle.await { + Ok(Ok(())) => { + // Enfant terminé normalement + } + Ok(Err(e)) => { + // Enfant en erreur → propager + tracing::error!("FileSource: child error: {}", e); + return Err(e); + } + Err(e) => { + // Panic dans l'enfant + tracing::error!("FileSource: child panic: {}", e); + return Err(AudioError::ProcessingError(format!("Child panic: {}", e))); + } + } + } + + // 6. Retourner notre propre résultat (montant vers le parent) + work_result + } +} + impl TypedAudioNode for FileSource { fn input_type(&self) -> Option { // FileSource est une source, elle ne consomme pas d'audio @@ -384,12 +466,68 @@ mod tests { file.flush().await.expect("flush file"); flac_stream.wait().await.unwrap(); + // Créer un collecteur simple qui transmet les segments à un channel de test + struct TestCollectorNode { + tx: mpsc::Sender>, + rx: mpsc::Receiver>, + test_tx: mpsc::Sender>, + } + + impl TestCollectorNode { + fn new(test_tx: mpsc::Sender>) -> Self { + let (tx, rx) = mpsc::channel(16); + Self { + tx, + rx, + test_tx, + } + } + } + + #[async_trait::async_trait] + impl AudioPipelineNode for TestCollectorNode { + fn get_tx(&self) -> Option>> { + Some(self.tx.clone()) + } + + fn register(&mut self, _child: Box) { + panic!("TestCollectorNode is a terminal node"); + } + + async fn run( + mut self: Box, + stop_token: CancellationToken, + ) -> Result<(), AudioError> { + // Transférer tous les segments au test + loop { + tokio::select! { + _ = stop_token.cancelled() => { + break; + } + segment = self.rx.recv() => { + match segment { + Some(seg) => { + if self.test_tx.send(seg).await.is_err() { + break; + } + } + None => break, + } + } + } + } + Ok(()) + } + } + + let (test_tx, mut rx) = mpsc::channel(1024); let mut source = FileSource::with_chunk_size(&flac_path, 64); - let (tx, mut rx) = mpsc::channel(16); - source.add_subscriber(tx); + let collector = TestCollectorNode::new(test_tx); + source.register(Box::new(collector)); tokio::spawn(async move { - source.run().await.unwrap(); + let token = CancellationToken::new(); + Box::new(source).run(token).await.unwrap(); }); let mut received_frames = 0usize; diff --git a/pmoaudio/src/nodes/flac_file_sink.rs b/pmoaudio/src/nodes/flac_file_sink.rs index 5068911f..32a5a0ee 100644 --- a/pmoaudio/src/nodes/flac_file_sink.rs +++ b/pmoaudio/src/nodes/flac_file_sink.rs @@ -1,7 +1,7 @@ use crate::{ nodes::{AudioError, TypedAudioNode, DEFAULT_CHANNEL_SIZE}, type_constraints::TypeRequirement, - AudioChunk, AudioSegment, SyncMarker, + AudioChunk, AudioPipelineNode, AudioSegment, SyncMarker, }; use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat}; use std::{ @@ -16,6 +16,7 @@ use tokio::{ io::{self, AsyncRead, AsyncWriteExt, ReadBuf}, sync::mpsc, }; +use tokio_util::sync::CancellationToken; /// Sink qui encode les `AudioSegment` reçus au format FLAC. /// @@ -25,6 +26,7 @@ use tokio::{ /// - Adapte automatiquement l'encodage FLAC selon la profondeur de bit du chunk (8/16/24/32-bit) /// - Termine l'encodage proprement quand il reçoit EndOfStream pub struct FlacFileSink { + tx: mpsc::Sender>, rx: mpsc::Receiver>, base_path: PathBuf, encoder_options: EncoderOptions, @@ -38,7 +40,7 @@ impl FlacFileSink { /// /// * `base_path` - Chemin de base pour les fichiers FLAC. Si des TrackBoundary sont reçus, /// des fichiers seront créés avec des suffixes (_01, _02, etc.) - pub fn new>(base_path: P) -> (Self, mpsc::Sender>) { + pub fn new>(base_path: P) -> Self { Self::with_channel_size(base_path, DEFAULT_CHANNEL_SIZE) } @@ -51,7 +53,7 @@ impl FlacFileSink { pub fn with_channel_size>( base_path: P, channel_size: usize, - ) -> (Self, mpsc::Sender>) { + ) -> Self { Self::with_config(base_path, channel_size, EncoderOptions::default()) } @@ -66,15 +68,15 @@ impl FlacFileSink { base_path: P, channel_size: usize, encoder_options: EncoderOptions, - ) -> (Self, mpsc::Sender>) { + ) -> Self { let (tx, rx) = mpsc::channel(channel_size); - let sink = Self { + Self { + tx, rx, base_path: base_path.into(), encoder_options, pcm_buffer_capacity: 8, - }; - (sink, tx) + } } /// Lance l'encodage vers le(s) fichier(s) cible(s). @@ -84,27 +86,28 @@ impl FlacFileSink { /// - Track 0 : base_path.flac /// - Track 1 : base_path_01.flac /// - Track 2 : base_path_02.flac, etc. - pub async fn run(self) -> Result { - let FlacFileSink { - mut rx, - base_path, - encoder_options, - pcm_buffer_capacity, - } = self; + async fn run_internal( + mut rx: mpsc::Receiver>, + base_path: PathBuf, + encoder_options: EncoderOptions, + pcm_buffer_capacity: usize, + stop_token: CancellationToken, + ) -> Result<(), AudioError> { - let mut all_tracks = Vec::new(); let mut track_number = 0; loop { + // Vérifier si l'arrêt a été demandé + if stop_token.is_cancelled() { + return Ok(()); + } + // Attendre le premier chunk audio pour cette track, en capturant les métadonnées du TrackBoundary - let (first_segment, track_metadata) = match wait_for_first_audio_chunk_with_metadata(&mut rx).await { + let (first_segment, track_metadata) = match wait_for_first_audio_chunk_with_metadata(&mut rx, &stop_token).await { Ok(result) => result, Err(_) => { - // Plus d'audio disponible - if all_tracks.is_empty() { - return Err(AudioError::ProcessingError("No audio data received".into())); - } - break; + // Plus d'audio disponible ou arrêt demandé + return Ok(()); } }; @@ -149,7 +152,7 @@ impl FlacFileSink { // Exécuter pump et copy en parallèle avec tokio::select! en boucle let pump_future = - pump_track_segments(first_segment, &mut rx, pcm_tx, bits_per_sample, sample_rate); + pump_track_segments(first_segment, &mut rx, pcm_tx, bits_per_sample, sample_rate, &stop_token); let copy_future = async { let copy_result = tokio::io::copy(&mut flac_stream, &mut output).await; let flush_result = output.flush().await; @@ -168,16 +171,7 @@ impl FlacFileSink { // Attendre les deux tâches en parallèle let (copy_result, pump_result) = tokio::join!(copy_future, pump_future); copy_result?; - let (chunks, samples, duration_sec, stop_reason) = pump_result?; - - // Ajouter les stats de cette track - all_tracks.push(TrackStats { - path: track_path, - track_number, - chunks_received: chunks, - total_samples: samples, - total_duration_sec: duration_sec, - }); + let stop_reason = pump_result?; // Vérifier le stop_reason pour savoir si on continue match stop_reason { @@ -186,14 +180,12 @@ impl FlacFileSink { track_number += 1; continue; } - StopReason::EndOfStream | StopReason::ChannelClosed => { + StopReason::EndOfStream | StopReason::ChannelClosed | StopReason::Cancelled => { // Fin de l'encodage - break; + return Ok(()); } } } - - Ok(FlacFileSinkStats { tracks: all_tracks }) } } @@ -223,20 +215,26 @@ enum StopReason { TrackBoundary(Arc>), EndOfStream, ChannelClosed, + Cancelled, } /// Attend et retourne le premier chunk audio avec les métadonnées du TrackBoundary si présent. -/// Retourne une erreur si EndOfStream est reçu avant tout audio. +/// Retourne une erreur si EndOfStream est reçu avant tout audio ou si l'arrêt est demandé. async fn wait_for_first_audio_chunk_with_metadata( rx: &mut mpsc::Receiver>, + stop_token: &CancellationToken, ) -> Result<(Arc, Option>>), AudioError> { let mut track_metadata: Option>> = None; loop { - let segment = rx - .recv() - .await - .ok_or_else(|| AudioError::ProcessingError("No audio data received".into()))?; + let segment = tokio::select! { + result = rx.recv() => { + result.ok_or_else(|| AudioError::ProcessingError("No audio data received".into()))? + } + _ = stop_token.cancelled() => { + return Err(AudioError::ProcessingError("Cancelled".into())); + } + }; match &segment.segment { crate::_AudioSegment::Chunk(chunk) => { @@ -274,11 +272,8 @@ async fn pump_track_segments( pcm_tx: mpsc::Sender>, bits_per_sample: u8, expected_rate: u32, -) -> Result<(u64, u64, f64, StopReason), AudioError> { - let mut chunks = 0u64; - let mut samples = 0u64; - let mut duration_sec = 0.0f64; - + stop_token: &CancellationToken, +) -> Result { // Traiter le premier segment if let Some(chunk) = first_segment.as_chunk() { let pcm_bytes = chunk_to_pcm_bytes(chunk, bits_per_sample)?; @@ -287,19 +282,24 @@ async fn pump_track_segments( .send(pcm_bytes) .await .map_err(|_| AudioError::SendError)?; - chunks += 1; - samples += chunk.len() as u64; - duration_sec += chunk.len() as f64 / expected_rate as f64; } } // Boucle sur les segments suivants loop { - let segment = match rx.recv().await { - Some(seg) => seg, - None => { - drop(pcm_tx); // Fermer le channel PCM - return Ok((chunks, samples, duration_sec, StopReason::ChannelClosed)); + let segment = tokio::select! { + result = rx.recv() => { + match result { + Some(seg) => seg, + None => { + drop(pcm_tx); + return Ok(StopReason::ChannelClosed); + } + } + } + _ = stop_token.cancelled() => { + drop(pcm_tx); + return Ok(StopReason::Cancelled); } }; @@ -323,25 +323,16 @@ async fn pump_track_segments( .send(pcm_bytes) .await .map_err(|_| AudioError::SendError)?; - - chunks += 1; - samples += chunk.len() as u64; - duration_sec += chunk.len() as f64 / expected_rate as f64; } crate::_AudioSegment::Sync(marker) => { match &**marker { SyncMarker::TrackBoundary { metadata, .. } => { drop(pcm_tx); // Fermer le channel PCM - return Ok(( - chunks, - samples, - duration_sec, - StopReason::TrackBoundary(metadata.clone()), - )); + return Ok(StopReason::TrackBoundary(metadata.clone())); } SyncMarker::EndOfStream => { drop(pcm_tx); // Fermer le channel PCM - return Ok((chunks, samples, duration_sec, StopReason::EndOfStream)); + return Ok(StopReason::EndOfStream); } _ => {} // Ignorer les autres syncmarkers } @@ -535,6 +526,32 @@ pub struct FlacFileSinkStats { pub tracks: Vec, } +#[async_trait::async_trait] +impl AudioPipelineNode for FlacFileSink { + fn get_tx(&self) -> Option>> { + Some(self.tx.clone()) + } + + fn register(&mut self, _child: Box) { + panic!("FlacFileSink is a terminal node and cannot have children"); + } + + async fn run( + self: Box, + stop_token: CancellationToken, + ) -> Result<(), AudioError> { + let FlacFileSink { + tx: _tx, + rx, + base_path, + encoder_options, + pcm_buffer_capacity, + } = *self; + + Self::run_internal(rx, base_path, encoder_options, pcm_buffer_capacity, stop_token).await + } +} + impl TypedAudioNode for FlacFileSink { fn input_type(&self) -> Option { // FlacFileSink accepte n'importe quel type entier (I16, I24, I32) @@ -565,8 +582,12 @@ mod tests { let frames = 256; // Créer le sink - let (sink, tx) = FlacFileSink::with_channel_size(&output_path, 16); - let sink_handle = tokio::spawn(async move { sink.run().await.unwrap() }); + let sink = FlacFileSink::with_channel_size(&output_path, 16); + let tx = sink.get_tx().unwrap(); + let stop_token = CancellationToken::new(); + let sink_handle = tokio::spawn(async move { + Box::new(sink).run(stop_token).await.unwrap() + }); // Envoyer des segments avec métadonnées tokio::spawn(async move { @@ -682,8 +703,12 @@ mod tests { flac_stream.wait().await.unwrap(); // Maintenant utiliser FlacFileSink pour réécrire le fichier - let (sink, tx) = FlacFileSink::with_channel_size(&output_path, 16); - let sink_handle = tokio::spawn(async move { sink.run().await.unwrap() }); + let sink = FlacFileSink::with_channel_size(&output_path, 16); + let tx = sink.get_tx().unwrap(); + let stop_token = CancellationToken::new(); + let sink_handle = tokio::spawn(async move { + Box::new(sink).run(stop_token).await.unwrap() + }); // Lire le fichier input et envoyer les segments au sink tokio::spawn(async move { @@ -745,10 +770,7 @@ mod tests { decode_stream.wait().await.unwrap(); }); - let stats = sink_handle.await.unwrap(); - assert_eq!(stats.tracks.len(), 1); - assert!(stats.tracks[0].chunks_received > 0); - assert_eq!(stats.tracks[0].total_samples, frames as u64); + sink_handle.await.unwrap(); // Vérifier que le fichier de sortie est valide let file = File::open(&output_path).await.unwrap(); diff --git a/pmoaudio/src/nodes/http_source.rs b/pmoaudio/src/nodes/http_source.rs index 38bb8c4c..12c7f3c9 100644 --- a/pmoaudio/src/nodes/http_source.rs +++ b/pmoaudio/src/nodes/http_source.rs @@ -1,14 +1,14 @@ use crate::{ - nodes::{AudioError, MultiSubscriberNode, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS}, + nodes::{AudioError, TypedAudioNode, DEFAULT_CHUNK_DURATION_MS}, type_constraints::TypeRequirement, - AudioChunk, AudioChunkData, AudioSegment, I24, + AudioChunk, AudioChunkData, AudioPipelineNode, AudioSegment, I24, }; use futures_util::StreamExt; use pmoflac::{decode_audio_stream, StreamInfo}; use pmometadata::{MemoryTrackMetadata, TrackMetadata}; use std::sync::Arc; use tokio::sync::mpsc; -use tokio_util::io::StreamReader; +use tokio_util::{io::StreamReader, sync::CancellationToken}; /// HttpSource - Récupère un fichier audio via HTTP et publie des `AudioSegment` /// @@ -93,7 +93,8 @@ use tokio_util::io::StreamReader; pub struct HttpSource { url: String, chunk_frames: usize, - subscribers: MultiSubscriberNode, + child_txs: Vec>>, + children: Vec>, } impl HttpSource { @@ -136,85 +137,22 @@ impl HttpSource { Self { url: url.into(), chunk_frames, - subscribers: MultiSubscriberNode::new(), + child_txs: Vec::new(), + children: Vec::new(), } } - /// Ajoute un abonné qui recevra les segments audio. - /// - /// Chaque abonné recevra une copie (via `Arc`) de tous les segments audio - /// produits par cette source, y compris les syncmarkers. - /// - /// # Arguments - /// - /// * `tx` - Channel sender pour recevoir les `AudioSegment` - /// - /// # Exemples - /// - /// ```no_run - /// use pmoaudio::HttpSource; - /// use tokio::sync::mpsc; - /// - /// let mut source = HttpSource::new("http://example.com/audio.flac"); - /// let (tx, rx) = mpsc::channel(16); - /// source.add_subscriber(tx); - /// ``` - pub fn add_subscriber(&mut self, tx: mpsc::Sender>) { - self.subscribers.add_subscriber(tx); - } - - /// Lance le téléchargement et la lecture du flux audio. - /// - /// Cette méthode consomme `self` et exécute le pipeline complet: - /// 1. Effectue la requête HTTP GET vers l'URL spécifiée - /// 2. Vérifie le status HTTP (doit être 2xx) - /// 3. Extrait les métadonnées des headers HTTP - /// 4. Décode le flux audio en streaming - /// 5. Émet les syncmarkers et chunks audio vers les abonnés - /// - /// La méthode se termine quand le flux est complètement lu ou en cas d'erreur. - /// - /// # Erreurs - /// - /// Retourne `AudioError::ProcessingError` si: - /// - La requête HTTP échoue (réseau, DNS, etc.) - /// - Le serveur retourne un status code non-2xx - /// - Le format audio n'est pas supporté - /// - Le décodage échoue - /// - Le fichier a un nombre de canaux non supporté (doit être 1 ou 2) - /// - La profondeur de bit n'est pas supportée (doit être 8, 16, 24 ou 32 bits) - /// - /// # Exemples - /// - /// ```no_run - /// use pmoaudio::HttpSource; - /// use tokio::sync::mpsc; - /// - /// #[tokio::main] - /// async fn main() -> Result<(), Box> { - /// let mut source = HttpSource::new("http://example.com/audio.flac"); - /// let (tx, mut rx) = mpsc::channel(16); - /// source.add_subscriber(tx); - /// - /// let handle = tokio::spawn(async move { - /// source.run().await - /// }); - /// - /// // Traiter les segments - /// while let Some(segment) = rx.recv().await { - /// println!("Segment reçu: order={}", segment.order); - /// } - /// - /// handle.await??; - /// Ok(()) - /// } - /// ``` - pub async fn run(self) -> Result<(), AudioError> { + async fn run_internal( + url: String, + chunk_frames: usize, + child_txs: Vec>>, + stop_token: CancellationToken, + ) -> Result<(), AudioError> { // Effectuer la requête HTTP - let response = reqwest::get(&self.url) + let response = reqwest::get(&url) .await .map_err(|e| { - AudioError::ProcessingError(format!("HTTP request failed for {}: {}", self.url, e)) + AudioError::ProcessingError(format!("HTTP request failed for {}: {}", url, e)) })?; // Vérifier le status @@ -222,12 +160,12 @@ impl HttpSource { return Err(AudioError::ProcessingError(format!( "HTTP request returned status {}: {}", response.status(), - self.url + url ))); } // Extraire les métadonnées depuis les headers HTTP - let metadata = extract_metadata_from_headers(&response, &self.url).await; + let metadata = extract_metadata_from_headers(&response, &url).await; // Convertir le stream de bytes en AsyncRead let bytes_stream = response.bytes_stream(); @@ -244,38 +182,54 @@ impl HttpSource { validate_stream(&stream_info)?; // Calculer la taille des chunks si non spécifiée (0 = auto) - let chunk_frames = if self.chunk_frames == 0 { + let chunk_frames_final = if chunk_frames == 0 { let frames = (stream_info.sample_rate as f64 * DEFAULT_CHUNK_DURATION_MS / 1000.0) as usize; frames.next_power_of_two().max(256) } else { - self.chunk_frames.max(1) + chunk_frames.max(1) }; // Émettre TopZeroSync let top_zero = AudioSegment::new_top_zero_sync(); - self.subscribers.push(top_zero).await?; + for tx in &child_txs { + tx.send(top_zero.clone()).await.map_err(|_| AudioError::SendError)?; + } // Émettre TrackBoundary avec les métadonnées HTTP let track_boundary = AudioSegment::new_track_boundary(0, 0.0, Arc::new(tokio::sync::RwLock::new(metadata))); - self.subscribers.push(track_boundary).await?; + for tx in &child_txs { + tx.send(track_boundary.clone()).await.map_err(|_| AudioError::SendError)?; + } // Préparer la lecture des chunks audio let frame_bytes = stream_info.bytes_per_sample() * stream_info.channels as usize; - let chunk_byte_len = chunk_frames * frame_bytes; + let chunk_byte_len = chunk_frames_final * frame_bytes; let mut pending = Vec::new(); - let mut read_buf = vec![0u8; frame_bytes * 512.max(chunk_frames)]; + let mut read_buf = vec![0u8; frame_bytes * 512.max(chunk_frames_final)]; let mut chunk_index = 0u64; let mut total_frames = 0u64; // Lire et émettre les chunks audio loop { + // Vérifier l'arrêt + if stop_token.is_cancelled() { + return Ok(()); + } + // Remplir le buffer if pending.len() < chunk_byte_len { use tokio::io::AsyncReadExt; - let read = stream.read(&mut read_buf).await.map_err(|e| { - AudioError::ProcessingError(format!("I/O error while decoding: {}", e)) - })?; + let read = tokio::select! { + result = stream.read(&mut read_buf) => { + result.map_err(|e| { + AudioError::ProcessingError(format!("I/O error while decoding: {}", e)) + })? + } + _ = stop_token.cancelled() => { + return Ok(()); + } + }; if read == 0 { break; } @@ -288,7 +242,7 @@ impl HttpSource { // Extraire un chunk let frames_in_pending = pending.len() / frame_bytes; - let frames_to_emit = frames_in_pending.min(chunk_frames); + let frames_to_emit = frames_in_pending.min(chunk_frames_final); let take_bytes = frames_to_emit * frame_bytes; let chunk_bytes = pending.drain(..take_bytes).collect::>(); @@ -303,7 +257,13 @@ impl HttpSource { chunk_index, timestamp_sec, )?; - self.subscribers.push(segment).await?; + + for tx in &child_txs { + if tx.send(segment.clone()).await.is_err() { + // Un enfant est mort, arrêter + return Ok(()); + } + } chunk_index += 1; total_frames += frames_to_emit as u64; @@ -316,7 +276,9 @@ impl HttpSource { let timestamp_sec = total_frames as f64 / stream_info.sample_rate as f64; let segment = bytes_to_segment(&pending, &stream_info, frames, chunk_index, timestamp_sec)?; - self.subscribers.push(segment).await?; + for tx in &child_txs { + let _ = tx.send(segment.clone()).await; + } total_frames += frames as u64; chunk_index += 1; } @@ -325,7 +287,9 @@ impl HttpSource { // Émettre EndOfStream let final_timestamp = total_frames as f64 / stream_info.sample_rate as f64; let eos = AudioSegment::new_end_of_stream(chunk_index, final_timestamp); - self.subscribers.push(eos).await?; + for tx in &child_txs { + let _ = tx.send(eos.clone()).await; + } // Attendre la fin du décodage stream @@ -510,6 +474,61 @@ fn bytes_to_segment( })) } +#[async_trait::async_trait] +impl AudioPipelineNode for HttpSource { + fn get_tx(&self) -> Option>> { + None // HttpSource est une source, pas d'input + } + + fn register(&mut self, child: Box) { + if let Some(tx) = child.get_tx() { + self.child_txs.push(tx); + } + self.children.push(child); + } + + async fn run( + self: Box, + stop_token: CancellationToken, + ) -> Result<(), AudioError> { + let HttpSource { + url, + chunk_frames, + child_txs, + children, + } = *self; + + // Spawner tous les enfants + let mut child_handles = Vec::new(); + for child in children { + let child_token = stop_token.child_token(); + let handle = tokio::spawn(async move { + child.run(child_token).await + }); + child_handles.push(handle); + } + + // Lancer la logique interne + let work_result = Self::run_internal(url, chunk_frames, child_txs, stop_token.clone()).await; + + // Attendre que tous les enfants se terminent + for handle in child_handles { + match handle.await { + Ok(Ok(())) => {}, + Ok(Err(e)) => return Err(e), + Err(e) => { + return Err(AudioError::ProcessingError(format!( + "Child task panicked: {}", + e + ))) + } + } + } + + work_result + } +} + impl TypedAudioNode for HttpSource { fn input_type(&self) -> Option { // HttpSource est une source, elle ne consomme pas d'audio @@ -534,6 +553,47 @@ mod tests { Mock, MockServer, ResponseTemplate, }; + /// Nœud de test qui collecte tous les segments et les envoie à un channel de test + struct TestCollectorNode { + input_tx: mpsc::Sender>, + input_rx: mpsc::Receiver>, + output_tx: mpsc::Sender>, + } + + impl TestCollectorNode { + fn new(output_tx: mpsc::Sender>) -> Self { + let (input_tx, input_rx) = mpsc::channel(16); + Self { + input_tx, + input_rx, + output_tx, + } + } + } + + #[async_trait::async_trait] + impl AudioPipelineNode for TestCollectorNode { + fn get_tx(&self) -> Option>> { + Some(self.input_tx.clone()) + } + + fn register(&mut self, _child: Box) { + panic!("TestCollectorNode is a sink and cannot have children"); + } + + async fn run( + mut self: Box, + _stop_token: CancellationToken, + ) -> Result<(), AudioError> { + while let Some(segment) = self.input_rx.recv().await { + if self.output_tx.send(segment).await.is_err() { + break; + } + } + Ok(()) + } + } + /// Test de création basique de HttpSource #[test] fn test_http_source_creation() { @@ -599,12 +659,18 @@ mod tests { // Créer la source HTTP pointant vers le mock let url = format!("{}/test.flac", mock_server.uri()); let mut source = HttpSource::with_chunk_size(&url, 64); - let (tx, mut rx) = mpsc::channel(16); - source.add_subscriber(tx); - // Lancer le téléchargement et le décodage + // Créer un noeud collecteur pour recevoir les segments + let (tx, mut rx) = mpsc::channel(16); + let collector = TestCollectorNode::new(tx); + + // Construire le pipeline + source.register(Box::new(collector)); + + // Lancer le pipeline + let stop_token = CancellationToken::new(); tokio::spawn(async move { - source.run().await.unwrap(); + Box::new(source).run(stop_token).await.unwrap(); }); // Vérifier les segments reçus @@ -683,10 +749,12 @@ mod tests { let url = format!("{}/stream", mock_server.uri()); let mut source = HttpSource::new(&url); let (tx, mut rx) = mpsc::channel(16); - source.add_subscriber(tx); + let collector = TestCollectorNode::new(tx); + source.register(Box::new(collector)); + let stop_token = CancellationToken::new(); tokio::spawn(async move { - source.run().await.unwrap(); + Box::new(source).run(stop_token).await.unwrap(); }); // Chercher le TrackBoundary pour vérifier les métadonnées @@ -720,9 +788,11 @@ mod tests { let url = format!("{}/notfound.flac", mock_server.uri()); let mut source = HttpSource::new(&url); let (tx, _rx) = mpsc::channel(16); - source.add_subscriber(tx); + let collector = TestCollectorNode::new(tx); + source.register(Box::new(collector)); - let result = source.run().await; + let stop_token = CancellationToken::new(); + let result = Box::new(source).run(stop_token).await; assert!(result.is_err(), "Doit retourner une erreur pour HTTP 404"); if let Err(AudioError::ProcessingError(msg)) = result { @@ -751,9 +821,11 @@ mod tests { let url = format!("{}/invalid.flac", mock_server.uri()); let mut source = HttpSource::new(&url); let (tx, _rx) = mpsc::channel(16); - source.add_subscriber(tx); + let collector = TestCollectorNode::new(tx); + source.register(Box::new(collector)); - let result = source.run().await; + let stop_token = CancellationToken::new(); + let result = Box::new(source).run(stop_token).await; assert!( result.is_err(), "Doit retourner une erreur pour un format invalide" @@ -804,10 +876,12 @@ mod tests { let url = format!("{}/my-song.flac", mock_server.uri()); let mut source = HttpSource::new(&url); let (tx, mut rx) = mpsc::channel(16); - source.add_subscriber(tx); + let collector = TestCollectorNode::new(tx); + source.register(Box::new(collector)); + let stop_token = CancellationToken::new(); tokio::spawn(async move { - source.run().await.unwrap(); + Box::new(source).run(stop_token).await.unwrap(); }); let mut found_title = false; diff --git a/pmoaudio/src/nodes/mod.rs b/pmoaudio/src/nodes/mod.rs index abc378f7..1af93b46 100644 --- a/pmoaudio/src/nodes/mod.rs +++ b/pmoaudio/src/nodes/mod.rs @@ -4,7 +4,6 @@ //! un pipeline audio, ainsi que les traits et structures de support. use std::sync::Arc; -use tokio::sync::mpsc; use crate::type_constraints::{TypeMismatch, TypeRequirement}; use crate::AudioSegment; @@ -109,94 +108,6 @@ pub trait TypedAudioNode { } } -/// Node avec un seul abonné (pas de clone inutile) -/// -/// Optimisé pour les cas où un node n'a qu'un seul destinataire. -/// Le Arc du chunk est simplement transféré sans clonage supplémentaire. -/// -/// # Exemples -/// -/// ``` -/// use pmoaudio::SingleSubscriberNode; -/// use tokio::sync::mpsc; -/// -/// let (tx, rx) = mpsc::channel(10); -/// let node = SingleSubscriberNode::new(tx); -/// ``` -pub struct SingleSubscriberNode { - tx: mpsc::Sender>, -} - -impl SingleSubscriberNode { - pub fn new(tx: mpsc::Sender>) -> Self { - Self { tx } - } - - pub async fn push(&self, chunk: Arc) -> Result<(), AudioError> { - self.tx.send(chunk).await.map_err(|_| AudioError::SendError) - } -} - -/// Node avec plusieurs abonnés (partage le même Arc) -/// -/// Permet de broadcaster un chunk à plusieurs destinations. -/// Tous les abonnés reçoivent le même `Arc`, donc pas de copie -/// des données audio - seul le compteur de référence Arc est incrémenté. -/// -/// # Exemples -/// -/// ``` -/// use pmoaudio::MultiSubscriberNode; -/// use tokio::sync::mpsc; -/// -/// let mut node = MultiSubscriberNode::new(); -/// let (tx1, rx1) = mpsc::channel(10); -/// let (tx2, rx2) = mpsc::channel(10); -/// -/// node.add_subscriber(tx1); -/// node.add_subscriber(tx2); -/// // Les deux abonnés recevront les mêmes chunks -/// ``` -pub struct MultiSubscriberNode { - subscribers: Vec>>, -} - -impl MultiSubscriberNode { - pub fn new() -> Self { - Self { - subscribers: Vec::new(), - } - } - - pub fn add_subscriber(&mut self, tx: mpsc::Sender>) { - self.subscribers.push(tx); - } - - pub async fn push(&self, chunk: Arc) -> Result<(), AudioError> { - for tx in &self.subscribers { - // On partage le même Arc avec tous les abonnés - tx.send(chunk.clone()) - .await - .map_err(|_| AudioError::SendError)?; - } - Ok(()) - } - - pub async fn try_push(&self, chunk: Arc) -> Result<(), AudioError> { - for tx in &self.subscribers { - // try_send non-bloquant, ignore si saturé - let _ = tx.try_send(chunk.clone()); - } - Ok(()) - } -} - -impl Default for MultiSubscriberNode { - fn default() -> Self { - Self::new() - } -} - /// Erreurs possibles dans le pipeline audio #[derive(Debug, Clone)] pub enum AudioError { diff --git a/pmoaudio/src/pipeline.rs b/pmoaudio/src/pipeline.rs new file mode 100644 index 00000000..c4d33131 --- /dev/null +++ b/pmoaudio/src/pipeline.rs @@ -0,0 +1,110 @@ +//! Architecture de pipeline audio avec propagation automatique du run et gestion d'arrêt +//! +//! Ce module définit le trait `AudioPipelineNode` qui permet de construire des arbres +//! de traitement audio avec : +//! - Démarrage automatique de tous les enfants lors du run de la tête +//! - Arrêt coordonné sur EOF ou erreur +//! - Propagation bidirectionnelle sans boucle infinie +//! +//! # Architecture +//! +//! Les pipelines forment des **arbres** (pas de DAG) où : +//! - Les sources n'ont pas d'input (get_tx retourne None) +//! - Les sinks n'ont pas d'enfants (register panic) +//! - Les convertisseurs ont à la fois un input et des enfants +//! +//! # Mécanisme d'arrêt +//! +//! - **Descendant** : `stop_token.cancel()` propage l'arrêt vers les fils +//! - **Montant** : Le retour de `run()` informe le parent +//! - **Détection** : Un enfant mort → parent voit `send().is_err()` ou `await handle` +//! +//! # Exemple +//! +//! ```no_run +//! use pmoaudio::{FileSource, AudioPipelineNode}; +//! use pmoaudio::nodes::FlacFileSink; +//! use tokio_util::sync::CancellationToken; +//! +//! # async fn example() -> Result<(), pmoaudio::nodes::AudioError> { +//! // Construire le pipeline +//! let mut source = FileSource::new("input.flac"); +//! let sink = FlacFileSink::new("output.flac"); +//! +//! source.register(Box::new(sink)); +//! +//! // Lancer avec contrôle d'arrêt +//! let stop_token = CancellationToken::new(); +//! Box::new(source).run(stop_token).await?; +//! # Ok(()) +//! # } +//! ``` + +use crate::{nodes::AudioError, AudioSegment}; +use std::sync::Arc; +use tokio::sync::mpsc; +use tokio_util::sync::CancellationToken; + +/// Trait pour les nœuds d'un pipeline audio +/// +/// Permet la construction d'arbres de traitement avec: +/// - Démarrage automatique de tous les enfants +/// - Arrêt coordonné sur EOF ou erreur +/// - Propagation bidirectionnelle sans boucle +#[async_trait::async_trait] +pub trait AudioPipelineNode: Send + 'static { + /// Retourne un clone du sender pour recevoir des segments + /// + /// # Retourne + /// + /// - `Some(tx)` pour les nœuds qui ont un input (sinks, convertisseurs) + /// - `None` pour les sources qui génèrent des données + /// + /// Le sender retourné est un clone, permettant au parent de l'extraire + /// avant de consommer le nœud dans `run()`. + fn get_tx(&self) -> Option>>; + + /// Enregistre un nœud enfant dans l'arbre + /// + /// # Arguments + /// + /// * `child` - Le nœud enfant à enregistrer + /// + /// # Comportement + /// + /// - Pour les sources et convertisseurs : enregistre l'enfant et clone son tx + /// - Pour les sinks : panic (nœuds terminaux) + /// + /// Le parent extrait le tx via `child.get_tx()` avant de stocker le child. + fn register(&mut self, child: Box); + + /// Lance le nœud et tous ses enfants + /// + /// # Arguments + /// + /// * `stop_token` - Token d'arrêt partagé pour coordination + /// + /// # Comportement + /// + /// 1. **Spawn enfants** : Tous les enfants sont spawned avant le traitement + /// 2. **Traitement** : Le nœud fait son travail (lecture, conversion, écriture) + /// 3. **Détection** : Si un enfant meurt, le parent le détecte via send().is_err() + /// 4. **Arrêt** : Sur EOF/erreur, appel de `stop_token.cancel()` pour les enfants + /// 5. **Attente** : Attend que tous les enfants se terminent + /// 6. **Retour** : Retourne pour informer le parent (propagation montante) + /// + /// # Propagation d'erreur + /// + /// - Erreur du nœud → propagée vers les enfants (cancel) puis vers le parent (return) + /// - Erreur d'un enfant → détectée à l'await du handle, propagée vers le parent + /// + /// # Arrêt sans boucle + /// + /// - Un seul `cancel()` par nœud (en sortant de la boucle de travail) + /// - L'enfant ne cancel JAMAIS le parent + /// - `cancel()` est idempotent (pas de problème si appelé plusieurs fois) + async fn run( + self: Box, + stop_token: CancellationToken, + ) -> Result<(), AudioError>; +} diff --git a/pmoaudiocache/Cargo.toml b/pmoaudiocache/Cargo.toml index f61dda24..c04f343a 100644 --- a/pmoaudiocache/Cargo.toml +++ b/pmoaudiocache/Cargo.toml @@ -13,7 +13,6 @@ pmodidl = { path = "../pmodidl" } # Streaming FLAC asynchrone pmoflac = { path = "../pmoflac" } pmometadata = { path = "../pmometadata" } -pmoaudio = { path = "../pmoaudio" } # Base de données rusqlite = { version = "0.37", features = ["bundled"] } diff --git a/pmoaudiocache/src/lib.rs b/pmoaudiocache/src/lib.rs index dffb8602..16e8888a 100644 --- a/pmoaudiocache/src/lib.rs +++ b/pmoaudiocache/src/lib.rs @@ -79,7 +79,6 @@ pub mod cache; pub mod metadata; pub mod metadata_ext; -pub mod nodes; pub mod streaming; pub mod track_metadata; @@ -93,7 +92,6 @@ pub mod config_ext; pub use cache::{add_with_metadata_extraction, get_metadata, new_cache, AudioConfig, Cache}; pub use metadata::AudioMetadata; pub use metadata_ext::{AudioMetadataExt, AudioTrackMetadataExt}; -pub use nodes::{FlacCacheSink, FlacCacheSinkStats}; pub use track_metadata::AudioCacheTrackMetadata; #[cfg(feature = "pmoconfig")] diff --git a/pmoaudiocache/src/nodes/mod.rs b/pmoaudiocache/src/nodes/mod.rs deleted file mode 100644 index 7d2d365d..00000000 --- a/pmoaudiocache/src/nodes/mod.rs +++ /dev/null @@ -1,8 +0,0 @@ -//! Nodes audio pour pmoaudiocache -//! -//! Ce module fournit des nodes audio spécialisés qui étendent pmoaudio -//! pour intégrer le cache audio. - -pub mod flac_cache_sink; - -pub use flac_cache_sink::{FlacCacheSink, FlacCacheSinkStats}; diff --git a/pmoplaylist/Cargo.toml b/pmoplaylist/Cargo.toml index eb5b37f3..b842ea27 100644 --- a/pmoplaylist/Cargo.toml +++ b/pmoplaylist/Cargo.toml @@ -8,6 +8,9 @@ edition = "2021" pmoaudiocache = { path = "../pmoaudiocache" } pmocache = { path = "../pmocache" } +# Métadonnées +pmometadata = { path = "../pmometadata" } + # DIDL-Lite pour UPnP pmodidl = { path = "../pmodidl" } diff --git a/pmoplaylist/src/track.rs b/pmoplaylist/src/track.rs index d4439b83..94bd493c 100644 --- a/pmoplaylist/src/track.rs +++ b/pmoplaylist/src/track.rs @@ -1,9 +1,11 @@ //! PlaylistTrack : résultat d'un pop() avec helpers pour accéder au cache use crate::Result; -use pmoaudiocache::AudioMetadataExt; +use pmoaudiocache::{AudioMetadataExt, AudioTrackMetadataExt}; use pmocache::cache_trait::FileCache; -use std::path::PathBuf; +use pmometadata::TrackMetadata; +use std::{path::PathBuf, sync::Arc}; +use tokio::sync::RwLock; /// Un morceau récupéré depuis une playlist /// @@ -87,6 +89,34 @@ impl PlaylistTrack { .map_err(|e| crate::Error::CacheError(e.to_string())) } + /// Retourne une instance de TrackMetadata pour ce morceau + /// + /// Cette méthode fournit un accès unifié aux métadonnées via le trait `TrackMetadata`. + /// L'instance retournée implémente toutes les méthodes du trait et permet un accès + /// asynchrone thread-safe aux métadonnées via RwLock. + /// + /// # Exemples + /// + /// ```no_run + /// # use pmoplaylist::*; + /// # async fn example(track: PlaylistTrack) -> Result<()> { + /// // Obtenir l'instance TrackMetadata + /// let metadata = track.track_metadata()?; + /// + /// // Accéder aux métadonnées via le trait + /// let title = metadata.read().await.get_title().await?; + /// let artist = metadata.read().await.get_artist().await?; + /// + /// println!("Titre: {:?}", title); + /// println!("Artiste: {:?}", artist); + /// # Ok(()) + /// # } + /// ``` + pub fn track_metadata(&self) -> Result>> { + let cache = crate::manager::audio_cache()?; + Ok(cache.track_metadata(&self.cache_pk)) + } + /// Récupère uniquement le titre du morceau (méthode légère) /// /// Utilise l'extension trait `AudioMetadataExt` pour récupérer qu'une seule valeur