From 50a222afd2dc4974d814a228d23d3cb104e46392 Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Thu, 14 Oct 2021 11:37:28 +0200 Subject: [PATCH 01/14] core: Switch to CPAL, Symphonia, and parking_lot --- Cargo.lock | 598 ++++++++++++++++++++++++++-- Cargo.toml | 7 +- psst-cli/src/main.rs | 11 +- psst-core/Cargo.toml | 8 +- psst-core/src/access_token.rs | 11 +- psst-core/src/actor.rs | 86 ++++ psst-core/src/audio_decode.rs | 162 ++++---- psst-core/src/audio_file.rs | 28 +- psst-core/src/audio_output.rs | 273 ++++++++----- psst-core/src/audio_player.rs | 377 ++++++++++-------- psst-core/src/lib.rs | 1 + psst-core/src/session.rs | 14 +- psst-core/src/stream_storage.rs | 76 ++-- psst-core/src/util.rs | 60 +-- psst-gui/src/controller/playback.rs | 17 +- 15 files changed, 1241 insertions(+), 488 deletions(-) create mode 100644 psst-core/src/actor.rs diff --git a/Cargo.lock b/Cargo.lock index d1349c6c..639b563b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,28 @@ dependencies = [ "memchr", ] +[[package]] +name = "alsa" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c4da790adcb2ce5e758c064b4f3ec17a30349f9961d3e5e6c9688b052a9e18" +dependencies = [ + "alsa-sys", + "bitflags", + "libc", + "nix", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -110,6 +132,25 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "bindgen" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da379dbebc0b76ef63ca68d8fc6e71c0f13e59432e0987e508c1820e6ab5239" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -158,6 +199,12 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + [[package]] name = "cairo-rs" version = "0.14.9" @@ -187,6 +234,24 @@ name = "cc" version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +dependencies = [ + "nom", +] [[package]] name = "cfg-expr" @@ -218,6 +283,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "clang-sys" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10612c0ec0e0a1ff0e97980647cb058a6e7aedb913d01d009c406b8b7d0b26ee" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "cocoa" version = "0.24.0" @@ -255,6 +331,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "combine" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a909e4d93292cd8e9c42e189f61681eff9d67b6541f96b8a1a737f23737bd001" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -330,6 +416,50 @@ dependencies = [ "libc", ] +[[package]] +name = "coreaudio-rs" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11894b20ebfe1ff903cbdc52259693389eea03b94918a2def2c30c3bf227ad88" +dependencies = [ + "bitflags", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b7e3347be6a09b46aba228d6608386739fb70beff4f61e07422da87b0bb31fa" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98f45f0a21f617cd2c788889ef710b63f075c949259593ea09c826f1e47a2418" +dependencies = [ + "alsa", + "core-foundation-sys", + "coreaudio-rs", + "jni", + "js-sys", + "lazy_static", + "libc", + "mach", + "ndk 0.3.0", + "ndk-glue 0.3.0", + "nix", + "oboe", + "parking_lot", + "stdweb 0.1.3", + "thiserror", + "web-sys", + "winapi 0.3.9", +] + [[package]] name = "cpufeatures" version = "0.2.1" @@ -387,11 +517,46 @@ dependencies = [ "cipher", ] +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "dbus" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8862bb50aa3b2a2db5bfd2c875c73b3038aa931c411087e335ca8ca0ed430b9" +checksum = "de0a745c25b32caa56b82a3950f5fec7893a960f4c10ca3b02060b0c38d8c2ce" dependencies = [ "libc", "libdbus-sys", @@ -417,6 +582,17 @@ dependencies = [ "byteorder", ] +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.9.0" @@ -544,6 +720,15 @@ version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +[[package]] +name = "encoding_rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_logger" version = "0.8.4" @@ -570,16 +755,6 @@ dependencies = [ "termcolor", ] -[[package]] -name = "ep-miniaudio-sys" -version = "2.4.0" -source = "git+https://github.com/jpochyla/miniaudio-rs#bad980450a072d582af2d88e5dcfd676f23e091f" -dependencies = [ - "bitflags", - "cc", - "libc", -] - [[package]] name = "field-offset" version = "0.3.4" @@ -849,7 +1024,7 @@ checksum = "2aad66361f66796bfc73f530c51ef123970eb895ffba991a234fcf7bea89e518" dependencies = [ "anyhow", "heck", - "proc-macro-crate", + "proc-macro-crate 1.1.0", "proc-macro-error", "proc-macro2", "quote", @@ -866,6 +1041,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "gobject-sys" version = "0.14.0" @@ -926,7 +1107,7 @@ checksum = "21de1da96dc117443fb03c2e270b2d34b7de98d0a79a19bbb689476173745b79" dependencies = [ "anyhow", "heck", - "proc-macro-crate", + "proc-macro-crate 1.1.0", "proc-macro-error", "proc-macro2", "quote", @@ -967,6 +1148,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.3" @@ -1056,6 +1243,35 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] + [[package]] name = "jpeg-decoder" version = "0.1.22" @@ -1096,6 +1312,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.106" @@ -1104,19 +1326,38 @@ checksum = "a60553f9a9e039a333b4e9b20573b9e9b9c0bb3a11e201ccc48ef4283456d673" [[package]] name = "libdbus-sys" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc12a3bc971424edbbf7edaf6e5740483444db63aa8e23d3751ff12a30f306f0" +checksum = "c185b5b7ad900923ef3a8ff594083d4d9b5aea80bb4f32b8342363138c0d456b" dependencies = [ "pkg-config", ] +[[package]] +name = "libloading" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cf036d15402bea3c5d4de17b3fce76b3e4a56ebc1f577be0e7a72f7c607cf0" +dependencies = [ + "cfg-if", + "winapi 0.3.9", +] + [[package]] name = "linked-hash-map" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" +[[package]] +name = "lock_api" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" +dependencies = [ + "scopeguard", +] + [[package]] name = "log" version = "0.4.14" @@ -1135,6 +1376,15 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -1166,35 +1416,106 @@ dependencies = [ ] [[package]] -name = "miniaudio" -version = "0.10.0" -source = "git+https://github.com/jpochyla/miniaudio-rs#bad980450a072d582af2d88e5dcfd676f23e091f" +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + +[[package]] +name = "ndk" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab" +dependencies = [ + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64d6af06fde0e527b1ba5c7b79a6cc89cfc46325b0b2887dffe8f70197e0c3c" dependencies = [ "bitflags", - "ep-miniaudio-sys", + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", ] [[package]] -name = "minivorbis" -version = "0.1.0" +name = "ndk-glue" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk 0.3.0", + "ndk-macro", + "ndk-sys", +] + +[[package]] +name = "ndk-glue" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e9e94628f24e7a3cb5b96a2dc5683acd9230bf11991c2a1677b87695138420" dependencies = [ - "minivorbis-sys", + "lazy_static", + "libc", + "log", + "ndk 0.4.0", + "ndk-macro", + "ndk-sys", ] [[package]] -name = "minivorbis-sys" -version = "0.1.0" +name = "ndk-macro" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" +dependencies = [ + "darling", + "proc-macro-crate 0.1.5", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ndk-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" + +[[package]] +name = "nix" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" dependencies = [ + "bitflags", "cc", + "cfg-if", + "libc", ] [[package]] -name = "miniz_oxide" -version = "0.3.7" +name = "nom" +version = "5.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" dependencies = [ - "adler32", + "memchr", + "version_check", ] [[package]] @@ -1209,6 +1530,17 @@ dependencies = [ "rand", ] +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -1250,6 +1582,28 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_enum" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9bd055fb730c4f8f4f57d45d35cd6b3f0980535b056dc7ff119cee6a66ed6f" +dependencies = [ + "derivative", + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9" +dependencies = [ + "proc-macro-crate 1.1.0", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "objc" version = "0.2.7" @@ -1259,6 +1613,29 @@ dependencies = [ "malloc_buf", ] +[[package]] +name = "oboe" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e15e22bc67e047fe342a32ecba55f555e3be6166b04dd157cd0f803dfa9f48e1" +dependencies = [ + "jni", + "ndk 0.4.0", + "ndk-glue 0.4.0", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338142ae5ab0aaedc8275aa8f67f460e43ae0fca76a695a742d56da0a269eadc" +dependencies = [ + "cc", +] + [[package]] name = "once_cell" version = "1.8.0" @@ -1333,12 +1710,43 @@ dependencies = [ "system-deps", ] +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi 0.3.9", +] + [[package]] name = "pathdiff" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "percent-encoding" version = "2.1.0" @@ -1487,6 +1895,15 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + [[package]] name = "proc-macro-crate" version = "1.1.0" @@ -1551,22 +1968,24 @@ version = "0.1.0" dependencies = [ "aes", "byteorder", + "cpal", "crossbeam-channel", "hmac", "log", - "miniaudio", - "minivorbis", "num-bigint", "num-traits", + "parking_lot", "psst-protocol", "quick-protobuf", "rand", "rangemap", + "rb", "serde", "serde_json", "sha-1", "shannon", "socks", + "symphonia", "tempfile", "ureq", "url", @@ -1694,6 +2113,12 @@ dependencies = [ "libc", ] +[[package]] +name = "rb" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27f4c5756bd2bfb5942758d8168805655c62388eef0544e581a2bfa5b532f15" + [[package]] name = "redox_syscall" version = "0.2.10" @@ -1796,6 +2221,15 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.1.0" @@ -1919,6 +2353,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + [[package]] name = "sized-chunks" version = "0.6.5" @@ -1985,6 +2425,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "stdweb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" + [[package]] name = "stdweb" version = "0.4.20" @@ -2034,6 +2480,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + [[package]] name = "strum" version = "0.21.0" @@ -2058,6 +2510,77 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +[[package]] +name = "symphonia" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7e5f38aa07e792f4eebb0faa93cee088ec82c48222dd332897aae1569d9a4b7" +dependencies = [ + "lazy_static", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-ogg", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a29ed6748078effb35a05064a451493a78038918981dc1a76bdf5a2752d441fa" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa135e97be0f4a666c31dfe5ef4c75435ba3d355fd6a73d2100aa79b14c104c9" +dependencies = [ + "arrayvec", + "bitflags", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7b2357288a79adfec532cfd86049696cfa5c58efeff83bd51687a528f18a519" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-metadata" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5260599daba18d8fe905ca3eb3b42ba210529a6276886632412cc74984e79b1a" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a37026c6948ff842e0bf94b4008579cc71ab16ed0ff9ca70a331f60f4f1e1e9" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + [[package]] name = "syn" version = "1.0.81" @@ -2148,7 +2671,7 @@ dependencies = [ "const_fn", "libc", "standback", - "stdweb", + "stdweb 0.4.20", "time-macros 0.1.1", "version_check", "winapi 0.3.9", @@ -2451,6 +2974,17 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi 0.3.9", + "winapi-util", +] + [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index c36cbb35..817d1394 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,9 +4,10 @@ members = [ "psst-core", "psst-cli", "psst-gui", - "minivorbis-sys", - "minivorbis", ] [profile.dev] -split-debuginfo = "unpacked" \ No newline at end of file +split-debuginfo = "unpacked" + +[profile.dev.package.symphonia] +opt-level = 2 \ No newline at end of file diff --git a/psst-cli/src/main.rs b/psst-cli/src/main.rs index 3d576c9a..61683dbd 100644 --- a/psst-cli/src/main.rs +++ b/psst-cli/src/main.rs @@ -55,15 +55,10 @@ fn play_item( let output_remote = output.remote(); let config = PlaybackConfig::default(); - let mut player = Player::new(session, cdn, cache, config, output.remote()); + let mut player = Player::new(session, cdn, cache, config, &output); - let output_thread = thread::spawn({ - let player_source = player.audio_source(); - move || { - output - .start_playback(player_source) - .expect("Playback failed"); - } + let output_thread = thread::spawn(move || { + output.play().expect("Playback failed"); }); let _ui_thread = thread::spawn({ diff --git a/psst-core/Cargo.toml b/psst-core/Cargo.toml index 8f2a093d..af941cf5 100644 --- a/psst-core/Cargo.toml +++ b/psst-core/Cargo.toml @@ -9,21 +9,23 @@ psst-protocol = { path = "../psst-protocol" } aes = { version = "0.7", features = ["ctr"] } byteorder = "1.4" +cpal = "0.13" crossbeam-channel = "0.5" hmac = "0.11" log = "0.4" -miniaudio = { git = "https://github.com/jpochyla/miniaudio-rs", default-features = false, features = ["ma-log-level-error", "ma-no-flac", "ma-no-mp3", "ma-no-wav"] } -minivorbis = { path = "../minivorbis"} num-bigint = { version = "0.4", features = ["rand"] } num-traits = "0.2" +parking_lot = "0.11" quick-protobuf = "0.8" rand = "0.8" rangemap = "0.1" +rb = "0.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha-1 = "0.9" shannon = "0.2" socks = "0.3" +symphonia = { version = "0.4", default-features = false, features = ["ogg", "vorbis"]} tempfile = "3.2" ureq = { version = "2.1", features = ["json"] } -url = "2.2" +url = "2.2" \ No newline at end of file diff --git a/psst-core/src/access_token.rs b/psst-core/src/access_token.rs index 88a9201c..47c06ee1 100644 --- a/psst-core/src/access_token.rs +++ b/psst-core/src/access_token.rs @@ -1,8 +1,6 @@ -use std::{ - sync::Mutex, - time::{Duration, Instant}, -}; +use std::time::{Duration, Instant}; +use parking_lot::Mutex; use serde::Deserialize; use crate::{error::Error, session::SessionService}; @@ -69,10 +67,7 @@ impl TokenProvider { } pub fn get(&self, session: &SessionService) -> Result { - let mut token = self - .token - .lock() - .expect("Failed to acquire access token lock"); + let mut token = self.token.lock(); if token.is_expired() { log::info!("access token expired, requesting"); *token = AccessToken::request(session)?; diff --git a/psst-core/src/actor.rs b/psst-core/src/actor.rs new file mode 100644 index 00000000..9e0c419f --- /dev/null +++ b/psst-core/src/actor.rs @@ -0,0 +1,86 @@ +use std::{ + fmt::Display, + thread::{self, JoinHandle}, +}; + +use crossbeam_channel::{bounded, unbounded, Receiver, Sender}; + +pub enum ActorOp { + Continue, + Shutdown, +} + +pub trait Actor: Sized { + type Message: Send + 'static; + type Error: Display; + + fn handle(&mut self, msg: Self::Message) -> Result; + + fn process(mut self, recv: Receiver) { + for msg in recv { + match self.handle(msg) { + Ok(ActorOp::Continue) => { + continue; + } + Ok(ActorOp::Shutdown) => { + break; + } + Err(err) => { + log::error!("error: {}", err); + break; + } + } + } + } + + fn spawn(cap: Capacity, factory: F) -> Handle + where + F: FnOnce(Sender) -> Self + Send + 'static, + { + let (send, recv) = cap.to_channel(); + Handle { + sender: send.clone(), + thread: thread::spawn(move || { + factory(send).process(recv); + }), + } + } + + fn spawn_default(factory: F) -> Handle + where + F: FnOnce(Sender) -> Self + Send + 'static, + { + Self::spawn(Capacity::Bounded(128), factory) + } +} + +pub struct Handle { + thread: JoinHandle<()>, + sender: Sender, +} + +impl Handle { + pub fn sender(&self) -> Sender { + self.sender.clone() + } + + pub fn join(self) { + let _ = self.thread.join(); + } +} + +pub enum Capacity { + Sync, + Bounded(usize), + Unbounded, +} + +impl Capacity { + pub fn to_channel(&self) -> (Sender, Receiver) { + match self { + Capacity::Sync => bounded(0), + Capacity::Bounded(cap) => bounded(*cap), + Capacity::Unbounded => unbounded(), + } + } +} diff --git a/psst-core/src/audio_decode.rs b/psst-core/src/audio_decode.rs index d873bed6..46378a35 100644 --- a/psst-core/src/audio_decode.rs +++ b/psst-core/src/audio_decode.rs @@ -1,106 +1,114 @@ -use std::{io, slice}; +use std::{io, time::Duration}; -use crate::error::Error; +use symphonia::{ + core::{ + audio::{AudioBufferRef, Channels}, + codecs::Decoder, + errors::Error as SymphoniaError, + formats::{FormatReader, SeekMode, SeekTo}, + io::{MediaSource, MediaSourceStream}, + units::TimeStamp, + }, + default::{codecs::VorbisDecoder, formats::OggReader}, +}; -pub struct VorbisDecoder +use crate::{error::Error, util::FileWithConstSize}; + +impl MediaSource for FileWithConstSize where - R: io::Read + io::Seek, + T: io::Read + io::Seek + Send, { - vorbis: minivorbis::Decoder, - // Buffer with enough capacity for `minivorbis` packets. - packet: Vec, - // Offset into `packet`, currently pending sample. - pos: usize, + fn is_seekable(&self) -> bool { + true + } + + fn byte_len(&self) -> Option { + Some(self.len()) + } } -impl VorbisDecoder -where - R: io::Read + io::Seek, -{ - pub fn new(input: R) -> Result { - let vorbis = minivorbis::Decoder::new(input)?; +pub struct AudioDecoder { + track_id: u32, // Internal OGG track index. + decoder: Box, + format: Box, + position: TimeStamp, +} + +impl AudioDecoder { + pub fn new(input: T) -> Result + where + T: io::Read + io::Seek + Send + 'static, + { + let mss_opts = Default::default(); + let mss = MediaSourceStream::new(Box::new(FileWithConstSize::new(input)), mss_opts); + + let format_opts = Default::default(); + let format = OggReader::try_new(mss, &format_opts)?; + + let track = format.default_track().unwrap(); + let decoder_opts = Default::default(); + let decoder = VorbisDecoder::try_new(&track.codec_params, &decoder_opts)?; Ok(Self { - vorbis, - packet: Vec::with_capacity(minivorbis::TYPICAL_PACKET_CAP), - pos: 0, // Buffer is initially empty. + track_id: track.id, + decoder: Box::new(decoder), + format: Box::new(format), + position: 0, }) } - pub fn seek(&mut self, pcm_frame: u64) { - self.vorbis - .seek_to_pcm(pcm_frame) - .expect("Failed to set current OGG stream position"); + pub fn channels(&self) -> Option { + self.decoder.codec_params().channels } - fn read_next_packet(&mut self) -> Result { - loop { - let packet = unsafe { - slice::from_raw_parts_mut(self.packet.as_mut_ptr(), self.packet.capacity()) - }; - match self.vorbis.read_packet(packet) { - Err(minivorbis::Error::Hole) => { - // Skip holes in decoding. - continue; - } - Ok(len) => { - unsafe { - self.packet.set_len(len); - } - return Ok(len); - } - other_result => { - return other_result; - } - } - } + pub fn sample_rate(&self) -> Option { + self.decoder.codec_params().sample_rate } - fn channels(&self) -> u8 { - self.vorbis.channels + pub fn max_frames_per_packet(&self) -> Option { + self.decoder.codec_params().max_frames_per_packet } - fn sample_rate(&self) -> u32 { - self.vorbis.sample_rate + pub fn seek(&mut self, time: Duration) -> Result { + let seeked_to = self.format.seek( + SeekMode::Accurate, + SeekTo::Time { + time: time.as_secs_f64().into(), + track_id: Some(self.track_id), + }, + )?; + self.position = seeked_to.actual_ts; + Ok(self.position) } -} -impl Iterator for VorbisDecoder -where - R: io::Read + io::Seek, -{ - type Item = f32; - - fn next(&mut self) -> Option { - if self.pos >= self.packet.len() { - // We have reached the end of the packet, try to read the next one. - match self.read_next_packet() { - Err(err) => { - log::error!("error while decoding: {:?}", err); - return None; // Signal an end of stream. - } - Ok(0) => { - return None; // End of stream. - } - Ok(_) => { - // We have read next packet, reset the cursor and continue. - self.pos = 0; - } + pub fn next_packet(&mut self) -> Option { + let packet = match self.format.next_packet() { + Ok(packet) => packet, + Err(err) => { + log::error!("format error: {}", err); + return None; + } + }; + match self.decoder.decode(&packet) { + Ok(packet) => { + self.position += packet.frames() as TimeStamp; + Some(packet) + } + // TODO: Handle non-fatal decoding errors and retry. + Err(err) => { + log::error!("fatal decode error: {}", err); + None } } - // Sample is available in this packet, return it. - let sample = self.packet[self.pos]; - self.pos += 1; - Some(sample) } - fn size_hint(&self) -> (usize, Option) { - (self.packet.len() - self.pos, None) + pub fn current_frame(&self) -> TimeStamp { + self.position } } -impl From for Error { - fn from(err: minivorbis::Error) -> Error { +impl From for Error { + fn from(err: SymphoniaError) -> Error { Error::AudioDecodingError(Box::new(err)) } } diff --git a/psst-core/src/audio_file.rs b/psst-core/src/audio_file.rs index ee94aafb..05395067 100644 --- a/psst-core/src/audio_file.rs +++ b/psst-core/src/audio_file.rs @@ -1,6 +1,6 @@ use std::{ io, - io::{BufReader, Seek, SeekFrom}, + io::{Seek, SeekFrom}, path::PathBuf, sync::Arc, thread, @@ -9,7 +9,7 @@ use std::{ }; use crate::{ - audio_decode::VorbisDecoder, + audio_decode::AudioDecoder, audio_decrypt::AudioDecrypt, audio_key::AudioKey, audio_normalize::NormalizationData, @@ -22,8 +22,6 @@ use crate::{ util::OffsetFile, }; -pub type FileAudioSource = VorbisDecoder>>>; - #[derive(Debug, Clone, Copy)] pub struct AudioPath { pub item_id: ItemId, @@ -93,19 +91,19 @@ impl AudioFile { } } - pub fn audio_source( - &self, - key: AudioKey, - ) -> Result<(FileAudioSource, NormalizationData), Error> { - let reader = match self { - Self::Streamed { streamed_file, .. } => streamed_file.storage.reader()?, - Self::Cached { cached_file, .. } => cached_file.storage.reader()?, - }; - let buffered = BufReader::new(reader); - let mut decrypted = AudioDecrypt::new(key, buffered); + pub fn reader(&self) -> Result { + match self { + Self::Streamed { streamed_file, .. } => Ok(streamed_file.storage.reader()?), + Self::Cached { cached_file, .. } => Ok(cached_file.storage.reader()?), + } + } + + pub fn audio_source(&self, key: AudioKey) -> Result<(AudioDecoder, NormalizationData), Error> { + let reader = self.reader()?; + let mut decrypted = AudioDecrypt::new(key, reader); let normalization = NormalizationData::parse(&mut decrypted)?; let encoded = OffsetFile::new(decrypted, self.header_length())?; - let decoded = VorbisDecoder::new(encoded)?; + let decoded = AudioDecoder::new(encoded)?; Ok((decoded, normalization)) } diff --git a/psst-core/src/audio_output.rs b/psst-core/src/audio_output.rs index 90626df5..c86b0a56 100644 --- a/psst-core/src/audio_output.rs +++ b/psst-core/src/audio_output.rs @@ -1,61 +1,40 @@ -use std::sync::{Arc, Mutex}; +use std::sync::Arc; -use crossbeam_channel::{unbounded, Receiver, Sender}; -use miniaudio::{Context, Device, DeviceConfig, DeviceType, Format}; +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use crossbeam_channel::{bounded, Receiver, Sender}; +use rb::{RbConsumer, RbProducer, RB}; +use symphonia::core::{ + audio::{Channels, SampleBuffer, SignalSpec}, + conv::ConvertibleSample, +}; use crate::error::Error; -pub type AudioSample = f32; - -pub trait AudioSource: Iterator { - fn channels(&self) -> u8; - fn sample_rate(&self) -> u32; - fn normalization_factor(&self) -> Option; -} - -pub struct AudioOutputRemote { - event_sender: Sender, -} - -impl AudioOutputRemote { - pub fn close(&self) { - self.send(InternalEvent::Close); - } - - pub fn pause(&self) { - self.send(InternalEvent::Pause); - } - - pub fn resume(&self) { - self.send(InternalEvent::Resume); - } - - pub fn set_volume(&self, volume: f64) { - self.send(InternalEvent::SetVolume(volume)); - } - - fn send(&self, event: InternalEvent) { - self.event_sender.send(event).expect("Audio output died"); - } -} +const SAMPLE_RATE: u32 = 41000; +const RING_BUF_SIZE: usize = 1024 * 4; pub struct AudioOutput { - context: Context, + device: cpal::Device, + sink: Arc>, event_sender: Sender, event_receiver: Receiver, } impl AudioOutput { pub fn open() -> Result { - let backends = &[]; // Use default backend order. - let config = None; // Use default context config. - let context = Context::new(backends, config)?; + // Open the default output device. + let device = cpal::default_host() + .default_output_device() + .ok_or(cpal::DefaultStreamConfigError::DeviceNotAvailable)?; + + // Channel used for controlling the audio output state. + let (event_sender, event_receiver) = bounded(0); - // Channel used for controlling the audio output. - let (event_sender, event_receiver) = unbounded(); + let sink = Arc::new(AudioSink::new(rb::SpscRb::new(RING_BUF_SIZE))); Ok(Self { - context, + device, + sink, event_sender, event_receiver, }) @@ -67,73 +46,41 @@ impl AudioOutput { } } - pub fn start_playback(&self, source: Arc>) -> Result<(), Error> - where - T: AudioSource + Send + 'static, - { - // Create a device config that describes the kind of device we want to use. - let mut config = DeviceConfig::new(DeviceType::Playback); - - { - // Setup the device config for playback with the channel count and sample rate - // from the audio source. - let source = source.lock().expect("Failed to acquire audio source lock"); - config.playback_mut().set_format(Format::F32); - config.playback_mut().set_channels(source.channels().into()); - config.set_sample_rate(source.sample_rate()); - }; - - // Move the source into the config's data callback. Callback will get cloned - // for each device we create. - config.set_data_callback(move |_device, output, _frames| { - let mut source = source.lock().expect("Failed to acquire audio source lock"); - // Get the audio normalization factor. - let norm_factor = source.normalization_factor().unwrap_or(1.0); - // Fill the buffer with audio samples from the source. - for sample in output.as_samples_mut() { - let s = source.next().unwrap_or(0.0); // Use silence in case the - // source has finished. - *sample = s * norm_factor; - } - }); - - let device = { - let context = self.context.clone(); - Device::new(Some(context), &config)? - }; + pub fn sink(&self) -> Arc> { + Arc::clone(&self.sink) + } - for event in self.event_receiver.iter() { + pub fn play(&self) -> Result<(), Error> { + // TODO: Add additional sample formats. + let stream = OutputStream::try_open::( + &self.device, + SignalSpec { + rate: SAMPLE_RATE, + channels: Channels::FRONT_LEFT | Channels::FRONT_RIGHT, + }, + self.sink.ring_buf.consumer(), + )?; + for event in &self.event_receiver { match event { InternalEvent::Close => { log::debug!("closing audio output"); - if device.is_started() { - if let Err(err) = device.stop() { - log::error!("failed to stop device: {}", err); - } - } + let _ = stream.pause(); break; } InternalEvent::Pause => { log::debug!("pausing audio output"); - if device.is_started() { - if let Err(err) = device.stop() { - log::error!("failed to stop device: {}", err); - } + if let Err(err) = stream.pause() { + log::error!("failed to stop stream: {}", err); } } InternalEvent::Resume => { log::debug!("resuming audio output"); - if !device.is_started() { - if let Err(err) = device.start() { - log::error!("failed to start device: {}", err); - } + if let Err(err) = stream.play() { + log::error!("failed to start stream: {}", err); } } - InternalEvent::SetVolume(volume) => { + InternalEvent::SetVolume(_vol) => { log::debug!("volume has changed"); - if let Err(err) = device.set_master_volume(volume as f32) { - log::error!("failed to set volume: {}", err); - } } } } @@ -142,6 +89,120 @@ impl AudioOutput { } } +pub trait OutputSample: cpal::Sample + ConvertibleSample + Send + 'static {} + +impl OutputSample for i16 {} +impl OutputSample for u16 {} +impl OutputSample for f32 {} + +struct OutputStream { + stream: cpal::Stream, +} + +impl OutputStream { + fn try_open( + device: &cpal::Device, + spec: SignalSpec, + ring_buf_cons: rb::Consumer, + ) -> Result { + let config = Self::config_for_spec(&spec); + let stream = device.build_output_stream( + &config, + move |output: &mut [T], _| { + // Write out as many samples as possible from the ring buffer to the audio + // output. + let written = ring_buf_cons.read(output).unwrap_or(0); + // Mute any remaining samples. + output[written..].iter_mut().for_each(|s| *s = T::MID); + }, + |err| { + log::error!("audio output error: {}", err); + }, + )?; + Ok(Self { stream }) + } + + fn config_for_spec(spec: &SignalSpec) -> cpal::StreamConfig { + cpal::StreamConfig { + channels: spec.channels.count() as cpal::ChannelCount, + sample_rate: cpal::SampleRate(spec.rate), + buffer_size: cpal::BufferSize::Default, + } + } + + fn play(&self) -> Result<(), Error> { + Ok(self.stream.play()?) + } + + fn pause(&self) -> Result<(), Error> { + Ok(self.stream.pause()?) + } +} + +pub struct AudioSink { + ring_buf: rb::SpscRb, + ring_buf_prod: rb::Producer, +} + +impl AudioSink { + fn new(ring_buf: rb::SpscRb) -> AudioSink { + Self { + ring_buf_prod: ring_buf.producer(), + ring_buf, + } + } + + pub fn clear(&self) { + self.ring_buf.clear(); + } + + pub fn write_blocking(&self, sample_buf: &SampleBuffer) -> Result<(), Error> { + // Write out all samples from the sample buffer to the ring buffer. + let mut i = 0; + while i < sample_buf.len() { + let writeable_samples = &sample_buf.samples()[i..]; + + // Write as many samples as possible to the ring buffer. This blocks until some + // samples are written or the consumer has been destroyed (None is returned). + if let Some(written) = self.ring_buf_prod.write_blocking(writeable_samples) { + i += written; + } else { + // Consumer destroyed, return an error. + return Err(cpal::PlayStreamError::DeviceNotAvailable.into()); + } + } + + Ok(()) + } +} + +#[derive(Clone)] +pub struct AudioOutputRemote { + event_sender: Sender, +} + +impl AudioOutputRemote { + pub fn close(&self) { + self.send(InternalEvent::Close); + } + + pub fn pause(&self) { + self.send(InternalEvent::Pause); + } + + pub fn resume(&self) { + self.send(InternalEvent::Resume); + } + + pub fn set_volume(&self, volume: f64) { + self.send(InternalEvent::SetVolume(volume)); + } + + fn send(&self, event: InternalEvent) { + self.event_sender.send(event).expect("Audio output died"); + } +} + enum InternalEvent { Close, Pause, @@ -149,8 +210,26 @@ enum InternalEvent { SetVolume(f64), } -impl From for Error { - fn from(err: miniaudio::Error) -> Error { +impl From for Error { + fn from(err: cpal::DefaultStreamConfigError) -> Error { + Error::AudioOutputError(Box::new(err)) + } +} + +impl From for Error { + fn from(err: cpal::BuildStreamError) -> Error { + Error::AudioOutputError(Box::new(err)) + } +} + +impl From for Error { + fn from(err: cpal::PlayStreamError) -> Error { + Error::AudioOutputError(Box::new(err)) + } +} + +impl From for Error { + fn from(err: cpal::PauseStreamError) -> Error { Error::AudioOutputError(Box::new(err)) } } diff --git a/psst-core/src/audio_player.rs b/psst-core/src/audio_player.rs index 3821af59..bc7bb478 100644 --- a/psst-core/src/audio_player.rs +++ b/psst-core/src/audio_player.rs @@ -1,18 +1,15 @@ -use std::{ - mem, - sync::{Arc, Mutex}, - thread, - thread::JoinHandle, - time::Duration, -}; +use std::{mem, sync::Arc, thread, thread::JoinHandle, time::Duration}; use crossbeam_channel::{unbounded, Receiver, Sender}; +use symphonia::core::audio::{SampleBuffer, SignalSpec}; use crate::{ - audio_file::{AudioFile, AudioPath, FileAudioSource}, + actor::{Actor, ActorOp, Handle}, + audio_decode::AudioDecoder, + audio_file::{AudioFile, AudioPath}, audio_key::AudioKey, audio_normalize::NormalizationLevel, - audio_output::{AudioOutputRemote, AudioSample, AudioSource}, + audio_output::{AudioOutput, AudioOutputRemote, AudioSink}, audio_queue::{Queue, QueueBehavior}, cache::CacheHandle, cdn::CdnHandle, @@ -168,7 +165,7 @@ fn load_audio_key( pub struct LoadedPlaybackItem { file: AudioFile, - source: FileAudioSource, + source: AudioDecoder, norm_factor: f32, } @@ -182,8 +179,8 @@ pub struct Player { queue: Queue, event_sender: Sender, event_receiver: Receiver, - audio_source: Arc>, audio_output_remote: AudioOutputRemote, + audio_output_sink: Arc>, consecutive_loading_failures: usize, } @@ -193,13 +190,9 @@ impl Player { cdn: CdnHandle, cache: CacheHandle, config: PlaybackConfig, - audio_output_remote: AudioOutputRemote, + audio_output: &AudioOutput, ) -> Self { let (event_sender, event_receiver) = unbounded(); - let audio_source = { - let event_sender = event_sender.clone(); - Arc::new(Mutex::new(PlayerAudioSource::new(event_sender))) - }; Self { session, cdn, @@ -207,8 +200,8 @@ impl Player { config, event_sender, event_receiver, - audio_source, - audio_output_remote, + audio_output_remote: audio_output.remote(), + audio_output_sink: audio_output.sink(), state: PlayerState::Stopped, preload: PreloadState::None, queue: Queue::new(), @@ -216,10 +209,6 @@ impl Player { } } - pub fn audio_source(&self) -> Arc> { - self.audio_source.clone() - } - pub fn event_sender(&self) -> Sender { self.event_sender.clone() } @@ -239,18 +228,18 @@ impl Player { PlayerEvent::Preloaded { item, result } => { self.handle_preloaded(item, result); } - PlayerEvent::Progress { duration, path } => { - self.handle_progress(duration, path); + PlayerEvent::Position { position, path } => { + self.handle_position(position, path); } - PlayerEvent::Finished { .. } => { - self.handle_finished(); + PlayerEvent::EndOfTrack { .. } => { + self.handle_end_of_track(); } PlayerEvent::Loading { .. } | PlayerEvent::Playing { .. } | PlayerEvent::Pausing { .. } | PlayerEvent::Resuming { .. } | PlayerEvent::Stopped { .. } - | PlayerEvent::Blocked => {} + | PlayerEvent::Blocked { .. } => {} }; } @@ -320,25 +309,26 @@ impl Player { } } - fn handle_progress(&mut self, progress: Duration, path: AudioPath) { + fn handle_position(&mut self, new_position: Duration, path: AudioPath) { match &mut self.state { - PlayerState::Playing { duration, .. } | PlayerState::Paused { duration, .. } => { - *duration = progress; + PlayerState::Playing { position, .. } | PlayerState::Paused { position, .. } => { + *position = new_position; } _ => { - log::warn!("received unexpected progress report"); + log::warn!("received unexpected position report"); } } const PRELOAD_BEFORE_END_OF_TRACK: Duration = Duration::from_secs(30); if let Some(&item_to_preload) = self.queue.get_following() { - let time_until_end_of_track = path.duration.checked_sub(progress).unwrap_or_default(); + let time_until_end_of_track = + path.duration.checked_sub(new_position).unwrap_or_default(); if time_until_end_of_track <= PRELOAD_BEFORE_END_OF_TRACK { self.preload(item_to_preload); } } } - fn handle_finished(&mut self) { + fn handle_end_of_track(&mut self) { self.queue.skip_to_following(); if let Some(&item) = self.queue.get_current() { self.load_and_play(item); @@ -387,8 +377,6 @@ impl Player { .expect("Failed to send PlayerEvent::Loaded"); } }); - // Make sure the output is paused, so any currently playing item is stopped. - self.audio_output_remote.pause(); self.event_sender .send(PlayerEvent::Loading { item }) .expect("Failed to send PlayerEvent::Loading"); @@ -428,27 +416,47 @@ impl Player { fn play_loaded(&mut self, loaded_item: LoadedPlaybackItem) { log::info!("starting playback"); let path = loaded_item.file.path(); - let duration = Duration::default(); - self.audio_source - .lock() - .expect("Failed to acquire audio source lock") - .play_now(loaded_item); + let position = Duration::default(); + let worker = PlaybackWorker { + actor: Decoding::spawn_default({ + let events = self.event_sender.clone(); + let sink = self.audio_output_sink.clone(); + move |this| Decoding::new(loaded_item, events, this, sink) + }), + }; + worker.start(); + self.state = PlayerState::Playing { + path, + position, + worker, + }; self.event_sender - .send(PlayerEvent::Playing { path, duration }) + .send(PlayerEvent::Playing { path, position }) .expect("Failed to send PlayerEvent::Playing"); - self.state = PlayerState::Playing { path, duration }; - self.audio_output_remote.resume(); } fn pause(&mut self) { match mem::replace(&mut self.state, PlayerState::Invalid) { - PlayerState::Playing { path, duration } | PlayerState::Paused { path, duration } => { + PlayerState::Playing { + path, + position, + worker, + } + | PlayerState::Paused { + path, + position, + worker, + } => { log::info!("pausing playback"); + worker.stop(); self.event_sender - .send(PlayerEvent::Pausing { path, duration }) + .send(PlayerEvent::Pausing { path, position }) .expect("Failed to send PlayerEvent::Paused"); - self.state = PlayerState::Paused { path, duration }; - self.audio_output_remote.pause(); + self.state = PlayerState::Paused { + path, + position, + worker, + }; } _ => { log::warn!("invalid state transition"); @@ -458,13 +466,26 @@ impl Player { fn resume(&mut self) { match mem::replace(&mut self.state, PlayerState::Invalid) { - PlayerState::Playing { path, duration } | PlayerState::Paused { path, duration } => { + PlayerState::Playing { + path, + position, + worker, + } + | PlayerState::Paused { + path, + position, + worker, + } => { log::info!("resuming playback"); + worker.start(); self.event_sender - .send(PlayerEvent::Resuming { path, duration }) + .send(PlayerEvent::Resuming { path, position }) .expect("Failed to send PlayerEvent::Resuming"); - self.state = PlayerState::Playing { path, duration }; - self.audio_output_remote.resume(); + self.state = PlayerState::Playing { + path, + position, + worker, + }; } _ => { log::warn!("invalid state transition"); @@ -509,16 +530,16 @@ impl Player { .send(PlayerEvent::Stopped) .expect("Failed to send PlayerEvent::Stopped"); self.state = PlayerState::Stopped; - self.audio_output_remote.pause(); self.queue.clear(); self.consecutive_loading_failures = 0; } fn seek(&mut self, position: Duration) { - self.audio_source - .lock() - .expect("Failed to acquire audio source lock") - .seek(position); + if let PlayerState::Playing { worker, .. } | PlayerState::Paused { worker, .. } = + &mut self.state + { + worker.seek(position); + } } fn configure(&mut self, config: PlaybackConfig) { @@ -527,8 +548,8 @@ impl Player { fn is_near_playback_start(&self) -> bool { match self.state { - PlayerState::Playing { duration, .. } | PlayerState::Paused { duration, .. } => { - duration < PREVIOUS_TRACK_THRESHOLD + PlayerState::Playing { position, .. } | PlayerState::Paused { position, .. } => { + position < PREVIOUS_TRACK_THRESHOLD } _ => false, } @@ -593,32 +614,34 @@ pub enum PlayerEvent { item: PlaybackItem, result: Result, }, - /// Player has started playing new track. `Progress` events will follow. + /// Player has started playing new track. `Position` events will follow. Playing { path: AudioPath, - duration: Duration, + position: Duration, }, /// Player is in a paused state. `Resuming` might follow. Pausing { path: AudioPath, - duration: Duration, + position: Duration, }, - /// Player is resuming playback of a track. `Progress` events will follow. + /// Player is resuming playback of a track. `Position` events will follow. Resuming { path: AudioPath, - duration: Duration, + position: Duration, }, - /// Player is either reacting to a seek event in a paused or playing state, - /// or track is naturally progressing during playback. - Progress { + /// Position of the playback head has changed. + Position { path: AudioPath, - duration: Duration, + position: Duration, }, /// Player would like to continue playing, but is blocked, waiting for I/O. - Blocked, + Blocked { + path: AudioPath, + position: Duration, + }, /// Player has finished playing a track. `Loading` or `Playing` might /// follow if the queue is not empty, `Stopped` will follow if it is. - Finished, + EndOfTrack, /// The queue is empty. Stopped, } @@ -630,11 +653,13 @@ enum PlayerState { }, Playing { path: AudioPath, - duration: Duration, + position: Duration, + worker: PlaybackWorker, }, Paused { path: AudioPath, - duration: Duration, + position: Duration, + worker: PlaybackWorker, }, Stopped, Invalid, @@ -652,116 +677,152 @@ enum PreloadState { None, } -const OUTPUT_CHANNELS: u8 = 2; -const OUTPUT_SAMPLE_RATE: u32 = 44100; -const PROGRESS_PRECISION_SAMPLES: u64 = OUTPUT_SAMPLE_RATE as u64 * OUTPUT_CHANNELS as u64; // 1 second. - -struct CurrentPlaybackItem { - file: AudioFile, - source: FileAudioSource, - norm_factor: f32, -} - -struct PlayerAudioSource { - current: Option, - event_sender: Sender, - samples: u64, +struct PlaybackWorker { + actor: Handle, } -impl PlayerAudioSource { - fn new(event_sender: Sender) -> Self { - Self { - event_sender, - current: None, - samples: 0, - } +impl PlaybackWorker { + fn start(&self) { + self.actor.sender().send(Decode::Start).unwrap(); } - fn seek(&mut self, position: Duration) { - if let Some(current) = &mut self.current { - let seconds = position.as_secs_f64(); - let frames = seconds * f64::from(OUTPUT_SAMPLE_RATE); - let samples = frames * f64::from(OUTPUT_CHANNELS); - current.source.seek(frames as u64); - self.samples = samples as u64; - self.report_audio_position(); - } + fn stop(&self) { + self.actor.sender().send(Decode::Stop).unwrap(); } - fn play_now(&mut self, item: LoadedPlaybackItem) { - self.current.replace(CurrentPlaybackItem { - norm_factor: item.norm_factor, - source: item.source, - file: item.file, - }); - self.samples = 0; + fn seek(&self, pos: Duration) { + self.actor.sender().send(Decode::Seek(pos)).unwrap(); } - fn next_sample(&mut self) -> Option { - if let Some(current) = &mut self.current { - let sample = current.source.next(); - if sample.is_some() { - self.samples += 1; - } else { - self.samples = 0; - } - sample - } else { - None - } + fn quit(&self) { + let _ = self.actor.sender().send(Decode::Quit); } +} - fn report_audio_position(&self) { - if let Some(current) = &self.current { - let duration = Duration::from_secs_f64( - self.samples as f64 / f64::from(OUTPUT_SAMPLE_RATE) / f64::from(OUTPUT_CHANNELS), - ); - let path = current.file.path(); - self.event_sender - .send(PlayerEvent::Progress { duration, path }) - .expect("Failed to send PlayerEvent::Progress"); - } +impl Drop for PlaybackWorker { + fn drop(&mut self) { + self.quit(); } +} - fn report_audio_end(&self) { - self.event_sender - .send(PlayerEvent::Finished) - .expect("Failed to send PlayerEvent::Finished"); - } +enum Decode { + Start, + Stop, + Seek(Duration), + ReadPacket, + FlushPacket, + Quit, } -impl AudioSource for PlayerAudioSource { - fn channels(&self) -> u8 { - OUTPUT_CHANNELS - } +enum DecState { + Started, + Stopped, +} + +struct Decoding { + file: AudioFile, + source: AudioDecoder, + norm_factor: f32, + samples: SampleBuffer, + events: Sender, + this: Sender, + sink: Arc>, + state: DecState, +} - fn sample_rate(&self) -> u32 { - OUTPUT_SAMPLE_RATE +impl Decoding { + fn new( + loaded: LoadedPlaybackItem, + events: Sender, + this: Sender, + sink: Arc>, + ) -> Self { + let file = loaded.file; + let source = loaded.source; + let norm_factor = loaded.norm_factor; + let samples = { + let max_frames = source.max_frames_per_packet().unwrap_or(1024 * 8); + let channels = source.channels().unwrap(); + let rate = source.sample_rate().unwrap(); + SampleBuffer::new(max_frames, SignalSpec { rate, channels }) + }; + Self { + file, + source, + norm_factor, + samples, + events, + this, + sink, + state: DecState::Stopped, + } } - fn normalization_factor(&self) -> Option { - self.current.as_ref().map(|current| current.norm_factor) + fn frames_to_duration(&self, frames: u64) -> Duration { + Duration::from_secs_f64(frames as f64 / self.source.sample_rate().unwrap() as f64) } } -impl Iterator for PlayerAudioSource { - type Item = AudioSample; +impl Actor for Decoding { + type Message = Decode; + type Error = Error; - fn next(&mut self) -> Option { - let sample = self.next_sample(); - if sample.is_some() { - // Report audio progress. - if self.samples % PROGRESS_PRECISION_SAMPLES == 0 { - self.report_audio_position(); + fn handle(&mut self, msg: Self::Message) -> Result { + match msg { + Decode::Start => { + if let DecState::Stopped = self.state { + self.state = DecState::Started; + self.this.send(Decode::ReadPacket).unwrap(); + } } - } else { - // We're at the end of track. If we still have the source, drop it and report. - // Player will pause the audio output and we will stop getting polled - // eventually. - if self.current.take().is_some() { - self.report_audio_end(); + Decode::Stop => { + self.state = DecState::Stopped; + } + Decode::Seek(pos) => match self.source.seek(pos) { + Ok(new_pos) => { + self.events + .send(PlayerEvent::Position { + path: self.file.path(), + position: self.frames_to_duration(new_pos), + }) + .unwrap(); + } + Err(err) => { + log::error!("failed to seek: {}", err); + } + }, + Decode::ReadPacket => { + if let DecState::Started = self.state { + match self.source.next_packet() { + Some(packet) => { + // TODO: Apply `self.norm_factor`. + // TODO: Apply global volume level. + self.samples.copy_interleaved_ref(packet); + self.this.send(Decode::FlushPacket).unwrap(); + self.events + .send(PlayerEvent::Position { + path: self.file.path(), + position: self.frames_to_duration(self.source.current_frame()), + }) + .unwrap(); + } + None => { + self.events.send(PlayerEvent::EndOfTrack).unwrap(); + return Ok(ActorOp::Shutdown); + } + } + } + } + Decode::FlushPacket => { + self.sink.write_blocking(&self.samples)?; + if let DecState::Started = self.state { + self.this.send(Decode::ReadPacket).unwrap(); + } + } + Decode::Quit => { + return Ok(ActorOp::Shutdown); } } - sample + Ok(ActorOp::Continue) } } diff --git a/psst-core/src/lib.rs b/psst-core/src/lib.rs index 9c31883a..4d9c5a31 100644 --- a/psst-core/src/lib.rs +++ b/psst-core/src/lib.rs @@ -1,6 +1,7 @@ #![allow(clippy::new_without_default)] pub mod access_token; +pub mod actor; pub mod audio_decode; pub mod audio_decrypt; pub mod audio_file; diff --git a/psst-core/src/session.rs b/psst-core/src/session.rs index 1ba688d7..d0492a2a 100644 --- a/psst-core/src/session.rs +++ b/psst-core/src/session.rs @@ -3,12 +3,13 @@ use std::{ net::{Shutdown, TcpStream}, sync::{ atomic::{AtomicBool, Ordering}, - Arc, Mutex, + Arc, }, thread::{self, JoinHandle}, }; use crossbeam_channel::{unbounded, Receiver, Sender}; +use parking_lot::Mutex; use quick_protobuf::MessageRead; use serde::de::DeserializeOwned; @@ -63,7 +64,7 @@ impl SessionService { /// Replace the active session config. If a session is already connected, /// shut it down and wait until it's terminated. pub fn update_config(&self, config: SessionConfig) { - self.config.lock().unwrap().replace(config); + self.config.lock().replace(config); self.shutdown(); } @@ -71,7 +72,7 @@ impl SessionService { /// session. We return false here after any case of I/O errors or an /// explicit session shutdown. pub fn is_connected(&self) -> bool { - matches!(self.connected.lock().unwrap().as_ref(), Some(worker) if !worker.has_terminated()) + matches!(self.connected.lock().as_ref(), Some(worker) if !worker.has_terminated()) } /// Return a handle for the connected session. In case no connection is @@ -80,14 +81,13 @@ impl SessionService { /// `SessionConnection::open` has an internal timeout, and should give up in /// a timely manner. pub fn connected(&self) -> Result { - let mut connected = self.connected.lock().unwrap(); + let mut connected = self.connected.lock(); let is_connected_and_not_terminated = matches!(connected.as_ref(), Some(worker) if !worker.has_terminated()); if !is_connected_and_not_terminated { let connection = SessionConnection::open( self.config .lock() - .unwrap() .as_ref() .ok_or(Error::SessionDisconnected)? .clone(), @@ -103,7 +103,7 @@ impl SessionService { /// Signal a shutdown to the active worker and wait until it terminates. pub fn shutdown(&self) { - if let Some(worker) = self.connected.lock().unwrap().take() { + if let Some(worker) = self.connected.lock().take() { worker.handle().request_shutdown(); worker.join(); } @@ -150,7 +150,7 @@ impl SessionWorker { let (disp_send, disp_recv) = unbounded(); let (msg_send, msg_recv) = unbounded(); let terminated = Arc::new(AtomicBool::new(false)); - SessionWorker { + Self { decoding_thread: { let decoder = transport.decoder; let disp_send = disp_send.clone(); diff --git a/psst-core/src/stream_storage.rs b/psst-core/src/stream_storage.rs index 1bbcb429..a026188e 100644 --- a/psst-core/src/stream_storage.rs +++ b/psst-core/src/stream_storage.rs @@ -4,10 +4,11 @@ use std::{ io::{Read, Seek, SeekFrom, Write}, ops::Range, path::{Path, PathBuf}, - sync::{Arc, Condvar, Mutex}, + sync::Arc, }; use crossbeam_channel::{unbounded, Receiver, Sender}; +use parking_lot::{Condvar, Mutex}; use rangemap::RangeSet; use tempfile::NamedTempFile; @@ -232,82 +233,63 @@ impl StreamDataMap { self.total_size.saturating_sub(offset) } - /// Return an iterator of sub-ranges of `offset..offset+length` that have - /// not yet been requested from the backend. - fn not_yet_requested(&self, offset: u64, length: u64) -> impl IntoIterator { - #[allow(clippy::needless_collect)] - let gaps: Vec<_> = self - .requested + /// Return a vector of sub-ranges of `offset..offset+length` that have not + /// yet been requested from the backend. + fn not_yet_requested(&self, offset: u64, length: u64) -> Vec<(u64, u64)> { + self.requested .lock() - .unwrap() .gaps(&(offset..offset + length)) - .collect(); - gaps.into_iter().map(|r| range_to_offset_and_length(&r)) + .into_iter() + .map(|r| range_to_offset_and_length(&r)) + .collect() } /// Mark given range as requested from the backend, so we can avoid /// requesting it more than once. fn mark_as_requested(&self, offset: u64, length: u64) { - self.requested - .lock() - .unwrap() - .insert(offset..offset + length); + self.requested.lock().insert(offset..offset + length); } /// Remove range previously marked as requested. fn mark_as_not_requested(&self, offset: u64, length: u64) { - self.requested - .lock() - .unwrap() - .remove(offset..offset + length); + self.requested.lock().remove(offset..offset + length); } /// Mark the range as downloaded and notify the `self.condvar`, so tasks /// currently blocked in `self.wait_for` are woken up. fn mark_as_downloaded(&self, offset: u64, length: u64) { - self.downloaded - .lock() - .unwrap() - .insert(offset..offset + length); + self.downloaded.lock().insert(offset..offset + length); self.condvar.notify_all(); } /// Block, waiting until at least some data at given offset is downloaded. /// Returns length that is available. See `self.mark_as_downloaded`. fn wait_for(&self, offset: u64, blocking_callback: impl Fn(u64)) -> u64 { + let mut downloaded = self.downloaded.lock(); let mut called_callback = false; - let mut available_len = 0; // Resulting length. - - let downloaded = self.downloaded.lock().unwrap(); - - let _mutex_guard = self - .condvar - .wait_while(downloaded, |downloaded| { - if let Some(range) = downloaded.get(&offset) { - let (over_ofs, over_len) = range_to_offset_and_length(range); - let offset_from_overlapping = offset - over_ofs; - available_len = over_len - offset_from_overlapping; - // There is `available_len` bytes of data downloaded, stop waiting. - false - } else { - // Call the blocking callback, but only the first time we are waiting. - if !called_callback { - called_callback = true; - blocking_callback(offset); - } - // There are no overlaps, wait. - true + loop { + if let Some(range) = downloaded.get(&offset) { + let (over_ofs, over_len) = range_to_offset_and_length(range); + let offset_from_overlapping = offset - over_ofs; + let available_len = over_len - offset_from_overlapping; + // There is `available_len` bytes of data downloaded, stop waiting. + break available_len; + } else { + // Call the blocking callback, but only the first time we are waiting. + if !called_callback { + called_callback = true; + blocking_callback(offset); } - }) - .unwrap(); - available_len + // There are no overlaps, wait. + self.condvar.wait(&mut downloaded); + } + } } // Returns true if data is completely downloaded. fn is_complete(&self) -> bool { self.downloaded .lock() - .unwrap() .gaps(&(0..self.total_size)) .next() .is_none() diff --git a/psst-core/src/util.rs b/psst-core/src/util.rs index ff795e30..46d2a132 100644 --- a/psst-core/src/util.rs +++ b/psst-core/src/util.rs @@ -1,10 +1,4 @@ -use std::{ - io, - io::SeekFrom, - mem, - net::{Shutdown, TcpStream}, - time::Duration, -}; +use std::{io, io::SeekFrom, mem, time::Duration}; use num_traits::{One, WrappingAdd}; use quick_protobuf::{BytesReader, MessageRead, MessageWrite, Writer}; @@ -82,28 +76,48 @@ impl io::Seek for OffsetFile { } } -pub struct TcpShutdown { - stream: TcpStream, - shutdown: bool, +pub struct FileWithConstSize { + stream: T, + len: u64, +} + +impl FileWithConstSize { + pub fn len(&self) -> u64 { + self.len + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } } -impl TcpShutdown { - pub fn new(stream: TcpStream) -> Self { - Self { - stream, - shutdown: false, - } +impl FileWithConstSize +where + T: io::Seek, +{ + pub fn new(mut stream: T) -> Self { + stream.seek(SeekFrom::End(0)).unwrap(); + let len = stream.stream_position().unwrap(); + stream.seek(SeekFrom::Start(0)).unwrap(); + Self { stream, len } } +} - pub fn shutdown(&mut self) { - if !self.shutdown { - self.shutdown = true; - let _ = self.stream.shutdown(Shutdown::Both); - } +impl io::Read for FileWithConstSize +where + T: io::Read, +{ + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.stream.read(buf) } +} - pub fn has_been_shut_down(&self) -> bool { - self.shutdown +impl io::Seek for FileWithConstSize +where + T: io::Seek, +{ + fn seek(&mut self, pos: io::SeekFrom) -> io::Result { + self.stream.seek(pos) } } diff --git a/psst-gui/src/controller/playback.rs b/psst-gui/src/controller/playback.rs index ec0c3e51..018f9b60 100644 --- a/psst-gui/src/controller/playback.rs +++ b/psst-gui/src/controller/playback.rs @@ -55,8 +55,6 @@ impl PlaybackController { #[allow(unused_variables)] window: &WindowHandle, ) { let output = AudioOutput::open().unwrap(); - let remote = output.remote(); - let cache_dir = Config::cache_dir().unwrap(); let proxy_url = Config::proxy(); let player = Player::new( @@ -64,16 +62,15 @@ impl PlaybackController { Cdn::new(session, proxy_url.as_deref()).unwrap(), Cache::new(cache_dir).unwrap(), config, - remote, + &output, ); let sender = player.event_sender(); - let source = player.audio_source(); let thread = thread::spawn(move || { Self::service_events(player, event_sink, widget_id); }); let output_thread = thread::spawn(move || { - output.start_playback(source).expect("Playback failed"); + output.play().expect("Playback failed"); }); let hwnd = { @@ -121,9 +118,9 @@ impl PlaybackController { .submit_command(cmd::PLAYBACK_LOADING, item, widget_id) .unwrap(); } - PlayerEvent::Playing { path, duration } => { + PlayerEvent::Playing { path, position } => { let item: TrackId = path.item_id.into(); - let progress = duration.to_owned(); + let progress = position.to_owned(); event_sink .submit_command(cmd::PLAYBACK_PLAYING, (item, progress), widget_id) .unwrap(); @@ -138,13 +135,13 @@ impl PlaybackController { .submit_command(cmd::PLAYBACK_RESUMING, (), widget_id) .unwrap(); } - PlayerEvent::Progress { duration, .. } => { - let progress = duration.to_owned(); + PlayerEvent::Position { position, .. } => { + let progress = position.to_owned(); event_sink .submit_command(cmd::PLAYBACK_PROGRESS, progress, widget_id) .unwrap(); } - PlayerEvent::Blocked => { + PlayerEvent::Blocked { .. } => { event_sink .submit_command(cmd::PLAYBACK_BLOCKED, (), widget_id) .unwrap(); From b2dd91d06db99551d5851044e956d21c959d75b9 Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Sat, 23 Oct 2021 00:28:02 +0200 Subject: [PATCH 02/14] core: Add experimental resampling --- Cargo.lock | 10 ++ Cargo.toml | 2 + psst-cli/src/main.rs | 5 - psst-core/Cargo.toml | 1 + psst-core/src/actor.rs | 10 +- psst-core/src/audio_decode.rs | 3 + psst-core/src/audio_output.rs | 266 +++++++++++++--------------- psst-core/src/audio_player.rs | 157 ++++++++++------ psst-core/src/audio_resample.rs | 86 +++++++++ psst-core/src/error.rs | 18 +- psst-core/src/lib.rs | 1 + psst-gui/src/controller/playback.rs | 9 +- 12 files changed, 350 insertions(+), 218 deletions(-) create mode 100644 psst-core/src/audio_resample.rs diff --git a/Cargo.lock b/Cargo.lock index 639b563b..7c001c84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1343,6 +1343,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "libsamplerate" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcbb68a750265482d1b1f70e3b4505da140c73960f8068726e2fd1ffb792c300" +dependencies = [ + "libc", +] + [[package]] name = "linked-hash-map" version = "0.5.4" @@ -1971,6 +1980,7 @@ dependencies = [ "cpal", "crossbeam-channel", "hmac", + "libsamplerate", "log", "num-bigint", "num-traits", diff --git a/Cargo.toml b/Cargo.toml index 817d1394..e770f399 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,6 @@ members = [ split-debuginfo = "unpacked" [profile.dev.package.symphonia] +opt-level = 2 +[profile.dev.package.libsamplerate] opt-level = 2 \ No newline at end of file diff --git a/psst-cli/src/main.rs b/psst-cli/src/main.rs index 61683dbd..84b83689 100644 --- a/psst-cli/src/main.rs +++ b/psst-cli/src/main.rs @@ -57,10 +57,6 @@ fn play_item( let mut player = Player::new(session, cdn, cache, config, &output); - let output_thread = thread::spawn(move || { - output.play().expect("Playback failed"); - }); - let _ui_thread = thread::spawn({ let player_sender = player.event_sender(); @@ -109,7 +105,6 @@ fn play_item( player.handle(event); } output_remote.close(); - output_thread.join().unwrap(); Ok(()) } diff --git a/psst-core/Cargo.toml b/psst-core/Cargo.toml index af941cf5..574050da 100644 --- a/psst-core/Cargo.toml +++ b/psst-core/Cargo.toml @@ -12,6 +12,7 @@ byteorder = "1.4" cpal = "0.13" crossbeam-channel = "0.5" hmac = "0.11" +libsamplerate = "0.1" log = "0.4" num-bigint = { version = "0.4", features = ["rand"] } num-traits = "0.2" diff --git a/psst-core/src/actor.rs b/psst-core/src/actor.rs index 9e0c419f..37527c3a 100644 --- a/psst-core/src/actor.rs +++ b/psst-core/src/actor.rs @@ -33,12 +33,12 @@ pub trait Actor: Sized { } } - fn spawn(cap: Capacity, factory: F) -> Handle + fn spawn(cap: Capacity, factory: F) -> ActorHandle where F: FnOnce(Sender) -> Self + Send + 'static, { let (send, recv) = cap.to_channel(); - Handle { + ActorHandle { sender: send.clone(), thread: thread::spawn(move || { factory(send).process(recv); @@ -46,7 +46,7 @@ pub trait Actor: Sized { } } - fn spawn_default(factory: F) -> Handle + fn spawn_default(factory: F) -> ActorHandle where F: FnOnce(Sender) -> Self + Send + 'static, { @@ -54,12 +54,12 @@ pub trait Actor: Sized { } } -pub struct Handle { +pub struct ActorHandle { thread: JoinHandle<()>, sender: Sender, } -impl Handle { +impl ActorHandle { pub fn sender(&self) -> Sender { self.sender.clone() } diff --git a/psst-core/src/audio_decode.rs b/psst-core/src/audio_decode.rs index 46378a35..6dc1352c 100644 --- a/psst-core/src/audio_decode.rs +++ b/psst-core/src/audio_decode.rs @@ -84,6 +84,9 @@ impl AudioDecoder { pub fn next_packet(&mut self) -> Option { let packet = match self.format.next_packet() { Ok(packet) => packet, + Err(SymphoniaError::IoError(io)) if io.kind() == io::ErrorKind::UnexpectedEof => { + return None; + } Err(err) => { log::error!("format error: {}", err); return None; diff --git a/psst-core/src/audio_output.rs b/psst-core/src/audio_output.rs index c86b0a56..43003e85 100644 --- a/psst-core/src/audio_output.rs +++ b/psst-core/src/audio_output.rs @@ -1,23 +1,22 @@ use std::sync::Arc; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use crossbeam_channel::{bounded, Receiver, Sender}; +use crossbeam_channel::Sender; use rb::{RbConsumer, RbProducer, RB}; -use symphonia::core::{ - audio::{Channels, SampleBuffer, SignalSpec}, - conv::ConvertibleSample, -}; +use symphonia::core::sample::Sample; -use crate::error::Error; +use crate::{ + actor::{Actor, ActorHandle, ActorOp}, + error::Error, +}; -const SAMPLE_RATE: u32 = 41000; const RING_BUF_SIZE: usize = 1024 * 4; pub struct AudioOutput { - device: cpal::Device, - sink: Arc>, - event_sender: Sender, - event_receiver: Receiver, + sink: AudioSink, + remote: AudioOutputRemote, + _handle: ActorHandle, + _ring_buf: rb::SpscRb, } impl AudioOutput { @@ -27,140 +26,95 @@ impl AudioOutput { .default_output_device() .ok_or(cpal::DefaultStreamConfigError::DeviceNotAvailable)?; - // Channel used for controlling the audio output state. - let (event_sender, event_receiver) = bounded(0); - - let sink = Arc::new(AudioSink::new(rb::SpscRb::new(RING_BUF_SIZE))); + // Get the default device config, so we know what sample format and sample rate + // the device supports. + let supported = device.default_output_config()?; + + // Open an output stream with a ring buffer that will get consumed in the audio + // thread and get written to in the playback threads (through an `AudioSink`). + // TODO: Support additional sample formats. + let ring_buf = rb::SpscRb::new(RING_BUF_SIZE); + let handle = OutputStream::spawn_default({ + let config = supported.config(); + let consumer = ring_buf.consumer(); + move |_| OutputStream::open::(device, config, consumer).unwrap() + }); + let sink = AudioSink { + ring_buf_prod: Arc::new(ring_buf.producer()), + sample_rate: supported.sample_rate(), + }; + let remote = AudioOutputRemote { + sender: handle.sender(), + }; Ok(Self { - device, + _ring_buf: ring_buf, + _handle: handle, sink, - event_sender, - event_receiver, + remote, }) } - pub fn remote(&self) -> AudioOutputRemote { - AudioOutputRemote { - event_sender: self.event_sender.clone(), - } - } - - pub fn sink(&self) -> Arc> { - Arc::clone(&self.sink) + pub fn sink(&self) -> AudioSink { + self.sink.clone() } - pub fn play(&self) -> Result<(), Error> { - // TODO: Add additional sample formats. - let stream = OutputStream::try_open::( - &self.device, - SignalSpec { - rate: SAMPLE_RATE, - channels: Channels::FRONT_LEFT | Channels::FRONT_RIGHT, - }, - self.sink.ring_buf.consumer(), - )?; - for event in &self.event_receiver { - match event { - InternalEvent::Close => { - log::debug!("closing audio output"); - let _ = stream.pause(); - break; - } - InternalEvent::Pause => { - log::debug!("pausing audio output"); - if let Err(err) = stream.pause() { - log::error!("failed to stop stream: {}", err); - } - } - InternalEvent::Resume => { - log::debug!("resuming audio output"); - if let Err(err) = stream.play() { - log::error!("failed to start stream: {}", err); - } - } - InternalEvent::SetVolume(_vol) => { - log::debug!("volume has changed"); - } - } - } - - Ok(()) + pub fn remote(&self) -> AudioOutputRemote { + self.remote.clone() } } -pub trait OutputSample: cpal::Sample + ConvertibleSample + Send + 'static {} - -impl OutputSample for i16 {} -impl OutputSample for u16 {} -impl OutputSample for f32 {} - -struct OutputStream { - stream: cpal::Stream, +#[derive(Clone)] +pub struct AudioOutputRemote { + sender: Sender, } -impl OutputStream { - fn try_open( - device: &cpal::Device, - spec: SignalSpec, - ring_buf_cons: rb::Consumer, - ) -> Result { - let config = Self::config_for_spec(&spec); - let stream = device.build_output_stream( - &config, - move |output: &mut [T], _| { - // Write out as many samples as possible from the ring buffer to the audio - // output. - let written = ring_buf_cons.read(output).unwrap_or(0); - // Mute any remaining samples. - output[written..].iter_mut().for_each(|s| *s = T::MID); - }, - |err| { - log::error!("audio output error: {}", err); - }, - )?; - Ok(Self { stream }) +impl AudioOutputRemote { + pub fn pause(&self) { + self.send(OutputStreamMsg::Pause); } - fn config_for_spec(spec: &SignalSpec) -> cpal::StreamConfig { - cpal::StreamConfig { - channels: spec.channels.count() as cpal::ChannelCount, - sample_rate: cpal::SampleRate(spec.rate), - buffer_size: cpal::BufferSize::Default, - } + pub fn resume(&self) { + self.send(OutputStreamMsg::Resume); } - fn play(&self) -> Result<(), Error> { - Ok(self.stream.play()?) + pub fn close(&self) { + self.send(OutputStreamMsg::Close); + } + + pub fn set_volume(&self, _volume: f64) { + // TODO } - fn pause(&self) -> Result<(), Error> { - Ok(self.stream.pause()?) + fn send(&self, msg: OutputStreamMsg) { + if self.sender.send(msg).is_err() { + log::error!("output stream actor is dead"); + } } } +pub trait OutputSample: cpal::Sample + Sample + Default + Send + 'static {} + +impl OutputSample for i16 {} +impl OutputSample for u16 {} +impl OutputSample for f32 {} + +#[derive(Clone)] pub struct AudioSink { - ring_buf: rb::SpscRb, - ring_buf_prod: rb::Producer, + ring_buf_prod: Arc>, + sample_rate: cpal::SampleRate, } impl AudioSink { - fn new(ring_buf: rb::SpscRb) -> AudioSink { - Self { - ring_buf_prod: ring_buf.producer(), - ring_buf, - } - } - - pub fn clear(&self) { - self.ring_buf.clear(); + pub fn sample_rate(&self) -> u32 { + self.sample_rate.0 } - pub fn write_blocking(&self, sample_buf: &SampleBuffer) -> Result<(), Error> { + pub fn write_blocking(&self, samples: &[T]) -> Result<(), Error> { // Write out all samples from the sample buffer to the ring buffer. let mut i = 0; - while i < sample_buf.len() { - let writeable_samples = &sample_buf.samples()[i..]; + while i < samples.len() { + let writeable_samples = &samples[i..]; // Write as many samples as possible to the ring buffer. This blocks until some // samples are written or the consumer has been destroyed (None is returned). @@ -176,40 +130,72 @@ impl AudioSink { } } -#[derive(Clone)] -pub struct AudioOutputRemote { - event_sender: Sender, +enum OutputStreamMsg { + Pause, + Resume, + Close, } -impl AudioOutputRemote { - pub fn close(&self) { - self.send(InternalEvent::Close); - } - - pub fn pause(&self) { - self.send(InternalEvent::Pause); - } +struct OutputStream { + _device: cpal::Device, + stream: cpal::Stream, +} - pub fn resume(&self) { - self.send(InternalEvent::Resume); +impl OutputStream { + fn open( + device: cpal::Device, + config: cpal::StreamConfig, + ring_buf_cons: rb::Consumer, + ) -> Result { + let stream = device.build_output_stream( + &config, + move |output: &mut [T], _| { + // Write out as many samples as possible from the ring buffer to the audio + // output. + let written = ring_buf_cons.read(output).unwrap_or(0); + // Mute any remaining samples. + output[written..].iter_mut().for_each(|s| *s = T::MID); + }, + |err| { + log::error!("audio output error: {}", err); + }, + )?; + Ok(Self { + _device: device, + stream, + }) } +} - pub fn set_volume(&self, volume: f64) { - self.send(InternalEvent::SetVolume(volume)); - } +impl Actor for OutputStream { + type Message = OutputStreamMsg; + type Error = Error; - fn send(&self, event: InternalEvent) { - self.event_sender.send(event).expect("Audio output died"); + fn handle(&mut self, msg: Self::Message) -> Result { + match msg { + OutputStreamMsg::Pause => { + log::debug!("pausing audio output stream"); + if let Err(err) = self.stream.pause() { + log::error!("failed to stop stream: {}", err); + } + Ok(ActorOp::Continue) + } + OutputStreamMsg::Resume => { + log::debug!("resuming audio output stream"); + if let Err(err) = self.stream.play() { + log::error!("failed to start stream: {}", err); + } + Ok(ActorOp::Continue) + } + OutputStreamMsg::Close => { + log::debug!("closing audio output stream"); + let _ = self.stream.pause(); + Ok(ActorOp::Shutdown) + } + } } } -enum InternalEvent { - Close, - Pause, - Resume, - SetVolume(f64), -} - impl From for Error { fn from(err: cpal::DefaultStreamConfigError) -> Error { Error::AudioOutputError(Box::new(err)) diff --git a/psst-core/src/audio_player.rs b/psst-core/src/audio_player.rs index bc7bb478..9a37c367 100644 --- a/psst-core/src/audio_player.rs +++ b/psst-core/src/audio_player.rs @@ -1,16 +1,17 @@ -use std::{mem, sync::Arc, thread, thread::JoinHandle, time::Duration}; +use std::{mem, thread, thread::JoinHandle, time::Duration}; use crossbeam_channel::{unbounded, Receiver, Sender}; use symphonia::core::audio::{SampleBuffer, SignalSpec}; use crate::{ - actor::{Actor, ActorOp, Handle}, + actor::{Actor, ActorHandle, ActorOp}, audio_decode::AudioDecoder, audio_file::{AudioFile, AudioPath}, audio_key::AudioKey, audio_normalize::NormalizationLevel, audio_output::{AudioOutput, AudioOutputRemote, AudioSink}, audio_queue::{Queue, QueueBehavior}, + audio_resample::{AudioResampler, ResamplingAlgo, ResamplingSpec}, cache::CacheHandle, cdn::CdnHandle, error::Error, @@ -180,7 +181,7 @@ pub struct Player { event_sender: Sender, event_receiver: Receiver, audio_output_remote: AudioOutputRemote, - audio_output_sink: Arc>, + audio_output_sink: AudioSink, consecutive_loading_failures: usize, } @@ -678,7 +679,7 @@ enum PreloadState { } struct PlaybackWorker { - actor: Handle, + actor: ActorHandle, } impl PlaybackWorker { @@ -705,6 +706,8 @@ impl Drop for PlaybackWorker { } } +const REPORT_POSITION_EACH: Duration = Duration::from_millis(1000); + enum Decode { Start, Stop, @@ -723,11 +726,13 @@ struct Decoding { file: AudioFile, source: AudioDecoder, norm_factor: f32, + resampler: AudioResampler, samples: SampleBuffer, events: Sender, this: Sender, - sink: Arc>, + sink: AudioSink, state: DecState, + last_reported_position: Duration, } impl Decoding { @@ -735,11 +740,24 @@ impl Decoding { loaded: LoadedPlaybackItem, events: Sender, this: Sender, - sink: Arc>, + sink: AudioSink, ) -> Self { - let file = loaded.file; - let source = loaded.source; - let norm_factor = loaded.norm_factor; + let LoadedPlaybackItem { + file, + source, + norm_factor, + } = loaded; + let resampler = AudioResampler::new( + // TODO: Make the quality configurable. + ResamplingAlgo::SincMediumQuality, + ResamplingSpec { + channels: source.channels().unwrap().count(), + from_rate: source.sample_rate().unwrap() as usize, + to_rate: sink.sample_rate() as usize, + }, + 1024 * 8, + ) + .unwrap(); let samples = { let max_frames = source.max_frames_per_packet().unwrap_or(1024 * 8); let channels = source.channels().unwrap(); @@ -750,17 +768,45 @@ impl Decoding { file, source, norm_factor, + resampler, samples, events, this, sink, state: DecState::Stopped, + last_reported_position: Duration::ZERO, } } fn frames_to_duration(&self, frames: u64) -> Duration { Duration::from_secs_f64(frames as f64 / self.source.sample_rate().unwrap() as f64) } + + fn report_position(&mut self, position: Duration) { + self.events + .send(PlayerEvent::Position { + path: self.file.path(), + position, + }) + .unwrap(); + self.last_reported_position = position; + } + + fn report_current_position(&mut self) { + let position = self.frames_to_duration(self.source.current_frame()); + self.report_position(position); + } + + fn report_current_position_if_neeeded(&mut self) { + let position = self.frames_to_duration(self.source.current_frame()); + if position.saturating_sub(self.last_reported_position) > REPORT_POSITION_EACH { + self.report_position(position); + } + } + + fn is_started(&self) -> bool { + matches!(self.state, DecState::Started) + } } impl Actor for Decoding { @@ -769,60 +815,57 @@ impl Actor for Decoding { fn handle(&mut self, msg: Self::Message) -> Result { match msg { - Decode::Start => { - if let DecState::Stopped = self.state { - self.state = DecState::Started; - self.this.send(Decode::ReadPacket).unwrap(); - } + Decode::Start if !self.is_started() => { + self.this.send(Decode::ReadPacket)?; + self.state = DecState::Started; + Ok(ActorOp::Continue) } - Decode::Stop => { + Decode::Stop if self.is_started() => { self.state = DecState::Stopped; + Ok(ActorOp::Continue) } - Decode::Seek(pos) => match self.source.seek(pos) { - Ok(new_pos) => { - self.events - .send(PlayerEvent::Position { - path: self.file.path(), - position: self.frames_to_duration(new_pos), - }) - .unwrap(); - } - Err(err) => { - log::error!("failed to seek: {}", err); - } - }, - Decode::ReadPacket => { - if let DecState::Started = self.state { - match self.source.next_packet() { - Some(packet) => { - // TODO: Apply `self.norm_factor`. - // TODO: Apply global volume level. - self.samples.copy_interleaved_ref(packet); - self.this.send(Decode::FlushPacket).unwrap(); - self.events - .send(PlayerEvent::Position { - path: self.file.path(), - position: self.frames_to_duration(self.source.current_frame()), - }) - .unwrap(); - } - None => { - self.events.send(PlayerEvent::EndOfTrack).unwrap(); - return Ok(ActorOp::Shutdown); - } - } - } - } - Decode::FlushPacket => { - self.sink.write_blocking(&self.samples)?; - if let DecState::Started = self.state { - self.this.send(Decode::ReadPacket).unwrap(); - } - } - Decode::Quit => { + Decode::Seek(pos) => self.handle_seek(pos), + Decode::ReadPacket => self.handle_read_packet(), + Decode::FlushPacket => self.handle_flush_packet(), + Decode::Quit => Ok(ActorOp::Shutdown), + _ => Ok(ActorOp::Continue), + } + } +} + +impl Decoding { + fn handle_seek(&mut self, position: Duration) -> Result { + if let Err(err) = self.source.seek(position) { + log::error!("failed to seek: {}", err); + } else { + self.report_current_position(); + } + Ok(ActorOp::Continue) + } + + fn handle_read_packet(&mut self) -> Result { + if self.is_started() { + if let Some(packet) = self.source.next_packet() { + self.samples.copy_interleaved_ref(packet); + self.report_current_position_if_neeeded(); + self.this.send(Decode::FlushPacket)?; + } else { + self.events.send(PlayerEvent::EndOfTrack)?; return Ok(ActorOp::Shutdown); } } Ok(ActorOp::Continue) } + + fn handle_flush_packet(&mut self) -> Result { + let samples = self.samples.samples(); + let resampled = self.resampler.resample(samples)?; + // TODO: Apply `self.norm_factor`. + // TODO: Apply global volume level. + self.sink.write_blocking(resampled)?; + if self.is_started() { + self.this.send(Decode::ReadPacket)?; + } + Ok(ActorOp::Continue) + } } diff --git a/psst-core/src/audio_resample.rs b/psst-core/src/audio_resample.rs new file mode 100644 index 00000000..a7ec8f39 --- /dev/null +++ b/psst-core/src/audio_resample.rs @@ -0,0 +1,86 @@ +use crate::error::Error; + +#[derive(Copy, Clone)] +pub enum ResamplingAlgo { + SincBestQuality = libsamplerate::SRC_SINC_BEST_QUALITY as isize, + SincMediumQuality = libsamplerate::SRC_SINC_MEDIUM_QUALITY as isize, + SincFastest = libsamplerate::SRC_SINC_FASTEST as isize, + ZeroOrderHold = libsamplerate::SRC_ZERO_ORDER_HOLD as isize, + Linear = libsamplerate::SRC_LINEAR as isize, +} + +pub struct ResamplingSpec { + pub from_rate: usize, + pub to_rate: usize, + pub channels: usize, +} + +impl ResamplingSpec { + fn ratio(&self) -> f64 { + self.to_rate as f64 / self.from_rate as f64 + } +} + +pub struct AudioResampler { + state: *mut libsamplerate::SRC_STATE, + spec: ResamplingSpec, + output: Vec, +} + +impl AudioResampler { + pub fn new(algo: ResamplingAlgo, spec: ResamplingSpec, capacity: usize) -> Result { + let mut error_int = 0i32; + let state = unsafe { + libsamplerate::src_new( + algo as i32, + spec.channels as i32, + &mut error_int as *mut i32, + ) + }; + if error_int != 0 { + Err(Error::ResamplingError(error_int)) + } else { + Ok(Self { + state, + spec, + output: vec![0.0; capacity], + }) + } + } + + pub fn resample(&mut self, inter_input: &[f32]) -> Result<&mut [f32], Error> { + if self.spec.from_rate == self.spec.to_rate { + // Bypass conversion completely in case the sample rates are equal. + let output = &mut self.output[..inter_input.len()]; + output.copy_from_slice(inter_input); + return Ok(output); + } + let mut src = libsamplerate::SRC_DATA { + data_in: inter_input.as_ptr(), + data_out: self.output.as_mut_ptr(), + input_frames: (inter_input.len() / self.spec.channels) as _, + output_frames: (self.output.len() / self.spec.channels) as _, + src_ratio: self.spec.ratio(), + end_of_input: 0, // TODO: Use this. + input_frames_used: 0, + output_frames_gen: 0, + }; + let error_int = unsafe { libsamplerate::src_process(self.state, &mut src as *mut _) }; + if error_int != 0 { + Err(Error::ResamplingError(error_int)) + } else { + let output_len = src.output_frames_gen as usize * self.spec.channels; + let processed_len = src.input_frames_used as usize * self.spec.channels; + if processed_len != inter_input.len() { + log::warn!("skipping frames while resampling"); + } + Ok(&mut self.output[..output_len]) + } + } +} + +impl Drop for AudioResampler { + fn drop(&mut self) { + unsafe { libsamplerate::src_delete(self.state) }; + } +} diff --git a/psst-core/src/error.rs b/psst-core/src/error.rs index 71f9abb8..9ffc1fed 100644 --- a/psst-core/src/error.rs +++ b/psst-core/src/error.rs @@ -11,7 +11,9 @@ pub enum Error { AudioFetchingError(Box), AudioDecodingError(Box), AudioOutputError(Box), + ResamplingError(i32), IoError(io::Error), + SendError, } impl error::Error for Error {} @@ -35,17 +37,17 @@ impl fmt::Display for Error { 15 => write!(f, "Authentication failed: extra verification required"), 16 => write!(f, "Authentication failed: invalid app key"), 17 => write!(f, "Authentication failed: application banned"), - _ => write!( - f, - "Authentication failed with error code {code}", - code = code - ), + _ => write!(f, "Authentication failed with error code {}", code), }, + Self::ResamplingError(code) => { + write!(f, "Resampling failed with error code {}", code) + } Self::JsonError(err) | Self::AudioFetchingError(err) | Self::AudioDecodingError(err) | Self::AudioOutputError(err) => err.fmt(f), Self::IoError(err) => err.fmt(f), + Self::SendError => write!(f, "Failed to send into a channel"), } } } @@ -55,3 +57,9 @@ impl From for Error { Error::IoError(err) } } + +impl From> for Error { + fn from(_: crossbeam_channel::SendError) -> Self { + Error::SendError + } +} diff --git a/psst-core/src/lib.rs b/psst-core/src/lib.rs index 4d9c5a31..d46e102e 100644 --- a/psst-core/src/lib.rs +++ b/psst-core/src/lib.rs @@ -10,6 +10,7 @@ pub mod audio_normalize; pub mod audio_output; pub mod audio_player; pub mod audio_queue; +pub mod audio_resample; pub mod cache; pub mod cdn; pub mod connection; diff --git a/psst-gui/src/controller/playback.rs b/psst-gui/src/controller/playback.rs index 018f9b60..19de3ff7 100644 --- a/psst-gui/src/controller/playback.rs +++ b/psst-gui/src/controller/playback.rs @@ -32,7 +32,7 @@ use crate::{ pub struct PlaybackController { sender: Option>, thread: Option>, - output_thread: Option>, + output: Option, media_controls: Option, } @@ -41,7 +41,7 @@ impl PlaybackController { Self { sender: None, thread: None, - output_thread: None, + output: None, media_controls: None, } } @@ -69,9 +69,6 @@ impl PlaybackController { let thread = thread::spawn(move || { Self::service_events(player, event_sink, widget_id); }); - let output_thread = thread::spawn(move || { - output.play().expect("Playback failed"); - }); let hwnd = { #[cfg(target_os = "windows")] @@ -104,7 +101,7 @@ impl PlaybackController { self.sender.replace(sender); self.thread.replace(thread); - self.output_thread.replace(output_thread); + self.output.replace(output); self.media_controls.replace(media_controls); } From 42da6b63a7ff1ce478df5ac3f130d10f3e58ef1b Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Mon, 1 Nov 2021 14:36:10 +0100 Subject: [PATCH 03/14] core: Apply global volume and norm factor --- psst-cli/src/main.rs | 3 +- psst-core/src/audio_output.rs | 66 +++++++++++++---------------------- psst-core/src/audio_player.rs | 59 ++++++++++++++++++++++++++----- 3 files changed, 76 insertions(+), 52 deletions(-) diff --git a/psst-cli/src/main.rs b/psst-cli/src/main.rs index 84b83689..4880e268 100644 --- a/psst-cli/src/main.rs +++ b/psst-cli/src/main.rs @@ -52,7 +52,6 @@ fn play_item( item: PlaybackItem, ) -> Result<(), Error> { let output = AudioOutput::open()?; - let output_remote = output.remote(); let config = PlaybackConfig::default(); let mut player = Player::new(session, cdn, cache, config, &output); @@ -104,7 +103,7 @@ fn play_item( for event in player.event_receiver() { player.handle(event); } - output_remote.close(); + output.sink().close(); Ok(()) } diff --git a/psst-core/src/audio_output.rs b/psst-core/src/audio_output.rs index 43003e85..fca70447 100644 --- a/psst-core/src/audio_output.rs +++ b/psst-core/src/audio_output.rs @@ -10,11 +10,10 @@ use crate::{ error::Error, }; -const RING_BUF_SIZE: usize = 1024 * 4; +const RING_BUF_SIZE: usize = 1024 * 8; pub struct AudioOutput { sink: AudioSink, - remote: AudioOutputRemote, _handle: ActorHandle, _ring_buf: rb::SpscRb, } @@ -32,65 +31,29 @@ impl AudioOutput { // Open an output stream with a ring buffer that will get consumed in the audio // thread and get written to in the playback threads (through an `AudioSink`). - // TODO: Support additional sample formats. let ring_buf = rb::SpscRb::new(RING_BUF_SIZE); let handle = OutputStream::spawn_default({ let config = supported.config(); let consumer = ring_buf.consumer(); + // TODO: Support additional sample formats. move |_| OutputStream::open::(device, config, consumer).unwrap() }); let sink = AudioSink { - ring_buf_prod: Arc::new(ring_buf.producer()), sample_rate: supported.sample_rate(), - }; - let remote = AudioOutputRemote { - sender: handle.sender(), + ring_buf_prod: Arc::new(ring_buf.producer()), + msg_sender: handle.sender(), }; Ok(Self { _ring_buf: ring_buf, _handle: handle, sink, - remote, }) } pub fn sink(&self) -> AudioSink { self.sink.clone() } - - pub fn remote(&self) -> AudioOutputRemote { - self.remote.clone() - } -} - -#[derive(Clone)] -pub struct AudioOutputRemote { - sender: Sender, -} - -impl AudioOutputRemote { - pub fn pause(&self) { - self.send(OutputStreamMsg::Pause); - } - - pub fn resume(&self) { - self.send(OutputStreamMsg::Resume); - } - - pub fn close(&self) { - self.send(OutputStreamMsg::Close); - } - - pub fn set_volume(&self, _volume: f64) { - // TODO - } - - fn send(&self, msg: OutputStreamMsg) { - if self.sender.send(msg).is_err() { - log::error!("output stream actor is dead"); - } - } } pub trait OutputSample: cpal::Sample + Sample + Default + Send + 'static {} @@ -101,8 +64,9 @@ impl OutputSample for f32 {} #[derive(Clone)] pub struct AudioSink { - ring_buf_prod: Arc>, sample_rate: cpal::SampleRate, + ring_buf_prod: Arc>, + msg_sender: Sender, } impl AudioSink { @@ -128,6 +92,24 @@ impl AudioSink { Ok(()) } + + pub fn pause(&self) { + self.send(OutputStreamMsg::Pause); + } + + pub fn resume(&self) { + self.send(OutputStreamMsg::Resume); + } + + pub fn close(&self) { + self.send(OutputStreamMsg::Close); + } + + fn send(&self, msg: OutputStreamMsg) { + if self.msg_sender.send(msg).is_err() { + log::error!("output stream actor is dead"); + } + } } enum OutputStreamMsg { diff --git a/psst-core/src/audio_player.rs b/psst-core/src/audio_player.rs index 9a37c367..06dd19a3 100644 --- a/psst-core/src/audio_player.rs +++ b/psst-core/src/audio_player.rs @@ -1,4 +1,13 @@ -use std::{mem, thread, thread::JoinHandle, time::Duration}; +use std::{ + mem, + sync::{ + atomic::{AtomicU32, Ordering}, + Arc, + }, + thread, + thread::JoinHandle, + time::Duration, +}; use crossbeam_channel::{unbounded, Receiver, Sender}; use symphonia::core::audio::{SampleBuffer, SignalSpec}; @@ -9,7 +18,7 @@ use crate::{ audio_file::{AudioFile, AudioPath}, audio_key::AudioKey, audio_normalize::NormalizationLevel, - audio_output::{AudioOutput, AudioOutputRemote, AudioSink}, + audio_output::{AudioOutput, AudioSink}, audio_queue::{Queue, QueueBehavior}, audio_resample::{AudioResampler, ResamplingAlgo, ResamplingSpec}, cache::CacheHandle, @@ -180,8 +189,8 @@ pub struct Player { queue: Queue, event_sender: Sender, event_receiver: Receiver, - audio_output_remote: AudioOutputRemote, audio_output_sink: AudioSink, + audio_volume: VolumeLevel, consecutive_loading_failures: usize, } @@ -201,8 +210,8 @@ impl Player { config, event_sender, event_receiver, - audio_output_remote: audio_output.remote(), audio_output_sink: audio_output.sink(), + audio_volume: VolumeLevel::new(), state: PlayerState::Stopped, preload: PreloadState::None, queue: Queue::new(), @@ -411,7 +420,7 @@ impl Player { } fn set_volume(&mut self, volume: f64) { - self.audio_output_remote.set_volume(volume); + self.audio_volume.set(volume as _); } fn play_loaded(&mut self, loaded_item: LoadedPlaybackItem) { @@ -422,7 +431,8 @@ impl Player { actor: Decoding::spawn_default({ let events = self.event_sender.clone(); let sink = self.audio_output_sink.clone(); - move |this| Decoding::new(loaded_item, events, this, sink) + let volume = self.audio_volume.clone(); + move |this| Decoding::new(loaded_item, events, this, sink, volume) }), }; worker.start(); @@ -731,6 +741,7 @@ struct Decoding { events: Sender, this: Sender, sink: AudioSink, + volume: VolumeLevel, state: DecState, last_reported_position: Duration, } @@ -741,6 +752,7 @@ impl Decoding { events: Sender, this: Sender, sink: AudioSink, + volume: VolumeLevel, ) -> Self { let LoadedPlaybackItem { file, @@ -773,6 +785,7 @@ impl Decoding { events, this, sink, + volume, state: DecState::Stopped, last_reported_position: Duration::ZERO, } @@ -859,13 +872,43 @@ impl Decoding { fn handle_flush_packet(&mut self) -> Result { let samples = self.samples.samples(); + + // Resample the sample buffer into a rate that the audio output supports. let resampled = self.resampler.resample(samples)?; - // TODO: Apply `self.norm_factor`. - // TODO: Apply global volume level. + + // Apply the global volume level and the normalization factor. + let factor = self.norm_factor * self.volume.get(); + for sample in resampled.iter_mut() { + *sample *= factor; + } + + // Write into the sink, block until all samples are committed to the ring buffer. self.sink.write_blocking(resampled)?; + if self.is_started() { self.this.send(Decode::ReadPacket)?; } Ok(ActorOp::Continue) } } + +#[derive(Clone)] +struct VolumeLevel { + volume: Arc, +} + +impl VolumeLevel { + fn new() -> Self { + Self { + volume: Arc::new(AtomicU32::new(0)), + } + } + + fn set(&self, volume: f32) { + self.volume.store(volume.to_bits(), Ordering::Relaxed) + } + + fn get(&self) -> f32 { + f32::from_bits(self.volume.load(Ordering::Relaxed)) + } +} From 00924048fe405833c0ca93681d98bcbd5ad962f5 Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Wed, 3 Nov 2021 11:14:28 +0100 Subject: [PATCH 04/14] core: Huge mod reorg --- psst-cli/src/main.rs | 9 +- psst-core/src/actor.rs | 10 +- .../src/{audio_decode.rs => audio/decode.rs} | 0 .../{audio_decrypt.rs => audio/decrypt.rs} | 13 +- psst-core/src/audio/mod.rs | 5 + .../normalize.rs} | 0 .../src/{audio_output.rs => audio/output.rs} | 8 +- .../{audio_resample.rs => audio/resample.rs} | 10 +- psst-core/src/cache.rs | 2 +- psst-core/src/cdn.rs | 4 +- psst-core/src/lib.rs | 14 +- psst-core/src/metadata.rs | 2 +- .../src/{audio_file.rs => player/file.rs} | 12 +- psst-core/src/player/item.rs | 147 ++++++ .../src/{audio_player.rs => player/mod.rs} | 446 +++--------------- .../src/{audio_queue.rs => player/queue.rs} | 2 +- .../{stream_storage.rs => player/storage.rs} | 0 psst-core/src/player/worker.rs | 206 ++++++++ psst-core/src/{ => session}/access_token.rs | 4 +- psst-core/src/{ => session}/audio_key.rs | 11 +- psst-core/src/{ => session}/mercury.rs | 0 psst-core/src/{session.rs => session/mod.rs} | 12 +- psst-gui/src/controller/playback.rs | 17 +- psst-gui/src/data/config.rs | 2 +- psst-gui/src/webapi/client.rs | 3 +- 25 files changed, 489 insertions(+), 450 deletions(-) rename psst-core/src/{audio_decode.rs => audio/decode.rs} (100%) rename psst-core/src/{audio_decrypt.rs => audio/decrypt.rs} (81%) create mode 100644 psst-core/src/audio/mod.rs rename psst-core/src/{audio_normalize.rs => audio/normalize.rs} (100%) rename psst-core/src/{audio_output.rs => audio/output.rs} (96%) rename psst-core/src/{audio_resample.rs => audio/resample.rs} (93%) rename psst-core/src/{audio_file.rs => player/file.rs} (97%) create mode 100644 psst-core/src/player/item.rs rename psst-core/src/{audio_player.rs => player/mod.rs} (55%) rename psst-core/src/{audio_queue.rs => player/queue.rs} (98%) rename psst-core/src/{stream_storage.rs => player/storage.rs} (100%) create mode 100644 psst-core/src/player/worker.rs rename psst-core/src/{ => session}/access_token.rs (97%) rename psst-core/src/{ => session}/audio_key.rs (90%) rename psst-core/src/{ => session}/mercury.rs (100%) rename psst-core/src/{session.rs => session/mod.rs} (98%) diff --git a/psst-cli/src/main.rs b/psst-cli/src/main.rs index 4880e268..391542fe 100644 --- a/psst-cli/src/main.rs +++ b/psst-cli/src/main.rs @@ -1,12 +1,11 @@ use psst_core::{ - audio_normalize::NormalizationLevel, - audio_output::AudioOutput, - audio_player::{PlaybackConfig, PlaybackItem, Player, PlayerCommand, PlayerEvent}, + audio::{normalize::NormalizationLevel, output::AudioOutput}, cache::{Cache, CacheHandle}, cdn::{Cdn, CdnHandle}, connection::Credentials, error::Error, item_id::{ItemId, ItemIdType}, + player::{item::PlaybackItem, PlaybackConfig, Player, PlayerCommand, PlayerEvent}, session::{SessionConfig, SessionService}, }; use std::{env, io, io::BufRead, path::PathBuf, thread}; @@ -57,7 +56,7 @@ fn play_item( let mut player = Player::new(session, cdn, cache, config, &output); let _ui_thread = thread::spawn({ - let player_sender = player.event_sender(); + let player_sender = player.sender(); player_sender .send(PlayerEvent::Command(PlayerCommand::LoadQueue { @@ -100,7 +99,7 @@ fn play_item( } }); - for event in player.event_receiver() { + for event in player.receiver() { player.handle(event); } output.sink().close(); diff --git a/psst-core/src/actor.rs b/psst-core/src/actor.rs index 37527c3a..45e0af8c 100644 --- a/psst-core/src/actor.rs +++ b/psst-core/src/actor.rs @@ -3,7 +3,7 @@ use std::{ thread::{self, JoinHandle}, }; -use crossbeam_channel::{bounded, unbounded, Receiver, Sender}; +use crossbeam_channel::{bounded, unbounded, Receiver, SendError, Sender, TrySendError}; pub enum ActorOp { Continue, @@ -67,6 +67,14 @@ impl ActorHandle { pub fn join(self) { let _ = self.thread.join(); } + + pub fn send(&self, msg: M) -> Result<(), SendError> { + self.sender.send(msg) + } + + pub fn try_send(&self, msg: M) -> Result<(), TrySendError> { + self.sender.try_send(msg) + } } pub enum Capacity { diff --git a/psst-core/src/audio_decode.rs b/psst-core/src/audio/decode.rs similarity index 100% rename from psst-core/src/audio_decode.rs rename to psst-core/src/audio/decode.rs diff --git a/psst-core/src/audio_decrypt.rs b/psst-core/src/audio/decrypt.rs similarity index 81% rename from psst-core/src/audio_decrypt.rs rename to psst-core/src/audio/decrypt.rs index ca38ebba..83c85bc2 100644 --- a/psst-core/src/audio_decrypt.rs +++ b/psst-core/src/audio/decrypt.rs @@ -1,16 +1,23 @@ -use std::io; +use std::{convert::TryInto, io}; use aes::{ cipher::{generic_array::GenericArray, NewCipher, StreamCipher, StreamCipherSeek}, Aes128Ctr, }; -use crate::audio_key::AudioKey; - const AUDIO_AESIV: [u8; 16] = [ 0x72, 0xe0, 0x67, 0xfb, 0xdd, 0xcb, 0xcf, 0x77, 0xeb, 0xe8, 0xbc, 0x64, 0x3f, 0x63, 0x0d, 0x93, ]; +#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] +pub struct AudioKey(pub [u8; 16]); + +impl AudioKey { + pub fn from_raw(data: &[u8]) -> Option { + Some(AudioKey(data.try_into().ok()?)) + } +} + pub struct AudioDecrypt { cipher: Aes128Ctr, reader: T, diff --git a/psst-core/src/audio/mod.rs b/psst-core/src/audio/mod.rs new file mode 100644 index 00000000..472a8aeb --- /dev/null +++ b/psst-core/src/audio/mod.rs @@ -0,0 +1,5 @@ +pub mod decode; +pub mod decrypt; +pub mod normalize; +pub mod output; +pub mod resample; diff --git a/psst-core/src/audio_normalize.rs b/psst-core/src/audio/normalize.rs similarity index 100% rename from psst-core/src/audio_normalize.rs rename to psst-core/src/audio/normalize.rs diff --git a/psst-core/src/audio_output.rs b/psst-core/src/audio/output.rs similarity index 96% rename from psst-core/src/audio_output.rs rename to psst-core/src/audio/output.rs index fca70447..d34b82e2 100644 --- a/psst-core/src/audio_output.rs +++ b/psst-core/src/audio/output.rs @@ -10,7 +10,7 @@ use crate::{ error::Error, }; -const RING_BUF_SIZE: usize = 1024 * 8; +const RING_BUF_SIZE: usize = 1024 * 16; pub struct AudioOutput { sink: AudioSink, @@ -25,6 +25,10 @@ impl AudioOutput { .default_output_device() .ok_or(cpal::DefaultStreamConfigError::DeviceNotAvailable)?; + if let Ok(name) = device.name() { + log::info!("using audio device: {:?}", name); + } + // Get the default device config, so we know what sample format and sample rate // the device supports. let supported = device.default_output_config()?; @@ -129,6 +133,8 @@ impl OutputStream { config: cpal::StreamConfig, ring_buf_cons: rb::Consumer, ) -> Result { + log::info!("opening output stream: {:?}", config); + let stream = device.build_output_stream( &config, move |output: &mut [T], _| { diff --git a/psst-core/src/audio_resample.rs b/psst-core/src/audio/resample.rs similarity index 93% rename from psst-core/src/audio_resample.rs rename to psst-core/src/audio/resample.rs index a7ec8f39..73345c9d 100644 --- a/psst-core/src/audio_resample.rs +++ b/psst-core/src/audio/resample.rs @@ -1,7 +1,7 @@ use crate::error::Error; #[derive(Copy, Clone)] -pub enum ResamplingAlgo { +pub enum ResamplingQuality { SincBestQuality = libsamplerate::SRC_SINC_BEST_QUALITY as isize, SincMediumQuality = libsamplerate::SRC_SINC_MEDIUM_QUALITY as isize, SincFastest = libsamplerate::SRC_SINC_FASTEST as isize, @@ -28,11 +28,15 @@ pub struct AudioResampler { } impl AudioResampler { - pub fn new(algo: ResamplingAlgo, spec: ResamplingSpec, capacity: usize) -> Result { + pub fn new( + quality: ResamplingQuality, + spec: ResamplingSpec, + capacity: usize, + ) -> Result { let mut error_int = 0i32; let state = unsafe { libsamplerate::src_new( - algo as i32, + quality as i32, spec.channels as i32, &mut error_int as *mut i32, ) diff --git a/psst-core/src/cache.rs b/psst-core/src/cache.rs index 65d4f6f1..2f112a72 100644 --- a/psst-core/src/cache.rs +++ b/psst-core/src/cache.rs @@ -7,7 +7,7 @@ use std::{ use psst_protocol::metadata::Track; use crate::{ - audio_key::AudioKey, + audio::decrypt::AudioKey, error::Error, item_id::{FileId, ItemId}, util::{deserialize_protobuf, serialize_protobuf}, diff --git a/psst-core/src/cdn.rs b/psst-core/src/cdn.rs index 36b083f7..0d4889d7 100644 --- a/psst-core/src/cdn.rs +++ b/psst-core/src/cdn.rs @@ -7,7 +7,9 @@ use std::{ use serde::Deserialize; use crate::{ - access_token::TokenProvider, error::Error, item_id::FileId, session::SessionService, + error::Error, + item_id::FileId, + session::{access_token::TokenProvider, SessionService}, util::default_ureq_agent_builder, }; diff --git a/psst-core/src/lib.rs b/psst-core/src/lib.rs index d46e102e..e0cce20c 100644 --- a/psst-core/src/lib.rs +++ b/psst-core/src/lib.rs @@ -1,25 +1,15 @@ #![allow(clippy::new_without_default)] -pub mod access_token; pub mod actor; -pub mod audio_decode; -pub mod audio_decrypt; -pub mod audio_file; -pub mod audio_key; -pub mod audio_normalize; -pub mod audio_output; -pub mod audio_player; -pub mod audio_queue; -pub mod audio_resample; +pub mod audio; pub mod cache; pub mod cdn; pub mod connection; pub mod error; pub mod item_id; -pub mod mercury; pub mod metadata; +pub mod player; pub mod session; -pub mod stream_storage; pub mod util; pub use psst_protocol as protocol; diff --git a/psst-core/src/metadata.rs b/psst-core/src/metadata.rs index 2185734d..ac18c92d 100644 --- a/psst-core/src/metadata.rs +++ b/psst-core/src/metadata.rs @@ -3,9 +3,9 @@ use std::time::Duration; use quick_protobuf::MessageRead; use crate::{ - audio_file::{AudioFile, AudioPath}, error::Error, item_id::{FileId, ItemId, ItemIdType}, + player::file::{AudioFile, AudioPath}, protocol::metadata::{Restriction, Track}, session::SessionService, }; diff --git a/psst-core/src/audio_file.rs b/psst-core/src/player/file.rs similarity index 97% rename from psst-core/src/audio_file.rs rename to psst-core/src/player/file.rs index 05395067..baa57f92 100644 --- a/psst-core/src/audio_file.rs +++ b/psst-core/src/player/file.rs @@ -9,19 +9,21 @@ use std::{ }; use crate::{ - audio_decode::AudioDecoder, - audio_decrypt::AudioDecrypt, - audio_key::AudioKey, - audio_normalize::NormalizationData, + audio::{ + decode::AudioDecoder, + decrypt::{AudioDecrypt, AudioKey}, + normalize::NormalizationData, + }, cache::CacheHandle, cdn::{CdnHandle, CdnUrl}, error::Error, item_id::{FileId, ItemId}, protocol::metadata::mod_AudioFile::Format, - stream_storage::{StreamReader, StreamRequest, StreamStorage, StreamWriter}, util::OffsetFile, }; +use super::storage::{StreamReader, StreamRequest, StreamStorage, StreamWriter}; + #[derive(Debug, Clone, Copy)] pub struct AudioPath { pub item_id: ItemId, diff --git a/psst-core/src/player/item.rs b/psst-core/src/player/item.rs new file mode 100644 index 00000000..7993c6fe --- /dev/null +++ b/psst-core/src/player/item.rs @@ -0,0 +1,147 @@ +use psst_protocol::metadata::Track; + +use crate::{ + audio::{decode::AudioDecoder, decrypt::AudioKey, normalize::NormalizationLevel}, + cache::CacheHandle, + cdn::CdnHandle, + error::Error, + item_id::{ItemId, ItemIdType}, + metadata::{Fetch, ToAudioPath}, + session::SessionService, +}; + +use super::{ + file::{AudioFile, AudioPath}, + PlaybackConfig, +}; + +pub struct LoadedPlaybackItem { + pub file: AudioFile, + pub source: AudioDecoder, + pub norm_factor: f32, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct PlaybackItem { + pub item_id: ItemId, + pub norm_level: NormalizationLevel, +} + +impl PlaybackItem { + pub fn load( + &self, + session: &SessionService, + cdn: CdnHandle, + cache: CacheHandle, + config: &PlaybackConfig, + ) -> Result { + let path = load_audio_path(self.item_id, session, &cache, config)?; + let key = load_audio_key(&path, session, &cache)?; + let file = AudioFile::open(path, cdn, cache)?; + let (source, norm_data) = file.audio_source(key)?; + let norm_factor = norm_data.factor_for_level(self.norm_level, config.pregain); + Ok(LoadedPlaybackItem { + file, + source, + norm_factor, + }) + } +} + +fn load_audio_path( + item_id: ItemId, + session: &SessionService, + cache: &CacheHandle, + config: &PlaybackConfig, +) -> Result { + match item_id.id_type { + ItemIdType::Track => { + load_audio_path_from_track_or_alternative(item_id, session, cache, config) + } + ItemIdType::Podcast | ItemIdType::Unknown => unimplemented!(), + } +} + +fn load_audio_path_from_track_or_alternative( + item_id: ItemId, + session: &SessionService, + cache: &CacheHandle, + config: &PlaybackConfig, +) -> Result { + let track = load_track(item_id, session, cache)?; + let country = get_country_code(session, cache); + let path = match country { + Some(user_country) if track.is_restricted_in_region(&user_country) => { + // The track is regionally restricted and is unavailable. Let's try to find an + // alternative track. + let alt_id = track + .find_allowed_alternative(&user_country) + .ok_or(Error::AudioFileNotFound)?; + let alt_track = load_track(alt_id, session, cache)?; + let alt_path = alt_track + .to_audio_path(config.bitrate) + .ok_or(Error::AudioFileNotFound)?; + // We've found an alternative track with a fitting audio file. Let's cheat a + // little and pretend we've obtained it from the requested track. + // TODO: We should be honest and display the real track information. + AudioPath { + item_id, + ..alt_path + } + } + _ => { + // Either we do not have a country code loaded or the track is available, return + // it. + track + .to_audio_path(config.bitrate) + .ok_or(Error::AudioFileNotFound)? + } + }; + Ok(path) +} + +fn get_country_code(session: &SessionService, cache: &CacheHandle) -> Option { + if let Some(cached_country_code) = cache.get_country_code() { + Some(cached_country_code) + } else { + let country_code = session.connected().ok()?.get_country_code()?; + if let Err(err) = cache.save_country_code(&country_code) { + log::warn!("failed to save country code to cache: {:?}", err); + } + Some(country_code) + } +} + +fn load_track( + item_id: ItemId, + session: &SessionService, + cache: &CacheHandle, +) -> Result { + if let Some(cached_track) = cache.get_track(item_id) { + Ok(cached_track) + } else { + let track = Track::fetch(session, item_id)?; + if let Err(err) = cache.save_track(item_id, &track) { + log::warn!("failed to save track to cache: {:?}", err); + } + Ok(track) + } +} + +fn load_audio_key( + path: &AudioPath, + session: &SessionService, + cache: &CacheHandle, +) -> Result { + if let Some(cached_key) = cache.get_audio_key(path.item_id, path.file_id) { + Ok(cached_key) + } else { + let key = session + .connected()? + .get_audio_key(path.item_id, path.file_id)?; + if let Err(err) = cache.save_audio_key(path.item_id, path.file_id, &key) { + log::warn!("failed to save audio key to cache: {:?}", err); + } + Ok(key) + } +} diff --git a/psst-core/src/audio_player.rs b/psst-core/src/player/mod.rs similarity index 55% rename from psst-core/src/audio_player.rs rename to psst-core/src/player/mod.rs index 06dd19a3..650932b5 100644 --- a/psst-core/src/audio_player.rs +++ b/psst-core/src/player/mod.rs @@ -1,3 +1,9 @@ +pub mod file; +pub mod item; +pub mod queue; +mod storage; +mod worker; + use std::{ mem, sync::{ @@ -10,26 +16,23 @@ use std::{ }; use crossbeam_channel::{unbounded, Receiver, Sender}; -use symphonia::core::audio::{SampleBuffer, SignalSpec}; use crate::{ - actor::{Actor, ActorHandle, ActorOp}, - audio_decode::AudioDecoder, - audio_file::{AudioFile, AudioPath}, - audio_key::AudioKey, - audio_normalize::NormalizationLevel, - audio_output::{AudioOutput, AudioSink}, - audio_queue::{Queue, QueueBehavior}, - audio_resample::{AudioResampler, ResamplingAlgo, ResamplingSpec}, + actor::Actor, + audio::output::{AudioOutput, AudioSink}, cache::CacheHandle, cdn::CdnHandle, error::Error, - item_id::{ItemId, ItemIdType}, - metadata::{Fetch, ToAudioPath}, - protocol::metadata::Track, session::SessionService, }; +use self::{ + file::AudioPath, + item::{LoadedPlaybackItem, PlaybackItem}, + queue::{Queue, QueueBehavior}, + worker::{Decode, Decoding, DecodingWorker}, +}; + const PREVIOUS_TRACK_THRESHOLD: Duration = Duration::from_secs(3); const STOP_AFTER_CONSECUTIVE_LOADING_FAILURES: usize = 3; @@ -48,137 +51,6 @@ impl Default for PlaybackConfig { } } -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub struct PlaybackItem { - pub item_id: ItemId, - pub norm_level: NormalizationLevel, -} - -impl PlaybackItem { - fn load( - &self, - session: &SessionService, - cdn: CdnHandle, - cache: CacheHandle, - config: &PlaybackConfig, - ) -> Result { - let path = load_audio_path(self.item_id, session, &cache, config)?; - let key = load_audio_key(&path, session, &cache)?; - let file = AudioFile::open(path, cdn, cache)?; - let (source, norm_data) = file.audio_source(key)?; - let norm_factor = norm_data.factor_for_level(self.norm_level, config.pregain); - Ok(LoadedPlaybackItem { - file, - source, - norm_factor, - }) - } -} - -fn load_audio_path( - item_id: ItemId, - session: &SessionService, - cache: &CacheHandle, - config: &PlaybackConfig, -) -> Result { - match item_id.id_type { - ItemIdType::Track => { - load_audio_path_from_track_or_alternative(item_id, session, cache, config) - } - ItemIdType::Podcast | ItemIdType::Unknown => unimplemented!(), - } -} - -fn load_audio_path_from_track_or_alternative( - item_id: ItemId, - session: &SessionService, - cache: &CacheHandle, - config: &PlaybackConfig, -) -> Result { - let track = load_track(item_id, session, cache)?; - let country = get_country_code(session, cache); - let path = match country { - Some(user_country) if track.is_restricted_in_region(&user_country) => { - // The track is regionally restricted and is unavailable. Let's try to find an - // alternative track. - let alt_id = track - .find_allowed_alternative(&user_country) - .ok_or(Error::AudioFileNotFound)?; - let alt_track = load_track(alt_id, session, cache)?; - let alt_path = alt_track - .to_audio_path(config.bitrate) - .ok_or(Error::AudioFileNotFound)?; - // We've found an alternative track with a fitting audio file. Let's cheat a - // little and pretend we've obtained it from the requested track. - // TODO: We should be honest and display the real track information. - AudioPath { - item_id, - ..alt_path - } - } - _ => { - // Either we do not have a country code loaded or the track is available, return - // it. - track - .to_audio_path(config.bitrate) - .ok_or(Error::AudioFileNotFound)? - } - }; - Ok(path) -} - -fn get_country_code(session: &SessionService, cache: &CacheHandle) -> Option { - if let Some(cached_country_code) = cache.get_country_code() { - Some(cached_country_code) - } else { - let country_code = session.connected().ok()?.get_country_code()?; - if let Err(err) = cache.save_country_code(&country_code) { - log::warn!("failed to save country code to cache: {:?}", err); - } - Some(country_code) - } -} - -fn load_track( - item_id: ItemId, - session: &SessionService, - cache: &CacheHandle, -) -> Result { - if let Some(cached_track) = cache.get_track(item_id) { - Ok(cached_track) - } else { - let track = Track::fetch(session, item_id)?; - if let Err(err) = cache.save_track(item_id, &track) { - log::warn!("failed to save track to cache: {:?}", err); - } - Ok(track) - } -} - -fn load_audio_key( - path: &AudioPath, - session: &SessionService, - cache: &CacheHandle, -) -> Result { - if let Some(cached_key) = cache.get_audio_key(path.item_id, path.file_id) { - Ok(cached_key) - } else { - let key = session - .connected()? - .get_audio_key(path.item_id, path.file_id)?; - if let Err(err) = cache.save_audio_key(path.item_id, path.file_id, &key) { - log::warn!("failed to save audio key to cache: {:?}", err); - } - Ok(key) - } -} - -pub struct LoadedPlaybackItem { - file: AudioFile, - source: AudioDecoder, - norm_factor: f32, -} - pub struct Player { state: PlayerState, preload: PreloadState, @@ -187,8 +59,8 @@ pub struct Player { cache: CacheHandle, config: PlaybackConfig, queue: Queue, - event_sender: Sender, - event_receiver: Receiver, + sender: Sender, + receiver: Receiver, audio_output_sink: AudioSink, audio_volume: VolumeLevel, consecutive_loading_failures: usize, @@ -202,14 +74,14 @@ impl Player { config: PlaybackConfig, audio_output: &AudioOutput, ) -> Self { - let (event_sender, event_receiver) = unbounded(); + let (sender, receiver) = unbounded(); Self { session, cdn, cache, config, - event_sender, - event_receiver, + sender, + receiver, audio_output_sink: audio_output.sink(), audio_volume: VolumeLevel::new(), state: PlayerState::Stopped, @@ -219,12 +91,12 @@ impl Player { } } - pub fn event_sender(&self) -> Sender { - self.event_sender.clone() + pub fn sender(&self) -> Sender { + self.sender.clone() } - pub fn event_receiver(&self) -> Receiver { - self.event_receiver.clone() + pub fn receiver(&self) -> Receiver { + self.receiver.clone() } pub fn handle(&mut self, event: PlayerEvent) { @@ -325,7 +197,7 @@ impl Player { *position = new_position; } _ => { - log::warn!("received unexpected position report"); + log::warn!("received ununwraped position report"); } } const PRELOAD_BEFORE_END_OF_TRACK: Duration = Duration::from_secs(30); @@ -375,7 +247,7 @@ impl Player { } // Item is not preloaded yet, load it in a background thread. let loading_handle = thread::spawn({ - let event_sender = self.event_sender.clone(); + let event_sender = self.sender.clone(); let session = self.session.clone(); let cdn = self.cdn.clone(); let cache = self.cache.clone(); @@ -384,12 +256,10 @@ impl Player { let result = item.load(&session, cdn, cache, &config); event_sender .send(PlayerEvent::Loaded { item, result }) - .expect("Failed to send PlayerEvent::Loaded"); + .unwrap(); } }); - self.event_sender - .send(PlayerEvent::Loading { item }) - .expect("Failed to send PlayerEvent::Loading"); + self.sender.send(PlayerEvent::Loading { item }).unwrap(); self.state = PlayerState::Loading { item, _loading_handle: loading_handle, @@ -401,7 +271,7 @@ impl Player { return; } let loading_handle = thread::spawn({ - let event_sender = self.event_sender.clone(); + let event_sender = self.sender.clone(); let session = self.session.clone(); let cdn = self.cdn.clone(); let cache = self.cache.clone(); @@ -410,7 +280,7 @@ impl Player { let result = item.load(&session, cdn, cache, &config); event_sender .send(PlayerEvent::Preloaded { item, result }) - .expect("Failed to send PlayerEvent::Preloaded"); + .unwrap(); } }); self.preload = PreloadState::Preloading { @@ -427,23 +297,21 @@ impl Player { log::info!("starting playback"); let path = loaded_item.file.path(); let position = Duration::default(); - let worker = PlaybackWorker { - actor: Decoding::spawn_default({ - let events = self.event_sender.clone(); - let sink = self.audio_output_sink.clone(); - let volume = self.audio_volume.clone(); - move |this| Decoding::new(loaded_item, events, this, sink, volume) - }), - }; - worker.start(); + let worker = Decoding::spawn_default({ + let events = self.sender.clone(); + let sink = self.audio_output_sink.clone(); + let volume = self.audio_volume.clone(); + move |this| Decoding::new(loaded_item, events, this, sink, volume) + }); + worker.send(Decode::Start).unwrap(); self.state = PlayerState::Playing { path, position, - worker, + worker: DecodingWorker { actor: worker }, }; - self.event_sender + self.sender .send(PlayerEvent::Playing { path, position }) - .expect("Failed to send PlayerEvent::Playing"); + .unwrap(); } fn pause(&mut self) { @@ -459,10 +327,10 @@ impl Player { worker, } => { log::info!("pausing playback"); - worker.stop(); - self.event_sender + worker.actor.send(Decode::Stop).unwrap(); + self.sender .send(PlayerEvent::Pausing { path, position }) - .expect("Failed to send PlayerEvent::Paused"); + .unwrap(); self.state = PlayerState::Paused { path, position, @@ -488,10 +356,10 @@ impl Player { worker, } => { log::info!("resuming playback"); - worker.start(); - self.event_sender + worker.actor.send(Decode::Start).unwrap(); + self.sender .send(PlayerEvent::Resuming { path, position }) - .expect("Failed to send PlayerEvent::Resuming"); + .unwrap(); self.state = PlayerState::Playing { path, position, @@ -537,9 +405,7 @@ impl Player { } fn stop(&mut self) { - self.event_sender - .send(PlayerEvent::Stopped) - .expect("Failed to send PlayerEvent::Stopped"); + self.sender.send(PlayerEvent::Stopped).unwrap(); self.state = PlayerState::Stopped; self.queue.clear(); self.consecutive_loading_failures = 0; @@ -549,7 +415,7 @@ impl Player { if let PlayerState::Playing { worker, .. } | PlayerState::Paused { worker, .. } = &mut self.state { - worker.seek(position); + worker.actor.send(Decode::Seek(position)).unwrap(); } } @@ -665,12 +531,12 @@ enum PlayerState { Playing { path: AudioPath, position: Duration, - worker: PlaybackWorker, + worker: DecodingWorker, }, Paused { path: AudioPath, position: Duration, - worker: PlaybackWorker, + worker: DecodingWorker, }, Stopped, Invalid, @@ -688,227 +554,23 @@ enum PreloadState { None, } -struct PlaybackWorker { - actor: ActorHandle, -} - -impl PlaybackWorker { - fn start(&self) { - self.actor.sender().send(Decode::Start).unwrap(); - } - - fn stop(&self) { - self.actor.sender().send(Decode::Stop).unwrap(); - } - - fn seek(&self, pos: Duration) { - self.actor.sender().send(Decode::Seek(pos)).unwrap(); - } - - fn quit(&self) { - let _ = self.actor.sender().send(Decode::Quit); - } -} - -impl Drop for PlaybackWorker { - fn drop(&mut self) { - self.quit(); - } -} - -const REPORT_POSITION_EACH: Duration = Duration::from_millis(1000); - -enum Decode { - Start, - Stop, - Seek(Duration), - ReadPacket, - FlushPacket, - Quit, -} - -enum DecState { - Started, - Stopped, -} - -struct Decoding { - file: AudioFile, - source: AudioDecoder, - norm_factor: f32, - resampler: AudioResampler, - samples: SampleBuffer, - events: Sender, - this: Sender, - sink: AudioSink, - volume: VolumeLevel, - state: DecState, - last_reported_position: Duration, -} - -impl Decoding { - fn new( - loaded: LoadedPlaybackItem, - events: Sender, - this: Sender, - sink: AudioSink, - volume: VolumeLevel, - ) -> Self { - let LoadedPlaybackItem { - file, - source, - norm_factor, - } = loaded; - let resampler = AudioResampler::new( - // TODO: Make the quality configurable. - ResamplingAlgo::SincMediumQuality, - ResamplingSpec { - channels: source.channels().unwrap().count(), - from_rate: source.sample_rate().unwrap() as usize, - to_rate: sink.sample_rate() as usize, - }, - 1024 * 8, - ) - .unwrap(); - let samples = { - let max_frames = source.max_frames_per_packet().unwrap_or(1024 * 8); - let channels = source.channels().unwrap(); - let rate = source.sample_rate().unwrap(); - SampleBuffer::new(max_frames, SignalSpec { rate, channels }) - }; - Self { - file, - source, - norm_factor, - resampler, - samples, - events, - this, - sink, - volume, - state: DecState::Stopped, - last_reported_position: Duration::ZERO, - } - } - - fn frames_to_duration(&self, frames: u64) -> Duration { - Duration::from_secs_f64(frames as f64 / self.source.sample_rate().unwrap() as f64) - } - - fn report_position(&mut self, position: Duration) { - self.events - .send(PlayerEvent::Position { - path: self.file.path(), - position, - }) - .unwrap(); - self.last_reported_position = position; - } - - fn report_current_position(&mut self) { - let position = self.frames_to_duration(self.source.current_frame()); - self.report_position(position); - } - - fn report_current_position_if_neeeded(&mut self) { - let position = self.frames_to_duration(self.source.current_frame()); - if position.saturating_sub(self.last_reported_position) > REPORT_POSITION_EACH { - self.report_position(position); - } - } - - fn is_started(&self) -> bool { - matches!(self.state, DecState::Started) - } -} - -impl Actor for Decoding { - type Message = Decode; - type Error = Error; - - fn handle(&mut self, msg: Self::Message) -> Result { - match msg { - Decode::Start if !self.is_started() => { - self.this.send(Decode::ReadPacket)?; - self.state = DecState::Started; - Ok(ActorOp::Continue) - } - Decode::Stop if self.is_started() => { - self.state = DecState::Stopped; - Ok(ActorOp::Continue) - } - Decode::Seek(pos) => self.handle_seek(pos), - Decode::ReadPacket => self.handle_read_packet(), - Decode::FlushPacket => self.handle_flush_packet(), - Decode::Quit => Ok(ActorOp::Shutdown), - _ => Ok(ActorOp::Continue), - } - } -} - -impl Decoding { - fn handle_seek(&mut self, position: Duration) -> Result { - if let Err(err) = self.source.seek(position) { - log::error!("failed to seek: {}", err); - } else { - self.report_current_position(); - } - Ok(ActorOp::Continue) - } - - fn handle_read_packet(&mut self) -> Result { - if self.is_started() { - if let Some(packet) = self.source.next_packet() { - self.samples.copy_interleaved_ref(packet); - self.report_current_position_if_neeeded(); - self.this.send(Decode::FlushPacket)?; - } else { - self.events.send(PlayerEvent::EndOfTrack)?; - return Ok(ActorOp::Shutdown); - } - } - Ok(ActorOp::Continue) - } - - fn handle_flush_packet(&mut self) -> Result { - let samples = self.samples.samples(); - - // Resample the sample buffer into a rate that the audio output supports. - let resampled = self.resampler.resample(samples)?; - - // Apply the global volume level and the normalization factor. - let factor = self.norm_factor * self.volume.get(); - for sample in resampled.iter_mut() { - *sample *= factor; - } - - // Write into the sink, block until all samples are committed to the ring buffer. - self.sink.write_blocking(resampled)?; - - if self.is_started() { - self.this.send(Decode::ReadPacket)?; - } - Ok(ActorOp::Continue) - } -} - #[derive(Clone)] -struct VolumeLevel { +pub struct VolumeLevel { volume: Arc, } impl VolumeLevel { - fn new() -> Self { + pub fn new() -> Self { Self { volume: Arc::new(AtomicU32::new(0)), } } - fn set(&self, volume: f32) { + pub fn set(&self, volume: f32) { self.volume.store(volume.to_bits(), Ordering::Relaxed) } - fn get(&self) -> f32 { + pub fn get(&self) -> f32 { f32::from_bits(self.volume.load(Ordering::Relaxed)) } } diff --git a/psst-core/src/audio_queue.rs b/psst-core/src/player/queue.rs similarity index 98% rename from psst-core/src/audio_queue.rs rename to psst-core/src/player/queue.rs index 2c40974e..243750eb 100644 --- a/psst-core/src/audio_queue.rs +++ b/psst-core/src/player/queue.rs @@ -1,6 +1,6 @@ use rand::prelude::SliceRandom; -use crate::audio_player::PlaybackItem; +use super::PlaybackItem; #[derive(Debug)] pub enum QueueBehavior { diff --git a/psst-core/src/stream_storage.rs b/psst-core/src/player/storage.rs similarity index 100% rename from psst-core/src/stream_storage.rs rename to psst-core/src/player/storage.rs diff --git a/psst-core/src/player/worker.rs b/psst-core/src/player/worker.rs new file mode 100644 index 00000000..e1465e05 --- /dev/null +++ b/psst-core/src/player/worker.rs @@ -0,0 +1,206 @@ +use std::time::Duration; + +use crossbeam_channel::Sender; +use symphonia::core::audio::{SampleBuffer, SignalSpec}; + +use crate::{ + actor::{Actor, ActorHandle, ActorOp}, + audio::{ + decode::AudioDecoder, + output::AudioSink, + resample::{AudioResampler, ResamplingQuality, ResamplingSpec}, + }, + error::Error, +}; + +use super::{file::AudioFile, LoadedPlaybackItem, PlayerEvent, VolumeLevel}; + +pub struct DecodingWorker { + pub actor: ActorHandle, +} + +impl Drop for DecodingWorker { + fn drop(&mut self) { + let _ = self.actor.send(Decode::Quit); + } +} + +const REPORT_POSITION_EACH: Duration = Duration::from_millis(1000); + +pub enum Decode { + Start, + Stop, + Seek(Duration), + ReadPacket, + FlushPacket, + Quit, +} + +enum DecState { + Started, + Stopped, +} + +pub struct Decoding { + file: AudioFile, + source: AudioDecoder, + norm_factor: f32, + resampler: AudioResampler, + samples: SampleBuffer, + events: Sender, + this: Sender, + sink: AudioSink, + volume: VolumeLevel, + state: DecState, + last_reported_position: Duration, +} + +impl Decoding { + pub fn new( + loaded: LoadedPlaybackItem, + events: Sender, + this: Sender, + sink: AudioSink, + volume: VolumeLevel, + ) -> Self { + let LoadedPlaybackItem { + file, + source, + norm_factor, + } = loaded; + let resampler = AudioResampler::new( + // TODO: Make the quality configurable. + ResamplingQuality::SincMediumQuality, + ResamplingSpec { + channels: source.channels().unwrap().count(), + from_rate: source.sample_rate().unwrap() as usize, + to_rate: sink.sample_rate() as usize, + }, + 1024 * 8, + ) + .unwrap(); + let samples = { + let max_frames = source.max_frames_per_packet().unwrap_or(1024 * 8); + let channels = source.channels().unwrap(); + let rate = source.sample_rate().unwrap(); + SampleBuffer::new(max_frames, SignalSpec { rate, channels }) + }; + Self { + file, + source, + norm_factor, + resampler, + samples, + events, + this, + sink, + volume, + state: DecState::Stopped, + last_reported_position: Duration::ZERO, + } + } + + fn frames_to_duration(&self, frames: u64) -> Duration { + Duration::from_secs_f64(frames as f64 / self.source.sample_rate().unwrap() as f64) + } + + fn report_position(&mut self, position: Duration) { + if self + .events + .try_send(PlayerEvent::Position { + path: self.file.path(), + position, + }) + .is_ok() + { + self.last_reported_position = position; + } + } + + fn report_current_position(&mut self) { + let position = self.frames_to_duration(self.source.current_frame()); + self.report_position(position); + } + + fn report_current_position_if_neeeded(&mut self) { + let position = self.frames_to_duration(self.source.current_frame()); + if position.saturating_sub(self.last_reported_position) > REPORT_POSITION_EACH { + self.report_position(position); + } + } + + fn is_started(&self) -> bool { + matches!(self.state, DecState::Started) + } +} + +impl Actor for Decoding { + type Message = Decode; + type Error = Error; + + fn handle(&mut self, msg: Self::Message) -> Result { + match msg { + Decode::Start if !self.is_started() => { + self.this.send(Decode::ReadPacket)?; + self.state = DecState::Started; + Ok(ActorOp::Continue) + } + Decode::Stop if self.is_started() => { + self.state = DecState::Stopped; + Ok(ActorOp::Continue) + } + Decode::Seek(pos) => self.handle_seek(pos), + Decode::ReadPacket => self.handle_read_packet(), + Decode::FlushPacket => self.handle_flush_packet(), + Decode::Quit => Ok(ActorOp::Shutdown), + _ => Ok(ActorOp::Continue), + } + } +} + +impl Decoding { + fn handle_seek(&mut self, position: Duration) -> Result { + if let Err(err) = self.source.seek(position) { + log::error!("failed to seek: {}", err); + } else { + self.report_current_position(); + } + Ok(ActorOp::Continue) + } + + fn handle_read_packet(&mut self) -> Result { + if self.is_started() { + if let Some(packet) = self.source.next_packet() { + self.samples.copy_interleaved_ref(packet); + self.report_current_position_if_neeeded(); + self.this.send(Decode::FlushPacket)?; + } else { + self.events.send(PlayerEvent::EndOfTrack)?; + return Ok(ActorOp::Shutdown); + } + } + Ok(ActorOp::Continue) + } + + fn handle_flush_packet(&mut self) -> Result { + let samples = self.samples.samples(); + + // Resample the sample buffer into a rate that the audio output supports. + let resampled = self.resampler.resample(samples)?; + + // Apply the global volume level and the normalization factor. + let factor = self.norm_factor * self.volume.get(); + for sample in resampled.iter_mut() { + *sample *= factor; + } + + // Write into the sink, block until all samples are committed to the ring + // buffer. + self.sink.write_blocking(resampled)?; + + if self.is_started() { + self.this.send(Decode::ReadPacket)?; + } + Ok(ActorOp::Continue) + } +} diff --git a/psst-core/src/access_token.rs b/psst-core/src/session/access_token.rs similarity index 97% rename from psst-core/src/access_token.rs rename to psst-core/src/session/access_token.rs index 47c06ee1..c962fa4d 100644 --- a/psst-core/src/access_token.rs +++ b/psst-core/src/session/access_token.rs @@ -3,7 +3,9 @@ use std::time::{Duration, Instant}; use parking_lot::Mutex; use serde::Deserialize; -use crate::{error::Error, session::SessionService}; +use crate::error::Error; + +use super::SessionService; // Client ID of the official Web Spotify front-end. const CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd"; diff --git a/psst-core/src/audio_key.rs b/psst-core/src/session/audio_key.rs similarity index 90% rename from psst-core/src/audio_key.rs rename to psst-core/src/session/audio_key.rs index 5a6adcb8..17d36f1b 100644 --- a/psst-core/src/audio_key.rs +++ b/psst-core/src/session/audio_key.rs @@ -1,6 +1,5 @@ use std::{ collections::HashMap, - convert::TryInto, io::{Cursor, Read}, }; @@ -8,21 +7,13 @@ use byteorder::{ReadBytesExt, BE}; use crossbeam_channel::Sender; use crate::{ + audio::decrypt::AudioKey, connection::shannon_codec::ShannonMsg, error::Error, item_id::{FileId, ItemId}, util::Sequence, }; -#[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] -pub struct AudioKey(pub [u8; 16]); - -impl AudioKey { - pub fn from_raw(data: &[u8]) -> Option { - Some(AudioKey(data.try_into().ok()?)) - } -} - pub struct AudioKeyDispatcher { sequence: Sequence, pending: HashMap>>, diff --git a/psst-core/src/mercury.rs b/psst-core/src/session/mercury.rs similarity index 100% rename from psst-core/src/mercury.rs rename to psst-core/src/session/mercury.rs diff --git a/psst-core/src/session.rs b/psst-core/src/session/mod.rs similarity index 98% rename from psst-core/src/session.rs rename to psst-core/src/session/mod.rs index d0492a2a..8204470e 100644 --- a/psst-core/src/session.rs +++ b/psst-core/src/session/mod.rs @@ -1,3 +1,7 @@ +pub mod access_token; +pub mod audio_key; +pub mod mercury; + use std::{ io, net::{Shutdown, TcpStream}, @@ -14,17 +18,21 @@ use quick_protobuf::MessageRead; use serde::de::DeserializeOwned; use crate::{ - audio_key::{AudioKey, AudioKeyDispatcher}, + audio::decrypt::AudioKey, connection::{ shannon_codec::{ShannonDecoder, ShannonEncoder, ShannonMsg}, Credentials, Transport, }, error::Error, item_id::{FileId, ItemId}, - mercury::{MercuryDispatcher, MercuryRequest, MercuryResponse}, util::deserialize_protobuf, }; +use self::{ + audio_key::AudioKeyDispatcher, + mercury::{MercuryDispatcher, MercuryRequest, MercuryResponse}, +}; + /// Configuration values needed to open the session connection. #[derive(Clone)] pub struct SessionConfig { diff --git a/psst-gui/src/controller/playback.rs b/psst-gui/src/controller/playback.rs index 19de3ff7..2e014a50 100644 --- a/psst-gui/src/controller/playback.rs +++ b/psst-gui/src/controller/playback.rs @@ -10,11 +10,10 @@ use druid::{ Code, ExtEventSink, InternalLifeCycle, KbKey, WindowHandle, }; use psst_core::{ - audio_normalize::NormalizationLevel, - audio_output::AudioOutput, - audio_player::{PlaybackConfig, PlaybackItem, Player, PlayerCommand, PlayerEvent}, + audio::{normalize::NormalizationLevel, output::AudioOutput}, cache::Cache, cdn::Cdn, + player::{item::PlaybackItem, PlaybackConfig, Player, PlayerCommand, PlayerEvent}, session::SessionService, }; use souvlaki::{ @@ -64,7 +63,7 @@ impl PlaybackController { config, &output, ); - let sender = player.event_sender(); + let sender = player.sender(); let thread = thread::spawn(move || { Self::service_events(player, event_sink, widget_id); @@ -106,7 +105,7 @@ impl PlaybackController { } fn service_events(mut player: Player, event_sink: ExtEventSink, widget_id: WidgetId) { - for event in player.event_receiver() { + for event in player.receiver() { // Forward events that affect the UI state to the UI thread. match &event { PlayerEvent::Loading { item } => { @@ -264,10 +263,10 @@ impl PlaybackController { fn set_queue_behavior(&mut self, behavior: QueueBehavior) { self.send(PlayerEvent::Command(PlayerCommand::SetQueueBehavior { behavior: match behavior { - QueueBehavior::Sequential => psst_core::audio_queue::QueueBehavior::Sequential, - QueueBehavior::Random => psst_core::audio_queue::QueueBehavior::Random, - QueueBehavior::LoopTrack => psst_core::audio_queue::QueueBehavior::LoopTrack, - QueueBehavior::LoopAll => psst_core::audio_queue::QueueBehavior::LoopAll, + QueueBehavior::Sequential => psst_core::player::queue::QueueBehavior::Sequential, + QueueBehavior::Random => psst_core::player::queue::QueueBehavior::Random, + QueueBehavior::LoopTrack => psst_core::player::queue::QueueBehavior::LoopTrack, + QueueBehavior::LoopAll => psst_core::player::queue::QueueBehavior::LoopAll, }, })); } diff --git a/psst-gui/src/data/config.rs b/psst-gui/src/data/config.rs index 5c9decc5..6fdd7414 100644 --- a/psst-gui/src/data/config.rs +++ b/psst-gui/src/data/config.rs @@ -3,9 +3,9 @@ use std::{env, env::VarError, fs::File, path::PathBuf}; use druid::{Data, Lens}; use platform_dirs::AppDirs; use psst_core::{ - audio_player::PlaybackConfig, cache::mkdir_if_not_exists, connection::Credentials, + player::PlaybackConfig, session::{SessionConfig, SessionConnection}, }; use serde::{Deserialize, Serialize}; diff --git a/psst-gui/src/webapi/client.rs b/psst-gui/src/webapi/client.rs index 520d062c..5580a232 100644 --- a/psst-gui/src/webapi/client.rs +++ b/psst-gui/src/webapi/client.rs @@ -13,7 +13,8 @@ use druid::{ use itertools::Itertools; use once_cell::sync::OnceCell; use psst_core::{ - access_token::TokenProvider, session::SessionService, util::default_ureq_agent_builder, + session::{access_token::TokenProvider, SessionService}, + util::default_ureq_agent_builder, }; use serde::{de::DeserializeOwned, Deserialize}; use std::{ From ea1a6ccca4760ba1a939e4abd0d9c431c31719ab Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Thu, 11 Nov 2021 17:01:58 +0100 Subject: [PATCH 05/14] Update ureq --- psst-core/Cargo.toml | 2 +- psst-gui/src/main.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/psst-core/Cargo.toml b/psst-core/Cargo.toml index 574050da..4004577d 100644 --- a/psst-core/Cargo.toml +++ b/psst-core/Cargo.toml @@ -28,5 +28,5 @@ shannon = "0.2" socks = "0.3" symphonia = { version = "0.4", default-features = false, features = ["ogg", "vorbis"]} tempfile = "3.2" -ureq = { version = "2.1", features = ["json"] } +ureq = { version = "2.2", features = ["json"] } url = "2.2" \ No newline at end of file diff --git a/psst-gui/src/main.rs b/psst-gui/src/main.rs index 11dc9ab1..b8c535eb 100644 --- a/psst-gui/src/main.rs +++ b/psst-gui/src/main.rs @@ -26,8 +26,7 @@ fn main() { // Setup logging from the env variables, with defaults. Builder::from_env( Env::new() - // `ureq` is a bit too noisy, log only warnings by default. - .filter_or(ENV_LOG, "info,ureq::unit=warn") + .filter_or(ENV_LOG, "info") .write_style(ENV_LOG_STYLE), ) .init(); From c0abb270f8dd0b32df7303abeefde4a9a863d2db Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Thu, 11 Nov 2021 17:03:46 +0100 Subject: [PATCH 06/14] Remove minivorbis --- .gitmodules | 3 - minivorbis-sys/Cargo.toml | 8 - minivorbis-sys/bindings.h | 3 - minivorbis-sys/build.rs | 10 - minivorbis-sys/gen.sh | 10 - minivorbis-sys/minivorbis | 1 - minivorbis-sys/minivorbis.c | 6 - minivorbis-sys/src/bindings.rs | 1919 -------------------------------- minivorbis-sys/src/lib.rs | 8 - minivorbis/Cargo.toml | 8 - minivorbis/src/lib.rs | 272 ----- 11 files changed, 2248 deletions(-) delete mode 100644 .gitmodules delete mode 100644 minivorbis-sys/Cargo.toml delete mode 100644 minivorbis-sys/bindings.h delete mode 100644 minivorbis-sys/build.rs delete mode 100755 minivorbis-sys/gen.sh delete mode 160000 minivorbis-sys/minivorbis delete mode 100644 minivorbis-sys/minivorbis.c delete mode 100644 minivorbis-sys/src/bindings.rs delete mode 100644 minivorbis-sys/src/lib.rs delete mode 100644 minivorbis/Cargo.toml delete mode 100644 minivorbis/src/lib.rs diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 2723412e..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "minivorbis-sys/minivorbis"] - path = minivorbis-sys/minivorbis - url = https://github.com/edubart/minivorbis.git diff --git a/minivorbis-sys/Cargo.toml b/minivorbis-sys/Cargo.toml deleted file mode 100644 index 810eb2a6..00000000 --- a/minivorbis-sys/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "minivorbis-sys" -version = "0.1.0" -authors = ["Jan Pochyla "] -edition = "2018" - -[build-dependencies] -cc = "1.0" \ No newline at end of file diff --git a/minivorbis-sys/bindings.h b/minivorbis-sys/bindings.h deleted file mode 100644 index 9770da2f..00000000 --- a/minivorbis-sys/bindings.h +++ /dev/null @@ -1,3 +0,0 @@ -#include - -#include "minivorbis.h" \ No newline at end of file diff --git a/minivorbis-sys/build.rs b/minivorbis-sys/build.rs deleted file mode 100644 index 08026fb3..00000000 --- a/minivorbis-sys/build.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub fn main() { - cc::Build::new() - .file("./minivorbis.c") - .include("./minivorbis") - .compile("libminivorbis"); - - println!("cargo:rerun-if-changed=./minivorbis/minivorbis.h"); - println!("cargo:rerun-if-changed=./minivorbis.c"); - println!("cargo:rerun-if-env-changed=CC"); -} diff --git a/minivorbis-sys/gen.sh b/minivorbis-sys/gen.sh deleted file mode 100755 index ee7f79af..00000000 --- a/minivorbis-sys/gen.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh - -bindgen \ - --whitelist-function "ov_.*" \ - --whitelist-var "OV_.*" \ - --whitelist-var "SEEK_.*" \ - --size_t-is-usize \ - bindings.h \ - -- -I minivorbis \ - > src/bindings.rs \ No newline at end of file diff --git a/minivorbis-sys/minivorbis b/minivorbis-sys/minivorbis deleted file mode 160000 index 4e40a167..00000000 --- a/minivorbis-sys/minivorbis +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4e40a167726effb0ae9d3065452a9ee2bb80a84f diff --git a/minivorbis-sys/minivorbis.c b/minivorbis-sys/minivorbis.c deleted file mode 100644 index 8a245d2c..00000000 --- a/minivorbis-sys/minivorbis.c +++ /dev/null @@ -1,6 +0,0 @@ -#include - -#define OGG_IMPL -#define VORBIS_IMPL -#define OV_EXCLUDE_STATIC_CALLBACKS -#include "minivorbis.h" \ No newline at end of file diff --git a/minivorbis-sys/src/bindings.rs b/minivorbis-sys/src/bindings.rs deleted file mode 100644 index c54663fa..00000000 --- a/minivorbis-sys/src/bindings.rs +++ /dev/null @@ -1,1919 +0,0 @@ -/* automatically generated by rust-bindgen 0.56.0 */ - -pub const OV_FALSE: i32 = -1; -pub const OV_EOF: i32 = -2; -pub const OV_HOLE: i32 = -3; -pub const OV_EREAD: i32 = -128; -pub const OV_EFAULT: i32 = -129; -pub const OV_EIMPL: i32 = -130; -pub const OV_EINVAL: i32 = -131; -pub const OV_ENOTVORBIS: i32 = -132; -pub const OV_EBADHEADER: i32 = -133; -pub const OV_EVERSION: i32 = -134; -pub const OV_ENOTAUDIO: i32 = -135; -pub const OV_EBADPACKET: i32 = -136; -pub const OV_EBADLINK: i32 = -137; -pub const OV_ENOSEEK: i32 = -138; -pub const SEEK_SET: u32 = 0; -pub const SEEK_CUR: u32 = 1; -pub const SEEK_END: u32 = 2; -pub type __int64_t = ::std::os::raw::c_longlong; -pub type __darwin_size_t = ::std::os::raw::c_ulong; -pub type __darwin_off_t = __int64_t; -pub type ogg_int64_t = i64; -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct oggpack_buffer { - pub endbyte: ::std::os::raw::c_long, - pub endbit: ::std::os::raw::c_int, - pub buffer: *mut ::std::os::raw::c_uchar, - pub ptr: *mut ::std::os::raw::c_uchar, - pub storage: ::std::os::raw::c_long, -} -#[test] -fn bindgen_test_layout_oggpack_buffer() { - assert_eq!( - ::std::mem::size_of::(), - 40usize, - concat!("Size of: ", stringify!(oggpack_buffer)) - ); - assert_eq!( - ::std::mem::align_of::(), - 8usize, - concat!("Alignment of ", stringify!(oggpack_buffer)) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).endbyte as *const _ as usize }, - 0usize, - concat!( - "Offset of field: ", - stringify!(oggpack_buffer), - "::", - stringify!(endbyte) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).endbit as *const _ as usize }, - 8usize, - concat!( - "Offset of field: ", - stringify!(oggpack_buffer), - "::", - stringify!(endbit) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).buffer as *const _ as usize }, - 16usize, - concat!( - "Offset of field: ", - stringify!(oggpack_buffer), - "::", - stringify!(buffer) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).ptr as *const _ as usize }, - 24usize, - concat!( - "Offset of field: ", - stringify!(oggpack_buffer), - "::", - stringify!(ptr) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).storage as *const _ as usize }, - 32usize, - concat!( - "Offset of field: ", - stringify!(oggpack_buffer), - "::", - stringify!(storage) - ) - ); -} -#[repr(C)] -#[derive(Copy, Clone)] -pub struct ogg_stream_state { - pub body_data: *mut ::std::os::raw::c_uchar, - pub body_storage: ::std::os::raw::c_long, - pub body_fill: ::std::os::raw::c_long, - pub body_returned: ::std::os::raw::c_long, - pub lacing_vals: *mut ::std::os::raw::c_int, - pub granule_vals: *mut ogg_int64_t, - pub lacing_storage: ::std::os::raw::c_long, - pub lacing_fill: ::std::os::raw::c_long, - pub lacing_packet: ::std::os::raw::c_long, - pub lacing_returned: ::std::os::raw::c_long, - pub header: [::std::os::raw::c_uchar; 282usize], - pub header_fill: ::std::os::raw::c_int, - pub e_o_s: ::std::os::raw::c_int, - pub b_o_s: ::std::os::raw::c_int, - pub serialno: ::std::os::raw::c_long, - pub pageno: ::std::os::raw::c_long, - pub packetno: ogg_int64_t, - pub granulepos: ogg_int64_t, -} -#[test] -fn bindgen_test_layout_ogg_stream_state() { - assert_eq!( - ::std::mem::size_of::(), - 408usize, - concat!("Size of: ", stringify!(ogg_stream_state)) - ); - assert_eq!( - ::std::mem::align_of::(), - 8usize, - concat!("Alignment of ", stringify!(ogg_stream_state)) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).body_data as *const _ as usize }, - 0usize, - concat!( - "Offset of field: ", - stringify!(ogg_stream_state), - "::", - stringify!(body_data) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).body_storage as *const _ as usize }, - 8usize, - concat!( - "Offset of field: ", - stringify!(ogg_stream_state), - "::", - stringify!(body_storage) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).body_fill as *const _ as usize }, - 16usize, - concat!( - "Offset of field: ", - stringify!(ogg_stream_state), - "::", - stringify!(body_fill) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).body_returned as *const _ as usize }, - 24usize, - concat!( - "Offset of field: ", - stringify!(ogg_stream_state), - "::", - stringify!(body_returned) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).lacing_vals as *const _ as usize }, - 32usize, - concat!( - "Offset of field: ", - stringify!(ogg_stream_state), - "::", - stringify!(lacing_vals) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).granule_vals as *const _ as usize }, - 40usize, - concat!( - "Offset of field: ", - stringify!(ogg_stream_state), - "::", - stringify!(granule_vals) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).lacing_storage as *const _ as usize }, - 48usize, - concat!( - "Offset of field: ", - stringify!(ogg_stream_state), - "::", - stringify!(lacing_storage) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).lacing_fill as *const _ as usize }, - 56usize, - concat!( - "Offset of field: ", - stringify!(ogg_stream_state), - "::", - stringify!(lacing_fill) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).lacing_packet as *const _ as usize }, - 64usize, - concat!( - "Offset of field: ", - stringify!(ogg_stream_state), - "::", - stringify!(lacing_packet) - ) - ); - assert_eq!( - unsafe { - &(*(::std::ptr::null::())).lacing_returned as *const _ as usize - }, - 72usize, - concat!( - "Offset of field: ", - stringify!(ogg_stream_state), - "::", - stringify!(lacing_returned) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).header as *const _ as usize }, - 80usize, - concat!( - "Offset of field: ", - stringify!(ogg_stream_state), - "::", - stringify!(header) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).header_fill as *const _ as usize }, - 364usize, - concat!( - "Offset of field: ", - stringify!(ogg_stream_state), - "::", - stringify!(header_fill) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).e_o_s as *const _ as usize }, - 368usize, - concat!( - "Offset of field: ", - stringify!(ogg_stream_state), - "::", - stringify!(e_o_s) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).b_o_s as *const _ as usize }, - 372usize, - concat!( - "Offset of field: ", - stringify!(ogg_stream_state), - "::", - stringify!(b_o_s) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).serialno as *const _ as usize }, - 376usize, - concat!( - "Offset of field: ", - stringify!(ogg_stream_state), - "::", - stringify!(serialno) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).pageno as *const _ as usize }, - 384usize, - concat!( - "Offset of field: ", - stringify!(ogg_stream_state), - "::", - stringify!(pageno) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).packetno as *const _ as usize }, - 392usize, - concat!( - "Offset of field: ", - stringify!(ogg_stream_state), - "::", - stringify!(packetno) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).granulepos as *const _ as usize }, - 400usize, - concat!( - "Offset of field: ", - stringify!(ogg_stream_state), - "::", - stringify!(granulepos) - ) - ); -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct ogg_sync_state { - pub data: *mut ::std::os::raw::c_uchar, - pub storage: ::std::os::raw::c_int, - pub fill: ::std::os::raw::c_int, - pub returned: ::std::os::raw::c_int, - pub unsynced: ::std::os::raw::c_int, - pub headerbytes: ::std::os::raw::c_int, - pub bodybytes: ::std::os::raw::c_int, -} -#[test] -fn bindgen_test_layout_ogg_sync_state() { - assert_eq!( - ::std::mem::size_of::(), - 32usize, - concat!("Size of: ", stringify!(ogg_sync_state)) - ); - assert_eq!( - ::std::mem::align_of::(), - 8usize, - concat!("Alignment of ", stringify!(ogg_sync_state)) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).data as *const _ as usize }, - 0usize, - concat!( - "Offset of field: ", - stringify!(ogg_sync_state), - "::", - stringify!(data) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).storage as *const _ as usize }, - 8usize, - concat!( - "Offset of field: ", - stringify!(ogg_sync_state), - "::", - stringify!(storage) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).fill as *const _ as usize }, - 12usize, - concat!( - "Offset of field: ", - stringify!(ogg_sync_state), - "::", - stringify!(fill) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).returned as *const _ as usize }, - 16usize, - concat!( - "Offset of field: ", - stringify!(ogg_sync_state), - "::", - stringify!(returned) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).unsynced as *const _ as usize }, - 20usize, - concat!( - "Offset of field: ", - stringify!(ogg_sync_state), - "::", - stringify!(unsynced) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).headerbytes as *const _ as usize }, - 24usize, - concat!( - "Offset of field: ", - stringify!(ogg_sync_state), - "::", - stringify!(headerbytes) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).bodybytes as *const _ as usize }, - 28usize, - concat!( - "Offset of field: ", - stringify!(ogg_sync_state), - "::", - stringify!(bodybytes) - ) - ); -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct vorbis_info { - pub version: ::std::os::raw::c_int, - pub channels: ::std::os::raw::c_int, - pub rate: ::std::os::raw::c_long, - pub bitrate_upper: ::std::os::raw::c_long, - pub bitrate_nominal: ::std::os::raw::c_long, - pub bitrate_lower: ::std::os::raw::c_long, - pub bitrate_window: ::std::os::raw::c_long, - pub codec_setup: *mut ::std::os::raw::c_void, -} -#[test] -fn bindgen_test_layout_vorbis_info() { - assert_eq!( - ::std::mem::size_of::(), - 56usize, - concat!("Size of: ", stringify!(vorbis_info)) - ); - assert_eq!( - ::std::mem::align_of::(), - 8usize, - concat!("Alignment of ", stringify!(vorbis_info)) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).version as *const _ as usize }, - 0usize, - concat!( - "Offset of field: ", - stringify!(vorbis_info), - "::", - stringify!(version) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).channels as *const _ as usize }, - 4usize, - concat!( - "Offset of field: ", - stringify!(vorbis_info), - "::", - stringify!(channels) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).rate as *const _ as usize }, - 8usize, - concat!( - "Offset of field: ", - stringify!(vorbis_info), - "::", - stringify!(rate) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).bitrate_upper as *const _ as usize }, - 16usize, - concat!( - "Offset of field: ", - stringify!(vorbis_info), - "::", - stringify!(bitrate_upper) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).bitrate_nominal as *const _ as usize }, - 24usize, - concat!( - "Offset of field: ", - stringify!(vorbis_info), - "::", - stringify!(bitrate_nominal) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).bitrate_lower as *const _ as usize }, - 32usize, - concat!( - "Offset of field: ", - stringify!(vorbis_info), - "::", - stringify!(bitrate_lower) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).bitrate_window as *const _ as usize }, - 40usize, - concat!( - "Offset of field: ", - stringify!(vorbis_info), - "::", - stringify!(bitrate_window) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).codec_setup as *const _ as usize }, - 48usize, - concat!( - "Offset of field: ", - stringify!(vorbis_info), - "::", - stringify!(codec_setup) - ) - ); -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct vorbis_dsp_state { - pub analysisp: ::std::os::raw::c_int, - pub vi: *mut vorbis_info, - pub pcm: *mut *mut f32, - pub pcmret: *mut *mut f32, - pub pcm_storage: ::std::os::raw::c_int, - pub pcm_current: ::std::os::raw::c_int, - pub pcm_returned: ::std::os::raw::c_int, - pub preextrapolate: ::std::os::raw::c_int, - pub eofflag: ::std::os::raw::c_int, - pub lW: ::std::os::raw::c_long, - pub W: ::std::os::raw::c_long, - pub nW: ::std::os::raw::c_long, - pub centerW: ::std::os::raw::c_long, - pub granulepos: ogg_int64_t, - pub sequence: ogg_int64_t, - pub glue_bits: ogg_int64_t, - pub time_bits: ogg_int64_t, - pub floor_bits: ogg_int64_t, - pub res_bits: ogg_int64_t, - pub backend_state: *mut ::std::os::raw::c_void, -} -#[test] -fn bindgen_test_layout_vorbis_dsp_state() { - assert_eq!( - ::std::mem::size_of::(), - 144usize, - concat!("Size of: ", stringify!(vorbis_dsp_state)) - ); - assert_eq!( - ::std::mem::align_of::(), - 8usize, - concat!("Alignment of ", stringify!(vorbis_dsp_state)) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).analysisp as *const _ as usize }, - 0usize, - concat!( - "Offset of field: ", - stringify!(vorbis_dsp_state), - "::", - stringify!(analysisp) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).vi as *const _ as usize }, - 8usize, - concat!( - "Offset of field: ", - stringify!(vorbis_dsp_state), - "::", - stringify!(vi) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).pcm as *const _ as usize }, - 16usize, - concat!( - "Offset of field: ", - stringify!(vorbis_dsp_state), - "::", - stringify!(pcm) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).pcmret as *const _ as usize }, - 24usize, - concat!( - "Offset of field: ", - stringify!(vorbis_dsp_state), - "::", - stringify!(pcmret) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).pcm_storage as *const _ as usize }, - 32usize, - concat!( - "Offset of field: ", - stringify!(vorbis_dsp_state), - "::", - stringify!(pcm_storage) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).pcm_current as *const _ as usize }, - 36usize, - concat!( - "Offset of field: ", - stringify!(vorbis_dsp_state), - "::", - stringify!(pcm_current) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).pcm_returned as *const _ as usize }, - 40usize, - concat!( - "Offset of field: ", - stringify!(vorbis_dsp_state), - "::", - stringify!(pcm_returned) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).preextrapolate as *const _ as usize }, - 44usize, - concat!( - "Offset of field: ", - stringify!(vorbis_dsp_state), - "::", - stringify!(preextrapolate) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).eofflag as *const _ as usize }, - 48usize, - concat!( - "Offset of field: ", - stringify!(vorbis_dsp_state), - "::", - stringify!(eofflag) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).lW as *const _ as usize }, - 56usize, - concat!( - "Offset of field: ", - stringify!(vorbis_dsp_state), - "::", - stringify!(lW) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).W as *const _ as usize }, - 64usize, - concat!( - "Offset of field: ", - stringify!(vorbis_dsp_state), - "::", - stringify!(W) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).nW as *const _ as usize }, - 72usize, - concat!( - "Offset of field: ", - stringify!(vorbis_dsp_state), - "::", - stringify!(nW) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).centerW as *const _ as usize }, - 80usize, - concat!( - "Offset of field: ", - stringify!(vorbis_dsp_state), - "::", - stringify!(centerW) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).granulepos as *const _ as usize }, - 88usize, - concat!( - "Offset of field: ", - stringify!(vorbis_dsp_state), - "::", - stringify!(granulepos) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).sequence as *const _ as usize }, - 96usize, - concat!( - "Offset of field: ", - stringify!(vorbis_dsp_state), - "::", - stringify!(sequence) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).glue_bits as *const _ as usize }, - 104usize, - concat!( - "Offset of field: ", - stringify!(vorbis_dsp_state), - "::", - stringify!(glue_bits) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).time_bits as *const _ as usize }, - 112usize, - concat!( - "Offset of field: ", - stringify!(vorbis_dsp_state), - "::", - stringify!(time_bits) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).floor_bits as *const _ as usize }, - 120usize, - concat!( - "Offset of field: ", - stringify!(vorbis_dsp_state), - "::", - stringify!(floor_bits) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).res_bits as *const _ as usize }, - 128usize, - concat!( - "Offset of field: ", - stringify!(vorbis_dsp_state), - "::", - stringify!(res_bits) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).backend_state as *const _ as usize }, - 136usize, - concat!( - "Offset of field: ", - stringify!(vorbis_dsp_state), - "::", - stringify!(backend_state) - ) - ); -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct vorbis_block { - pub pcm: *mut *mut f32, - pub opb: oggpack_buffer, - pub lW: ::std::os::raw::c_long, - pub W: ::std::os::raw::c_long, - pub nW: ::std::os::raw::c_long, - pub pcmend: ::std::os::raw::c_int, - pub mode: ::std::os::raw::c_int, - pub eofflag: ::std::os::raw::c_int, - pub granulepos: ogg_int64_t, - pub sequence: ogg_int64_t, - pub vd: *mut vorbis_dsp_state, - pub localstore: *mut ::std::os::raw::c_void, - pub localtop: ::std::os::raw::c_long, - pub localalloc: ::std::os::raw::c_long, - pub totaluse: ::std::os::raw::c_long, - pub reap: *mut alloc_chain, - pub glue_bits: ::std::os::raw::c_long, - pub time_bits: ::std::os::raw::c_long, - pub floor_bits: ::std::os::raw::c_long, - pub res_bits: ::std::os::raw::c_long, - pub internal: *mut ::std::os::raw::c_void, -} -#[test] -fn bindgen_test_layout_vorbis_block() { - assert_eq!( - ::std::mem::size_of::(), - 192usize, - concat!("Size of: ", stringify!(vorbis_block)) - ); - assert_eq!( - ::std::mem::align_of::(), - 8usize, - concat!("Alignment of ", stringify!(vorbis_block)) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).pcm as *const _ as usize }, - 0usize, - concat!( - "Offset of field: ", - stringify!(vorbis_block), - "::", - stringify!(pcm) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).opb as *const _ as usize }, - 8usize, - concat!( - "Offset of field: ", - stringify!(vorbis_block), - "::", - stringify!(opb) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).lW as *const _ as usize }, - 48usize, - concat!( - "Offset of field: ", - stringify!(vorbis_block), - "::", - stringify!(lW) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).W as *const _ as usize }, - 56usize, - concat!( - "Offset of field: ", - stringify!(vorbis_block), - "::", - stringify!(W) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).nW as *const _ as usize }, - 64usize, - concat!( - "Offset of field: ", - stringify!(vorbis_block), - "::", - stringify!(nW) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).pcmend as *const _ as usize }, - 72usize, - concat!( - "Offset of field: ", - stringify!(vorbis_block), - "::", - stringify!(pcmend) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).mode as *const _ as usize }, - 76usize, - concat!( - "Offset of field: ", - stringify!(vorbis_block), - "::", - stringify!(mode) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).eofflag as *const _ as usize }, - 80usize, - concat!( - "Offset of field: ", - stringify!(vorbis_block), - "::", - stringify!(eofflag) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).granulepos as *const _ as usize }, - 88usize, - concat!( - "Offset of field: ", - stringify!(vorbis_block), - "::", - stringify!(granulepos) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).sequence as *const _ as usize }, - 96usize, - concat!( - "Offset of field: ", - stringify!(vorbis_block), - "::", - stringify!(sequence) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).vd as *const _ as usize }, - 104usize, - concat!( - "Offset of field: ", - stringify!(vorbis_block), - "::", - stringify!(vd) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).localstore as *const _ as usize }, - 112usize, - concat!( - "Offset of field: ", - stringify!(vorbis_block), - "::", - stringify!(localstore) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).localtop as *const _ as usize }, - 120usize, - concat!( - "Offset of field: ", - stringify!(vorbis_block), - "::", - stringify!(localtop) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).localalloc as *const _ as usize }, - 128usize, - concat!( - "Offset of field: ", - stringify!(vorbis_block), - "::", - stringify!(localalloc) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).totaluse as *const _ as usize }, - 136usize, - concat!( - "Offset of field: ", - stringify!(vorbis_block), - "::", - stringify!(totaluse) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).reap as *const _ as usize }, - 144usize, - concat!( - "Offset of field: ", - stringify!(vorbis_block), - "::", - stringify!(reap) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).glue_bits as *const _ as usize }, - 152usize, - concat!( - "Offset of field: ", - stringify!(vorbis_block), - "::", - stringify!(glue_bits) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).time_bits as *const _ as usize }, - 160usize, - concat!( - "Offset of field: ", - stringify!(vorbis_block), - "::", - stringify!(time_bits) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).floor_bits as *const _ as usize }, - 168usize, - concat!( - "Offset of field: ", - stringify!(vorbis_block), - "::", - stringify!(floor_bits) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).res_bits as *const _ as usize }, - 176usize, - concat!( - "Offset of field: ", - stringify!(vorbis_block), - "::", - stringify!(res_bits) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).internal as *const _ as usize }, - 184usize, - concat!( - "Offset of field: ", - stringify!(vorbis_block), - "::", - stringify!(internal) - ) - ); -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct alloc_chain { - pub ptr: *mut ::std::os::raw::c_void, - pub next: *mut alloc_chain, -} -#[test] -fn bindgen_test_layout_alloc_chain() { - assert_eq!( - ::std::mem::size_of::(), - 16usize, - concat!("Size of: ", stringify!(alloc_chain)) - ); - assert_eq!( - ::std::mem::align_of::(), - 8usize, - concat!("Alignment of ", stringify!(alloc_chain)) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).ptr as *const _ as usize }, - 0usize, - concat!( - "Offset of field: ", - stringify!(alloc_chain), - "::", - stringify!(ptr) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).next as *const _ as usize }, - 8usize, - concat!( - "Offset of field: ", - stringify!(alloc_chain), - "::", - stringify!(next) - ) - ); -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct vorbis_comment { - pub user_comments: *mut *mut ::std::os::raw::c_char, - pub comment_lengths: *mut ::std::os::raw::c_int, - pub comments: ::std::os::raw::c_int, - pub vendor: *mut ::std::os::raw::c_char, -} -#[test] -fn bindgen_test_layout_vorbis_comment() { - assert_eq!( - ::std::mem::size_of::(), - 32usize, - concat!("Size of: ", stringify!(vorbis_comment)) - ); - assert_eq!( - ::std::mem::align_of::(), - 8usize, - concat!("Alignment of ", stringify!(vorbis_comment)) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).user_comments as *const _ as usize }, - 0usize, - concat!( - "Offset of field: ", - stringify!(vorbis_comment), - "::", - stringify!(user_comments) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).comment_lengths as *const _ as usize }, - 8usize, - concat!( - "Offset of field: ", - stringify!(vorbis_comment), - "::", - stringify!(comment_lengths) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).comments as *const _ as usize }, - 16usize, - concat!( - "Offset of field: ", - stringify!(vorbis_comment), - "::", - stringify!(comments) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).vendor as *const _ as usize }, - 24usize, - concat!( - "Offset of field: ", - stringify!(vorbis_comment), - "::", - stringify!(vendor) - ) - ); -} -pub type fpos_t = __darwin_off_t; -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct __sbuf { - pub _base: *mut ::std::os::raw::c_uchar, - pub _size: ::std::os::raw::c_int, -} -#[test] -fn bindgen_test_layout___sbuf() { - assert_eq!( - ::std::mem::size_of::<__sbuf>(), - 16usize, - concat!("Size of: ", stringify!(__sbuf)) - ); - assert_eq!( - ::std::mem::align_of::<__sbuf>(), - 8usize, - concat!("Alignment of ", stringify!(__sbuf)) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sbuf>()))._base as *const _ as usize }, - 0usize, - concat!( - "Offset of field: ", - stringify!(__sbuf), - "::", - stringify!(_base) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sbuf>()))._size as *const _ as usize }, - 8usize, - concat!( - "Offset of field: ", - stringify!(__sbuf), - "::", - stringify!(_size) - ) - ); -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct __sFILEX { - _unused: [u8; 0], -} -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct __sFILE { - pub _p: *mut ::std::os::raw::c_uchar, - pub _r: ::std::os::raw::c_int, - pub _w: ::std::os::raw::c_int, - pub _flags: ::std::os::raw::c_short, - pub _file: ::std::os::raw::c_short, - pub _bf: __sbuf, - pub _lbfsize: ::std::os::raw::c_int, - pub _cookie: *mut ::std::os::raw::c_void, - pub _close: ::std::option::Option< - unsafe extern "C" fn(arg1: *mut ::std::os::raw::c_void) -> ::std::os::raw::c_int, - >, - pub _read: ::std::option::Option< - unsafe extern "C" fn( - arg1: *mut ::std::os::raw::c_void, - arg2: *mut ::std::os::raw::c_char, - arg3: ::std::os::raw::c_int, - ) -> ::std::os::raw::c_int, - >, - pub _seek: ::std::option::Option< - unsafe extern "C" fn( - arg1: *mut ::std::os::raw::c_void, - arg2: fpos_t, - arg3: ::std::os::raw::c_int, - ) -> fpos_t, - >, - pub _write: ::std::option::Option< - unsafe extern "C" fn( - arg1: *mut ::std::os::raw::c_void, - arg2: *const ::std::os::raw::c_char, - arg3: ::std::os::raw::c_int, - ) -> ::std::os::raw::c_int, - >, - pub _ub: __sbuf, - pub _extra: *mut __sFILEX, - pub _ur: ::std::os::raw::c_int, - pub _ubuf: [::std::os::raw::c_uchar; 3usize], - pub _nbuf: [::std::os::raw::c_uchar; 1usize], - pub _lb: __sbuf, - pub _blksize: ::std::os::raw::c_int, - pub _offset: fpos_t, -} -#[test] -fn bindgen_test_layout___sFILE() { - assert_eq!( - ::std::mem::size_of::<__sFILE>(), - 152usize, - concat!("Size of: ", stringify!(__sFILE)) - ); - assert_eq!( - ::std::mem::align_of::<__sFILE>(), - 8usize, - concat!("Alignment of ", stringify!(__sFILE)) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sFILE>()))._p as *const _ as usize }, - 0usize, - concat!( - "Offset of field: ", - stringify!(__sFILE), - "::", - stringify!(_p) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sFILE>()))._r as *const _ as usize }, - 8usize, - concat!( - "Offset of field: ", - stringify!(__sFILE), - "::", - stringify!(_r) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sFILE>()))._w as *const _ as usize }, - 12usize, - concat!( - "Offset of field: ", - stringify!(__sFILE), - "::", - stringify!(_w) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sFILE>()))._flags as *const _ as usize }, - 16usize, - concat!( - "Offset of field: ", - stringify!(__sFILE), - "::", - stringify!(_flags) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sFILE>()))._file as *const _ as usize }, - 18usize, - concat!( - "Offset of field: ", - stringify!(__sFILE), - "::", - stringify!(_file) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sFILE>()))._bf as *const _ as usize }, - 24usize, - concat!( - "Offset of field: ", - stringify!(__sFILE), - "::", - stringify!(_bf) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sFILE>()))._lbfsize as *const _ as usize }, - 40usize, - concat!( - "Offset of field: ", - stringify!(__sFILE), - "::", - stringify!(_lbfsize) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sFILE>()))._cookie as *const _ as usize }, - 48usize, - concat!( - "Offset of field: ", - stringify!(__sFILE), - "::", - stringify!(_cookie) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sFILE>()))._close as *const _ as usize }, - 56usize, - concat!( - "Offset of field: ", - stringify!(__sFILE), - "::", - stringify!(_close) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sFILE>()))._read as *const _ as usize }, - 64usize, - concat!( - "Offset of field: ", - stringify!(__sFILE), - "::", - stringify!(_read) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sFILE>()))._seek as *const _ as usize }, - 72usize, - concat!( - "Offset of field: ", - stringify!(__sFILE), - "::", - stringify!(_seek) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sFILE>()))._write as *const _ as usize }, - 80usize, - concat!( - "Offset of field: ", - stringify!(__sFILE), - "::", - stringify!(_write) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sFILE>()))._ub as *const _ as usize }, - 88usize, - concat!( - "Offset of field: ", - stringify!(__sFILE), - "::", - stringify!(_ub) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sFILE>()))._extra as *const _ as usize }, - 104usize, - concat!( - "Offset of field: ", - stringify!(__sFILE), - "::", - stringify!(_extra) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sFILE>()))._ur as *const _ as usize }, - 112usize, - concat!( - "Offset of field: ", - stringify!(__sFILE), - "::", - stringify!(_ur) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sFILE>()))._ubuf as *const _ as usize }, - 116usize, - concat!( - "Offset of field: ", - stringify!(__sFILE), - "::", - stringify!(_ubuf) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sFILE>()))._nbuf as *const _ as usize }, - 119usize, - concat!( - "Offset of field: ", - stringify!(__sFILE), - "::", - stringify!(_nbuf) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sFILE>()))._lb as *const _ as usize }, - 120usize, - concat!( - "Offset of field: ", - stringify!(__sFILE), - "::", - stringify!(_lb) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sFILE>()))._blksize as *const _ as usize }, - 136usize, - concat!( - "Offset of field: ", - stringify!(__sFILE), - "::", - stringify!(_blksize) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::<__sFILE>()))._offset as *const _ as usize }, - 144usize, - concat!( - "Offset of field: ", - stringify!(__sFILE), - "::", - stringify!(_offset) - ) - ); -} -pub type FILE = __sFILE; -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct ov_callbacks { - pub read_func: ::std::option::Option< - unsafe extern "C" fn( - ptr: *mut ::std::os::raw::c_void, - size: usize, - nmemb: usize, - datasource: *mut ::std::os::raw::c_void, - ) -> usize, - >, - pub seek_func: ::std::option::Option< - unsafe extern "C" fn( - datasource: *mut ::std::os::raw::c_void, - offset: ogg_int64_t, - whence: ::std::os::raw::c_int, - ) -> ::std::os::raw::c_int, - >, - pub close_func: ::std::option::Option< - unsafe extern "C" fn(datasource: *mut ::std::os::raw::c_void) -> ::std::os::raw::c_int, - >, - pub tell_func: ::std::option::Option< - unsafe extern "C" fn(datasource: *mut ::std::os::raw::c_void) -> ::std::os::raw::c_long, - >, -} -#[test] -fn bindgen_test_layout_ov_callbacks() { - assert_eq!( - ::std::mem::size_of::(), - 32usize, - concat!("Size of: ", stringify!(ov_callbacks)) - ); - assert_eq!( - ::std::mem::align_of::(), - 8usize, - concat!("Alignment of ", stringify!(ov_callbacks)) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).read_func as *const _ as usize }, - 0usize, - concat!( - "Offset of field: ", - stringify!(ov_callbacks), - "::", - stringify!(read_func) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).seek_func as *const _ as usize }, - 8usize, - concat!( - "Offset of field: ", - stringify!(ov_callbacks), - "::", - stringify!(seek_func) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).close_func as *const _ as usize }, - 16usize, - concat!( - "Offset of field: ", - stringify!(ov_callbacks), - "::", - stringify!(close_func) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).tell_func as *const _ as usize }, - 24usize, - concat!( - "Offset of field: ", - stringify!(ov_callbacks), - "::", - stringify!(tell_func) - ) - ); -} -extern "C" { - pub static mut OV_CALLBACKS_DEFAULT: ov_callbacks; -} -extern "C" { - pub static mut OV_CALLBACKS_NOCLOSE: ov_callbacks; -} -extern "C" { - pub static mut OV_CALLBACKS_STREAMONLY: ov_callbacks; -} -extern "C" { - pub static mut OV_CALLBACKS_STREAMONLY_NOCLOSE: ov_callbacks; -} -#[repr(C)] -#[derive(Copy, Clone)] -pub struct OggVorbis_File { - pub datasource: *mut ::std::os::raw::c_void, - pub seekable: ::std::os::raw::c_int, - pub offset: ogg_int64_t, - pub end: ogg_int64_t, - pub oy: ogg_sync_state, - pub links: ::std::os::raw::c_int, - pub offsets: *mut ogg_int64_t, - pub dataoffsets: *mut ogg_int64_t, - pub serialnos: *mut ::std::os::raw::c_long, - pub pcmlengths: *mut ogg_int64_t, - pub vi: *mut vorbis_info, - pub vc: *mut vorbis_comment, - pub pcm_offset: ogg_int64_t, - pub ready_state: ::std::os::raw::c_int, - pub current_serialno: ::std::os::raw::c_long, - pub current_link: ::std::os::raw::c_int, - pub bittrack: f64, - pub samptrack: f64, - pub os: ogg_stream_state, - pub vd: vorbis_dsp_state, - pub vb: vorbis_block, - pub callbacks: ov_callbacks, -} -#[test] -fn bindgen_test_layout_OggVorbis_File() { - assert_eq!( - ::std::mem::size_of::(), - 944usize, - concat!("Size of: ", stringify!(OggVorbis_File)) - ); - assert_eq!( - ::std::mem::align_of::(), - 8usize, - concat!("Alignment of ", stringify!(OggVorbis_File)) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).datasource as *const _ as usize }, - 0usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(datasource) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).seekable as *const _ as usize }, - 8usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(seekable) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).offset as *const _ as usize }, - 16usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(offset) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).end as *const _ as usize }, - 24usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(end) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).oy as *const _ as usize }, - 32usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(oy) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).links as *const _ as usize }, - 64usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(links) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).offsets as *const _ as usize }, - 72usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(offsets) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).dataoffsets as *const _ as usize }, - 80usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(dataoffsets) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).serialnos as *const _ as usize }, - 88usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(serialnos) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).pcmlengths as *const _ as usize }, - 96usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(pcmlengths) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).vi as *const _ as usize }, - 104usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(vi) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).vc as *const _ as usize }, - 112usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(vc) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).pcm_offset as *const _ as usize }, - 120usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(pcm_offset) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).ready_state as *const _ as usize }, - 128usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(ready_state) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).current_serialno as *const _ as usize }, - 136usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(current_serialno) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).current_link as *const _ as usize }, - 144usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(current_link) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).bittrack as *const _ as usize }, - 152usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(bittrack) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).samptrack as *const _ as usize }, - 160usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(samptrack) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).os as *const _ as usize }, - 168usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(os) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).vd as *const _ as usize }, - 576usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(vd) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).vb as *const _ as usize }, - 720usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(vb) - ) - ); - assert_eq!( - unsafe { &(*(::std::ptr::null::())).callbacks as *const _ as usize }, - 912usize, - concat!( - "Offset of field: ", - stringify!(OggVorbis_File), - "::", - stringify!(callbacks) - ) - ); -} -extern "C" { - pub fn ov_clear(vf: *mut OggVorbis_File) -> ::std::os::raw::c_int; -} -extern "C" { - pub fn ov_fopen( - path: *const ::std::os::raw::c_char, - vf: *mut OggVorbis_File, - ) -> ::std::os::raw::c_int; -} -extern "C" { - pub fn ov_open( - f: *mut FILE, - vf: *mut OggVorbis_File, - initial: *const ::std::os::raw::c_char, - ibytes: ::std::os::raw::c_long, - ) -> ::std::os::raw::c_int; -} -extern "C" { - pub fn ov_open_callbacks( - datasource: *mut ::std::os::raw::c_void, - vf: *mut OggVorbis_File, - initial: *const ::std::os::raw::c_char, - ibytes: ::std::os::raw::c_long, - callbacks: ov_callbacks, - ) -> ::std::os::raw::c_int; -} -extern "C" { - pub fn ov_test( - f: *mut FILE, - vf: *mut OggVorbis_File, - initial: *const ::std::os::raw::c_char, - ibytes: ::std::os::raw::c_long, - ) -> ::std::os::raw::c_int; -} -extern "C" { - pub fn ov_test_callbacks( - datasource: *mut ::std::os::raw::c_void, - vf: *mut OggVorbis_File, - initial: *const ::std::os::raw::c_char, - ibytes: ::std::os::raw::c_long, - callbacks: ov_callbacks, - ) -> ::std::os::raw::c_int; -} -extern "C" { - pub fn ov_test_open(vf: *mut OggVorbis_File) -> ::std::os::raw::c_int; -} -extern "C" { - pub fn ov_bitrate(vf: *mut OggVorbis_File, i: ::std::os::raw::c_int) -> ::std::os::raw::c_long; -} -extern "C" { - pub fn ov_bitrate_instant(vf: *mut OggVorbis_File) -> ::std::os::raw::c_long; -} -extern "C" { - pub fn ov_streams(vf: *mut OggVorbis_File) -> ::std::os::raw::c_long; -} -extern "C" { - pub fn ov_seekable(vf: *mut OggVorbis_File) -> ::std::os::raw::c_long; -} -extern "C" { - pub fn ov_serialnumber( - vf: *mut OggVorbis_File, - i: ::std::os::raw::c_int, - ) -> ::std::os::raw::c_long; -} -extern "C" { - pub fn ov_raw_total(vf: *mut OggVorbis_File, i: ::std::os::raw::c_int) -> ogg_int64_t; -} -extern "C" { - pub fn ov_pcm_total(vf: *mut OggVorbis_File, i: ::std::os::raw::c_int) -> ogg_int64_t; -} -extern "C" { - pub fn ov_time_total(vf: *mut OggVorbis_File, i: ::std::os::raw::c_int) -> f64; -} -extern "C" { - pub fn ov_raw_seek(vf: *mut OggVorbis_File, pos: ogg_int64_t) -> ::std::os::raw::c_int; -} -extern "C" { - pub fn ov_pcm_seek(vf: *mut OggVorbis_File, pos: ogg_int64_t) -> ::std::os::raw::c_int; -} -extern "C" { - pub fn ov_pcm_seek_page(vf: *mut OggVorbis_File, pos: ogg_int64_t) -> ::std::os::raw::c_int; -} -extern "C" { - pub fn ov_time_seek(vf: *mut OggVorbis_File, pos: f64) -> ::std::os::raw::c_int; -} -extern "C" { - pub fn ov_time_seek_page(vf: *mut OggVorbis_File, pos: f64) -> ::std::os::raw::c_int; -} -extern "C" { - pub fn ov_raw_seek_lap(vf: *mut OggVorbis_File, pos: ogg_int64_t) -> ::std::os::raw::c_int; -} -extern "C" { - pub fn ov_pcm_seek_lap(vf: *mut OggVorbis_File, pos: ogg_int64_t) -> ::std::os::raw::c_int; -} -extern "C" { - pub fn ov_pcm_seek_page_lap(vf: *mut OggVorbis_File, pos: ogg_int64_t) - -> ::std::os::raw::c_int; -} -extern "C" { - pub fn ov_time_seek_lap(vf: *mut OggVorbis_File, pos: f64) -> ::std::os::raw::c_int; -} -extern "C" { - pub fn ov_time_seek_page_lap(vf: *mut OggVorbis_File, pos: f64) -> ::std::os::raw::c_int; -} -extern "C" { - pub fn ov_raw_tell(vf: *mut OggVorbis_File) -> ogg_int64_t; -} -extern "C" { - pub fn ov_pcm_tell(vf: *mut OggVorbis_File) -> ogg_int64_t; -} -extern "C" { - pub fn ov_time_tell(vf: *mut OggVorbis_File) -> f64; -} -extern "C" { - pub fn ov_info(vf: *mut OggVorbis_File, link: ::std::os::raw::c_int) -> *mut vorbis_info; -} -extern "C" { - pub fn ov_comment(vf: *mut OggVorbis_File, link: ::std::os::raw::c_int) -> *mut vorbis_comment; -} -extern "C" { - pub fn ov_read_float( - vf: *mut OggVorbis_File, - pcm_channels: *mut *mut *mut f32, - samples: ::std::os::raw::c_int, - bitstream: *mut ::std::os::raw::c_int, - ) -> ::std::os::raw::c_long; -} -extern "C" { - pub fn ov_read_filter( - vf: *mut OggVorbis_File, - buffer: *mut ::std::os::raw::c_char, - length: ::std::os::raw::c_int, - bigendianp: ::std::os::raw::c_int, - word: ::std::os::raw::c_int, - sgned: ::std::os::raw::c_int, - bitstream: *mut ::std::os::raw::c_int, - filter: ::std::option::Option< - unsafe extern "C" fn( - pcm: *mut *mut f32, - channels: ::std::os::raw::c_long, - samples: ::std::os::raw::c_long, - filter_param: *mut ::std::os::raw::c_void, - ), - >, - filter_param: *mut ::std::os::raw::c_void, - ) -> ::std::os::raw::c_long; -} -extern "C" { - pub fn ov_read( - vf: *mut OggVorbis_File, - buffer: *mut ::std::os::raw::c_char, - length: ::std::os::raw::c_int, - bigendianp: ::std::os::raw::c_int, - word: ::std::os::raw::c_int, - sgned: ::std::os::raw::c_int, - bitstream: *mut ::std::os::raw::c_int, - ) -> ::std::os::raw::c_long; -} -extern "C" { - pub fn ov_crosslap(vf1: *mut OggVorbis_File, vf2: *mut OggVorbis_File) - -> ::std::os::raw::c_int; -} -extern "C" { - pub fn ov_halfrate( - vf: *mut OggVorbis_File, - flag: ::std::os::raw::c_int, - ) -> ::std::os::raw::c_int; -} -extern "C" { - pub fn ov_halfrate_p(vf: *mut OggVorbis_File) -> ::std::os::raw::c_int; -} diff --git a/minivorbis-sys/src/lib.rs b/minivorbis-sys/src/lib.rs deleted file mode 100644 index a4b813b9..00000000 --- a/minivorbis-sys/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -#![allow(clippy::all)] -#![allow(non_camel_case_types)] -#![allow(non_upper_case_globals)] -#![allow(non_snake_case)] - -mod bindings; - -pub use bindings::*; diff --git a/minivorbis/Cargo.toml b/minivorbis/Cargo.toml deleted file mode 100644 index eaafa2eb..00000000 --- a/minivorbis/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "minivorbis" -version = "0.1.0" -authors = ["Jan Pochyla "] -edition = "2018" - -[dependencies] -minivorbis-sys = { path = "../minivorbis-sys" } \ No newline at end of file diff --git a/minivorbis/src/lib.rs b/minivorbis/src/lib.rs deleted file mode 100644 index dca92d0b..00000000 --- a/minivorbis/src/lib.rs +++ /dev/null @@ -1,272 +0,0 @@ -#![allow(clippy::all)] - -use std::{ - convert::TryInto, - error, fmt, - fmt::Formatter, - io::{self, Read, Seek}, - mem, - os::raw, - ptr, slice, -}; - -pub struct Decoder { - data: Box>, - pub sample_rate: u32, - pub channels: u8, -} - -pub const TYPICAL_PACKET_CAP: usize = 2048; - -impl Decoder { - pub fn new(reader: R) -> Result, Error> { - let mut data = Box::new(State { - vorbis: unsafe { mem::zeroed() }, - read_error: None, - bitstream: 0, - reader, - }); - - unsafe { - let datasource = data.as_mut() as *mut _ as *mut _; - let res_code = minivorbis_sys::ov_open_callbacks( - datasource, - &mut data.vorbis, - std::ptr::null(), // zero initial data - 0, // zero initial data - minivorbis_sys::ov_callbacks { - read_func: Some(read_func::), - seek_func: Some(seek_func::), - tell_func: Some(tell_func::), - close_func: None, - }, - ); - Error::from_code(res_code)?; - } - - let info = unsafe { - minivorbis_sys::ov_info( - &mut data.vorbis, - -1, // for current bitstream - ) - .as_ref() - .unwrap() - }; - let channels = info.channels.try_into().unwrap(); - let sample_rate = info.rate.try_into().unwrap(); - - Ok(Decoder { - data, - channels, - sample_rate, - }) - } - - pub fn seek_to_pcm(&mut self, frame: u64) -> Result<(), Error> { - let res_code = unsafe { - minivorbis_sys::ov_pcm_seek(&mut self.data.vorbis, frame as minivorbis_sys::ogg_int64_t) - }; - Error::from_code(res_code)?; - Ok(()) - } - - pub fn position_in_pcm(&mut self) -> Result { - let frame = unsafe { minivorbis_sys::ov_pcm_tell(&mut self.data.vorbis) }; - if frame < 0 { - Err(Error::from_code(frame as raw::c_int).unwrap_err()) - } else { - Ok(frame as u64) - } - } - - pub fn read_packet(&mut self, samples: &mut [f32]) -> Result { - let mut pcm_channels = ptr::null_mut(); - let previous_bitstream = self.data.bitstream; - match unsafe { - minivorbis_sys::ov_read_float( - &mut self.data.vorbis, - &mut pcm_channels, - samples.len() as i32, - &mut self.data.bitstream, - ) - } { - 0 => { - // Either the underlying reader reached the EOF, or encountered an error. - match self.data.read_error.take() { - Some(err) => Err(Error::ReadError(err)), - None => Ok(0), // EOF - } - } - code if code < 0 => { - // Return a `minivorbis` error. - let err = Error::from_code(code as raw::c_int).unwrap_err(); - Err(err) - } - decoded_samples => { - if previous_bitstream != self.data.bitstream { - // The section has changed, assert that the number of channels and the sample - // rate is the same. - let info = unsafe { - minivorbis_sys::ov_info( - &mut self.data.vorbis, - -1, // for current bitstream - ) - .as_ref() - .unwrap() - }; - let channels: u8 = info.channels.try_into().unwrap(); - let sample_rate: u32 = info.rate.try_into().unwrap(); - assert_eq!( - channels, self.channels, - "the number of channels have changed between sections", - ); - assert_eq!( - sample_rate, self.sample_rate, - "the sample rate have changed between sections", - ); - } - let mut written_samples = 0; - for isample in 0..decoded_samples { - for ichan in 0..self.channels { - samples[written_samples] = unsafe { - pcm_channels - .add(ichan as usize) - .read() - .add(isample as usize) - .read() - }; - written_samples += 1; - } - } - Ok(written_samples) - } - } - } -} - -impl Drop for Decoder { - fn drop(&mut self) { - unsafe { - minivorbis_sys::ov_clear(&mut self.data.vorbis); - } - } -} - -struct State { - reader: R, - vorbis: minivorbis_sys::OggVorbis_File, - bitstream: raw::c_int, - read_error: Option, -} - -unsafe impl Send for State {} - -extern "C" fn read_func( - ptr: *mut raw::c_void, - size: usize, - nmemb: usize, - datasource: *mut raw::c_void, -) -> usize { - let data = unsafe { &mut *(datasource as *mut State) }; - - // In practice `minivorbis` always sets size to 1. This assumption makes things - // much simpler. - assert_eq!(size, 1); - - let buffer = unsafe { slice::from_raw_parts_mut(ptr as *mut u8, nmemb as usize) }; - - loop { - match data.reader.read(buffer) { - Ok(read_bytes) => { - return read_bytes as usize; - } - Err(err) if err.kind() == io::ErrorKind::Interrupted => { - // Ignore and try again. - } - Err(err) => { - // Keep the error and pick it up later in `Decoder::read_packet`. - data.read_error.replace(err); - - // Correctly, we should set `errno` to indicate a reading error to `minivorbis`, - // but because interfacing `errno` is a hassle in Rust, let's pretend we have an - // EOF. - return 0; - } - } - } -} - -extern "C" fn seek_func( - datasource: *mut raw::c_void, - offset: minivorbis_sys::ogg_int64_t, - whence: raw::c_int, -) -> raw::c_int { - let data = unsafe { &mut *(datasource as *mut State) }; - - let pos = match whence as u32 { - minivorbis_sys::SEEK_SET => io::SeekFrom::Start(offset as u64), - minivorbis_sys::SEEK_CUR => io::SeekFrom::Current(offset), - minivorbis_sys::SEEK_END => io::SeekFrom::End(offset), - _ => unreachable!(), - }; - match data.reader.seek(pos) { - Ok(_) => 0, - Err(_) => -1, - } -} - -extern "C" fn tell_func(datasource: *mut raw::c_void) -> raw::c_long { - let data = unsafe { &mut *(datasource as *mut State) }; - - data.reader - .seek(io::SeekFrom::Current(0)) - .map(|v| v as raw::c_long) - .unwrap_or(-1) -} - -#[derive(Debug)] -pub enum Error { - ReadError(io::Error), - NotVorbis, - VersionMismatch, - BadHeader, - InvalidSetup, - Hole, - Unimplemented, -} - -impl Error { - fn from_code(code: raw::c_int) -> Result<(), Self> { - match code { - 0 => Ok(()), - minivorbis_sys::OV_ENOTVORBIS => Err(Self::NotVorbis), - minivorbis_sys::OV_EVERSION => Err(Self::VersionMismatch), - minivorbis_sys::OV_EBADHEADER => Err(Self::BadHeader), - minivorbis_sys::OV_EINVAL => Err(Self::InvalidSetup), - minivorbis_sys::OV_HOLE => Err(Self::Hole), - minivorbis_sys::OV_EIMPL => Err(Self::Unimplemented), - minivorbis_sys::OV_EFAULT => { - panic!("Internal vorbis error"); - } - unknown_code => { - panic!("Unknown vorbis error: {}", unknown_code); - } - } - } -} - -impl error::Error for Error {} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match &self { - Error::ReadError(err) => err.fmt(f), - Error::NotVorbis => write!(f, "OV_ENOTVORBIS"), - Error::VersionMismatch => write!(f, "OV_EVERSION"), - Error::BadHeader => write!(f, "OV_EBADHEADER"), - Error::InvalidSetup => write!(f, "OV_EINVAL"), - Error::Hole => write!(f, "OV_HOLE"), - Error::Unimplemented => write!(f, "OV_EIMPL"), - } - } -} From 7dceae09dde6b0bf305ff71f32c8d51f661e1723 Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Thu, 11 Nov 2021 17:04:33 +0100 Subject: [PATCH 07/14] core: Rework the audio buffering pipeline --- psst-core/src/actor.rs | 44 +++- psst-core/src/audio/decode.rs | 44 ++-- psst-core/src/audio/mod.rs | 1 + psst-core/src/audio/output.rs | 170 ++++++++------ psst-core/src/audio/source.rs | 27 +++ psst-core/src/error.rs | 4 +- psst-core/src/metadata.rs | 14 +- psst-core/src/player/file.rs | 28 +-- psst-core/src/player/item.rs | 26 +-- psst-core/src/player/mod.rs | 204 ++++++---------- psst-core/src/player/worker.rs | 412 +++++++++++++++++++++------------ 11 files changed, 546 insertions(+), 428 deletions(-) create mode 100644 psst-core/src/audio/source.rs diff --git a/psst-core/src/actor.rs b/psst-core/src/actor.rs index 45e0af8c..66beb90a 100644 --- a/psst-core/src/actor.rs +++ b/psst-core/src/actor.rs @@ -1,12 +1,19 @@ use std::{ fmt::Display, thread::{self, JoinHandle}, + time::Duration, }; -use crossbeam_channel::{bounded, unbounded, Receiver, SendError, Sender, TrySendError}; +use crossbeam_channel::{ + bounded, unbounded, Receiver, RecvTimeoutError, SendError, Sender, TrySendError, +}; -pub enum ActorOp { +pub enum Act { Continue, + WaitOr { + timeout: Duration, + timeout_msg: T::Message, + }, Shutdown, } @@ -14,22 +21,39 @@ pub trait Actor: Sized { type Message: Send + 'static; type Error: Display; - fn handle(&mut self, msg: Self::Message) -> Result; + fn handle(&mut self, msg: Self::Message) -> Result, Self::Error>; fn process(mut self, recv: Receiver) { - for msg in recv { - match self.handle(msg) { - Ok(ActorOp::Continue) => { - continue; - } - Ok(ActorOp::Shutdown) => { + let mut act = Act::Continue; + loop { + let msg = match act { + Act::Continue => match recv.recv() { + Ok(msg) => msg, + Err(_) => { + break; + } + }, + Act::WaitOr { + timeout, + timeout_msg, + } => match recv.recv_timeout(timeout) { + Ok(msg) => msg, + Err(RecvTimeoutError::Timeout) => timeout_msg, + Err(RecvTimeoutError::Disconnected) => { + break; + } + }, + Act::Shutdown => { break; } + }; + act = match self.handle(msg) { + Ok(act) => act, Err(err) => { log::error!("error: {}", err); break; } - } + }; } } diff --git a/psst-core/src/audio/decode.rs b/psst-core/src/audio/decode.rs index 6dc1352c..f5907f51 100644 --- a/psst-core/src/audio/decode.rs +++ b/psst-core/src/audio/decode.rs @@ -2,8 +2,8 @@ use std::{io, time::Duration}; use symphonia::{ core::{ - audio::{AudioBufferRef, Channels}, - codecs::Decoder, + audio::{AudioBufferRef, SignalSpec}, + codecs::{CodecParameters, Decoder}, errors::Error as SymphoniaError, formats::{FormatReader, SeekMode, SeekTo}, io::{MediaSource, MediaSourceStream}, @@ -31,7 +31,6 @@ pub struct AudioDecoder { track_id: u32, // Internal OGG track index. decoder: Box, format: Box, - position: TimeStamp, } impl AudioDecoder { @@ -49,24 +48,31 @@ impl AudioDecoder { let decoder_opts = Default::default(); let decoder = VorbisDecoder::try_new(&track.codec_params, &decoder_opts)?; + let p = &track.codec_params; + log::debug!( + "loaded vorbis: sample_rate={:?} n_frames={:?} start_ts={:?} channels={:?}", + p.sample_rate, + p.n_frames, + p.start_ts, + p.channels + ); + Ok(Self { track_id: track.id, decoder: Box::new(decoder), format: Box::new(format), - position: 0, }) } - pub fn channels(&self) -> Option { - self.decoder.codec_params().channels - } - - pub fn sample_rate(&self) -> Option { - self.decoder.codec_params().sample_rate + pub fn codec_params(&self) -> &CodecParameters { + self.decoder.codec_params() } - pub fn max_frames_per_packet(&self) -> Option { - self.decoder.codec_params().max_frames_per_packet + pub fn signal_spec(&self) -> SignalSpec { + SignalSpec { + rate: self.codec_params().sample_rate.unwrap(), + channels: self.codec_params().channels.unwrap(), + } } pub fn seek(&mut self, time: Duration) -> Result { @@ -77,11 +83,10 @@ impl AudioDecoder { track_id: Some(self.track_id), }, )?; - self.position = seeked_to.actual_ts; - Ok(self.position) + Ok(seeked_to.actual_ts) } - pub fn next_packet(&mut self) -> Option { + pub fn next_packet(&mut self) -> Option<(TimeStamp, AudioBufferRef)> { let packet = match self.format.next_packet() { Ok(packet) => packet, Err(SymphoniaError::IoError(io)) if io.kind() == io::ErrorKind::UnexpectedEof => { @@ -93,10 +98,7 @@ impl AudioDecoder { } }; match self.decoder.decode(&packet) { - Ok(packet) => { - self.position += packet.frames() as TimeStamp; - Some(packet) - } + Ok(decoded) => Some((packet.pts(), decoded)), // TODO: Handle non-fatal decoding errors and retry. Err(err) => { log::error!("fatal decode error: {}", err); @@ -104,10 +106,6 @@ impl AudioDecoder { } } } - - pub fn current_frame(&self) -> TimeStamp { - self.position - } } impl From for Error { diff --git a/psst-core/src/audio/mod.rs b/psst-core/src/audio/mod.rs index 472a8aeb..8a6f2eae 100644 --- a/psst-core/src/audio/mod.rs +++ b/psst-core/src/audio/mod.rs @@ -3,3 +3,4 @@ pub mod decrypt; pub mod normalize; pub mod output; pub mod resample; +pub mod source; diff --git a/psst-core/src/audio/output.rs b/psst-core/src/audio/output.rs index d34b82e2..3c7d81d3 100644 --- a/psst-core/src/audio/output.rs +++ b/psst-core/src/audio/output.rs @@ -1,21 +1,17 @@ -use std::sync::Arc; - use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use crossbeam_channel::Sender; -use rb::{RbConsumer, RbProducer, RB}; -use symphonia::core::sample::Sample; +use crossbeam_channel::{bounded, Receiver, Sender}; use crate::{ - actor::{Actor, ActorHandle, ActorOp}, + actor::{Act, Actor, ActorHandle}, + audio::source::Empty, error::Error, }; -const RING_BUF_SIZE: usize = 1024 * 16; +use super::source::AudioSource; pub struct AudioOutput { - sink: AudioSink, - _handle: ActorHandle, - _ring_buf: rb::SpscRb, + _handle: ActorHandle, + sink: AudioSink, } impl AudioOutput { @@ -33,121 +29,111 @@ impl AudioOutput { // the device supports. let supported = device.default_output_config()?; - // Open an output stream with a ring buffer that will get consumed in the audio - // thread and get written to in the playback threads (through an `AudioSink`). - let ring_buf = rb::SpscRb::new(RING_BUF_SIZE); - let handle = OutputStream::spawn_default({ + let (callback_send, callback_recv) = bounded(16); + + let handle = Stream::spawn_default({ let config = supported.config(); - let consumer = ring_buf.consumer(); // TODO: Support additional sample formats. - move |_| OutputStream::open::(device, config, consumer).unwrap() + move |this| Stream::open(device, config, callback_recv, this).unwrap() }); let sink = AudioSink { sample_rate: supported.sample_rate(), - ring_buf_prod: Arc::new(ring_buf.producer()), - msg_sender: handle.sender(), + stream_send: handle.sender(), + callback_send, }; Ok(Self { - _ring_buf: ring_buf, _handle: handle, sink, }) } - pub fn sink(&self) -> AudioSink { + pub fn sink(&self) -> AudioSink { self.sink.clone() } } -pub trait OutputSample: cpal::Sample + Sample + Default + Send + 'static {} - -impl OutputSample for i16 {} -impl OutputSample for u16 {} -impl OutputSample for f32 {} - #[derive(Clone)] -pub struct AudioSink { +pub struct AudioSink { sample_rate: cpal::SampleRate, - ring_buf_prod: Arc>, - msg_sender: Sender, + callback_send: Sender, + stream_send: Sender, } -impl AudioSink { +impl AudioSink { pub fn sample_rate(&self) -> u32 { self.sample_rate.0 } - pub fn write_blocking(&self, samples: &[T]) -> Result<(), Error> { - // Write out all samples from the sample buffer to the ring buffer. - let mut i = 0; - while i < samples.len() { - let writeable_samples = &samples[i..]; - - // Write as many samples as possible to the ring buffer. This blocks until some - // samples are written or the consumer has been destroyed (None is returned). - if let Some(written) = self.ring_buf_prod.write_blocking(writeable_samples) { - i += written; - } else { - // Consumer destroyed, return an error. - return Err(cpal::PlayStreamError::DeviceNotAvailable.into()); - } - } + pub fn set_volume(&self, volume: f32) { + self.send_to_callback(CallbackMsg::SetVolume(volume)); + } - Ok(()) + pub fn play(&self, source: impl AudioSource) { + self.send_to_callback(CallbackMsg::PlaySource(Box::new(source))); } pub fn pause(&self) { - self.send(OutputStreamMsg::Pause); + self.send_to_stream(StreamMsg::Pause); } pub fn resume(&self) { - self.send(OutputStreamMsg::Resume); + self.send_to_stream(StreamMsg::Resume); } pub fn close(&self) { - self.send(OutputStreamMsg::Close); + self.send_to_stream(StreamMsg::Close); } - fn send(&self, msg: OutputStreamMsg) { - if self.msg_sender.send(msg).is_err() { + fn send_to_callback(&self, msg: CallbackMsg) { + if self.callback_send.send(msg).is_err() { + log::error!("output stream actor is dead"); + } + } + + fn send_to_stream(&self, msg: StreamMsg) { + if self.stream_send.send(msg).is_err() { log::error!("output stream actor is dead"); } } } -enum OutputStreamMsg { +enum StreamMsg { Pause, Resume, Close, } -struct OutputStream { - _device: cpal::Device, +struct Stream { stream: cpal::Stream, + _device: cpal::Device, } -impl OutputStream { - fn open( +impl Stream { + fn open( device: cpal::Device, config: cpal::StreamConfig, - ring_buf_cons: rb::Consumer, + callback_recv: Receiver, + stream_send: Sender, ) -> Result { - log::info!("opening output stream: {:?}", config); + let mut callback = StreamCallback { + callback_recv, + _stream_send: stream_send, + source: Box::new(Empty), + volume: 1.0, // We start with the full volume. + }; + log::info!("opening output stream: {:?}", config); let stream = device.build_output_stream( &config, - move |output: &mut [T], _| { - // Write out as many samples as possible from the ring buffer to the audio - // output. - let written = ring_buf_cons.read(output).unwrap_or(0); - // Mute any remaining samples. - output[written..].iter_mut().for_each(|s| *s = T::MID); + move |output, _| { + callback.write_samples(output); }, |err| { log::error!("audio output error: {}", err); }, )?; + Ok(Self { _device: device, stream, @@ -155,32 +141,70 @@ impl OutputStream { } } -impl Actor for OutputStream { - type Message = OutputStreamMsg; +impl Actor for Stream { + type Message = StreamMsg; type Error = Error; - fn handle(&mut self, msg: Self::Message) -> Result { + fn handle(&mut self, msg: Self::Message) -> Result, Self::Error> { match msg { - OutputStreamMsg::Pause => { + StreamMsg::Pause => { log::debug!("pausing audio output stream"); if let Err(err) = self.stream.pause() { log::error!("failed to stop stream: {}", err); } - Ok(ActorOp::Continue) + Ok(Act::Continue) } - OutputStreamMsg::Resume => { + StreamMsg::Resume => { log::debug!("resuming audio output stream"); if let Err(err) = self.stream.play() { log::error!("failed to start stream: {}", err); } - Ok(ActorOp::Continue) + Ok(Act::Continue) } - OutputStreamMsg::Close => { + StreamMsg::Close => { log::debug!("closing audio output stream"); let _ = self.stream.pause(); - Ok(ActorOp::Shutdown) + Ok(Act::Shutdown) + } + } + } +} + +enum CallbackMsg { + PlaySource(Box), + SetVolume(f32), +} + +struct StreamCallback { + callback_recv: Receiver, + _stream_send: Sender, + source: Box, + volume: f32, +} + +impl StreamCallback { + fn write_samples(&mut self, output: &mut [f32]) { + // Process any pending data messages. + while let Ok(msg) = self.callback_recv.try_recv() { + match msg { + CallbackMsg::PlaySource(src) => { + self.source = src; + } + CallbackMsg::SetVolume(volume) => { + self.volume = volume; + } } } + + // Write out as many samples as possible from the audio source to the + // output buffer. + let written = self.source.write(output); + + // Apply the global volume level. + output[..written].iter_mut().for_each(|s| *s *= self.volume); + + // Mute any remaining samples. + output[written..].iter_mut().for_each(|s| *s = 0.0); } } diff --git a/psst-core/src/audio/source.rs b/psst-core/src/audio/source.rs new file mode 100644 index 00000000..cadc3d73 --- /dev/null +++ b/psst-core/src/audio/source.rs @@ -0,0 +1,27 @@ +use rb::{Consumer, RbConsumer}; + +/// Types that can produce audio samples in `f32` format. `Send`able across +/// threads. +pub trait AudioSource: Send + 'static { + /// Write at most of `output.len()` samples into the `output`. Returns the + /// number of written samples. Should take care to always output a full + /// frame, and should _never_ block. + fn write(&mut self, output: &mut [f32]) -> usize; +} + +/// Ring-buffers of `f32` can act as an audio source, provided the producer +/// always pushes in whole frames and we don't block on reading. +impl AudioSource for Consumer { + fn write(&mut self, output: &mut [f32]) -> usize { + self.read(output).unwrap_or(0) + } +} + +/// Empty audio source. Does not produce any samples. +pub struct Empty; + +impl AudioSource for Empty { + fn write(&mut self, _output: &mut [f32]) -> usize { + 0 + } +} diff --git a/psst-core/src/error.rs b/psst-core/src/error.rs index 9ffc1fed..437a96b0 100644 --- a/psst-core/src/error.rs +++ b/psst-core/src/error.rs @@ -4,7 +4,7 @@ use std::{error, fmt, io}; pub enum Error { SessionDisconnected, UnexpectedResponse, - AudioFileNotFound, + MediaFileNotFound, ProxyUrlInvalid, AuthFailed { code: i32 }, JsonError(Box), @@ -23,7 +23,7 @@ impl fmt::Display for Error { match self { Self::SessionDisconnected => write!(f, "Session disconnected"), Self::UnexpectedResponse => write!(f, "Unknown server response"), - Self::AudioFileNotFound => write!(f, "Audio file not found"), + Self::MediaFileNotFound => write!(f, "Audio file not found"), Self::ProxyUrlInvalid => write!(f, "Invalid proxy URL"), Self::AuthFailed { code } => match code { 0 => write!(f, "Authentication failed: protocol error"), diff --git a/psst-core/src/metadata.rs b/psst-core/src/metadata.rs index ac18c92d..e0a1d695 100644 --- a/psst-core/src/metadata.rs +++ b/psst-core/src/metadata.rs @@ -5,7 +5,7 @@ use quick_protobuf::MessageRead; use crate::{ error::Error, item_id::{FileId, ItemId, ItemIdType}, - player::file::{AudioFile, AudioPath}, + player::file::{MediaFile, MediaPath}, protocol::metadata::{Restriction, Track}, session::SessionService, }; @@ -23,13 +23,13 @@ impl Fetch for Track { } } -pub trait ToAudioPath { +pub trait ToMediaPath { fn is_restricted_in_region(&self, country: &str) -> bool; fn find_allowed_alternative(&self, country: &str) -> Option; - fn to_audio_path(&self, preferred_bitrate: usize) -> Option; + fn to_media_path(&self, preferred_bitrate: usize) -> Option; } -impl ToAudioPath for Track { +impl ToMediaPath for Track { fn is_restricted_in_region(&self, country: &str) -> bool { self.restriction .iter() @@ -44,8 +44,8 @@ impl ToAudioPath for Track { ItemId::from_raw(alt_track.gid.as_ref()?, ItemIdType::Track) } - fn to_audio_path(&self, preferred_bitrate: usize) -> Option { - let file = AudioFile::compatible_audio_formats(preferred_bitrate) + fn to_media_path(&self, preferred_bitrate: usize) -> Option { + let file = MediaFile::compatible_audio_formats(preferred_bitrate) .iter() .find_map(|&preferred_format| { self.file @@ -56,7 +56,7 @@ impl ToAudioPath for Track { let item_id = ItemId::from_raw(self.gid.as_ref()?, ItemIdType::Track)?; let file_id = FileId::from_raw(file.file_id.as_ref()?)?; let duration = Duration::from_millis(self.duration? as u64); - Some(AudioPath { + Some(MediaPath { item_id, file_id, file_format, diff --git a/psst-core/src/player/file.rs b/psst-core/src/player/file.rs index baa57f92..da621d80 100644 --- a/psst-core/src/player/file.rs +++ b/psst-core/src/player/file.rs @@ -22,17 +22,17 @@ use crate::{ util::OffsetFile, }; -use super::storage::{StreamReader, StreamRequest, StreamStorage, StreamWriter}; +use super::storage::{StreamRequest, StreamStorage, StreamWriter}; #[derive(Debug, Clone, Copy)] -pub struct AudioPath { +pub struct MediaPath { pub item_id: ItemId, pub file_id: FileId, pub file_format: Format, pub duration: Duration, } -pub enum AudioFile { +pub enum MediaFile { Streamed { streamed_file: Arc, servicing_handle: JoinHandle<()>, @@ -42,7 +42,7 @@ pub enum AudioFile { }, } -impl AudioFile { +impl MediaFile { pub fn compatible_audio_formats(preferred_bitrate: usize) -> &'static [Format] { match preferred_bitrate { 96 => &[ @@ -64,7 +64,7 @@ impl AudioFile { } } - pub fn open(path: AudioPath, cdn: CdnHandle, cache: CacheHandle) -> Result { + pub fn open(path: MediaPath, cdn: CdnHandle, cache: CacheHandle) -> Result { let cached_path = cache.audio_file_path(path.file_id); if cached_path.exists() { let cached_file = CachedFile::open(path, cached_path)?; @@ -86,22 +86,22 @@ impl AudioFile { } } - pub fn path(&self) -> AudioPath { + pub fn path(&self) -> MediaPath { match self { Self::Streamed { streamed_file, .. } => streamed_file.path, Self::Cached { cached_file, .. } => cached_file.path, } } - pub fn reader(&self) -> Result { + pub fn storage(&self) -> &StreamStorage { match self { - Self::Streamed { streamed_file, .. } => Ok(streamed_file.storage.reader()?), - Self::Cached { cached_file, .. } => Ok(cached_file.storage.reader()?), + Self::Streamed { streamed_file, .. } => &streamed_file.storage, + Self::Cached { cached_file, .. } => &cached_file.storage, } } pub fn audio_source(&self, key: AudioKey) -> Result<(AudioDecoder, NormalizationData), Error> { - let reader = self.reader()?; + let reader = self.storage().reader()?; let mut decrypted = AudioDecrypt::new(key, reader); let normalization = NormalizationData::parse(&mut decrypted)?; let encoded = OffsetFile::new(decrypted, self.header_length())?; @@ -118,7 +118,7 @@ impl AudioFile { } pub struct StreamedFile { - path: AudioPath, + path: MediaPath, storage: StreamStorage, url: CdnUrl, cdn: CdnHandle, @@ -126,7 +126,7 @@ pub struct StreamedFile { } impl StreamedFile { - fn open(path: AudioPath, cdn: CdnHandle, cache: CacheHandle) -> Result { + fn open(path: MediaPath, cdn: CdnHandle, cache: CacheHandle) -> Result { // First, we need to resolve URL of the file contents. let url = cdn.resolve_audio_file_url(path.file_id)?; log::debug!("resolved file URL: {:?}", url.url); @@ -216,12 +216,12 @@ impl StreamedFile { } pub struct CachedFile { - path: AudioPath, + path: MediaPath, storage: StreamStorage, } impl CachedFile { - fn open(path: AudioPath, file_path: PathBuf) -> Result { + fn open(path: MediaPath, file_path: PathBuf) -> Result { Ok(Self { path, storage: StreamStorage::from_complete_file(file_path)?, diff --git a/psst-core/src/player/item.rs b/psst-core/src/player/item.rs index 7993c6fe..c4b15232 100644 --- a/psst-core/src/player/item.rs +++ b/psst-core/src/player/item.rs @@ -6,17 +6,17 @@ use crate::{ cdn::CdnHandle, error::Error, item_id::{ItemId, ItemIdType}, - metadata::{Fetch, ToAudioPath}, + metadata::{Fetch, ToMediaPath}, session::SessionService, }; use super::{ - file::{AudioFile, AudioPath}, + file::{MediaFile, MediaPath}, PlaybackConfig, }; pub struct LoadedPlaybackItem { - pub file: AudioFile, + pub file: MediaFile, pub source: AudioDecoder, pub norm_factor: f32, } @@ -37,7 +37,7 @@ impl PlaybackItem { ) -> Result { let path = load_audio_path(self.item_id, session, &cache, config)?; let key = load_audio_key(&path, session, &cache)?; - let file = AudioFile::open(path, cdn, cache)?; + let file = MediaFile::open(path, cdn, cache)?; let (source, norm_data) = file.audio_source(key)?; let norm_factor = norm_data.factor_for_level(self.norm_level, config.pregain); Ok(LoadedPlaybackItem { @@ -53,7 +53,7 @@ fn load_audio_path( session: &SessionService, cache: &CacheHandle, config: &PlaybackConfig, -) -> Result { +) -> Result { match item_id.id_type { ItemIdType::Track => { load_audio_path_from_track_or_alternative(item_id, session, cache, config) @@ -67,7 +67,7 @@ fn load_audio_path_from_track_or_alternative( session: &SessionService, cache: &CacheHandle, config: &PlaybackConfig, -) -> Result { +) -> Result { let track = load_track(item_id, session, cache)?; let country = get_country_code(session, cache); let path = match country { @@ -76,15 +76,15 @@ fn load_audio_path_from_track_or_alternative( // alternative track. let alt_id = track .find_allowed_alternative(&user_country) - .ok_or(Error::AudioFileNotFound)?; + .ok_or(Error::MediaFileNotFound)?; let alt_track = load_track(alt_id, session, cache)?; let alt_path = alt_track - .to_audio_path(config.bitrate) - .ok_or(Error::AudioFileNotFound)?; + .to_media_path(config.bitrate) + .ok_or(Error::MediaFileNotFound)?; // We've found an alternative track with a fitting audio file. Let's cheat a // little and pretend we've obtained it from the requested track. // TODO: We should be honest and display the real track information. - AudioPath { + MediaPath { item_id, ..alt_path } @@ -93,8 +93,8 @@ fn load_audio_path_from_track_or_alternative( // Either we do not have a country code loaded or the track is available, return // it. track - .to_audio_path(config.bitrate) - .ok_or(Error::AudioFileNotFound)? + .to_media_path(config.bitrate) + .ok_or(Error::MediaFileNotFound)? } }; Ok(path) @@ -129,7 +129,7 @@ fn load_track( } fn load_audio_key( - path: &AudioPath, + path: &MediaPath, session: &SessionService, cache: &CacheHandle, ) -> Result { diff --git a/psst-core/src/player/mod.rs b/psst-core/src/player/mod.rs index 650932b5..b89d2ea4 100644 --- a/psst-core/src/player/mod.rs +++ b/psst-core/src/player/mod.rs @@ -4,21 +4,11 @@ pub mod queue; mod storage; mod worker; -use std::{ - mem, - sync::{ - atomic::{AtomicU32, Ordering}, - Arc, - }, - thread, - thread::JoinHandle, - time::Duration, -}; +use std::{mem, thread, thread::JoinHandle, time::Duration}; use crossbeam_channel::{unbounded, Receiver, Sender}; use crate::{ - actor::Actor, audio::output::{AudioOutput, AudioSink}, cache::CacheHandle, cdn::CdnHandle, @@ -27,10 +17,10 @@ use crate::{ }; use self::{ - file::AudioPath, + file::MediaPath, item::{LoadedPlaybackItem, PlaybackItem}, queue::{Queue, QueueBehavior}, - worker::{Decode, Decoding, DecodingWorker}, + worker::PlaybackManager, }; const PREVIOUS_TRACK_THRESHOLD: Duration = Duration::from_secs(3); @@ -61,8 +51,8 @@ pub struct Player { queue: Queue, sender: Sender, receiver: Receiver, - audio_output_sink: AudioSink, - audio_volume: VolumeLevel, + audio_output_sink: AudioSink, + playback_mgr: PlaybackManager, consecutive_loading_failures: usize, } @@ -76,6 +66,7 @@ impl Player { ) -> Self { let (sender, receiver) = unbounded(); Self { + playback_mgr: PlaybackManager::new(audio_output.sink(), sender.clone()), session, cdn, cache, @@ -83,7 +74,6 @@ impl Player { sender, receiver, audio_output_sink: audio_output.sink(), - audio_volume: VolumeLevel::new(), state: PlayerState::Stopped, preload: PreloadState::None, queue: Queue::new(), @@ -101,21 +91,11 @@ impl Player { pub fn handle(&mut self, event: PlayerEvent) { match event { - PlayerEvent::Command(cmd) => { - self.handle_command(cmd); - } - PlayerEvent::Loaded { item, result } => { - self.handle_loaded(item, result); - } - PlayerEvent::Preloaded { item, result } => { - self.handle_preloaded(item, result); - } - PlayerEvent::Position { position, path } => { - self.handle_position(position, path); - } - PlayerEvent::EndOfTrack { .. } => { - self.handle_end_of_track(); - } + PlayerEvent::Command(cmd) => self.handle_command(cmd), + PlayerEvent::Loaded { item, result } => self.handle_loaded(item, result), + PlayerEvent::Preloaded { item, result } => self.handle_preloaded(item, result), + PlayerEvent::Position { position, path } => self.handle_position(position, path), + PlayerEvent::EndOfTrack { .. } => self.handle_end_of_track(), PlayerEvent::Loading { .. } | PlayerEvent::Playing { .. } | PlayerEvent::Pausing { .. } @@ -187,17 +167,22 @@ impl Player { }, _ => { log::info!("stale preload result received, ignoring"); + + // We are not preloading this item, but because we sometimes extract the + // preloading thread and use it for loading, let's check if the item is not + // being loaded now. + self.handle_loaded(item, result); } } } - fn handle_position(&mut self, new_position: Duration, path: AudioPath) { + fn handle_position(&mut self, new_position: Duration, path: MediaPath) { match &mut self.state { PlayerState::Playing { position, .. } | PlayerState::Paused { position, .. } => { *position = new_position; } _ => { - log::warn!("received ununwraped position report"); + log::warn!("received unexpected position report"); } } const PRELOAD_BEFORE_END_OF_TRACK: Duration = Duration::from_secs(30); @@ -229,36 +214,42 @@ impl Player { } fn load_and_play(&mut self, item: PlaybackItem) { - // Check if the item is already preloaded, and if so, take it out of the - // preloader state, and start the playback. - match mem::replace(&mut self.preload, PreloadState::None) { + // Check if the item is already in the preloader state. + let loading_handle = match mem::replace(&mut self.preload, PreloadState::None) { PreloadState::Preloaded { item: preloaded_item, loaded_item, } if preloaded_item == item => { + // This item is already loaded in the preloader state. self.play_loaded(loaded_item); return; } - preloading_or_none => { - // Restore the preloader to the previous state. - // TODO: If the item is being preloaded, extract the loading handle. - self.preload = preloading_or_none; + + PreloadState::Preloading { + item: preloaded_item, + loading_handle, + } if preloaded_item == item => { + // This item is being preloaded. Take it out of the preloader state. + loading_handle } - } - // Item is not preloaded yet, load it in a background thread. - let loading_handle = thread::spawn({ - let event_sender = self.sender.clone(); - let session = self.session.clone(); - let cdn = self.cdn.clone(); - let cache = self.cache.clone(); - let config = self.config.clone(); - move || { - let result = item.load(&session, cdn, cache, &config); - event_sender - .send(PlayerEvent::Loaded { item, result }) - .unwrap(); + + preloading_other_file_or_none => { + self.preload = preloading_other_file_or_none; + // Item is not preloaded yet, load it in a background thread. + thread::spawn({ + let sender = self.sender.clone(); + let session = self.session.clone(); + let cdn = self.cdn.clone(); + let cache = self.cache.clone(); + let config = self.config.clone(); + move || { + let result = item.load(&session, cdn, cache, &config); + sender.send(PlayerEvent::Loaded { item, result }).unwrap(); + } + }) } - }); + }; + self.sender.send(PlayerEvent::Loading { item }).unwrap(); self.state = PlayerState::Loading { item, @@ -271,44 +262,34 @@ impl Player { return; } let loading_handle = thread::spawn({ - let event_sender = self.sender.clone(); + let sender = self.sender.clone(); let session = self.session.clone(); let cdn = self.cdn.clone(); let cache = self.cache.clone(); let config = self.config.clone(); move || { let result = item.load(&session, cdn, cache, &config); - event_sender + sender .send(PlayerEvent::Preloaded { item, result }) .unwrap(); } }); self.preload = PreloadState::Preloading { item, - _loading_handle: loading_handle, + loading_handle, }; } fn set_volume(&mut self, volume: f64) { - self.audio_volume.set(volume as _); + self.audio_output_sink.set_volume(volume as f32); } fn play_loaded(&mut self, loaded_item: LoadedPlaybackItem) { log::info!("starting playback"); let path = loaded_item.file.path(); let position = Duration::default(); - let worker = Decoding::spawn_default({ - let events = self.sender.clone(); - let sink = self.audio_output_sink.clone(); - let volume = self.audio_volume.clone(); - move |this| Decoding::new(loaded_item, events, this, sink, volume) - }); - worker.send(Decode::Start).unwrap(); - self.state = PlayerState::Playing { - path, - position, - worker: DecodingWorker { actor: worker }, - }; + self.playback_mgr.play(loaded_item); + self.state = PlayerState::Playing { path, position }; self.sender .send(PlayerEvent::Playing { path, position }) .unwrap(); @@ -316,26 +297,13 @@ impl Player { fn pause(&mut self) { match mem::replace(&mut self.state, PlayerState::Invalid) { - PlayerState::Playing { - path, - position, - worker, - } - | PlayerState::Paused { - path, - position, - worker, - } => { + PlayerState::Playing { path, position } | PlayerState::Paused { path, position } => { log::info!("pausing playback"); - worker.actor.send(Decode::Stop).unwrap(); + self.audio_output_sink.pause(); self.sender .send(PlayerEvent::Pausing { path, position }) .unwrap(); - self.state = PlayerState::Paused { - path, - position, - worker, - }; + self.state = PlayerState::Paused { path, position }; } _ => { log::warn!("invalid state transition"); @@ -345,26 +313,13 @@ impl Player { fn resume(&mut self) { match mem::replace(&mut self.state, PlayerState::Invalid) { - PlayerState::Playing { - path, - position, - worker, - } - | PlayerState::Paused { - path, - position, - worker, - } => { + PlayerState::Playing { path, position } | PlayerState::Paused { path, position } => { log::info!("resuming playback"); - worker.actor.send(Decode::Start).unwrap(); + self.audio_output_sink.resume(); self.sender .send(PlayerEvent::Resuming { path, position }) .unwrap(); - self.state = PlayerState::Playing { - path, - position, - worker, - }; + self.state = PlayerState::Playing { path, position }; } _ => { log::warn!("invalid state transition"); @@ -412,11 +367,7 @@ impl Player { } fn seek(&mut self, position: Duration) { - if let PlayerState::Playing { worker, .. } | PlayerState::Paused { worker, .. } = - &mut self.state - { - worker.actor.send(Decode::Seek(position)).unwrap(); - } + self.playback_mgr.seek(position); } fn configure(&mut self, config: PlaybackConfig) { @@ -493,27 +444,27 @@ pub enum PlayerEvent { }, /// Player has started playing new track. `Position` events will follow. Playing { - path: AudioPath, + path: MediaPath, position: Duration, }, /// Player is in a paused state. `Resuming` might follow. Pausing { - path: AudioPath, + path: MediaPath, position: Duration, }, /// Player is resuming playback of a track. `Position` events will follow. Resuming { - path: AudioPath, + path: MediaPath, position: Duration, }, /// Position of the playback head has changed. Position { - path: AudioPath, + path: MediaPath, position: Duration, }, /// Player would like to continue playing, but is blocked, waiting for I/O. Blocked { - path: AudioPath, + path: MediaPath, position: Duration, }, /// Player has finished playing a track. `Loading` or `Playing` might @@ -529,14 +480,12 @@ enum PlayerState { _loading_handle: JoinHandle<()>, }, Playing { - path: AudioPath, + path: MediaPath, position: Duration, - worker: DecodingWorker, }, Paused { - path: AudioPath, + path: MediaPath, position: Duration, - worker: DecodingWorker, }, Stopped, Invalid, @@ -545,7 +494,7 @@ enum PlayerState { enum PreloadState { Preloading { item: PlaybackItem, - _loading_handle: JoinHandle<()>, + loading_handle: JoinHandle<()>, }, Preloaded { item: PlaybackItem, @@ -553,24 +502,3 @@ enum PreloadState { }, None, } - -#[derive(Clone)] -pub struct VolumeLevel { - volume: Arc, -} - -impl VolumeLevel { - pub fn new() -> Self { - Self { - volume: Arc::new(AtomicU32::new(0)), - } - } - - pub fn set(&self, volume: f32) { - self.volume.store(volume.to_bits(), Ordering::Relaxed) - } - - pub fn get(&self) -> f32 { - f32::from_bits(self.volume.load(Ordering::Relaxed)) - } -} diff --git a/psst-core/src/player/worker.rs b/psst-core/src/player/worker.rs index e1465e05..774e4bf6 100644 --- a/psst-core/src/player/worker.rs +++ b/psst-core/src/player/worker.rs @@ -1,206 +1,322 @@ -use std::time::Duration; +use std::{ + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, + time::Duration, +}; use crossbeam_channel::Sender; -use symphonia::core::audio::{SampleBuffer, SignalSpec}; +use rb::{Consumer, Producer, RbProducer, SpscRb, RB}; +use symphonia::core::{ + audio::{SampleBuffer, SignalSpec}, + units::TimeBase, +}; use crate::{ - actor::{Actor, ActorHandle, ActorOp}, - audio::{ - decode::AudioDecoder, - output::AudioSink, - resample::{AudioResampler, ResamplingQuality, ResamplingSpec}, - }, + actor::{Act, Actor, ActorHandle}, + audio::{decode::AudioDecoder, output::AudioSink, source::AudioSource}, error::Error, }; -use super::{file::AudioFile, LoadedPlaybackItem, PlayerEvent, VolumeLevel}; +use super::{ + file::{MediaFile, MediaPath}, + LoadedPlaybackItem, PlayerEvent, +}; -pub struct DecodingWorker { - pub actor: ActorHandle, +pub struct PlaybackManager { + sink: AudioSink, + event_send: Sender, + current: Option<(MediaPath, Sender)>, } -impl Drop for DecodingWorker { - fn drop(&mut self) { - let _ = self.actor.send(Decode::Quit); +impl PlaybackManager { + pub fn new(sink: AudioSink, event_send: Sender) -> Self { + Self { + sink, + event_send, + current: None, + } } -} -const REPORT_POSITION_EACH: Duration = Duration::from_millis(1000); + pub fn play(&mut self, loaded: LoadedPlaybackItem) { + let path = loaded.file.path(); + let source = DecoderSource::new(loaded, self.event_send.clone()); + self.current = Some((path, source.actor.sender())); + self.sink.play(source); + self.sink.resume(); + } -pub enum Decode { - Start, - Stop, - Seek(Duration), - ReadPacket, - FlushPacket, - Quit, -} + pub fn seek(&self, position: Duration) { + if let Some((path, worker)) = &self.current { + let _ = worker.send(Msg::Seek(position)); -enum DecState { - Started, - Stopped, + // Because the position events are sent in the `DecoderSource`, doing this here + // is slightly hacky. The alternative would be propagating `event_send` into the + // worker. + let _ = self.event_send.send(PlayerEvent::Position { + path: path.to_owned(), + position, + }); + } + } } -pub struct Decoding { - file: AudioFile, - source: AudioDecoder, +pub struct DecoderSource { + file: MediaFile, + actor: ActorHandle, + consumer: Consumer, + event_send: Sender, + position: Arc, + precision: u64, + reported: u64, + total_samples: u64, + end_of_track: bool, norm_factor: f32, - resampler: AudioResampler, - samples: SampleBuffer, - events: Sender, - this: Sender, - sink: AudioSink, - volume: VolumeLevel, - state: DecState, - last_reported_position: Duration, + time_base: TimeBase, + signal_spec: SignalSpec, } -impl Decoding { - pub fn new( - loaded: LoadedPlaybackItem, - events: Sender, - this: Sender, - sink: AudioSink, - volume: VolumeLevel, - ) -> Self { +impl DecoderSource { + pub fn new(loaded: LoadedPlaybackItem, event_send: Sender) -> Self { let LoadedPlaybackItem { file, source, norm_factor, } = loaded; - let resampler = AudioResampler::new( - // TODO: Make the quality configurable. - ResamplingQuality::SincMediumQuality, - ResamplingSpec { - channels: source.channels().unwrap().count(), - from_rate: source.sample_rate().unwrap() as usize, - to_rate: sink.sample_rate() as usize, - }, - 1024 * 8, - ) - .unwrap(); - let samples = { - let max_frames = source.max_frames_per_packet().unwrap_or(1024 * 8); - let channels = source.channels().unwrap(); - let rate = source.sample_rate().unwrap(); - SampleBuffer::new(max_frames, SignalSpec { rate, channels }) - }; + + const REPORT_PRECISION: Duration = Duration::from_millis(1000); + + // Gather the source signal parameters and compute how often we should report + // the play-head position. + let signal_spec = source.signal_spec(); + let total_samples = + source.codec_params().n_frames.unwrap() * signal_spec.channels.count() as u64; + let time_base = source.codec_params().time_base.unwrap(); + let precision = REPORT_PRECISION.as_millis() as u64 + / (signal_spec.rate as u64 * signal_spec.channels.count() as u64); + + // Create a ring-buffer for the decoded samples. Worker thread is producing, + // we are consuming in the `AudioSource` impl. + let buffer = Worker::default_buffer(); + let consumer = buffer.consumer(); + + // We keep track of the current play-head position by sharing an atomic sample + // counter with the decoding worker. Worker is setting this on seek, we are + // incrementing on reading from the ring-buffer. + let position = Arc::new(AtomicU64::new(0)); + + // Spawn the worker and kick-start the decoding. The buffer will start filling + // now. + let actor = Worker::spawn_default({ + let position = Arc::clone(&position); + move |this| Worker::new(this, source, buffer, position) + }); + let _ = actor.send(Msg::Read); + Self { file, - source, + actor, + consumer, + event_send, norm_factor, - resampler, - samples, - events, - this, - sink, - volume, - state: DecState::Stopped, - last_reported_position: Duration::ZERO, + time_base, + signal_spec, + total_samples, + end_of_track: false, + position, + precision, + reported: u64::MAX, // Something sufficiently distinct from any position. } } - fn frames_to_duration(&self, frames: u64) -> Duration { - Duration::from_secs_f64(frames as f64 / self.source.sample_rate().unwrap() as f64) + fn written_samples(&self, position: u64) -> u64 { + self.position.fetch_add(position, Ordering::Relaxed) + position } - fn report_position(&mut self, position: Duration) { - if self - .events - .try_send(PlayerEvent::Position { - path: self.file.path(), - position, - }) - .is_ok() - { - self.last_reported_position = position; - } + fn should_report(&self, pos: u64) -> bool { + self.reported > pos || pos - self.reported >= self.precision } - fn report_current_position(&mut self) { - let position = self.frames_to_duration(self.source.current_frame()); - self.report_position(position); + fn samples_to_duration(&self, samples: u64) -> Duration { + let frames = samples / self.signal_spec.channels.count() as u64; + let time = self.time_base.calc_time(frames); + Duration::from_secs(time.seconds) + Duration::from_secs_f64(time.frac) } +} + +impl AudioSource for DecoderSource { + fn write(&mut self, output: &mut [f32]) -> usize { + if self.end_of_track { + return 0; + } + let written = self.consumer.write(output); + + // Apply the normalization factor. + output[..written] + .iter_mut() + .for_each(|s| *s *= self.norm_factor); + + let position = self.written_samples(written as u64); + if self.should_report(position) { + // Send a position report, so the upper layers can visualize the playback + // progress and preload the next track. We cannot block here, so if the channel + // is full, we just try the next time instead of waiting. + if self + .event_send + .try_send(PlayerEvent::Position { + path: self.file.path(), + position: self.samples_to_duration(position), + }) + .is_ok() + { + self.reported = position; + } + } - fn report_current_position_if_neeeded(&mut self) { - let position = self.frames_to_duration(self.source.current_frame()); - if position.saturating_sub(self.last_reported_position) > REPORT_POSITION_EACH { - self.report_position(position); + if position >= self.total_samples { + // After reading the number of samples from the `CodecParameters`, we stop. + // Signal to the upper layer this track is over and short-circuit all further + // reads from this source. + if self.event_send.try_send(PlayerEvent::EndOfTrack).is_ok() { + self.end_of_track = true; + } } + + written + } +} + +impl Drop for DecoderSource { + fn drop(&mut self) { + let _ = self.actor.send(Msg::Stop); + } +} + +enum Msg { + Seek(Duration), + Read, + Stop, +} + +struct Worker { + decoder: AudioDecoder, + spec: SignalSpec, + this: Sender, + output: SpscRb, + producer: Producer, + packet: SampleBuffer, + position: Arc, + samples_written: usize, + samples_to_flush: usize, + is_reading: bool, +} + +impl Worker { + fn default_buffer() -> SpscRb { + const DEFAULT_BUFFER_SIZE: usize = 64 * 1024; + + SpscRb::new(DEFAULT_BUFFER_SIZE) } - fn is_started(&self) -> bool { - matches!(self.state, DecState::Started) + fn new( + this: Sender, + decoder: AudioDecoder, + output: SpscRb, + position: Arc, + ) -> Self { + const DEFAULT_MAX_FRAMES: u64 = 1024 * 8; + + Self { + producer: output.producer(), + packet: SampleBuffer::new( + decoder + .codec_params() + .max_frames_per_packet + .unwrap_or(DEFAULT_MAX_FRAMES), + decoder.signal_spec(), + ), + spec: decoder.signal_spec(), + decoder, + this, + output, + position, + samples_written: 0, + samples_to_flush: 0, + is_reading: false, + } } } -impl Actor for Decoding { - type Message = Decode; +impl Actor for Worker { + type Message = Msg; type Error = Error; - fn handle(&mut self, msg: Self::Message) -> Result { + fn handle(&mut self, msg: Msg) -> Result, Self::Error> { match msg { - Decode::Start if !self.is_started() => { - self.this.send(Decode::ReadPacket)?; - self.state = DecState::Started; - Ok(ActorOp::Continue) - } - Decode::Stop if self.is_started() => { - self.state = DecState::Stopped; - Ok(ActorOp::Continue) - } - Decode::Seek(pos) => self.handle_seek(pos), - Decode::ReadPacket => self.handle_read_packet(), - Decode::FlushPacket => self.handle_flush_packet(), - Decode::Quit => Ok(ActorOp::Shutdown), - _ => Ok(ActorOp::Continue), + Msg::Seek(time) => self.on_seek(time), + Msg::Read => self.on_read(), + Msg::Stop => Ok(Act::Shutdown), } } } -impl Decoding { - fn handle_seek(&mut self, position: Duration) -> Result { - if let Err(err) = self.source.seek(position) { - log::error!("failed to seek: {}", err); - } else { - self.report_current_position(); +impl Worker { + fn on_seek(&mut self, time: Duration) -> Result, Error> { + match self.decoder.seek(time) { + Ok(timestamp) => { + if self.is_reading { + self.samples_to_flush = 0; + } else { + self.this.send(Msg::Read)?; + } + let position = timestamp * self.spec.channels.count() as u64; + self.position.store(position, Ordering::Relaxed); + self.output.clear(); + } + Err(err) => { + log::error!("failed to seek: {}", err); + } } - Ok(ActorOp::Continue) + Ok(Act::Continue) } - fn handle_read_packet(&mut self) -> Result { - if self.is_started() { - if let Some(packet) = self.source.next_packet() { - self.samples.copy_interleaved_ref(packet); - self.report_current_position_if_neeeded(); - self.this.send(Decode::FlushPacket)?; + fn on_read(&mut self) -> Result, Error> { + if self.samples_to_flush > 0 { + let writable = last_n(self.packet.samples(), self.samples_to_flush); + if let Ok(written) = self.producer.write(writable) { + self.samples_written += written; + self.samples_to_flush -= written; + self.is_reading = true; + self.this.send(Msg::Read)?; + Ok(Act::Continue) } else { - self.events.send(PlayerEvent::EndOfTrack)?; - return Ok(ActorOp::Shutdown); + // Buffer is full. Wait a bit a try again. We also have to indicate that the + // read loop is not running at the moment (if we receive a `Seek` while waiting, + // we need it to explicitly kickstart reading again). + self.is_reading = false; + Ok(Act::WaitOr { + timeout: Duration::from_millis(500), + timeout_msg: Msg::Read, + }) + } + } else { + match self.decoder.next_packet() { + Some((_, packet)) => { + self.packet.copy_interleaved_ref(packet); + self.samples_to_flush = self.packet.samples().len(); + self.is_reading = true; + self.this.send(Msg::Read)?; + } + None => { + self.is_reading = false; + } } + Ok(Act::Continue) } - Ok(ActorOp::Continue) } +} - fn handle_flush_packet(&mut self) -> Result { - let samples = self.samples.samples(); - - // Resample the sample buffer into a rate that the audio output supports. - let resampled = self.resampler.resample(samples)?; - - // Apply the global volume level and the normalization factor. - let factor = self.norm_factor * self.volume.get(); - for sample in resampled.iter_mut() { - *sample *= factor; - } - - // Write into the sink, block until all samples are committed to the ring - // buffer. - self.sink.write_blocking(resampled)?; - - if self.is_started() { - self.this.send(Decode::ReadPacket)?; - } - Ok(ActorOp::Continue) - } +fn last_n(slice: &[T], n: usize) -> &[T] { + &slice[slice.len() - n..] } From 583e4e6a2971239f483bfec908eab6f6434cbbd4 Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Fri, 12 Nov 2021 10:00:58 +0100 Subject: [PATCH 08/14] core: Add resampling to the new Worker --- psst-core/src/audio/resample.rs | 40 ++++++------- psst-core/src/player/worker.rs | 101 +++++++++++++++++++++----------- 2 files changed, 86 insertions(+), 55 deletions(-) diff --git a/psst-core/src/audio/resample.rs b/psst-core/src/audio/resample.rs index 73345c9d..ac867f7a 100644 --- a/psst-core/src/audio/resample.rs +++ b/psst-core/src/audio/resample.rs @@ -9,6 +9,7 @@ pub enum ResamplingQuality { Linear = libsamplerate::SRC_LINEAR as isize, } +#[derive(Copy, Clone)] pub struct ResamplingSpec { pub from_rate: usize, pub to_rate: usize, @@ -16,7 +17,11 @@ pub struct ResamplingSpec { } impl ResamplingSpec { - fn ratio(&self) -> f64 { + pub fn max_output_size(&self, max_input_size: usize) -> usize { + (self.ratio() * max_input_size as f64 * 1.2) as usize + } + + pub fn ratio(&self) -> f64 { self.to_rate as f64 / self.from_rate as f64 } } @@ -24,15 +29,10 @@ impl ResamplingSpec { pub struct AudioResampler { state: *mut libsamplerate::SRC_STATE, spec: ResamplingSpec, - output: Vec, } impl AudioResampler { - pub fn new( - quality: ResamplingQuality, - spec: ResamplingSpec, - capacity: usize, - ) -> Result { + pub fn new(quality: ResamplingQuality, spec: ResamplingSpec) -> Result { let mut error_int = 0i32; let state = unsafe { libsamplerate::src_new( @@ -44,26 +44,22 @@ impl AudioResampler { if error_int != 0 { Err(Error::ResamplingError(error_int)) } else { - Ok(Self { - state, - spec, - output: vec![0.0; capacity], - }) + Ok(Self { state, spec }) } } - pub fn resample(&mut self, inter_input: &[f32]) -> Result<&mut [f32], Error> { + pub fn resample(&mut self, input: &[f32], output: &mut [f32]) -> Result { if self.spec.from_rate == self.spec.to_rate { // Bypass conversion completely in case the sample rates are equal. - let output = &mut self.output[..inter_input.len()]; - output.copy_from_slice(inter_input); - return Ok(output); + let output = &mut output[..input.len()]; + output.copy_from_slice(input); + return Ok(output.len()); } let mut src = libsamplerate::SRC_DATA { - data_in: inter_input.as_ptr(), - data_out: self.output.as_mut_ptr(), - input_frames: (inter_input.len() / self.spec.channels) as _, - output_frames: (self.output.len() / self.spec.channels) as _, + data_in: input.as_ptr(), + data_out: output.as_mut_ptr(), + input_frames: (input.len() / self.spec.channels) as _, + output_frames: (output.len() / self.spec.channels) as _, src_ratio: self.spec.ratio(), end_of_input: 0, // TODO: Use this. input_frames_used: 0, @@ -75,10 +71,10 @@ impl AudioResampler { } else { let output_len = src.output_frames_gen as usize * self.spec.channels; let processed_len = src.input_frames_used as usize * self.spec.channels; - if processed_len != inter_input.len() { + if processed_len != input.len() { log::warn!("skipping frames while resampling"); } - Ok(&mut self.output[..output_len]) + Ok(output_len) } } } diff --git a/psst-core/src/player/worker.rs b/psst-core/src/player/worker.rs index 774e4bf6..3421acd7 100644 --- a/psst-core/src/player/worker.rs +++ b/psst-core/src/player/worker.rs @@ -1,4 +1,5 @@ use std::{ + ops::Range, sync::{ atomic::{AtomicU64, Ordering}, Arc, @@ -15,7 +16,12 @@ use symphonia::core::{ use crate::{ actor::{Act, Actor, ActorHandle}, - audio::{decode::AudioDecoder, output::AudioSink, source::AudioSource}, + audio::{ + decode::AudioDecoder, + output::AudioSink, + resample::{AudioResampler, ResamplingQuality, ResamplingSpec}, + source::AudioSource, + }, error::Error, }; @@ -41,7 +47,7 @@ impl PlaybackManager { pub fn play(&mut self, loaded: LoadedPlaybackItem) { let path = loaded.file.path(); - let source = DecoderSource::new(loaded, self.event_send.clone()); + let source = DecoderSource::new(loaded, self.event_send.clone(), self.sink.sample_rate()); self.current = Some((path, source.actor.sender())); self.sink.play(source); self.sink.resume(); @@ -78,7 +84,11 @@ pub struct DecoderSource { } impl DecoderSource { - pub fn new(loaded: LoadedPlaybackItem, event_send: Sender) -> Self { + pub fn new( + loaded: LoadedPlaybackItem, + event_send: Sender, + output_sample_rate: u32, + ) -> Self { let LoadedPlaybackItem { file, source, @@ -90,11 +100,12 @@ impl DecoderSource { // Gather the source signal parameters and compute how often we should report // the play-head position. let signal_spec = source.signal_spec(); - let total_samples = - source.codec_params().n_frames.unwrap() * signal_spec.channels.count() as u64; - let time_base = source.codec_params().time_base.unwrap(); - let precision = REPORT_PRECISION.as_millis() as u64 - / (signal_spec.rate as u64 * signal_spec.channels.count() as u64); + let chan_count = signal_spec.channels.count(); + let cp = source.codec_params(); + let total_samples = cp.n_frames.unwrap() * chan_count as u64; + let time_base = cp.time_base.unwrap(); + let precision = + REPORT_PRECISION.as_millis() as u64 / (signal_spec.rate as u64 * chan_count as u64); // Create a ring-buffer for the decoded samples. Worker thread is producing, // we are consuming in the `AudioSource` impl. @@ -106,11 +117,19 @@ impl DecoderSource { // incrementing on reading from the ring-buffer. let position = Arc::new(AtomicU64::new(0)); + // Some output streams have different sample rate than the source, so we need to + // resample before pushing to the sink. + let res_spec = ResamplingSpec { + from_rate: signal_spec.rate as usize, + to_rate: output_sample_rate as usize, + channels: chan_count, + }; + // Spawn the worker and kick-start the decoding. The buffer will start filling // now. let actor = Worker::spawn_default({ let position = Arc::clone(&position); - move |this| Worker::new(this, source, buffer, position) + move |this| Worker::new(this, source, buffer, position, res_spec) }); let _ = actor.send(Msg::Read); @@ -175,9 +194,9 @@ impl AudioSource for DecoderSource { } if position >= self.total_samples { - // After reading the number of samples from the `CodecParameters`, we stop. - // Signal to the upper layer this track is over and short-circuit all further - // reads from this source. + // After reading the total number of samples from the `CodecParameters`, we + // stop. Signal to the upper layer this track is over and short-circuit all + // further reads from this source. if self.event_send.try_send(PlayerEvent::EndOfTrack).is_ok() { self.end_of_track = true; } @@ -200,15 +219,27 @@ enum Msg { } struct Worker { + /// Decoder we are reading packets/samples from. decoder: AudioDecoder, + /// Audio properties of the decoded signal. spec: SignalSpec, + /// Sending part of our own actor channel. this: Sender, + /// Ring-buffer for the output signal. output: SpscRb, + /// Producing part of the output ring-buffer. producer: Producer, + /// Sample buffer containing samples read in the last packet. packet: SampleBuffer, + /// Buffer containing signal resampled from `packet`. + resampled: Vec, + /// Resampling structure we use to resample the signal. + resampler: AudioResampler, + /// Shared atomic position. Worker updates this on seek only. position: Arc, - samples_written: usize, - samples_to_flush: usize, + /// Range of samples in `resampled` that are awaiting flush into `output`. + samples_to_flush: Range, + /// Are we in the middle of automatic read loop? is_reading: bool, } @@ -224,25 +255,31 @@ impl Worker { decoder: AudioDecoder, output: SpscRb, position: Arc, + res_spec: ResamplingSpec, ) -> Self { const DEFAULT_MAX_FRAMES: u64 = 1024 * 8; + let max_frames = decoder + .codec_params() + .max_frames_per_packet + .unwrap_or(DEFAULT_MAX_FRAMES); + let max_samples = max_frames as usize * res_spec.channels; + let max_output_size = res_spec.max_output_size(max_samples); + let resampled = vec![0.0; max_output_size]; + let resampler = + AudioResampler::new(ResamplingQuality::SincMediumQuality, res_spec).unwrap(); + Self { producer: output.producer(), - packet: SampleBuffer::new( - decoder - .codec_params() - .max_frames_per_packet - .unwrap_or(DEFAULT_MAX_FRAMES), - decoder.signal_spec(), - ), + packet: SampleBuffer::new(max_frames, decoder.signal_spec()), spec: decoder.signal_spec(), + resampled, + resampler, decoder, this, output, position, - samples_written: 0, - samples_to_flush: 0, + samples_to_flush: 0..0, // Arbitrary empty range. is_reading: false, } } @@ -266,7 +303,7 @@ impl Worker { match self.decoder.seek(time) { Ok(timestamp) => { if self.is_reading { - self.samples_to_flush = 0; + self.samples_to_flush = 0..0; } else { self.this.send(Msg::Read)?; } @@ -282,11 +319,10 @@ impl Worker { } fn on_read(&mut self) -> Result, Error> { - if self.samples_to_flush > 0 { - let writable = last_n(self.packet.samples(), self.samples_to_flush); + if !self.samples_to_flush.is_empty() { + let writable = &self.resampled[self.samples_to_flush.clone()]; if let Ok(written) = self.producer.write(writable) { - self.samples_written += written; - self.samples_to_flush -= written; + self.samples_to_flush.start += written; self.is_reading = true; self.this.send(Msg::Read)?; Ok(Act::Continue) @@ -304,7 +340,10 @@ impl Worker { match self.decoder.next_packet() { Some((_, packet)) => { self.packet.copy_interleaved_ref(packet); - self.samples_to_flush = self.packet.samples().len(); + let to_flush = self + .resampler + .resample(self.packet.samples(), &mut self.resampled)?; + self.samples_to_flush = 0..to_flush; self.is_reading = true; self.this.send(Msg::Read)?; } @@ -316,7 +355,3 @@ impl Worker { } } } - -fn last_n(slice: &[T], n: usize) -> &[T] { - &slice[slice.len() - n..] -} From b689920d79141a35bf8df51107ce0028626736ba Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Fri, 12 Nov 2021 13:43:49 +0100 Subject: [PATCH 09/14] core: Fix reporting rate calculation --- psst-core/src/player/mod.rs | 7 +++---- psst-core/src/player/worker.rs | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/psst-core/src/player/mod.rs b/psst-core/src/player/mod.rs index b89d2ea4..58de16e0 100644 --- a/psst-core/src/player/mod.rs +++ b/psst-core/src/player/mod.rs @@ -186,10 +186,9 @@ impl Player { } } const PRELOAD_BEFORE_END_OF_TRACK: Duration = Duration::from_secs(30); - if let Some(&item_to_preload) = self.queue.get_following() { - let time_until_end_of_track = - path.duration.checked_sub(new_position).unwrap_or_default(); - if time_until_end_of_track <= PRELOAD_BEFORE_END_OF_TRACK { + let time_until_end_of_track = path.duration.checked_sub(new_position).unwrap_or_default(); + if time_until_end_of_track <= PRELOAD_BEFORE_END_OF_TRACK { + if let Some(&item_to_preload) = self.queue.get_following() { self.preload(item_to_preload); } } diff --git a/psst-core/src/player/worker.rs b/psst-core/src/player/worker.rs index 3421acd7..114daee8 100644 --- a/psst-core/src/player/worker.rs +++ b/psst-core/src/player/worker.rs @@ -105,7 +105,7 @@ impl DecoderSource { let total_samples = cp.n_frames.unwrap() * chan_count as u64; let time_base = cp.time_base.unwrap(); let precision = - REPORT_PRECISION.as_millis() as u64 / (signal_spec.rate as u64 * chan_count as u64); + (signal_spec.rate as f64 * chan_count as f64 * REPORT_PRECISION.as_secs_f64()) as u64; // Create a ring-buffer for the decoded samples. Worker thread is producing, // we are consuming in the `AudioSource` impl. From 8220990eae0f79345ed23685381f3e613869c4f7 Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Fri, 12 Nov 2021 13:55:01 +0100 Subject: [PATCH 10/14] Nit --- psst-gui/src/data/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psst-gui/src/data/utils.rs b/psst-gui/src/data/utils.rs index aad1d33e..8335b199 100644 --- a/psst-gui/src/data/utils.rs +++ b/psst-gui/src/data/utils.rs @@ -7,7 +7,7 @@ use std::{ use druid::{im::Vector, Data, Lens}; use serde::{Deserialize, Deserializer, Serialize}; -use time::{Date, Month, OffsetDateTime}; +use time::{Date, Month}; #[derive(Clone, Data, Lens)] pub struct Cached { From adec8469a15341f300d2bed9d8e48905d9bcd3cb Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Fri, 12 Nov 2021 14:19:22 +0100 Subject: [PATCH 11/14] github: Add ALSA dependency --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f1d429ba..7dabe725 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: if: ${{ runner.os == 'Linux' }} - name: Install Linux Dependencies - run: sudo apt-get install -y libgtk-3-dev libssl-dev + run: sudo apt-get install -y libgtk-3-dev libssl-dev libasound2-dev if: ${{ runner.os == 'Linux' }} - name: Check Formatting From e037c421cccb5f796efd1e98ccd83b8fe08f6a8b Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Fri, 12 Nov 2021 19:21:35 +0100 Subject: [PATCH 12/14] core: Add client-side output pausing --- psst-core/src/audio/output.rs | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/psst-core/src/audio/output.rs b/psst-core/src/audio/output.rs index 3c7d81d3..aa5ad339 100644 --- a/psst-core/src/audio/output.rs +++ b/psst-core/src/audio/output.rs @@ -75,10 +75,12 @@ impl AudioSink { pub fn pause(&self) { self.send_to_stream(StreamMsg::Pause); + self.send_to_callback(CallbackMsg::Pause); } pub fn resume(&self) { self.send_to_stream(StreamMsg::Resume); + self.send_to_callback(CallbackMsg::Resume); } pub fn close(&self) { @@ -121,6 +123,7 @@ impl Stream { _stream_send: stream_send, source: Box::new(Empty), volume: 1.0, // We start with the full volume. + state: CallbackState::Paused, }; log::info!("opening output stream: {:?}", config); @@ -173,12 +176,20 @@ impl Actor for Stream { enum CallbackMsg { PlaySource(Box), SetVolume(f32), + Pause, + Resume, +} + +enum CallbackState { + Playing, + Paused, } struct StreamCallback { - callback_recv: Receiver, _stream_send: Sender, + callback_recv: Receiver, source: Box, + state: CallbackState, volume: f32, } @@ -193,15 +204,27 @@ impl StreamCallback { CallbackMsg::SetVolume(volume) => { self.volume = volume; } + CallbackMsg::Pause => { + self.state = CallbackState::Paused; + } + CallbackMsg::Resume => { + self.state = CallbackState::Playing; + } } } - // Write out as many samples as possible from the audio source to the - // output buffer. - let written = self.source.write(output); + let written = if matches!(self.state, CallbackState::Playing) { + // Write out as many samples as possible from the audio source to the + // output buffer. + let written = self.source.write(output); - // Apply the global volume level. - output[..written].iter_mut().for_each(|s| *s *= self.volume); + // Apply the global volume level. + output[..written].iter_mut().for_each(|s| *s *= self.volume); + + written + } else { + 0 + }; // Mute any remaining samples. output[written..].iter_mut().for_each(|s| *s = 0.0); From 945570ab732217ddf909cedc33ff863cdeb318f4 Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Tue, 16 Nov 2021 10:54:04 +0100 Subject: [PATCH 13/14] core: Cleanup Worker, calc total length ourselves --- psst-core/src/audio/resample.rs | 2 +- psst-core/src/player/worker.rs | 139 ++++++++++++++++++-------------- 2 files changed, 81 insertions(+), 60 deletions(-) diff --git a/psst-core/src/audio/resample.rs b/psst-core/src/audio/resample.rs index ac867f7a..7f83be0a 100644 --- a/psst-core/src/audio/resample.rs +++ b/psst-core/src/audio/resample.rs @@ -27,8 +27,8 @@ impl ResamplingSpec { } pub struct AudioResampler { + pub spec: ResamplingSpec, state: *mut libsamplerate::SRC_STATE, - spec: ResamplingSpec, } impl AudioResampler { diff --git a/psst-core/src/player/worker.rs b/psst-core/src/player/worker.rs index 114daee8..39aa5dd4 100644 --- a/psst-core/src/player/worker.rs +++ b/psst-core/src/player/worker.rs @@ -73,14 +73,14 @@ pub struct DecoderSource { actor: ActorHandle, consumer: Consumer, event_send: Sender, + total_samples: Arc, position: Arc, precision: u64, reported: u64, - total_samples: u64, end_of_track: bool, norm_factor: f32, - time_base: TimeBase, - signal_spec: SignalSpec, + input_spec: SignalSpec, + output_time_base: TimeBase, } impl DecoderSource { @@ -99,13 +99,11 @@ impl DecoderSource { // Gather the source signal parameters and compute how often we should report // the play-head position. - let signal_spec = source.signal_spec(); - let chan_count = signal_spec.channels.count(); - let cp = source.codec_params(); - let total_samples = cp.n_frames.unwrap() * chan_count as u64; - let time_base = cp.time_base.unwrap(); - let precision = - (signal_spec.rate as f64 * chan_count as f64 * REPORT_PRECISION.as_secs_f64()) as u64; + let input_spec = source.signal_spec(); + let output_time_base = TimeBase::new(1, output_sample_rate); + let precision = (output_sample_rate as f64 + * input_spec.channels.count() as f64 + * REPORT_PRECISION.as_secs_f64()) as u64; // Create a ring-buffer for the decoded samples. Worker thread is producing, // we are consuming in the `AudioSource` impl. @@ -117,19 +115,25 @@ impl DecoderSource { // incrementing on reading from the ring-buffer. let position = Arc::new(AtomicU64::new(0)); + // Because the `n_frames` count that Symphonia gives us can be a bit unreliable, + // we track the total number of samples in this stream in this atomic, set when + // the underlying decoder returns EOF. + let total_samples = Arc::new(AtomicU64::new(u64::MAX)); + // Some output streams have different sample rate than the source, so we need to // resample before pushing to the sink. let res_spec = ResamplingSpec { - from_rate: signal_spec.rate as usize, + from_rate: input_spec.rate as usize, to_rate: output_sample_rate as usize, - channels: chan_count, + channels: input_spec.channels.count(), }; // Spawn the worker and kick-start the decoding. The buffer will start filling // now. let actor = Worker::spawn_default({ let position = Arc::clone(&position); - move |this| Worker::new(this, source, buffer, position, res_spec) + let total_samples = Arc::clone(&total_samples); + move |this| Worker::new(this, source, buffer, position, total_samples, res_spec) }); let _ = actor.send(Msg::Read); @@ -139,8 +143,8 @@ impl DecoderSource { consumer, event_send, norm_factor, - time_base, - signal_spec, + input_spec, + output_time_base, total_samples, end_of_track: false, position, @@ -158,8 +162,8 @@ impl DecoderSource { } fn samples_to_duration(&self, samples: u64) -> Duration { - let frames = samples / self.signal_spec.channels.count() as u64; - let time = self.time_base.calc_time(frames); + let frames = samples / self.input_spec.channels.count() as u64; + let time = self.output_time_base.calc_time(frames); Duration::from_secs(time.seconds) + Duration::from_secs_f64(time.frac) } } @@ -193,13 +197,18 @@ impl AudioSource for DecoderSource { } } - if position >= self.total_samples { - // After reading the total number of samples from the `CodecParameters`, we - // stop. Signal to the upper layer this track is over and short-circuit all - // further reads from this source. + let total_samples = self.total_samples.load(Ordering::Relaxed); + if position >= total_samples { + // After reading the total number of samples, we stop. Signal to the upper layer + // this track is over and short-circuit all further reads from this source. if self.event_send.try_send(PlayerEvent::EndOfTrack).is_ok() { self.end_of_track = true; } + log::debug!( + "end of track, position: {}, total: {}", + position, + total_samples + ); } written @@ -219,26 +228,30 @@ enum Msg { } struct Worker { - /// Decoder we are reading packets/samples from. - decoder: AudioDecoder, - /// Audio properties of the decoded signal. - spec: SignalSpec, /// Sending part of our own actor channel. this: Sender, - /// Ring-buffer for the output signal. - output: SpscRb, - /// Producing part of the output ring-buffer. - producer: Producer, + /// Decoder we are reading packets/samples from. + input: AudioDecoder, + /// Audio properties of the decoded signal. + input_spec: SignalSpec, /// Sample buffer containing samples read in the last packet. - packet: SampleBuffer, - /// Buffer containing signal resampled from `packet`. - resampled: Vec, + input_packet: SampleBuffer, /// Resampling structure we use to resample the signal. resampler: AudioResampler, - /// Shared atomic position. Worker updates this on seek only. - position: Arc, + /// Buffer containing signal resampled from `packet`. + resampled: Vec, + /// Ring-buffer for the output signal. + output: SpscRb, + /// Producing part of the output ring-buffer. + output_producer: Producer, + /// Shared atomic position. We update this on seek only. + output_position: Arc, + /// Shared atomic for total number of samples. We set this on EOF. + output_total_samples: Arc, /// Range of samples in `resampled` that are awaiting flush into `output`. - samples_to_flush: Range, + output_samples_to_write: Range, + /// Number of samples written into the output channel. + output_samples_written: u64, /// Are we in the middle of automatic read loop? is_reading: bool, } @@ -252,34 +265,37 @@ impl Worker { fn new( this: Sender, - decoder: AudioDecoder, + input: AudioDecoder, output: SpscRb, position: Arc, + total_samples: Arc, res_spec: ResamplingSpec, ) -> Self { const DEFAULT_MAX_FRAMES: u64 = 1024 * 8; - let max_frames = decoder + let max_input_frames = input .codec_params() .max_frames_per_packet .unwrap_or(DEFAULT_MAX_FRAMES); - let max_samples = max_frames as usize * res_spec.channels; - let max_output_size = res_spec.max_output_size(max_samples); - let resampled = vec![0.0; max_output_size]; + let max_input_samples = max_input_frames as usize * res_spec.channels; + let max_output_samples = res_spec.max_output_size(max_input_samples); + let resampled = vec![0.0; max_output_samples]; let resampler = AudioResampler::new(ResamplingQuality::SincMediumQuality, res_spec).unwrap(); Self { - producer: output.producer(), - packet: SampleBuffer::new(max_frames, decoder.signal_spec()), - spec: decoder.signal_spec(), + output_producer: output.producer(), + input_packet: SampleBuffer::new(max_input_frames, input.signal_spec()), + input_spec: input.signal_spec(), resampled, resampler, - decoder, + input, this, output, - position, - samples_to_flush: 0..0, // Arbitrary empty range. + output_position: position, + output_total_samples: total_samples, + output_samples_written: 0, + output_samples_to_write: 0..0, // Arbitrary empty range. is_reading: false, } } @@ -300,15 +316,17 @@ impl Actor for Worker { impl Worker { fn on_seek(&mut self, time: Duration) -> Result, Error> { - match self.decoder.seek(time) { - Ok(timestamp) => { + match self.input.seek(time) { + Ok(input_ts) => { if self.is_reading { - self.samples_to_flush = 0..0; + self.output_samples_to_write = 0..0; } else { self.this.send(Msg::Read)?; } - let position = timestamp * self.spec.channels.count() as u64; - self.position.store(position, Ordering::Relaxed); + let output_ts = (input_ts as f64 * self.resampler.spec.ratio()) as u64; + let output_pos = output_ts * self.input_spec.channels.count() as u64; + self.output_samples_written = output_pos; + self.output_position.store(output_pos, Ordering::Relaxed); self.output.clear(); } Err(err) => { @@ -319,10 +337,11 @@ impl Worker { } fn on_read(&mut self) -> Result, Error> { - if !self.samples_to_flush.is_empty() { - let writable = &self.resampled[self.samples_to_flush.clone()]; - if let Ok(written) = self.producer.write(writable) { - self.samples_to_flush.start += written; + if !self.output_samples_to_write.is_empty() { + let writable = &self.resampled[self.output_samples_to_write.clone()]; + if let Ok(written) = self.output_producer.write(writable) { + self.output_samples_written += written as u64; + self.output_samples_to_write.start += written; self.is_reading = true; self.this.send(Msg::Read)?; Ok(Act::Continue) @@ -337,18 +356,20 @@ impl Worker { }) } } else { - match self.decoder.next_packet() { + match self.input.next_packet() { Some((_, packet)) => { - self.packet.copy_interleaved_ref(packet); + self.input_packet.copy_interleaved_ref(packet); let to_flush = self .resampler - .resample(self.packet.samples(), &mut self.resampled)?; - self.samples_to_flush = 0..to_flush; + .resample(self.input_packet.samples(), &mut self.resampled)?; + self.output_samples_to_write = 0..to_flush; self.is_reading = true; self.this.send(Msg::Read)?; } None => { self.is_reading = false; + self.output_total_samples + .store(self.output_samples_written, Ordering::Relaxed); } } Ok(Act::Continue) From 7d1e0fd81c69a3c17d57fc0ae83eb20ee504b9ce Mon Sep 17 00:00:00 2001 From: Jan Pochyla Date: Tue, 16 Nov 2021 15:45:21 +0100 Subject: [PATCH 14/14] core: Increase buffer size, try to open 44.1Khz stream --- psst-core/src/audio/output.rs | 26 +++++++++++++++++++++++++- psst-core/src/player/worker.rs | 4 ++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/psst-core/src/audio/output.rs b/psst-core/src/audio/output.rs index aa5ad339..dbc79c3a 100644 --- a/psst-core/src/audio/output.rs +++ b/psst-core/src/audio/output.rs @@ -27,7 +27,7 @@ impl AudioOutput { // Get the default device config, so we know what sample format and sample rate // the device supports. - let supported = device.default_output_config()?; + let supported = Self::preferred_output_config(&device)?; let (callback_send, callback_recv) = bounded(16); @@ -48,6 +48,24 @@ impl AudioOutput { }) } + fn preferred_output_config( + device: &cpal::Device, + ) -> Result { + const PREFERRED_SAMPLE_RATE: cpal::SampleRate = cpal::SampleRate(44_100); + + let mut configs: Vec<_> = device.supported_output_configs()?.collect(); + configs.sort_by(|a, b| a.cmp_default_heuristics(b)); + + for range in configs { + let r = range.min_sample_rate()..=range.max_sample_rate(); + if r.contains(&PREFERRED_SAMPLE_RATE) { + return Ok(range.with_sample_rate(PREFERRED_SAMPLE_RATE)); + } + } + + Ok(device.default_output_config()?) + } + pub fn sink(&self) -> AudioSink { self.sink.clone() } @@ -237,6 +255,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: cpal::SupportedStreamConfigsError) -> Error { + Error::AudioOutputError(Box::new(err)) + } +} + impl From for Error { fn from(err: cpal::BuildStreamError) -> Error { Error::AudioOutputError(Box::new(err)) diff --git a/psst-core/src/player/worker.rs b/psst-core/src/player/worker.rs index 39aa5dd4..dd4ad7c4 100644 --- a/psst-core/src/player/worker.rs +++ b/psst-core/src/player/worker.rs @@ -95,7 +95,7 @@ impl DecoderSource { norm_factor, } = loaded; - const REPORT_PRECISION: Duration = Duration::from_millis(1000); + const REPORT_PRECISION: Duration = Duration::from_millis(900); // Gather the source signal parameters and compute how often we should report // the play-head position. @@ -258,7 +258,7 @@ struct Worker { impl Worker { fn default_buffer() -> SpscRb { - const DEFAULT_BUFFER_SIZE: usize = 64 * 1024; + const DEFAULT_BUFFER_SIZE: usize = 128 * 1024; SpscRb::new(DEFAULT_BUFFER_SIZE) }