diff --git a/Cargo.lock b/Cargo.lock index 9a2e42ea..837df2fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,26 +1,5 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -[[package]] -name = "addr2line" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" - -[[package]] -name = "adler32" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" - [[package]] name = "aes" version = "0.6.0" @@ -66,9 +45,9 @@ dependencies = [ [[package]] name = "alsa" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb213f6b3e4b1480a60931ca2035794aa67b73103d254715b1db7b70dcb3c934" +checksum = "75c4da790adcb2ce5e758c064b4f3ec17a30349f9961d3e5e6c9688b052a9e18" dependencies = [ "alsa-sys", "bitflags", @@ -92,12 +71,6 @@ version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1" -[[package]] -name = "ascii" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" - [[package]] name = "atty" version = "0.2.14" @@ -115,26 +88,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" -[[package]] -name = "backtrace" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef5140344c85b01f9bbb4d4b7288a8aa4b3287ccef913a14bcc78a1063623598" -dependencies = [ - "addr2line", - "cfg-if 1.0.0", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "base-x" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" - [[package]] name = "base64" version = "0.9.3" @@ -276,6 +229,9 @@ name = "cc" version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" +dependencies = [ + "jobserver", +] [[package]] name = "cesu8" @@ -313,16 +269,10 @@ dependencies = [ "libc", "num-integer", "num-traits", - "time 0.1.43", + "time", "winapi 0.3.9", ] -[[package]] -name = "chunked_transfer" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" - [[package]] name = "cipher" version = "0.2.5" @@ -352,19 +302,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "combine" -version = "3.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" -dependencies = [ - "ascii", - "byteorder", - "either", - "memchr", - "unreachable", -] - [[package]] name = "combine" version = "4.5.2" @@ -375,39 +312,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "const_fn" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6" - -[[package]] -name = "cookie" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784ad0fbab4f3e9cef09f20e0aea6000ae08d2cb98ac4c0abc53df18803d702f" -dependencies = [ - "percent-encoding 2.1.0", - "time 0.2.25", - "version_check", -] - -[[package]] -name = "cookie_store" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3818dfca4b0cb5211a659bbcbb94225b7127407b2b135e650d717bfb78ab10d3" -dependencies = [ - "cookie", - "idna 0.2.1", - "log 0.4.14", - "publicsuffix", - "serde", - "serde_json", - "time 0.2.25", - "url 2.2.0", -] - [[package]] name = "core-foundation-sys" version = "0.6.2" @@ -416,9 +320,9 @@ checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" [[package]] name = "coreaudio-rs" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f229761965dad3e9b11081668a6ea00f1def7aa46062321b5ec245b834f6e491" +checksum = "11894b20ebfe1ff903cbdc52259693389eea03b94918a2def2c30c3bf227ad88" dependencies = [ "bitflags", "coreaudio-sys", @@ -435,15 +339,15 @@ dependencies = [ [[package]] name = "cpal" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05631e2089dfa5d3b6ea1cfbbfd092e2ee5deeb69698911bc976b28b746d3657" +checksum = "8351ddf2aaa3c583fa388029f8b3d26f3c7035a20911fdd5f2e2ed7ab57dad25" dependencies = [ "alsa", "core-foundation-sys", "coreaudio-rs", "jack", - "jni 0.17.0", + "jni", "js-sys", "lazy_static", "libc", @@ -453,7 +357,7 @@ dependencies = [ "nix", "oboe", "parking_lot 0.11.1", - "stdweb 0.1.3", + "stdweb", "thiserror", "web-sys", "winapi 0.3.9", @@ -465,15 +369,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" -[[package]] -name = "crc32fast" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" -dependencies = [ - "cfg-if 1.0.0", -] - [[package]] name = "crossbeam-deque" version = "0.7.3" @@ -624,12 +519,6 @@ dependencies = [ "generic-array 0.14.4", ] -[[package]] -name = "discard" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" - [[package]] name = "dns-sd" version = "0.1.3" @@ -664,7 +553,6 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" dependencies = [ - "backtrace", "version_check", ] @@ -674,45 +562,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" -[[package]] -name = "fetch_unroll" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d44807d562d137f063cbfe209da1c3f9f2fa8375e11166ef495daab7b847f9" -dependencies = [ - "libflate", - "tar", - "ureq", -] - -[[package]] -name = "filetime" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "redox_syscall 0.2.4", - "winapi 0.3.9", -] - [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "form_urlencoded" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece68d15c92e84fa4f19d3780f1294e5ca82a78a6d515f1efaabcc144688be00" -dependencies = [ - "matches", - "percent-encoding 2.1.0", -] - [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -876,12 +731,6 @@ dependencies = [ "wasi 0.10.2+wasi-snapshot-preview1", ] -[[package]] -name = "gimli" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" - [[package]] name = "glib" version = "0.10.3" @@ -946,9 +795,9 @@ dependencies = [ [[package]] name = "gstreamer" -version = "0.16.5" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d50f822055923f1cbede233aa5dfd4ee957cf328fb3076e330886094e11d6cf" +checksum = "9ff5d0f7ff308ae37e6eb47b6ded17785bdea06e438a708cd09e0288c1862f33" dependencies = [ "bitflags", "cfg-if 1.0.0", @@ -1115,9 +964,9 @@ dependencies = [ "log 0.4.14", "mime", "net2", - "percent-encoding 1.0.1", + "percent-encoding", "relay", - "time 0.1.43", + "time", "tokio-core", "tokio-io", "tokio-proto", @@ -1156,17 +1005,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "idna" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de910d521f7cc3135c4de8db1cb910e0b5ed1dc6f57c381cd07e8e661ce10094" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "if-addrs" version = "0.6.5" @@ -1223,9 +1061,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" [[package]] name = "jack" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c1871c91fa65aa328f3bedbaa54a6e5d1de009264684c153eb708ba933aa6f5" +checksum = "2deb4974bd7e6b2fb7784f27fa13d819d11292b3b004dce0185ec08163cf686a" dependencies = [ "bitflags", "jack-sys", @@ -1235,9 +1073,9 @@ dependencies = [ [[package]] name = "jack-sys" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d6ab7ada402b6a27912a2b86504be62a48c58313c886fe72a059127acb4d7" +checksum = "57983f0d72dfecf2b719ed39bc9cacd85194e1a94cb3f9146009eff9856fef41" dependencies = [ "lazy_static", "libc", @@ -1246,29 +1084,15 @@ dependencies = [ [[package]] name = "jni" -version = "0.14.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1981310da491a4f0f815238097d0d43d8072732b5ae5f8bd0d8eadf5bf245402" +checksum = "24967112a1e4301ca5342ea339763613a37592b8a6ce6cf2e4494537c7a42faf" dependencies = [ "cesu8", - "combine 3.8.1", - "error-chain", - "jni-sys", - "log 0.4.14", - "walkdir", -] - -[[package]] -name = "jni" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36bcc950632e48b86da402c5c077590583da5ac0d480103611d5374e7c967a3c" -dependencies = [ - "cesu8", - "combine 4.5.2", - "error-chain", + "combine", "jni-sys", "log 0.4.14", + "thiserror", "walkdir", ] @@ -1278,6 +1102,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.47" @@ -1328,27 +1161,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.85" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3" - -[[package]] -name = "libflate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389de7875e06476365974da3e7ff85d55f1972188ccd9f6020dd7c8156e17914" -dependencies = [ - "adler32", - "crc32fast", - "libflate_lz77", - "rle-decode-fast", -] - -[[package]] -name = "libflate_lz77" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3286f09f7d4926fc486334f28d8d2e6ebe4f7f9994494b6dab27ddfad2c9b11b" +checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" [[package]] name = "libloading" @@ -1380,9 +1195,9 @@ dependencies = [ [[package]] name = "libpulse-binding" -version = "2.23.0" +version = "2.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2405f806801527dfb3d2b6d48a282cdebe9a1b41b0652e0d7b5bad81dbc700e" +checksum = "db951f37898e19a6785208e3a290261e0f1a8e086716be596aaad684882ca8e3" dependencies = [ "bitflags", "libc", @@ -1452,7 +1267,7 @@ dependencies = [ "tokio-io", "tokio-process", "tokio-signal", - "url 1.7.2", + "url", ] [[package]] @@ -1473,6 +1288,7 @@ dependencies = [ "ogg", "tempfile", "vorbis", + "zerocopy", ] [[package]] @@ -1499,7 +1315,7 @@ dependencies = [ "serde_json", "sha-1 0.9.3", "tokio-core", - "url 1.7.2", + "url", ] [[package]] @@ -1533,7 +1349,7 @@ dependencies = [ "tokio-codec", "tokio-core", "tokio-io", - "url 1.7.2", + "url", "uuid", "vergen", ] @@ -1689,16 +1505,6 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" -[[package]] -name = "miniz_oxide" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" -dependencies = [ - "adler", - "autocfg", -] - [[package]] name = "mio" version = "0.6.23" @@ -1780,9 +1586,9 @@ dependencies = [ [[package]] name = "ndk" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb167c1febed0a496639034d0c76b3b74263636045db5489eee52143c246e73" +checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab" dependencies = [ "jni-sys", "ndk-sys", @@ -1792,9 +1598,9 @@ dependencies = [ [[package]] name = "ndk-glue" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdf399b8b7a39c6fb153c4ec32c72fd5fe789df24a647f229c239aa7adb15241" +checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385" dependencies = [ "lazy_static", "libc", @@ -1836,15 +1642,14 @@ dependencies = [ [[package]] name = "nix" -version = "0.15.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2e0b4f3320ed72aaedb9a5ac838690a8047c7b275da22711fddff4f8a14229" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" dependencies = [ "bitflags", "cc", - "cfg-if 0.1.10", + "cfg-if 1.0.0", "libc", - "void", ] [[package]] @@ -1921,9 +1726,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.4.3" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca565a7df06f3d4b485494f25ba05da1435950f4dc263440eda7a6fa9b8e36e4" +checksum = "226b45a5c2ac4dd696ed30fa6b94b057ad909c7b7fc2e0d0808192bced894066" dependencies = [ "derivative", "num_enum_derive", @@ -1931,9 +1736,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.4.3" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffa5a33ddddfee04c0283a7653987d634e880347e96b5b2ed64de07efb59db9d" +checksum = "1c0fd9eba1d5db0994a239e09c1be402d35622277e35468ba891aa5e3188ce7e" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1941,19 +1746,13 @@ dependencies = [ "syn", ] -[[package]] -name = "object" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" - [[package]] name = "oboe" -version = "0.3.1" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aadc2b0867bdbb9a81c4d99b9b682958f49dbea1295a81d2f646cca2afdd9fc" +checksum = "4cfb2390bddb9546c0f7448fd1d2abdd39e6075206f960991eb28c7fa7f126c4" dependencies = [ - "jni 0.14.0", + "jni", "ndk", "ndk-glue", "num-derive", @@ -1963,11 +1762,11 @@ dependencies = [ [[package]] name = "oboe-sys" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ff7a51600eabe34e189eec5c995a62f151d8d97e5fbca39e87ca738bb99b82" +checksum = "fe069264d082fc820dfa172f79be3f2e088ecfece9b1c47b0c9fd838d2bef103" dependencies = [ - "fetch_unroll", + "cc", ] [[package]] @@ -2087,12 +1886,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" -[[package]] -name = "percent-encoding" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" - [[package]] name = "pin-project-lite" version = "0.2.4" @@ -2223,28 +2016,6 @@ dependencies = [ "protobuf-codegen", ] -[[package]] -name = "publicsuffix" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bbaa49075179162b49acac1c6aa45fb4dafb5f13cf6794276d77bc7fd95757b" -dependencies = [ - "error-chain", - "idna 0.2.1", - "lazy_static", - "regex", - "url 2.2.0", -] - -[[package]] -name = "qstring" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" -dependencies = [ - "percent-encoding 2.1.0", -] - [[package]] name = "quick-error" version = "1.2.3" @@ -2436,32 +2207,11 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin", - "untrusted", - "web-sys", - "winapi 0.3.9", -] - -[[package]] -name = "rle-decode-fast" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cabe4fa914dec5870285fa7f71f602645da47c486e68486d2b4ceb4a343e90ac" - [[package]] name = "rodio" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9683532495146e98878d4948fa1a1953f584cd923f2a5f5c26b7a8701b56943" +checksum = "b65c2eda643191f6d1bb12ea323a9db8d9ba95374e9be3780b5a9fb5cfb8520f" dependencies = [ "cpal", ] @@ -2476,12 +2226,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "rustc-demangle" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" - [[package]] name = "rustc-hash" version = "1.1.0" @@ -2497,19 +2241,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustls" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "064fd21ff87c6e87ed4506e68beb42459caa4a0e2eb144932e6776768556980b" -dependencies = [ - "base64 0.13.0", - "log 0.4.14", - "ring", - "sct", - "webpki", -] - [[package]] name = "ryu" version = "1.0.5" @@ -2543,16 +2274,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -[[package]] -name = "sct" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "sdl2" version = "0.34.3" @@ -2567,9 +2288,9 @@ dependencies = [ [[package]] name = "sdl2-sys" -version = "0.34.3" +version = "0.34.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d81feded049b9c14eceb4a4f6d596a98cebbd59abdba949c5552a015466d33" +checksum = "4cb164f53dbcad111de976bbf1f3083d3fcdeda88da9cfa281c70822720ee3da" dependencies = [ "cfg-if 0.1.10", "libc", @@ -2596,9 +2317,6 @@ name = "serde" version = "1.0.123" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" -dependencies = [ - "serde_derive", -] [[package]] name = "serde_derive" @@ -2647,12 +2365,6 @@ dependencies = [ "opaque-debug 0.3.0", ] -[[package]] -name = "sha1" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" - [[package]] name = "shannon" version = "0.2.0" @@ -2727,76 +2439,12 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "standback" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2beb4d1860a61f571530b3f855a1b538d0200f7871c63331ecd6f17b1f014f8" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" -dependencies = [ - "discard", - "rustc_version", - "stdweb-derive", - "stdweb-internal-macros", - "stdweb-internal-runtime", - "wasm-bindgen", -] - -[[package]] -name = "stdweb-derive" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "serde_derive", - "syn", -] - -[[package]] -name = "stdweb-internal-macros" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" -dependencies = [ - "base-x", - "proc-macro2", - "quote", - "serde", - "serde_derive", - "serde_json", - "sha1", - "syn", -] - -[[package]] -name = "stdweb-internal-runtime" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" - [[package]] name = "strsim" version = "0.9.3" @@ -2871,17 +2519,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b157868d8ac1f56b64604539990685fa7611d8fa9e5476cf0c02cf34d32917c5" -[[package]] -name = "tar" -version = "0.4.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0313546c01d59e29be4f09687bcb4fb6690cec931cc3607b6aec7a0e417f4cc6" -dependencies = [ - "filetime", - "libc", - "xattr", -] - [[package]] name = "tempfile" version = "3.2.0" @@ -2935,44 +2572,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "time" -version = "0.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1195b046942c221454c2539395f85413b33383a067449d78aab2b7b052a142f7" -dependencies = [ - "const_fn", - "libc", - "standback", - "stdweb 0.4.20", - "time-macros", - "version_check", - "winapi 0.3.9", -] - -[[package]] -name = "time-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" -dependencies = [ - "proc-macro-hack", - "time-macros-impl", -] - -[[package]] -name = "time-macros-impl" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" -dependencies = [ - "proc-macro-hack", - "proc-macro2", - "quote", - "standback", - "syn", -] - [[package]] name = "tinyvec" version = "1.1.1" @@ -3318,61 +2917,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" -[[package]] -name = "unreachable" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" -dependencies = [ - "void", -] - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - -[[package]] -name = "ureq" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "294b85ef5dbc3670a72e82a89971608a1fcc4ed5c7c5a2895230d31a95f0569b" -dependencies = [ - "base64 0.13.0", - "chunked_transfer", - "cookie", - "cookie_store", - "log 0.4.14", - "once_cell", - "qstring", - "rustls", - "url 2.2.0", - "webpki", - "webpki-roots", -] - [[package]] name = "url" version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" dependencies = [ - "idna 0.1.5", + "idna", "matches", - "percent-encoding 1.0.1", -] - -[[package]] -name = "url" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5909f2b0817350449ed73e8bcd81c8c3c8d9a7a5d8acba4b27db277f1868976e" -dependencies = [ - "form_urlencoded", - "idna 0.2.1", - "matches", - "percent-encoding 2.1.0", + "percent-encoding", ] [[package]] @@ -3406,12 +2959,6 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" -[[package]] -name = "void" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" - [[package]] name = "vorbis" version = "0.0.14" @@ -3547,25 +3094,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "webpki-roots" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82015b7e0b8bad8185994674a13a93306bea76cf5a16c5a181382fd3a5ec2376" -dependencies = [ - "webpki", -] - [[package]] name = "winapi" version = "0.2.8" @@ -3619,15 +3147,6 @@ dependencies = [ "winapi-build", ] -[[package]] -name = "xattr" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" -dependencies = [ - "libc", -] - [[package]] name = "zerocopy" version = "0.3.0" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index b7e6e35f..2d67f4ce 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -22,9 +22,10 @@ log = "0.4" num-bigint = "0.3" num-traits = "0.2" tempfile = "3.1" +zerocopy = "0.3" -librespot-tremor = { version = "0.2.0", optional = true } -vorbis = { version ="0.0.14", optional = true } +librespot-tremor = { version = "0.2", optional = true } +vorbis = { version ="0.0", optional = true } [features] with-tremor = ["librespot-tremor"] diff --git a/audio/src/convert.rs b/audio/src/convert.rs new file mode 100644 index 00000000..e291c804 --- /dev/null +++ b/audio/src/convert.rs @@ -0,0 +1,59 @@ +use zerocopy::AsBytes; + +#[derive(AsBytes, Copy, Clone, Debug)] +#[allow(non_camel_case_types)] +#[repr(transparent)] +pub struct i24([u8; 3]); +impl i24 { + fn pcm_from_i32(sample: i32) -> Self { + // drop the least significant byte + let [a, b, c, _d] = (sample >> 8).to_le_bytes(); + i24([a, b, c]) + } +} + +// Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] while maintaining DC linearity. +macro_rules! convert_samples_to { + ($type: ident, $samples: expr) => { + convert_samples_to!($type, $samples, 0) + }; + ($type: ident, $samples: expr, $drop_bits: expr) => { + $samples + .iter() + .map(|sample| { + // Losslessly represent [-1.0, 1.0] to [$type::MIN, $type::MAX] + // while maintaining DC linearity. There is nothing to be gained + // by doing this in f64, as the significand of a f32 is 24 bits, + // just like the maximum bit depth we are converting to. + let int_value = *sample * (std::$type::MAX as f32 + 0.5) - 0.5; + + // Casting floats to ints truncates by default, which results + // in larger quantization error than rounding arithmetically. + // Flooring is faster, but again with larger error. + int_value.round() as $type >> $drop_bits + }) + .collect() + }; +} + +pub struct SamplesConverter {} +impl SamplesConverter { + pub fn to_s32(samples: &[f32]) -> Vec { + convert_samples_to!(i32, samples) + } + + pub fn to_s24(samples: &[f32]) -> Vec { + convert_samples_to!(i32, samples, 8) + } + + pub fn to_s24_3(samples: &[f32]) -> Vec { + Self::to_s32(samples) + .iter() + .map(|sample| i24::pcm_from_i32(*sample)) + .collect() + } + + pub fn to_s16(samples: &[f32]) -> Vec { + convert_samples_to!(i16, samples) + } +} diff --git a/audio/src/lewton_decoder.rs b/audio/src/lewton_decoder.rs index 1addaa01..8e7d089e 100644 --- a/audio/src/lewton_decoder.rs +++ b/audio/src/lewton_decoder.rs @@ -37,8 +37,11 @@ where use self::lewton::VorbisError::BadAudio; use self::lewton::VorbisError::OggError; loop { - match self.0.read_dec_packet_itl() { - Ok(Some(packet)) => return Ok(Some(AudioPacket::Samples(packet))), + match self + .0 + .read_dec_packet_generic::>() + { + Ok(Some(packet)) => return Ok(Some(AudioPacket::Samples(packet.samples))), Ok(None) => return Ok(None), Err(BadAudio(AudioIsHeader)) => (), diff --git a/audio/src/lib.rs b/audio/src/lib.rs index fd764071..44f732b3 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -13,6 +13,7 @@ extern crate tempfile; extern crate librespot_core; +mod convert; mod decrypt; mod fetch; @@ -24,6 +25,7 @@ mod passthrough_decoder; mod range_set; +pub use convert::{i24, SamplesConverter}; pub use decrypt::AudioDecrypt; pub use fetch::{AudioFile, AudioFileOpen, StreamLoaderController}; pub use fetch::{ @@ -33,12 +35,12 @@ pub use fetch::{ use std::fmt; pub enum AudioPacket { - Samples(Vec), + Samples(Vec), OggData(Vec), } impl AudioPacket { - pub fn samples(&self) -> &[i16] { + pub fn samples(&self) -> &[f32] { match self { AudioPacket::Samples(s) => s, AudioPacket::OggData(_) => panic!("can't return OggData on samples"), diff --git a/audio/src/libvorbis_decoder.rs b/audio/src/libvorbis_decoder.rs index 8aced556..5ad48f0d 100644 --- a/audio/src/libvorbis_decoder.rs +++ b/audio/src/libvorbis_decoder.rs @@ -39,7 +39,18 @@ where fn next_packet(&mut self) -> Result, AudioError> { loop { match self.0.packets().next() { - Some(Ok(packet)) => return Ok(Some(AudioPacket::Samples(packet.data))), + Some(Ok(packet)) => { + // Losslessly represent [-32768, 32767] to [-1.0, 1.0] while maintaining DC linearity. + return Ok(Some(AudioPacket::Samples( + packet + .data + .iter() + .map(|sample| { + ((*sample as f64 + 0.5) / (std::i16::MAX as f64 + 0.5)) as f32 + }) + .collect(), + ))); + } None => return Ok(None), Some(Err(vorbis::VorbisError::Hole)) => (), diff --git a/examples/play.rs b/examples/play.rs index 4ba4c5b5..2c239ef3 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -5,7 +5,7 @@ use librespot::core::authentication::Credentials; use librespot::core::config::SessionConfig; use librespot::core::session::Session; use librespot::core::spotify_id::SpotifyId; -use librespot::playback::config::PlayerConfig; +use librespot::playback::config::{AudioFormat, PlayerConfig}; use librespot::playback::audio_backend; use librespot::playback::player::Player; @@ -16,6 +16,7 @@ fn main() { let session_config = SessionConfig::default(); let player_config = PlayerConfig::default(); + let audio_format = AudioFormat::default(); let args: Vec<_> = env::args().collect(); if args.len() != 4 { @@ -35,7 +36,7 @@ fn main() { .unwrap(); let (mut player, _) = Player::new(player_config, session.clone(), None, move || { - (backend)(None) + (backend)(None, audio_format) }); player.load(track, true, 0); diff --git a/playback/Cargo.toml b/playback/Cargo.toml index b8995a4b..0666259d 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -23,19 +23,19 @@ log = "0.4" byteorder = "1.3" shell-words = "1.0.0" -alsa = { version = "0.4", optional = true } +alsa = { version = "0.5", optional = true } portaudio-rs = { version = "0.3", optional = true } -libpulse-binding = { version = "2.13", optional = true, default-features = false } -libpulse-simple-binding = { version = "2.13", optional = true, default-features = false } +libpulse-binding = { version = "2", optional = true, default-features = false } +libpulse-simple-binding = { version = "2", optional = true, default-features = false } jack = { version = "0.6", optional = true } libc = { version = "0.2", optional = true } rodio = { version = "0.13", optional = true, default-features = false } cpal = { version = "0.13", optional = true } -sdl2 = { version = "0.34", optional = true } +sdl2 = { version = "0.34.3", optional = true } gstreamer = { version = "0.16", optional = true } gstreamer-app = { version = "0.16", optional = true } glib = { version = "0.10", optional = true } -zerocopy = { version = "0.3", optional = true } +zerocopy = { version = "0.3" } [features] alsa-backend = ["alsa"] @@ -45,4 +45,4 @@ jackaudio-backend = ["jack"] rodiojack-backend = ["rodio", "cpal/jack"] rodio-backend = ["rodio", "cpal"] sdl-backend = ["sdl2"] -gstreamer-backend = ["gstreamer", "gstreamer-app", "glib", "zerocopy"] +gstreamer-backend = ["gstreamer", "gstreamer-app", "glib"] diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index bf7b1376..1d551878 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -1,5 +1,7 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLES_PER_SECOND, SAMPLE_RATE}; use alsa::device_name::HintIter; use alsa::pcm::{Access, Format, Frames, HwParams, PCM}; use alsa::{Direction, Error, ValueOr}; @@ -8,13 +10,14 @@ use std::ffi::CString; use std::io; use std::process::exit; -const PREFERED_PERIOD_SIZE: Frames = 5512; // Period of roughly 125ms +const BUFFERED_LATENCY: f32 = 0.125; // seconds const BUFFERED_PERIODS: Frames = 4; pub struct AlsaSink { pcm: Option, + format: AudioFormat, device: String, - buffer: Vec, + buffer: Vec, } fn list_outputs() { @@ -34,23 +37,27 @@ fn list_outputs() { } } -fn open_device(dev_name: &str) -> Result<(PCM, Frames), Box> { +fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box> { let pcm = PCM::new(dev_name, Direction::Playback, false)?; - let mut period_size = PREFERED_PERIOD_SIZE; + let alsa_format = match format { + AudioFormat::F32 => Format::float(), + AudioFormat::S32 => Format::s32(), + AudioFormat::S24 => Format::s24(), + AudioFormat::S24_3 => Format::S243LE, + AudioFormat::S16 => Format::s16(), + }; + // http://www.linuxjournal.com/article/6735?page=0,1#N0x19ab2890.0x19ba78d8 // latency = period_size * periods / (rate * bytes_per_frame) - // For 16 Bit stereo data, one frame has a length of four bytes. - // 500ms = buffer_size / (44100 * 4) - // buffer_size_bytes = 0.5 * 44100 / 4 - // buffer_size_frames = 0.5 * 44100 = 22050 + // For stereo samples encoded as 32-bit float, one frame has a length of eight bytes. + let mut period_size = ((SAMPLES_PER_SECOND * format.size() as u32) as f32 + * (BUFFERED_LATENCY / BUFFERED_PERIODS as f32)) as Frames; { - // Set hardware parameters: 44100 Hz / Stereo / 16 bit let hwp = HwParams::any(&pcm)?; - hwp.set_access(Access::RWInterleaved)?; - hwp.set_format(Format::s16())?; - hwp.set_rate(44100, ValueOr::Nearest)?; - hwp.set_channels(2)?; + hwp.set_format(alsa_format)?; + hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest)?; + hwp.set_channels(NUM_CHANNELS as u32)?; period_size = hwp.set_period_size_near(period_size, ValueOr::Greater)?; hwp.set_buffer_size_near(period_size * BUFFERED_PERIODS)?; pcm.hw_params(&hwp)?; @@ -64,12 +71,12 @@ fn open_device(dev_name: &str) -> Result<(PCM, Frames), Box> { } impl Open for AlsaSink { - fn open(device: Option) -> AlsaSink { - info!("Using alsa sink"); + fn open(device: Option, format: AudioFormat) -> Self { + info!("Using Alsa sink with format: {:?}", format); let name = match device.as_ref().map(AsRef::as_ref) { Some("?") => { - println!("Listing available alsa outputs"); + println!("Listing available Alsa outputs:"); list_outputs(); exit(0) } @@ -78,8 +85,9 @@ impl Open for AlsaSink { } .to_string(); - AlsaSink { + Self { pcm: None, + format: format, device: name, buffer: vec![], } @@ -89,12 +97,14 @@ impl Open for AlsaSink { impl Sink for AlsaSink { fn start(&mut self) -> io::Result<()> { if self.pcm.is_none() { - let pcm = open_device(&self.device); + let pcm = open_device(&self.device, self.format); match pcm { Ok((p, period_size)) => { self.pcm = Some(p); // Create a buffer for all samples for a full period - self.buffer = Vec::with_capacity((period_size * 2) as usize); + self.buffer = Vec::with_capacity( + period_size as usize * BUFFERED_PERIODS as usize * self.format.size(), + ); } Err(e) => { error!("Alsa error PCM open {}", e); @@ -111,23 +121,22 @@ impl Sink for AlsaSink { fn stop(&mut self) -> io::Result<()> { { - let pcm = self.pcm.as_mut().unwrap(); // Write any leftover data in the period buffer // before draining the actual buffer - let io = pcm.io_i16().unwrap(); - match io.writei(&self.buffer[..]) { - Ok(_) => (), - Err(err) => pcm.try_recover(err, false).unwrap(), - } + self.write_bytes(&[]).expect("could not flush buffer"); + let pcm = self.pcm.as_mut().unwrap(); pcm.drain().unwrap(); } self.pcm = None; Ok(()) } - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + sink_as_bytes!(); +} + +impl SinkAsBytes for AlsaSink { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { let mut processed_data = 0; - let data = packet.samples(); while processed_data < data.len() { let data_to_buffer = min( self.buffer.capacity() - self.buffer.len(), @@ -137,12 +146,7 @@ impl Sink for AlsaSink { .extend_from_slice(&data[processed_data..processed_data + data_to_buffer]); processed_data += data_to_buffer; if self.buffer.len() == self.buffer.capacity() { - let pcm = self.pcm.as_mut().unwrap(); - let io = pcm.io_i16().unwrap(); - match io.writei(&self.buffer) { - Ok(_) => (), - Err(err) => pcm.try_recover(err, false).unwrap(), - } + self.write_buf().expect("could not append to buffer"); self.buffer.clear(); } } @@ -150,3 +154,16 @@ impl Sink for AlsaSink { Ok(()) } } + +impl AlsaSink { + fn write_buf(&mut self) -> io::Result<()> { + let pcm = self.pcm.as_mut().unwrap(); + let io = pcm.io_bytes(); + match io.writei(&self.buffer) { + Ok(_) => (), + Err(err) => pcm.try_recover(err, false).unwrap(), + }; + + Ok(()) + } +} diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 6be6dd72..d59677ac 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -1,21 +1,38 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use gst::prelude::*; use gst::*; use std::sync::mpsc::{sync_channel, SyncSender}; use std::{io, thread}; -use zerocopy::*; +use zerocopy::AsBytes; #[allow(dead_code)] pub struct GstreamerSink { tx: SyncSender>, pipeline: gst::Pipeline, + format: AudioFormat, } impl Open for GstreamerSink { - fn open(device: Option) -> GstreamerSink { - gst::init().expect("Failed to init gstreamer!"); - let pipeline_str_preamble = r#"appsrc caps="audio/x-raw,format=S16LE,layout=interleaved,channels=2,rate=44100" block=true max-bytes=4096 name=appsrc0 "#; + fn open(device: Option, format: AudioFormat) -> Self { + info!("Using GStreamer sink with format: {:?}", format); + gst::init().expect("failed to init GStreamer!"); + + // GStreamer calls S24 and S24_3 different from the rest of the world + let gst_format = match format { + AudioFormat::S24 => "S24_32".to_string(), + AudioFormat::S24_3 => "S24".to_string(), + _ => format!("{:?}", format), + }; + let sample_size = format.size(); + let gst_bytes = 2048 * sample_size; + + let pipeline_str_preamble = format!( + "appsrc caps=\"audio/x-raw,format={}LE,layout=interleaved,channels={},rate={}\" block=true max-bytes={} name=appsrc0 ", + gst_format, NUM_CHANNELS, SAMPLE_RATE, gst_bytes + ); let pipeline_str_rest = r#" ! audioconvert ! autoaudiosink"#; let pipeline_str: String = match device { Some(x) => format!("{}{}", pipeline_str_preamble, x), @@ -27,27 +44,27 @@ impl Open for GstreamerSink { let pipelinee = gst::parse_launch(&*pipeline_str).expect("Couldn't launch pipeline; likely a GStreamer issue or an error in the pipeline string you specified in the 'device' argument to librespot."); let pipeline = pipelinee .dynamic_cast::() - .expect("Couldn't cast pipeline element at runtime!"); - let bus = pipeline.get_bus().expect("Couldn't get bus from pipeline"); + .expect("couldn't cast pipeline element at runtime!"); + let bus = pipeline.get_bus().expect("couldn't get bus from pipeline"); let mainloop = glib::MainLoop::new(None, false); let appsrce: gst::Element = pipeline .get_by_name("appsrc0") - .expect("Couldn't get appsrc from pipeline"); + .expect("couldn't get appsrc from pipeline"); let appsrc: gst_app::AppSrc = appsrce .dynamic_cast::() - .expect("Couldn't cast AppSrc element at runtime!"); + .expect("couldn't cast AppSrc element at runtime!"); let bufferpool = gst::BufferPool::new(); - let appsrc_caps = appsrc.get_caps().expect("Couldn't get appsrc caps"); + let appsrc_caps = appsrc.get_caps().expect("couldn't get appsrc caps"); let mut conf = bufferpool.get_config(); - conf.set_params(Some(&appsrc_caps), 8192, 0, 0); + conf.set_params(Some(&appsrc_caps), 4096 * sample_size as u32, 0, 0); bufferpool .set_config(conf) - .expect("Couldn't configure the buffer pool"); + .expect("couldn't configure the buffer pool"); bufferpool .set_active(true) - .expect("Couldn't activate buffer pool"); + .expect("couldn't activate buffer pool"); - let (tx, rx) = sync_channel::>(128); + let (tx, rx) = sync_channel::>(64 * sample_size); thread::spawn(move || { for data in rx { let buffer = bufferpool.acquire_buffer(None); @@ -57,7 +74,7 @@ impl Open for GstreamerSink { mutbuf.set_size(data.len()); mutbuf .copy_from_slice(0, data.as_bytes()) - .expect("Failed to copy from slice"); + .expect("failed to copy from slice"); let _eat = appsrc.push_buffer(okbuffer); } } @@ -83,33 +100,32 @@ impl Open for GstreamerSink { glib::Continue(true) }) - .expect("Failed to add bus watch"); + .expect("failed to add bus watch"); thread_mainloop.run(); }); pipeline .set_state(gst::State::Playing) - .expect("Unable to set the pipeline to the `Playing` state"); + .expect("unable to set the pipeline to the `Playing` state"); - GstreamerSink { + Self { tx: tx, pipeline: pipeline, + format: format, } } } impl Sink for GstreamerSink { - fn start(&mut self) -> io::Result<()> { - Ok(()) - } - fn stop(&mut self) -> io::Result<()> { - Ok(()) - } - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + start_stop_noop!(); + sink_as_bytes!(); +} + +impl SinkAsBytes for GstreamerSink { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { // Copy expensively (in to_vec()) to avoid thread synchronization - let deighta: &[u8] = packet.samples().as_bytes(); self.tx - .send(deighta.to_vec()) + .send(data.to_vec()) .expect("tx send failed in write function"); Ok(()) } diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 4699c182..24a94a3e 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -1,5 +1,7 @@ use super::{Open, Sink}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; +use crate::player::NUM_CHANNELS; use jack::{ AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope, }; @@ -7,20 +9,18 @@ use std::io; use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; pub struct JackSink { - send: SyncSender, + send: SyncSender, + // We have to keep hold of this object, or the Sink can't play... + #[allow(dead_code)] active_client: AsyncClient<(), JackData>, } pub struct JackData { - rec: Receiver, + rec: Receiver, port_l: Port, port_r: Port, } -fn pcm_to_f32(sample: i16) -> f32 { - sample as f32 / 32768.0 -} - impl ProcessHandler for JackData { fn process(&mut self, _: &Client, ps: &ProcessScope) -> Control { // get output port buffers @@ -33,16 +33,19 @@ impl ProcessHandler for JackData { let buf_size = buf_r.len(); for i in 0..buf_size { - buf_r[i] = pcm_to_f32(queue_iter.next().unwrap_or(0)); - buf_l[i] = pcm_to_f32(queue_iter.next().unwrap_or(0)); + buf_r[i] = queue_iter.next().unwrap_or(0.0); + buf_l[i] = queue_iter.next().unwrap_or(0.0); } Control::Continue } } impl Open for JackSink { - fn open(client_name: Option) -> JackSink { - info!("Using jack sink!"); + fn open(client_name: Option, format: AudioFormat) -> Self { + if format != AudioFormat::F32 { + warn!("JACK currently does not support {:?} output", format); + } + info!("Using JACK sink with format {:?}", AudioFormat::F32); let client_name = client_name.unwrap_or("librespot".to_string()); let (client, _status) = @@ -50,7 +53,7 @@ impl Open for JackSink { let ch_r = client.register_port("out_0", AudioOut::default()).unwrap(); let ch_l = client.register_port("out_1", AudioOut::default()).unwrap(); // buffer for samples from librespot (~10ms) - let (tx, rx) = sync_channel(2 * 1024 * 4); + let (tx, rx) = sync_channel::(NUM_CHANNELS as usize * 1024 * AudioFormat::F32.size()); let jack_data = JackData { rec: rx, port_l: ch_l, @@ -58,7 +61,7 @@ impl Open for JackSink { }; let active_client = AsyncClient::new(client, (), jack_data).unwrap(); - JackSink { + Self { send: tx, active_client: active_client, } @@ -66,19 +69,13 @@ impl Open for JackSink { } impl Sink for JackSink { - fn start(&mut self) -> io::Result<()> { - Ok(()) - } - - fn stop(&mut self) -> io::Result<()> { - Ok(()) - } + start_stop_noop!(); fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { for s in packet.samples().iter() { let res = self.send.send(*s); if res.is_err() { - error!("jackaudio: cannot write to channel"); + error!("cannot write to channel"); } } Ok(()) diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 3f5dae8d..94b6a529 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -1,8 +1,9 @@ use crate::audio::AudioPacket; +use crate::config::AudioFormat; use std::io; pub trait Open { - fn open(_: Option) -> Self; + fn open(_: Option, format: AudioFormat) -> Self; } pub trait Sink { @@ -11,8 +12,55 @@ pub trait Sink { fn write(&mut self, packet: &AudioPacket) -> io::Result<()>; } -fn mk_sink(device: Option) -> Box { - Box::new(S::open(device)) +pub trait SinkAsBytes { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()>; +} + +fn mk_sink(device: Option, format: AudioFormat) -> Box { + Box::new(S::open(device, format)) +} + +// reuse code for various backends +macro_rules! sink_as_bytes { + () => { + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + use crate::audio::{i24, SamplesConverter}; + use zerocopy::AsBytes; + match packet { + AudioPacket::Samples(samples) => match self.format { + AudioFormat::F32 => self.write_bytes(samples.as_bytes()), + AudioFormat::S32 => { + let samples_s32: &[i32] = &SamplesConverter::to_s32(samples); + self.write_bytes(samples_s32.as_bytes()) + } + AudioFormat::S24 => { + let samples_s24: &[i32] = &SamplesConverter::to_s24(samples); + self.write_bytes(samples_s24.as_bytes()) + } + AudioFormat::S24_3 => { + let samples_s24_3: &[i24] = &SamplesConverter::to_s24_3(samples); + self.write_bytes(samples_s24_3.as_bytes()) + } + AudioFormat::S16 => { + let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); + self.write_bytes(samples_s16.as_bytes()) + } + }, + AudioPacket::OggData(samples) => self.write_bytes(samples), + } + } + }; +} + +macro_rules! start_stop_noop { + () => { + fn start(&mut self) -> io::Result<()> { + Ok(()) + } + fn stop(&mut self) -> io::Result<()> { + Ok(()) + } + }; } #[cfg(feature = "alsa-backend")] @@ -68,7 +116,10 @@ use self::pipe::StdoutSink; mod subprocess; use self::subprocess::SubprocessSink; -pub const BACKENDS: &'static [(&'static str, fn(Option) -> Box)] = &[ +pub const BACKENDS: &'static [( + &'static str, + fn(Option, AudioFormat) -> Box, +)] = &[ #[cfg(feature = "alsa-backend")] ("alsa", mk_sink::), #[cfg(feature = "portaudio-backend")] @@ -92,7 +143,7 @@ pub const BACKENDS: &'static [(&'static str, fn(Option) -> Box ("subprocess", mk_sink::), ]; -pub fn find(name: Option) -> Option) -> Box> { +pub fn find(name: Option) -> Option, AudioFormat) -> Box> { if let Some(name) = name { BACKENDS .iter() diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 210c0ce9..6948db94 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -1,46 +1,39 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; use std::fs::OpenOptions; use std::io::{self, Write}; -use std::mem; -use std::slice; -pub struct StdoutSink(Box); +pub struct StdoutSink { + output: Box, + format: AudioFormat, +} impl Open for StdoutSink { - fn open(path: Option) -> StdoutSink { - if let Some(path) = path { - let file = OpenOptions::new().write(true).open(path).unwrap(); - StdoutSink(Box::new(file)) - } else { - StdoutSink(Box::new(io::stdout())) + fn open(path: Option, format: AudioFormat) -> Self { + info!("Using pipe sink with format: {:?}", format); + + let output: Box = match path { + Some(path) => Box::new(OpenOptions::new().write(true).open(path).unwrap()), + _ => Box::new(io::stdout()), + }; + + Self { + output: output, + format: format, } } } impl Sink for StdoutSink { - fn start(&mut self) -> io::Result<()> { - Ok(()) - } - - fn stop(&mut self) -> io::Result<()> { - Ok(()) - } - - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - let data: &[u8] = match packet { - AudioPacket::Samples(data) => unsafe { - slice::from_raw_parts( - data.as_ptr() as *const u8, - data.len() * mem::size_of::(), - ) - }, - AudioPacket::OggData(data) => data, - }; - - self.0.write_all(data)?; - self.0.flush()?; + start_stop_noop!(); + sink_as_bytes!(); +} +impl SinkAsBytes for StdoutSink { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { + self.output.write_all(data)?; + self.output.flush()?; Ok(()) } } diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 0e25021e..5faff6ca 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -1,5 +1,7 @@ use super::{Open, Sink}; -use crate::audio::AudioPacket; +use crate::audio::{AudioPacket, SamplesConverter}; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use portaudio_rs; use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo}; use portaudio_rs::stream::*; @@ -7,10 +9,20 @@ use std::io; use std::process::exit; use std::time::Duration; -pub struct PortAudioSink<'a>( - Option>, - StreamParameters, -); +pub enum PortAudioSink<'a> { + F32( + Option>, + StreamParameters, + ), + S32( + Option>, + StreamParameters, + ), + S16( + Option>, + StreamParameters, + ), +} fn output_devices() -> Box> { let count = portaudio_rs::device::get_count().unwrap(); @@ -40,8 +52,11 @@ fn find_output(device: &str) -> Option { } impl<'a> Open for PortAudioSink<'a> { - fn open(device: Option) -> PortAudioSink<'a> { - debug!("Using PortAudio sink"); + fn open(device: Option, format: AudioFormat) -> PortAudioSink<'a> { + info!("Using PortAudio sink with format: {:?}", format); + + warn!("This backend is known to panic on several platforms."); + warn!("Consider using some other backend, or better yet, contributing a fix."); portaudio_rs::initialize().unwrap(); @@ -53,7 +68,7 @@ impl<'a> Open for PortAudioSink<'a> { Some(device) => find_output(device), None => get_default_output_index(), } - .expect("Could not find device"); + .expect("could not find device"); let info = portaudio_rs::device::get_info(device_idx); let latency = match info { @@ -61,46 +76,99 @@ impl<'a> Open for PortAudioSink<'a> { None => Duration::new(0, 0), }; - let params = StreamParameters { - device: device_idx, - channel_count: 2, - suggested_latency: latency, - data: 0i16, - }; - - PortAudioSink(None, params) + macro_rules! open_sink { + ($sink: expr, $type: ty) => {{ + let params = StreamParameters { + device: device_idx, + channel_count: NUM_CHANNELS as u32, + suggested_latency: latency, + data: 0.0 as $type, + }; + $sink(None, params) + }}; + } + match format { + AudioFormat::F32 => open_sink!(Self::F32, f32), + AudioFormat::S32 => open_sink!(Self::S32, i32), + AudioFormat::S16 => open_sink!(Self::S16, i16), + _ => { + unimplemented!("PortAudio currently does not support {:?} output", format) + } + } } } impl<'a> Sink for PortAudioSink<'a> { fn start(&mut self) -> io::Result<()> { - if self.0.is_none() { - self.0 = Some( - Stream::open( - None, - Some(self.1), - 44100.0, - FRAMES_PER_BUFFER_UNSPECIFIED, - StreamFlags::empty(), - None, - ) - .unwrap(), - ); + macro_rules! start_sink { + (ref mut $stream: ident, ref $parameters: ident) => {{ + if $stream.is_none() { + *$stream = Some( + Stream::open( + None, + Some(*$parameters), + SAMPLE_RATE as f64, + FRAMES_PER_BUFFER_UNSPECIFIED, + StreamFlags::empty(), + None, + ) + .unwrap(), + ); + } + $stream.as_mut().unwrap().start().unwrap() + }}; } - self.0.as_mut().unwrap().start().unwrap(); + match self { + Self::F32(stream, parameters) => start_sink!(ref mut stream, ref parameters), + Self::S32(stream, parameters) => start_sink!(ref mut stream, ref parameters), + Self::S16(stream, parameters) => start_sink!(ref mut stream, ref parameters), + }; + Ok(()) } + fn stop(&mut self) -> io::Result<()> { - self.0.as_mut().unwrap().stop().unwrap(); - self.0 = None; + macro_rules! stop_sink { + (ref mut $stream: ident) => {{ + $stream.as_mut().unwrap().stop().unwrap(); + *$stream = None; + }}; + } + match self { + Self::F32(stream, _parameters) => stop_sink!(ref mut stream), + Self::S32(stream, _parameters) => stop_sink!(ref mut stream), + Self::S16(stream, _parameters) => stop_sink!(ref mut stream), + }; + Ok(()) } + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - match self.0.as_mut().unwrap().write(packet.samples()) { + macro_rules! write_sink { + (ref mut $stream: expr, $samples: expr) => { + $stream.as_mut().unwrap().write($samples) + }; + } + + let samples = packet.samples(); + let result = match self { + Self::F32(stream, _parameters) => { + write_sink!(ref mut stream, samples) + } + Self::S32(stream, _parameters) => { + let samples_s32: &[i32] = &SamplesConverter::to_s32(samples); + write_sink!(ref mut stream, samples_s32) + } + Self::S16(stream, _parameters) => { + let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); + write_sink!(ref mut stream, samples_s16) + } + }; + match result { Ok(_) => (), Err(portaudio_rs::PaError::OutputUnderflowed) => error!("PortAudio write underflow"), - Err(e) => panic!("PA Error {}", e), + Err(e) => panic!("PortAudio error {}", e), }; Ok(()) diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 11ea026a..507e453c 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -1,5 +1,7 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use libpulse_binding::{self as pulse, stream::Direction}; use libpulse_simple_binding::Simple; use std::io; @@ -11,23 +13,34 @@ pub struct PulseAudioSink { s: Option, ss: pulse::sample::Spec, device: Option, + format: AudioFormat, } impl Open for PulseAudioSink { - fn open(device: Option) -> PulseAudioSink { - debug!("Using PulseAudio sink"); + fn open(device: Option, format: AudioFormat) -> Self { + info!("Using PulseAudio sink with format: {:?}", format); + + // PulseAudio calls S24 and S24_3 different from the rest of the world + let pulse_format = match format { + AudioFormat::F32 => pulse::sample::Format::F32le, + AudioFormat::S32 => pulse::sample::Format::S32le, + AudioFormat::S24 => pulse::sample::Format::S24_32le, + AudioFormat::S24_3 => pulse::sample::Format::S24le, + AudioFormat::S16 => pulse::sample::Format::S16le, + }; let ss = pulse::sample::Spec { - format: pulse::sample::Format::S16le, - channels: 2, // stereo - rate: 44100, + format: pulse_format, + channels: NUM_CHANNELS, + rate: SAMPLE_RATE, }; debug_assert!(ss.is_valid()); - PulseAudioSink { + Self { s: None, ss: ss, device: device, + format: format, } } } @@ -66,19 +79,13 @@ impl Sink for PulseAudioSink { Ok(()) } - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - if let Some(s) = &self.s { - // SAFETY: An i16 consists of two bytes, so that the given slice can be interpreted - // as a byte array of double length. Each byte pointer is validly aligned, and so - // is the newly created slice. - let d: &[u8] = unsafe { - std::slice::from_raw_parts( - packet.samples().as_ptr() as *const u8, - packet.samples().len() * 2, - ) - }; + sink_as_bytes!(); +} - match s.write(d) { +impl SinkAsBytes for PulseAudioSink { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { + if let Some(s) = &self.s { + match s.write(data) { Ok(_) => Ok(()), Err(e) => Err(io::Error::new( io::ErrorKind::BrokenPipe, @@ -88,7 +95,7 @@ impl Sink for PulseAudioSink { } else { Err(io::Error::new( io::ErrorKind::NotConnected, - "Not connected to pulseaudio", + "Not connected to PulseAudio", )) } } diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 3b920c30..908099f1 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -1,34 +1,96 @@ use super::{Open, Sink}; extern crate cpal; extern crate rodio; -use crate::audio::AudioPacket; +use crate::audio::{AudioPacket, SamplesConverter}; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use cpal::traits::{DeviceTrait, HostTrait}; use std::process::exit; use std::{io, thread, time}; -pub struct RodioSink { - rodio_sink: rodio::Sink, - // We have to keep hold of this object, or the Sink can't play... - #[allow(dead_code)] - stream: rodio::OutputStream, +// most code is shared between RodioSink and JackRodioSink +macro_rules! rodio_sink { + ($name: ident) => { + pub struct $name { + rodio_sink: rodio::Sink, + // We have to keep hold of this object, or the Sink can't play... + #[allow(dead_code)] + stream: rodio::OutputStream, + format: AudioFormat, + } + + impl Sink for $name { + start_stop_noop!(); + + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + let samples = packet.samples(); + match self.format { + AudioFormat::F32 => { + let source = rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples); + self.rodio_sink.append(source); + }, + AudioFormat::S16 => { + let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); + let source = rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples_s16); + self.rodio_sink.append(source); + }, + _ => unreachable!(), + }; + + // Chunk sizes seem to be about 256 to 3000 ish items long. + // Assuming they're on average 1628 then a half second buffer is: + // 44100 elements --> about 27 chunks + while self.rodio_sink.len() > 26 { + // sleep and wait for rodio to drain a bit + thread::sleep(time::Duration::from_millis(10)); + } + Ok(()) + } + } + + impl $name { + fn open_sink(host: &cpal::Host, device: Option, format: AudioFormat) -> $name { + match format { + AudioFormat::F32 => { + #[cfg(target_os = "linux")] + { + warn!("Rodio output to Alsa is known to cause garbled sound, consider using `--backend alsa`"); + } + }, + AudioFormat::S16 => {}, + _ => unimplemented!("Rodio currently only supports F32 and S16 formats"), + } + + let rodio_device = match_device(&host, device); + debug!("Using cpal device"); + let (stream, stream_handle) = rodio::OutputStream::try_from_device(&rodio_device) + .expect("couldn't open output stream."); + debug!("Using Rodio stream"); + let sink = rodio::Sink::try_new(&stream_handle).expect("couldn't create output sink."); + debug!("Using Rodio sink"); + + Self { + rodio_sink: sink, + stream: stream, + format: format, + } + } + } + }; } +rodio_sink!(RodioSink); #[cfg(all( feature = "rodiojack-backend", any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd") ))] -pub struct JackRodioSink { - jackrodio_sink: rodio::Sink, - // We have to keep hold of this object, or the Sink can't play... - #[allow(dead_code)] - stream: rodio::OutputStream, -} +rodio_sink!(JackRodioSink); fn list_formats(ref device: &rodio::Device) { let default_fmt = match device.default_output_config() { Ok(fmt) => cpal::SupportedStreamConfig::from(fmt), Err(e) => { - warn!("Error getting default rodio::Sink config: {}", e); + warn!("Error getting default Rodio output config: {}", e); return; } }; @@ -38,13 +100,13 @@ fn list_formats(ref device: &rodio::Device) { let mut output_configs = match device.supported_output_configs() { Ok(f) => f.peekable(), Err(e) => { - warn!("Error getting supported rodio::Sink configs: {}", e); + warn!("Error getting supported Rodio output configs: {}", e); return; } }; if output_configs.peek().is_some() { - debug!(" Available configs:"); + debug!(" Available output configs:"); for format in output_configs { debug!(" {:?}", format); } @@ -54,13 +116,13 @@ fn list_formats(ref device: &rodio::Device) { fn list_outputs(ref host: &cpal::Host) { let default_device = get_default_device(host); let default_device_name = default_device.name().expect("cannot get output name"); - println!("Default Audio Device:\n {}", default_device_name); + println!("Default audio device:\n {}", default_device_name); list_formats(&default_device); - println!("Other Available Audio Devices:"); + println!("Other available audio devices:"); let found_devices = host.output_devices().expect(&format!( - "Cannot get list of output devices of Host: {:?}", + "Cannot get list of output devices of host: {:?}", host.id() )); for device in found_devices { @@ -86,7 +148,7 @@ fn match_device(ref host: &cpal::Host, device: Option) -> rodio::Device } let found_devices = host.output_devices().expect(&format!( - "Cannot get list of output devices of Host: {:?}", + "cannot get list of output devices of host: {:?}", host.id() )); for d in found_devices { @@ -102,22 +164,14 @@ fn match_device(ref host: &cpal::Host, device: Option) -> rodio::Device } impl Open for RodioSink { - fn open(device: Option) -> RodioSink { + fn open(device: Option, format: AudioFormat) -> RodioSink { let host = cpal::default_host(); - debug!("Using rodio sink with cpal host: {:?}", host.id()); - - let rodio_device = match_device(&host, device); - debug!("Using cpal device"); - let stream = rodio::OutputStream::try_from_device(&rodio_device) - .expect("Couldn't open output stream."); - debug!("Using rodio stream"); - let sink = rodio::Sink::try_new(&stream.1).expect("Couldn't create output sink."); - debug!("Using rodio sink"); - - RodioSink { - rodio_sink: sink, - stream: stream.0, - } + info!( + "Using Rodio sink with format {:?} and cpal host: {:?}", + format, + host.id() + ); + Self::open_sink(&host, device, format) } } @@ -126,89 +180,18 @@ impl Open for RodioSink { any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd") ))] impl Open for JackRodioSink { - fn open(device: Option) -> JackRodioSink { + fn open(device: Option, format: AudioFormat) -> JackRodioSink { let host = cpal::host_from_id( cpal::available_hosts() .into_iter() .find(|id| *id == cpal::HostId::Jack) - .expect("Jack Host not found"), + .expect("JACK host not found"), ) - .expect("Jack Host not found"); - debug!("Using jack rodio sink with cpal Jack host"); - - let rodio_device = match_device(&host, device); - debug!("Using cpal device"); - let stream = rodio::OutputStream::try_from_device(&rodio_device) - .expect("Couldn't open output stream."); - debug!("Using jack rodio stream"); - let sink = rodio::Sink::try_new(&stream.1).expect("Couldn't create output sink."); - debug!("Using jack rodio sink"); - - JackRodioSink { - jackrodio_sink: sink, - stream: stream.0, - } - } -} - -impl Sink for RodioSink { - fn start(&mut self) -> io::Result<()> { - // More similar to an "unpause" than "play". Doesn't undo "stop". - // self.rodio_sink.play(); - Ok(()) - } - - fn stop(&mut self) -> io::Result<()> { - // This will immediately stop playback, but the sink is then unusable. - // We just have to let the current buffer play till the end. - // self.rodio_sink.stop(); - Ok(()) - } - - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - let source = rodio::buffer::SamplesBuffer::new(2, 44100, packet.samples()); - self.rodio_sink.append(source); - - // Chunk sizes seem to be about 256 to 3000 ish items long. - // Assuming they're on average 1628 then a half second buffer is: - // 44100 elements --> about 27 chunks - while self.rodio_sink.len() > 26 { - // sleep and wait for rodio to drain a bit - thread::sleep(time::Duration::from_millis(10)); - } - Ok(()) - } -} - -#[cfg(all( - feature = "rodiojack-backend", - any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd") -))] -impl Sink for JackRodioSink { - fn start(&mut self) -> io::Result<()> { - // More similar to an "unpause" than "play". Doesn't undo "stop". - // self.rodio_sink.play(); - Ok(()) - } - - fn stop(&mut self) -> io::Result<()> { - // This will immediately stop playback, but the sink is then unusable. - // We just have to let the current buffer play till the end. - // self.rodio_sink.stop(); - Ok(()) - } - - fn write(&mut self, data: &[i16]) -> io::Result<()> { - let source = rodio::buffer::SamplesBuffer::new(2, 44100, data); - self.jackrodio_sink.append(source); - - // Chunk sizes seem to be about 256 to 3000 ish items long. - // Assuming they're on average 1628 then a half second buffer is: - // 44100 elements --> about 27 chunks - while self.jackrodio_sink.len() > 26 { - // sleep and wait for rodio to drain a bit - thread::sleep(time::Duration::from_millis(10)); - } - Ok(()) + .expect("JACK host not found"); + info!( + "Using JACK Rodio sink with format {:?} and cpal JACK host", + format + ); + Self::open_sink(&host, device, format) } } diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 27d650f9..0a3fd433 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -1,57 +1,112 @@ use super::{Open, Sink}; -use crate::audio::AudioPacket; +use crate::audio::{AudioPacket, SamplesConverter}; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use sdl2::audio::{AudioQueue, AudioSpecDesired}; use std::{io, thread, time}; -type Channel = i16; - -pub struct SdlSink { - queue: AudioQueue, +pub enum SdlSink { + F32(AudioQueue), + S32(AudioQueue), + S16(AudioQueue), } impl Open for SdlSink { - fn open(device: Option) -> SdlSink { - debug!("Using SDL sink"); + fn open(device: Option, format: AudioFormat) -> Self { + info!("Using SDL sink with format: {:?}", format); if device.is_some() { - panic!("SDL sink does not support specifying a device name"); + warn!("SDL sink does not support specifying a device name"); } - let ctx = sdl2::init().expect("Could not init SDL"); - let audio = ctx.audio().expect("Could not init SDL audio subsystem"); + let ctx = sdl2::init().expect("could not initialize SDL"); + let audio = ctx + .audio() + .expect("could not initialize SDL audio subsystem"); let desired_spec = AudioSpecDesired { - freq: Some(44_100), - channels: Some(2), + freq: Some(SAMPLE_RATE as i32), + channels: Some(NUM_CHANNELS), samples: None, }; - let queue = audio - .open_queue(None, &desired_spec) - .expect("Could not open SDL audio device"); - SdlSink { queue: queue } + macro_rules! open_sink { + ($sink: expr, $type: ty) => {{ + let queue: AudioQueue<$type> = audio + .open_queue(None, &desired_spec) + .expect("could not open SDL audio device"); + $sink(queue) + }}; + } + match format { + AudioFormat::F32 => open_sink!(Self::F32, f32), + AudioFormat::S32 => open_sink!(Self::S32, i32), + AudioFormat::S16 => open_sink!(Self::S16, i16), + _ => { + unimplemented!("SDL currently does not support {:?} output", format) + } + } } } impl Sink for SdlSink { fn start(&mut self) -> io::Result<()> { - self.queue.clear(); - self.queue.resume(); + macro_rules! start_sink { + ($queue: expr) => {{ + $queue.clear(); + $queue.resume(); + }}; + } + match self { + Self::F32(queue) => start_sink!(queue), + Self::S32(queue) => start_sink!(queue), + Self::S16(queue) => start_sink!(queue), + }; Ok(()) } fn stop(&mut self) -> io::Result<()> { - self.queue.pause(); - self.queue.clear(); + macro_rules! stop_sink { + ($queue: expr) => {{ + $queue.pause(); + $queue.clear(); + }}; + } + match self { + Self::F32(queue) => stop_sink!(queue), + Self::S32(queue) => stop_sink!(queue), + Self::S16(queue) => stop_sink!(queue), + }; Ok(()) } fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - while self.queue.size() > (2 * 2 * 44_100) { - // sleep and wait for sdl thread to drain the queue a bit - thread::sleep(time::Duration::from_millis(10)); + macro_rules! drain_sink { + ($queue: expr, $size: expr) => {{ + // sleep and wait for sdl thread to drain the queue a bit + while $queue.size() > (NUM_CHANNELS as u32 * $size as u32 * SAMPLE_RATE) { + thread::sleep(time::Duration::from_millis(10)); + } + }}; } - self.queue.queue(packet.samples()); + + let samples = packet.samples(); + match self { + Self::F32(queue) => { + drain_sink!(queue, AudioFormat::F32.size()); + queue.queue(samples) + } + Self::S32(queue) => { + let samples_s32: &[i32] = &SamplesConverter::to_s32(samples); + drain_sink!(queue, AudioFormat::S32.size()); + queue.queue(samples_s32) + } + Self::S16(queue) => { + let samples_s16: &[i16] = &SamplesConverter::to_s16(samples); + drain_sink!(queue, AudioFormat::S16.size()); + queue.queue(samples_s16) + } + }; Ok(()) } } diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index 0dd25638..bebb6ea0 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -1,22 +1,25 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; use shell_words::split; use std::io::{self, Write}; -use std::mem; use std::process::{Child, Command, Stdio}; -use std::slice; pub struct SubprocessSink { shell_command: String, child: Option, + format: AudioFormat, } impl Open for SubprocessSink { - fn open(shell_command: Option) -> SubprocessSink { + fn open(shell_command: Option, format: AudioFormat) -> Self { + info!("Using subprocess sink with format: {:?}", format); + if let Some(shell_command) = shell_command { SubprocessSink { shell_command: shell_command, child: None, + format: format, } } else { panic!("subprocess sink requires specifying a shell command"); @@ -44,16 +47,15 @@ impl Sink for SubprocessSink { Ok(()) } - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - let data: &[u8] = unsafe { - slice::from_raw_parts( - packet.samples().as_ptr() as *const u8, - packet.samples().len() * mem::size_of::(), - ) - }; + sink_as_bytes!(); +} + +impl SinkAsBytes for SubprocessSink { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { if let Some(child) = &mut self.child { let child_stdin = child.stdin.as_mut().unwrap(); child_stdin.write_all(data)?; + child_stdin.flush()?; } Ok(()) } diff --git a/playback/src/config.rs b/playback/src/config.rs index 31f63626..95c97092 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,3 +1,6 @@ +use crate::audio::i24; +use std::convert::TryFrom; +use std::mem; use std::str::FromStr; #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] @@ -11,17 +14,58 @@ impl FromStr for Bitrate { type Err = (); fn from_str(s: &str) -> Result { match s { - "96" => Ok(Bitrate::Bitrate96), - "160" => Ok(Bitrate::Bitrate160), - "320" => Ok(Bitrate::Bitrate320), + "96" => Ok(Self::Bitrate96), + "160" => Ok(Self::Bitrate160), + "320" => Ok(Self::Bitrate320), _ => Err(()), } } } impl Default for Bitrate { - fn default() -> Bitrate { - Bitrate::Bitrate160 + fn default() -> Self { + Self::Bitrate160 + } +} + +#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] +pub enum AudioFormat { + F32, + S32, + S24, + S24_3, + S16, +} + +impl TryFrom<&String> for AudioFormat { + type Error = (); + fn try_from(s: &String) -> Result { + match s.to_uppercase().as_str() { + "F32" => Ok(Self::F32), + "S32" => Ok(Self::S32), + "S24" => Ok(Self::S24), + "S24_3" => Ok(Self::S24_3), + "S16" => Ok(Self::S16), + _ => Err(()), + } + } +} + +impl Default for AudioFormat { + fn default() -> Self { + Self::S16 + } +} + +impl AudioFormat { + // not used by all backends + #[allow(dead_code)] + pub fn size(&self) -> usize { + match self { + Self::S24_3 => mem::size_of::(), + Self::S16 => mem::size_of::(), + _ => mem::size_of::(), // S32 and S24 are both stored in i32 + } } } @@ -35,16 +79,39 @@ impl FromStr for NormalisationType { type Err = (); fn from_str(s: &str) -> Result { match s { - "album" => Ok(NormalisationType::Album), - "track" => Ok(NormalisationType::Track), + "album" => Ok(Self::Album), + "track" => Ok(Self::Track), _ => Err(()), } } } impl Default for NormalisationType { - fn default() -> NormalisationType { - NormalisationType::Album + fn default() -> Self { + Self::Album + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum NormalisationMethod { + Basic, + Dynamic, +} + +impl FromStr for NormalisationMethod { + type Err = (); + fn from_str(s: &str) -> Result { + match s { + "basic" => Ok(Self::Basic), + "dynamic" => Ok(Self::Dynamic), + _ => Err(()), + } + } +} + +impl Default for NormalisationMethod { + fn default() -> Self { + Self::Dynamic } } @@ -53,7 +120,12 @@ pub struct PlayerConfig { pub bitrate: Bitrate, pub normalisation: bool, pub normalisation_type: NormalisationType, + pub normalisation_method: NormalisationMethod, pub normalisation_pregain: f32, + pub normalisation_threshold: f32, + pub normalisation_attack: f32, + pub normalisation_release: f32, + pub normalisation_knee: f32, pub gapless: bool, pub passthrough: bool, } @@ -64,7 +136,12 @@ impl Default for PlayerConfig { bitrate: Bitrate::default(), normalisation: false, normalisation_type: NormalisationType::default(), + normalisation_method: NormalisationMethod::default(), normalisation_pregain: 0.0, + normalisation_threshold: -1.0, + normalisation_attack: 0.005, + normalisation_release: 0.1, + normalisation_knee: 1.0, gapless: true, passthrough: false, } diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index 325c1e18..3424526a 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -12,7 +12,7 @@ pub trait Mixer: Send { } pub trait AudioFilter { - fn modify_stream(&self, data: &mut [i16]); + fn modify_stream(&self, data: &mut [f32]); } #[cfg(feature = "alsa-backend")] diff --git a/playback/src/mixer/softmixer.rs b/playback/src/mixer/softmixer.rs index 28e1cf57..ec8ed6b2 100644 --- a/playback/src/mixer/softmixer.rs +++ b/playback/src/mixer/softmixer.rs @@ -35,11 +35,12 @@ struct SoftVolumeApplier { } impl AudioFilter for SoftVolumeApplier { - fn modify_stream(&self, data: &mut [i16]) { + fn modify_stream(&self, data: &mut [f32]) { let volume = self.volume.load(Ordering::Relaxed) as u16; if volume != 0xFFFF { + let volume_factor = volume as f64 / 0xFFFF as f64; for x in data.iter_mut() { - *x = (*x as i32 * volume as i32 / 0xFFFF) as i16; + *x = (*x as f64 * volume_factor) as f32; } } } diff --git a/playback/src/player.rs b/playback/src/player.rs index 9b4eefb9..0a573c93 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -9,7 +9,7 @@ use std::mem; use std::thread; use std::time::{Duration, Instant}; -use crate::config::{Bitrate, NormalisationType, PlayerConfig}; +use crate::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}; use librespot_core::session::Session; use librespot_core::spotify_id::SpotifyId; @@ -25,7 +25,12 @@ use crate::audio_backend::Sink; use crate::metadata::{AudioItem, FileFormat}; use crate::mixer::AudioFilter; +pub const SAMPLE_RATE: u32 = 44100; +pub const NUM_CHANNELS: u8 = 2; +pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE as u32 * NUM_CHANNELS as u32; + const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; +const DB_VOLTAGE_RATIO: f32 = 20.0; pub struct Player { commands: Option>, @@ -54,6 +59,13 @@ struct PlayerInternal { sink_event_callback: Option, audio_filter: Option>, event_senders: Vec>, + + limiter_active: bool, + limiter_attack_counter: u32, + limiter_release_counter: u32, + limiter_peak_sample: f32, + limiter_factor: f32, + limiter_strength: f32, } enum PlayerCommand { @@ -185,7 +197,7 @@ impl PlayerEvent { pub type PlayerEventChannel = futures::sync::mpsc::UnboundedReceiver; #[derive(Clone, Copy, Debug)] -struct NormalisationData { +pub struct NormalisationData { track_gain_db: f32, track_peak: f32, album_gain_db: f32, @@ -193,6 +205,14 @@ struct NormalisationData { } impl NormalisationData { + pub fn db_to_ratio(db: f32) -> f32 { + return f32::powf(10.0, db / DB_VOLTAGE_RATIO); + } + + pub fn ratio_to_db(ratio: f32) -> f32 { + return ratio.log10() * DB_VOLTAGE_RATIO; + } + fn parse_from_file(mut file: T) -> Result { const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144; file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET)) @@ -218,17 +238,41 @@ impl NormalisationData { NormalisationType::Album => [data.album_gain_db, data.album_peak], NormalisationType::Track => [data.track_gain_db, data.track_peak], }; - let mut normalisation_factor = - f32::powf(10.0, (gain_db + config.normalisation_pregain) / 20.0); - if normalisation_factor * gain_peak > 1.0 { - warn!("Reducing normalisation factor to prevent clipping. Please add negative pregain to avoid."); - normalisation_factor = 1.0 / gain_peak; + let normalisation_power = gain_db + config.normalisation_pregain; + let mut normalisation_factor = Self::db_to_ratio(normalisation_power); + + if normalisation_factor * gain_peak > config.normalisation_threshold { + let limited_normalisation_factor = config.normalisation_threshold / gain_peak; + let limited_normalisation_power = Self::ratio_to_db(limited_normalisation_factor); + + if config.normalisation_method == NormalisationMethod::Basic { + warn!("Limiting gain to {:.2} for the duration of this track to stay under normalisation threshold.", limited_normalisation_power); + normalisation_factor = limited_normalisation_factor; + } else { + warn!( + "This track will at its peak be subject to {:.2} dB of dynamic limiting.", + normalisation_power - limited_normalisation_power + ); + } + + warn!("Please lower pregain to avoid."); } debug!("Normalisation Data: {:?}", data); debug!("Normalisation Type: {:?}", config.normalisation_type); - debug!("Applied normalisation factor: {}", normalisation_factor); + debug!( + "Normalisation Threshold: {:.1}", + Self::ratio_to_db(config.normalisation_threshold) + ); + debug!("Normalisation Method: {:?}", config.normalisation_method); + debug!("Normalisation Factor: {}", normalisation_factor); + + if config.normalisation_method == NormalisationMethod::Dynamic { + debug!("Normalisation Attack: {:?}", config.normalisation_attack); + debug!("Normalisation Release: {:?}", config.normalisation_release); + debug!("Normalisation Knee: {:?}", config.normalisation_knee); + } normalisation_factor } @@ -262,6 +306,13 @@ impl Player { sink_event_callback: None, audio_filter: audio_filter, event_senders: [event_sender].to_vec(), + + limiter_active: false, + limiter_attack_counter: 0, + limiter_release_counter: 0, + limiter_peak_sample: 0.0, + limiter_factor: 1.0, + limiter_strength: 0.0, }; // While PlayerInternal is written as a future, it still contains blocking code. @@ -887,8 +938,8 @@ impl Future for PlayerInternal { if !self.config.passthrough { if let Some(ref packet) = packet { - *stream_position_pcm = - *stream_position_pcm + (packet.samples().len() / 2) as u64; + *stream_position_pcm = *stream_position_pcm + + (packet.samples().len() / NUM_CHANNELS as usize) as u64; let stream_position_millis = Self::position_pcm_to_ms(*stream_position_pcm); @@ -1113,9 +1164,116 @@ impl PlayerInternal { editor.modify_stream(data) } - if self.config.normalisation && normalisation_factor != 1.0 { - for x in data.iter_mut() { - *x = (*x as f32 * normalisation_factor) as i16; + if self.config.normalisation + && (normalisation_factor != 1.0 + || self.config.normalisation_method != NormalisationMethod::Basic) + { + for sample in data.iter_mut() { + let mut actual_normalisation_factor = normalisation_factor; + if self.config.normalisation_method == NormalisationMethod::Dynamic + { + if self.limiter_active { + // "S"-shaped curve with a configurable knee during attack and release: + // - > 1.0 yields soft knees at start and end, steeper in between + // - 1.0 yields a linear function from 0-100% + // - between 0.0 and 1.0 yields hard knees at start and end, flatter in between + // - 0.0 yields a step response to 50%, causing distortion + // - Rates < 0.0 invert the limiter and are invalid + let mut shaped_limiter_strength = self.limiter_strength; + if shaped_limiter_strength > 0.0 + && shaped_limiter_strength < 1.0 + { + shaped_limiter_strength = 1.0 + / (1.0 + + f32::powf( + shaped_limiter_strength + / (1.0 - shaped_limiter_strength), + -1.0 * self.config.normalisation_knee, + )); + } + actual_normalisation_factor = + (1.0 - shaped_limiter_strength) * normalisation_factor + + shaped_limiter_strength * self.limiter_factor; + }; + + // Always check for peaks, even when the limiter is already active. + // There may be even higher peaks than we initially targeted. + // Check against the normalisation factor that would be applied normally. + let abs_sample = + ((*sample as f64 * normalisation_factor as f64) as f32) + .abs(); + if abs_sample > self.config.normalisation_threshold { + self.limiter_active = true; + if self.limiter_release_counter > 0 { + // A peak was encountered while releasing the limiter; + // synchronize with the current release limiter strength. + self.limiter_attack_counter = (((SAMPLES_PER_SECOND + as f32 + * self.config.normalisation_release) + - self.limiter_release_counter as f32) + / (self.config.normalisation_release + / self.config.normalisation_attack)) + as u32; + self.limiter_release_counter = 0; + } + + self.limiter_attack_counter = + self.limiter_attack_counter.saturating_add(1); + self.limiter_strength = self.limiter_attack_counter as f32 + / (SAMPLES_PER_SECOND as f32 + * self.config.normalisation_attack); + + if abs_sample > self.limiter_peak_sample { + self.limiter_peak_sample = abs_sample; + self.limiter_factor = + self.config.normalisation_threshold + / self.limiter_peak_sample; + } + } else if self.limiter_active { + if self.limiter_attack_counter > 0 { + // Release may start within the attack period, before + // the limiter reached full strength. For that reason + // start the release by synchronizing with the current + // attack limiter strength. + self.limiter_release_counter = (((SAMPLES_PER_SECOND + as f32 + * self.config.normalisation_attack) + - self.limiter_attack_counter as f32) + * (self.config.normalisation_release + / self.config.normalisation_attack)) + as u32; + self.limiter_attack_counter = 0; + } + + self.limiter_release_counter = + self.limiter_release_counter.saturating_add(1); + + if self.limiter_release_counter + > (SAMPLES_PER_SECOND as f32 + * self.config.normalisation_release) + as u32 + { + self.reset_limiter(); + } else { + self.limiter_strength = ((SAMPLES_PER_SECOND as f32 + * self.config.normalisation_release) + - self.limiter_release_counter as f32) + / (SAMPLES_PER_SECOND as f32 + * self.config.normalisation_release); + } + } + } + + *sample = + (*sample as f64 * actual_normalisation_factor as f64) as f32; + + // Extremely sharp attacks, however unlikely, *may* still clip and provide + // undefined results, so strictly enforce output within [-1.0, 1.0]. + if *sample < -1.0 { + *sample = -1.0; + } else if *sample > 1.0 { + *sample = 1.0; + } } } } @@ -1146,6 +1304,15 @@ impl PlayerInternal { } } + fn reset_limiter(&mut self) { + self.limiter_active = false; + self.limiter_release_counter = 0; + self.limiter_attack_counter = 0; + self.limiter_peak_sample = 0.0; + self.limiter_factor = 1.0; + self.limiter_strength = 0.0; + } + fn start_playback( &mut self, track_id: SpotifyId, diff --git a/src/main.rs b/src/main.rs index 53603152..7426e2c4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use futures::sync::mpsc::UnboundedReceiver; use futures::{Async, Future, Poll, Stream}; use log::{error, info, trace, warn}; use sha1::{Digest, Sha1}; +use std::convert::TryFrom; use std::env; use std::io::{stderr, Write}; use std::mem; @@ -22,13 +23,17 @@ use librespot::core::version; use librespot::connect::discovery::{discovery, DiscoveryStream}; use librespot::connect::spirc::{Spirc, SpircTask}; use librespot::playback::audio_backend::{self, Sink, BACKENDS}; -use librespot::playback::config::{Bitrate, NormalisationType, PlayerConfig}; +use librespot::playback::config::{ + AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, +}; use librespot::playback::mixer::{self, Mixer, MixerConfig}; -use librespot::playback::player::{Player, PlayerEvent}; +use librespot::playback::player::{NormalisationData, Player, PlayerEvent}; mod player_event_handler; use crate::player_event_handler::{emit_sink_event, run_program_on_events}; +const MILLIS: f32 = 1000.0; + fn device_id(name: &str) -> String { hex::encode(Sha1::digest(name.as_bytes())) } @@ -83,7 +88,8 @@ fn print_version() { #[derive(Clone)] struct Setup { - backend: fn(Option) -> Box, + format: AudioFormat, + backend: fn(Option, AudioFormat) -> Box, device: Option, mixer: fn(Option) -> Box, @@ -147,6 +153,12 @@ fn setup(args: &[String]) -> Setup { "Audio device to use. Use '?' to list options if using portaudio or alsa", "DEVICE", ) + .optopt( + "", + "format", + "Output format (F32, S32, S24, S24_3 or S16). Defaults to S16", + "FORMAT", + ) .optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER") .optopt( "m", @@ -188,6 +200,12 @@ fn setup(args: &[String]) -> Setup { "enable-volume-normalisation", "Play all tracks at the same volume", ) + .optopt( + "", + "normalisation-method", + "Specify the normalisation method to use - [basic, dynamic]. Default is dynamic.", + "NORMALISATION_METHOD", + ) .optopt( "", "normalisation-gain-type", @@ -200,6 +218,30 @@ fn setup(args: &[String]) -> Setup { "Pregain (dB) applied by volume normalisation", "PREGAIN", ) + .optopt( + "", + "normalisation-threshold", + "Threshold (dBFS) to prevent clipping. Default is -1.0.", + "THRESHOLD", + ) + .optopt( + "", + "normalisation-attack", + "Attack time (ms) in which the dynamic limiter is reducing gain. Default is 5.", + "ATTACK", + ) + .optopt( + "", + "normalisation-release", + "Release or decay time (ms) in which the dynamic limiter is restoring gain. Default is 100.", + "RELEASE", + ) + .optopt( + "", + "normalisation-knee", + "Knee steepness of the dynamic limiter. Default is 1.0.", + "KNEE", + ) .optopt( "", "volume-ctrl", @@ -260,9 +302,15 @@ fn setup(args: &[String]) -> Setup { let backend = audio_backend::find(backend_name).expect("Invalid backend"); + let format = matches + .opt_str("format") + .as_ref() + .map(|format| AudioFormat::try_from(format).expect("Invalid output format")) + .unwrap_or(AudioFormat::default()); + let device = matches.opt_str("device"); if device == Some("?".into()) { - backend(device); + backend(device, format); exit(0); } @@ -390,15 +438,47 @@ fn setup(args: &[String]) -> Setup { NormalisationType::from_str(gain_type).expect("Invalid normalisation type") }) .unwrap_or(NormalisationType::default()); + let normalisation_method = matches + .opt_str("normalisation-method") + .as_ref() + .map(|gain_type| { + NormalisationMethod::from_str(gain_type).expect("Invalid normalisation method") + }) + .unwrap_or(NormalisationMethod::default()); PlayerConfig { bitrate: bitrate, gapless: !matches.opt_present("disable-gapless"), normalisation: matches.opt_present("enable-volume-normalisation"), + normalisation_method: normalisation_method, normalisation_type: gain_type, normalisation_pregain: matches .opt_str("normalisation-pregain") .map(|pregain| pregain.parse::().expect("Invalid pregain float value")) .unwrap_or(PlayerConfig::default().normalisation_pregain), + normalisation_threshold: NormalisationData::db_to_ratio( + matches + .opt_str("normalisation-threshold") + .map(|threshold| { + threshold + .parse::() + .expect("Invalid threshold float value") + }) + .unwrap_or(PlayerConfig::default().normalisation_threshold), + ), + normalisation_attack: matches + .opt_str("normalisation-attack") + .map(|attack| attack.parse::().expect("Invalid attack float value")) + .unwrap_or(PlayerConfig::default().normalisation_attack * MILLIS) + / MILLIS, + normalisation_release: matches + .opt_str("normalisation-release") + .map(|release| release.parse::().expect("Invalid release float value")) + .unwrap_or(PlayerConfig::default().normalisation_release * MILLIS) + / MILLIS, + normalisation_knee: matches + .opt_str("normalisation-knee") + .map(|knee| knee.parse::().expect("Invalid knee float value")) + .unwrap_or(PlayerConfig::default().normalisation_knee), passthrough, } }; @@ -428,6 +508,7 @@ fn setup(args: &[String]) -> Setup { let enable_discovery = !matches.opt_present("disable-discovery"); Setup { + format: format, backend: backend, cache: cache, session_config: session_config, @@ -449,7 +530,8 @@ struct Main { player_config: PlayerConfig, session_config: SessionConfig, connect_config: ConnectConfig, - backend: fn(Option) -> Box, + format: AudioFormat, + backend: fn(Option, AudioFormat) -> Box, device: Option, mixer: fn(Option) -> Box, mixer_config: MixerConfig, @@ -479,6 +561,7 @@ impl Main { session_config: setup.session_config, player_config: setup.player_config, connect_config: setup.connect_config, + format: setup.format, backend: setup.backend, device: setup.device, mixer: setup.mixer, @@ -558,11 +641,12 @@ impl Future for Main { let connect_config = self.connect_config.clone(); let audio_filter = mixer.get_audio_filter(); + let format = self.format; let backend = self.backend; let device = self.device.clone(); let (player, event_channel) = Player::new(player_config, session.clone(), audio_filter, move || { - (backend)(device) + (backend)(device, format) }); if self.emit_sink_events {