Merge pull request #660 from roderickvd/hi-res-volume-control

High-resolution volume control and normalisation
This commit is contained in:
Sasha Hilton 2021-04-10 01:26:20 +01:00 committed by GitHub
commit 8fe2e01166
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1023 additions and 909 deletions

609
Cargo.lock generated
View file

@ -1,26 +1,5 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # 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]] [[package]]
name = "aes" name = "aes"
version = "0.6.0" version = "0.6.0"
@ -66,9 +45,9 @@ dependencies = [
[[package]] [[package]]
name = "alsa" name = "alsa"
version = "0.4.3" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb213f6b3e4b1480a60931ca2035794aa67b73103d254715b1db7b70dcb3c934" checksum = "75c4da790adcb2ce5e758c064b4f3ec17a30349f9961d3e5e6c9688b052a9e18"
dependencies = [ dependencies = [
"alsa-sys", "alsa-sys",
"bitflags", "bitflags",
@ -92,12 +71,6 @@ version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1" checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1"
[[package]]
name = "ascii"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e"
[[package]] [[package]]
name = "atty" name = "atty"
version = "0.2.14" version = "0.2.14"
@ -115,26 +88,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 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]] [[package]]
name = "base64" name = "base64"
version = "0.9.3" version = "0.9.3"
@ -276,6 +229,9 @@ name = "cc"
version = "1.0.66" version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48"
dependencies = [
"jobserver",
]
[[package]] [[package]]
name = "cesu8" name = "cesu8"
@ -313,16 +269,10 @@ dependencies = [
"libc", "libc",
"num-integer", "num-integer",
"num-traits", "num-traits",
"time 0.1.43", "time",
"winapi 0.3.9", "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]] [[package]]
name = "cipher" name = "cipher"
version = "0.2.5" version = "0.2.5"
@ -352,19 +302,6 @@ dependencies = [
"bitflags", "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]] [[package]]
name = "combine" name = "combine"
version = "4.5.2" version = "4.5.2"
@ -375,39 +312,6 @@ dependencies = [
"memchr", "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]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.6.2" version = "0.6.2"
@ -416,9 +320,9 @@ checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b"
[[package]] [[package]]
name = "coreaudio-rs" name = "coreaudio-rs"
version = "0.9.1" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f229761965dad3e9b11081668a6ea00f1def7aa46062321b5ec245b834f6e491" checksum = "11894b20ebfe1ff903cbdc52259693389eea03b94918a2def2c30c3bf227ad88"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"coreaudio-sys", "coreaudio-sys",
@ -435,15 +339,15 @@ dependencies = [
[[package]] [[package]]
name = "cpal" name = "cpal"
version = "0.13.1" version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05631e2089dfa5d3b6ea1cfbbfd092e2ee5deeb69698911bc976b28b746d3657" checksum = "8351ddf2aaa3c583fa388029f8b3d26f3c7035a20911fdd5f2e2ed7ab57dad25"
dependencies = [ dependencies = [
"alsa", "alsa",
"core-foundation-sys", "core-foundation-sys",
"coreaudio-rs", "coreaudio-rs",
"jack", "jack",
"jni 0.17.0", "jni",
"js-sys", "js-sys",
"lazy_static", "lazy_static",
"libc", "libc",
@ -453,7 +357,7 @@ dependencies = [
"nix", "nix",
"oboe", "oboe",
"parking_lot 0.11.1", "parking_lot 0.11.1",
"stdweb 0.1.3", "stdweb",
"thiserror", "thiserror",
"web-sys", "web-sys",
"winapi 0.3.9", "winapi 0.3.9",
@ -465,15 +369,6 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" 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]] [[package]]
name = "crossbeam-deque" name = "crossbeam-deque"
version = "0.7.3" version = "0.7.3"
@ -624,12 +519,6 @@ dependencies = [
"generic-array 0.14.4", "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]] [[package]]
name = "dns-sd" name = "dns-sd"
version = "0.1.3" version = "0.1.3"
@ -664,7 +553,6 @@ version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc"
dependencies = [ dependencies = [
"backtrace",
"version_check", "version_check",
] ]
@ -674,45 +562,12 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" 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]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 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]] [[package]]
name = "fuchsia-cprng" name = "fuchsia-cprng"
version = "0.1.1" version = "0.1.1"
@ -876,12 +731,6 @@ dependencies = [
"wasi 0.10.2+wasi-snapshot-preview1", "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]] [[package]]
name = "glib" name = "glib"
version = "0.10.3" version = "0.10.3"
@ -946,9 +795,9 @@ dependencies = [
[[package]] [[package]]
name = "gstreamer" name = "gstreamer"
version = "0.16.5" version = "0.16.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d50f822055923f1cbede233aa5dfd4ee957cf328fb3076e330886094e11d6cf" checksum = "9ff5d0f7ff308ae37e6eb47b6ded17785bdea06e438a708cd09e0288c1862f33"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cfg-if 1.0.0", "cfg-if 1.0.0",
@ -1115,9 +964,9 @@ dependencies = [
"log 0.4.14", "log 0.4.14",
"mime", "mime",
"net2", "net2",
"percent-encoding 1.0.1", "percent-encoding",
"relay", "relay",
"time 0.1.43", "time",
"tokio-core", "tokio-core",
"tokio-io", "tokio-io",
"tokio-proto", "tokio-proto",
@ -1156,17 +1005,6 @@ dependencies = [
"unicode-normalization", "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]] [[package]]
name = "if-addrs" name = "if-addrs"
version = "0.6.5" version = "0.6.5"
@ -1223,9 +1061,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
[[package]] [[package]]
name = "jack" name = "jack"
version = "0.6.5" version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c1871c91fa65aa328f3bedbaa54a6e5d1de009264684c153eb708ba933aa6f5" checksum = "2deb4974bd7e6b2fb7784f27fa13d819d11292b3b004dce0185ec08163cf686a"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"jack-sys", "jack-sys",
@ -1235,9 +1073,9 @@ dependencies = [
[[package]] [[package]]
name = "jack-sys" name = "jack-sys"
version = "0.2.1" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d6ab7ada402b6a27912a2b86504be62a48c58313c886fe72a059127acb4d7" checksum = "57983f0d72dfecf2b719ed39bc9cacd85194e1a94cb3f9146009eff9856fef41"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"libc", "libc",
@ -1246,29 +1084,15 @@ dependencies = [
[[package]] [[package]]
name = "jni" name = "jni"
version = "0.14.0" version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1981310da491a4f0f815238097d0d43d8072732b5ae5f8bd0d8eadf5bf245402" checksum = "24967112a1e4301ca5342ea339763613a37592b8a6ce6cf2e4494537c7a42faf"
dependencies = [ dependencies = [
"cesu8", "cesu8",
"combine 3.8.1", "combine",
"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",
"jni-sys", "jni-sys",
"log 0.4.14", "log 0.4.14",
"thiserror",
"walkdir", "walkdir",
] ]
@ -1278,6 +1102,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jobserver"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.47" version = "0.3.47"
@ -1328,27 +1161,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.85" version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ccac4b00700875e6a07c6cde370d44d32fa01c5a65cdd2fca6858c479d28bb3" checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41"
[[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"
[[package]] [[package]]
name = "libloading" name = "libloading"
@ -1380,9 +1195,9 @@ dependencies = [
[[package]] [[package]]
name = "libpulse-binding" name = "libpulse-binding"
version = "2.23.0" version = "2.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2405f806801527dfb3d2b6d48a282cdebe9a1b41b0652e0d7b5bad81dbc700e" checksum = "db951f37898e19a6785208e3a290261e0f1a8e086716be596aaad684882ca8e3"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"libc", "libc",
@ -1452,7 +1267,7 @@ dependencies = [
"tokio-io", "tokio-io",
"tokio-process", "tokio-process",
"tokio-signal", "tokio-signal",
"url 1.7.2", "url",
] ]
[[package]] [[package]]
@ -1473,6 +1288,7 @@ dependencies = [
"ogg", "ogg",
"tempfile", "tempfile",
"vorbis", "vorbis",
"zerocopy",
] ]
[[package]] [[package]]
@ -1499,7 +1315,7 @@ dependencies = [
"serde_json", "serde_json",
"sha-1 0.9.3", "sha-1 0.9.3",
"tokio-core", "tokio-core",
"url 1.7.2", "url",
] ]
[[package]] [[package]]
@ -1533,7 +1349,7 @@ dependencies = [
"tokio-codec", "tokio-codec",
"tokio-core", "tokio-core",
"tokio-io", "tokio-io",
"url 1.7.2", "url",
"uuid", "uuid",
"vergen", "vergen",
] ]
@ -1689,16 +1505,6 @@ version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 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]] [[package]]
name = "mio" name = "mio"
version = "0.6.23" version = "0.6.23"
@ -1780,9 +1586,9 @@ dependencies = [
[[package]] [[package]]
name = "ndk" name = "ndk"
version = "0.2.1" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eb167c1febed0a496639034d0c76b3b74263636045db5489eee52143c246e73" checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab"
dependencies = [ dependencies = [
"jni-sys", "jni-sys",
"ndk-sys", "ndk-sys",
@ -1792,9 +1598,9 @@ dependencies = [
[[package]] [[package]]
name = "ndk-glue" name = "ndk-glue"
version = "0.2.1" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdf399b8b7a39c6fb153c4ec32c72fd5fe789df24a647f229c239aa7adb15241" checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"libc", "libc",
@ -1836,15 +1642,14 @@ dependencies = [
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.15.0" version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2e0b4f3320ed72aaedb9a5ac838690a8047c7b275da22711fddff4f8a14229" checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cc", "cc",
"cfg-if 0.1.10", "cfg-if 1.0.0",
"libc", "libc",
"void",
] ]
[[package]] [[package]]
@ -1921,9 +1726,9 @@ dependencies = [
[[package]] [[package]]
name = "num_enum" name = "num_enum"
version = "0.4.3" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca565a7df06f3d4b485494f25ba05da1435950f4dc263440eda7a6fa9b8e36e4" checksum = "226b45a5c2ac4dd696ed30fa6b94b057ad909c7b7fc2e0d0808192bced894066"
dependencies = [ dependencies = [
"derivative", "derivative",
"num_enum_derive", "num_enum_derive",
@ -1931,9 +1736,9 @@ dependencies = [
[[package]] [[package]]
name = "num_enum_derive" name = "num_enum_derive"
version = "0.4.3" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffa5a33ddddfee04c0283a7653987d634e880347e96b5b2ed64de07efb59db9d" checksum = "1c0fd9eba1d5db0994a239e09c1be402d35622277e35468ba891aa5e3188ce7e"
dependencies = [ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
@ -1941,19 +1746,13 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "object"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397"
[[package]] [[package]]
name = "oboe" name = "oboe"
version = "0.3.1" version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aadc2b0867bdbb9a81c4d99b9b682958f49dbea1295a81d2f646cca2afdd9fc" checksum = "4cfb2390bddb9546c0f7448fd1d2abdd39e6075206f960991eb28c7fa7f126c4"
dependencies = [ dependencies = [
"jni 0.14.0", "jni",
"ndk", "ndk",
"ndk-glue", "ndk-glue",
"num-derive", "num-derive",
@ -1963,11 +1762,11 @@ dependencies = [
[[package]] [[package]]
name = "oboe-sys" name = "oboe-sys"
version = "0.3.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ff7a51600eabe34e189eec5c995a62f151d8d97e5fbca39e87ca738bb99b82" checksum = "fe069264d082fc820dfa172f79be3f2e088ecfece9b1c47b0c9fd838d2bef103"
dependencies = [ dependencies = [
"fetch_unroll", "cc",
] ]
[[package]] [[package]]
@ -2087,12 +1886,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" checksum = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831"
[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.4" version = "0.2.4"
@ -2223,28 +2016,6 @@ dependencies = [
"protobuf-codegen", "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]] [[package]]
name = "quick-error" name = "quick-error"
version = "1.2.3" version = "1.2.3"
@ -2436,32 +2207,11 @@ dependencies = [
"winapi 0.3.9", "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]] [[package]]
name = "rodio" name = "rodio"
version = "0.13.0" version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9683532495146e98878d4948fa1a1953f584cd923f2a5f5c26b7a8701b56943" checksum = "b65c2eda643191f6d1bb12ea323a9db8d9ba95374e9be3780b5a9fb5cfb8520f"
dependencies = [ dependencies = [
"cpal", "cpal",
] ]
@ -2476,12 +2226,6 @@ dependencies = [
"winapi 0.3.9", "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]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "1.1.0" version = "1.1.0"
@ -2497,19 +2241,6 @@ dependencies = [
"semver", "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]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.5" version = "1.0.5"
@ -2543,16 +2274,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 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]] [[package]]
name = "sdl2" name = "sdl2"
version = "0.34.3" version = "0.34.3"
@ -2567,9 +2288,9 @@ dependencies = [
[[package]] [[package]]
name = "sdl2-sys" name = "sdl2-sys"
version = "0.34.3" version = "0.34.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d81feded049b9c14eceb4a4f6d596a98cebbd59abdba949c5552a015466d33" checksum = "4cb164f53dbcad111de976bbf1f3083d3fcdeda88da9cfa281c70822720ee3da"
dependencies = [ dependencies = [
"cfg-if 0.1.10", "cfg-if 0.1.10",
"libc", "libc",
@ -2596,9 +2317,6 @@ name = "serde"
version = "1.0.123" version = "1.0.123"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae"
dependencies = [
"serde_derive",
]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
@ -2647,12 +2365,6 @@ dependencies = [
"opaque-debug 0.3.0", "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]] [[package]]
name = "shannon" name = "shannon"
version = "0.2.0" version = "0.2.0"
@ -2727,76 +2439,12 @@ dependencies = [
"winapi 0.3.9", "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]] [[package]]
name = "stdweb" name = "stdweb"
version = "0.1.3" version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" 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]] [[package]]
name = "strsim" name = "strsim"
version = "0.9.3" version = "0.9.3"
@ -2871,17 +2519,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b157868d8ac1f56b64604539990685fa7611d8fa9e5476cf0c02cf34d32917c5" 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]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.2.0" version = "3.2.0"
@ -2935,44 +2572,6 @@ dependencies = [
"winapi 0.3.9", "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]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.1.1" version = "1.1.1"
@ -3318,61 +2917,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 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]] [[package]]
name = "url" name = "url"
version = "1.7.2" version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" checksum = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a"
dependencies = [ dependencies = [
"idna 0.1.5", "idna",
"matches", "matches",
"percent-encoding 1.0.1", "percent-encoding",
]
[[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",
] ]
[[package]] [[package]]
@ -3406,12 +2959,6 @@ version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
[[package]]
name = "void"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
[[package]] [[package]]
name = "vorbis" name = "vorbis"
version = "0.0.14" version = "0.0.14"
@ -3547,25 +3094,6 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "winapi" name = "winapi"
version = "0.2.8" version = "0.2.8"
@ -3619,15 +3147,6 @@ dependencies = [
"winapi-build", "winapi-build",
] ]
[[package]]
name = "xattr"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.3.0" version = "0.3.0"

View file

@ -22,9 +22,10 @@ log = "0.4"
num-bigint = "0.3" num-bigint = "0.3"
num-traits = "0.2" num-traits = "0.2"
tempfile = "3.1" tempfile = "3.1"
zerocopy = "0.3"
librespot-tremor = { version = "0.2.0", optional = true } librespot-tremor = { version = "0.2", optional = true }
vorbis = { version ="0.0.14", optional = true } vorbis = { version ="0.0", optional = true }
[features] [features]
with-tremor = ["librespot-tremor"] with-tremor = ["librespot-tremor"]

59
audio/src/convert.rs Normal file
View file

@ -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<i32> {
convert_samples_to!(i32, samples)
}
pub fn to_s24(samples: &[f32]) -> Vec<i32> {
convert_samples_to!(i32, samples, 8)
}
pub fn to_s24_3(samples: &[f32]) -> Vec<i24> {
Self::to_s32(samples)
.iter()
.map(|sample| i24::pcm_from_i32(*sample))
.collect()
}
pub fn to_s16(samples: &[f32]) -> Vec<i16> {
convert_samples_to!(i16, samples)
}
}

View file

@ -37,8 +37,11 @@ where
use self::lewton::VorbisError::BadAudio; use self::lewton::VorbisError::BadAudio;
use self::lewton::VorbisError::OggError; use self::lewton::VorbisError::OggError;
loop { loop {
match self.0.read_dec_packet_itl() { match self
Ok(Some(packet)) => return Ok(Some(AudioPacket::Samples(packet))), .0
.read_dec_packet_generic::<lewton::samples::InterleavedSamples<f32>>()
{
Ok(Some(packet)) => return Ok(Some(AudioPacket::Samples(packet.samples))),
Ok(None) => return Ok(None), Ok(None) => return Ok(None),
Err(BadAudio(AudioIsHeader)) => (), Err(BadAudio(AudioIsHeader)) => (),

View file

@ -13,6 +13,7 @@ extern crate tempfile;
extern crate librespot_core; extern crate librespot_core;
mod convert;
mod decrypt; mod decrypt;
mod fetch; mod fetch;
@ -24,6 +25,7 @@ mod passthrough_decoder;
mod range_set; mod range_set;
pub use convert::{i24, SamplesConverter};
pub use decrypt::AudioDecrypt; pub use decrypt::AudioDecrypt;
pub use fetch::{AudioFile, AudioFileOpen, StreamLoaderController}; pub use fetch::{AudioFile, AudioFileOpen, StreamLoaderController};
pub use fetch::{ pub use fetch::{
@ -33,12 +35,12 @@ pub use fetch::{
use std::fmt; use std::fmt;
pub enum AudioPacket { pub enum AudioPacket {
Samples(Vec<i16>), Samples(Vec<f32>),
OggData(Vec<u8>), OggData(Vec<u8>),
} }
impl AudioPacket { impl AudioPacket {
pub fn samples(&self) -> &[i16] { pub fn samples(&self) -> &[f32] {
match self { match self {
AudioPacket::Samples(s) => s, AudioPacket::Samples(s) => s,
AudioPacket::OggData(_) => panic!("can't return OggData on samples"), AudioPacket::OggData(_) => panic!("can't return OggData on samples"),

View file

@ -39,7 +39,18 @@ where
fn next_packet(&mut self) -> Result<Option<AudioPacket>, AudioError> { fn next_packet(&mut self) -> Result<Option<AudioPacket>, AudioError> {
loop { loop {
match self.0.packets().next() { 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), None => return Ok(None),
Some(Err(vorbis::VorbisError::Hole)) => (), Some(Err(vorbis::VorbisError::Hole)) => (),

View file

@ -5,7 +5,7 @@ use librespot::core::authentication::Credentials;
use librespot::core::config::SessionConfig; use librespot::core::config::SessionConfig;
use librespot::core::session::Session; use librespot::core::session::Session;
use librespot::core::spotify_id::SpotifyId; 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::audio_backend;
use librespot::playback::player::Player; use librespot::playback::player::Player;
@ -16,6 +16,7 @@ fn main() {
let session_config = SessionConfig::default(); let session_config = SessionConfig::default();
let player_config = PlayerConfig::default(); let player_config = PlayerConfig::default();
let audio_format = AudioFormat::default();
let args: Vec<_> = env::args().collect(); let args: Vec<_> = env::args().collect();
if args.len() != 4 { if args.len() != 4 {
@ -35,7 +36,7 @@ fn main() {
.unwrap(); .unwrap();
let (mut player, _) = Player::new(player_config, session.clone(), None, move || { let (mut player, _) = Player::new(player_config, session.clone(), None, move || {
(backend)(None) (backend)(None, audio_format)
}); });
player.load(track, true, 0); player.load(track, true, 0);

View file

@ -23,19 +23,19 @@ log = "0.4"
byteorder = "1.3" byteorder = "1.3"
shell-words = "1.0.0" shell-words = "1.0.0"
alsa = { version = "0.4", optional = true } alsa = { version = "0.5", optional = true }
portaudio-rs = { version = "0.3", optional = true } portaudio-rs = { version = "0.3", optional = true }
libpulse-binding = { version = "2.13", optional = true, default-features = false } libpulse-binding = { version = "2", optional = true, default-features = false }
libpulse-simple-binding = { version = "2.13", optional = true, default-features = false } libpulse-simple-binding = { version = "2", optional = true, default-features = false }
jack = { version = "0.6", optional = true } jack = { version = "0.6", optional = true }
libc = { version = "0.2", optional = true } libc = { version = "0.2", optional = true }
rodio = { version = "0.13", optional = true, default-features = false } rodio = { version = "0.13", optional = true, default-features = false }
cpal = { version = "0.13", optional = true } 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 = { version = "0.16", optional = true }
gstreamer-app = { version = "0.16", optional = true } gstreamer-app = { version = "0.16", optional = true }
glib = { version = "0.10", optional = true } glib = { version = "0.10", optional = true }
zerocopy = { version = "0.3", optional = true } zerocopy = { version = "0.3" }
[features] [features]
alsa-backend = ["alsa"] alsa-backend = ["alsa"]
@ -45,4 +45,4 @@ jackaudio-backend = ["jack"]
rodiojack-backend = ["rodio", "cpal/jack"] rodiojack-backend = ["rodio", "cpal/jack"]
rodio-backend = ["rodio", "cpal"] rodio-backend = ["rodio", "cpal"]
sdl-backend = ["sdl2"] sdl-backend = ["sdl2"]
gstreamer-backend = ["gstreamer", "gstreamer-app", "glib", "zerocopy"] gstreamer-backend = ["gstreamer", "gstreamer-app", "glib"]

View file

@ -1,5 +1,7 @@
use super::{Open, Sink}; use super::{Open, Sink, SinkAsBytes};
use crate::audio::AudioPacket; 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::device_name::HintIter;
use alsa::pcm::{Access, Format, Frames, HwParams, PCM}; use alsa::pcm::{Access, Format, Frames, HwParams, PCM};
use alsa::{Direction, Error, ValueOr}; use alsa::{Direction, Error, ValueOr};
@ -8,13 +10,14 @@ use std::ffi::CString;
use std::io; use std::io;
use std::process::exit; 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; const BUFFERED_PERIODS: Frames = 4;
pub struct AlsaSink { pub struct AlsaSink {
pcm: Option<PCM>, pcm: Option<PCM>,
format: AudioFormat,
device: String, device: String,
buffer: Vec<i16>, buffer: Vec<u8>,
} }
fn list_outputs() { fn list_outputs() {
@ -34,23 +37,27 @@ fn list_outputs() {
} }
} }
fn open_device(dev_name: &str) -> Result<(PCM, Frames), Box<Error>> { fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box<Error>> {
let pcm = PCM::new(dev_name, Direction::Playback, false)?; 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 // http://www.linuxjournal.com/article/6735?page=0,1#N0x19ab2890.0x19ba78d8
// latency = period_size * periods / (rate * bytes_per_frame) // latency = period_size * periods / (rate * bytes_per_frame)
// For 16 Bit stereo data, one frame has a length of four bytes. // For stereo samples encoded as 32-bit float, one frame has a length of eight bytes.
// 500ms = buffer_size / (44100 * 4) let mut period_size = ((SAMPLES_PER_SECOND * format.size() as u32) as f32
// buffer_size_bytes = 0.5 * 44100 / 4 * (BUFFERED_LATENCY / BUFFERED_PERIODS as f32)) as Frames;
// buffer_size_frames = 0.5 * 44100 = 22050
{ {
// Set hardware parameters: 44100 Hz / Stereo / 16 bit
let hwp = HwParams::any(&pcm)?; let hwp = HwParams::any(&pcm)?;
hwp.set_access(Access::RWInterleaved)?; hwp.set_access(Access::RWInterleaved)?;
hwp.set_format(Format::s16())?; hwp.set_format(alsa_format)?;
hwp.set_rate(44100, ValueOr::Nearest)?; hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest)?;
hwp.set_channels(2)?; hwp.set_channels(NUM_CHANNELS as u32)?;
period_size = hwp.set_period_size_near(period_size, ValueOr::Greater)?; period_size = hwp.set_period_size_near(period_size, ValueOr::Greater)?;
hwp.set_buffer_size_near(period_size * BUFFERED_PERIODS)?; hwp.set_buffer_size_near(period_size * BUFFERED_PERIODS)?;
pcm.hw_params(&hwp)?; pcm.hw_params(&hwp)?;
@ -64,12 +71,12 @@ fn open_device(dev_name: &str) -> Result<(PCM, Frames), Box<Error>> {
} }
impl Open for AlsaSink { impl Open for AlsaSink {
fn open(device: Option<String>) -> AlsaSink { fn open(device: Option<String>, format: AudioFormat) -> Self {
info!("Using alsa sink"); info!("Using Alsa sink with format: {:?}", format);
let name = match device.as_ref().map(AsRef::as_ref) { let name = match device.as_ref().map(AsRef::as_ref) {
Some("?") => { Some("?") => {
println!("Listing available alsa outputs"); println!("Listing available Alsa outputs:");
list_outputs(); list_outputs();
exit(0) exit(0)
} }
@ -78,8 +85,9 @@ impl Open for AlsaSink {
} }
.to_string(); .to_string();
AlsaSink { Self {
pcm: None, pcm: None,
format: format,
device: name, device: name,
buffer: vec![], buffer: vec![],
} }
@ -89,12 +97,14 @@ impl Open for AlsaSink {
impl Sink for AlsaSink { impl Sink for AlsaSink {
fn start(&mut self) -> io::Result<()> { fn start(&mut self) -> io::Result<()> {
if self.pcm.is_none() { if self.pcm.is_none() {
let pcm = open_device(&self.device); let pcm = open_device(&self.device, self.format);
match pcm { match pcm {
Ok((p, period_size)) => { Ok((p, period_size)) => {
self.pcm = Some(p); self.pcm = Some(p);
// Create a buffer for all samples for a full period // 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) => { Err(e) => {
error!("Alsa error PCM open {}", e); error!("Alsa error PCM open {}", e);
@ -111,23 +121,22 @@ impl Sink for AlsaSink {
fn stop(&mut self) -> io::Result<()> { fn stop(&mut self) -> io::Result<()> {
{ {
let pcm = self.pcm.as_mut().unwrap();
// Write any leftover data in the period buffer // Write any leftover data in the period buffer
// before draining the actual buffer // before draining the actual buffer
let io = pcm.io_i16().unwrap(); self.write_bytes(&[]).expect("could not flush buffer");
match io.writei(&self.buffer[..]) { let pcm = self.pcm.as_mut().unwrap();
Ok(_) => (),
Err(err) => pcm.try_recover(err, false).unwrap(),
}
pcm.drain().unwrap(); pcm.drain().unwrap();
} }
self.pcm = None; self.pcm = None;
Ok(()) 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 mut processed_data = 0;
let data = packet.samples();
while processed_data < data.len() { while processed_data < data.len() {
let data_to_buffer = min( let data_to_buffer = min(
self.buffer.capacity() - self.buffer.len(), 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]); .extend_from_slice(&data[processed_data..processed_data + data_to_buffer]);
processed_data += data_to_buffer; processed_data += data_to_buffer;
if self.buffer.len() == self.buffer.capacity() { if self.buffer.len() == self.buffer.capacity() {
let pcm = self.pcm.as_mut().unwrap(); self.write_buf().expect("could not append to buffer");
let io = pcm.io_i16().unwrap();
match io.writei(&self.buffer) {
Ok(_) => (),
Err(err) => pcm.try_recover(err, false).unwrap(),
}
self.buffer.clear(); self.buffer.clear();
} }
} }
@ -150,3 +154,16 @@ impl Sink for AlsaSink {
Ok(()) 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(())
}
}

View file

@ -1,21 +1,38 @@
use super::{Open, Sink}; use super::{Open, Sink, SinkAsBytes};
use crate::audio::AudioPacket; use crate::audio::AudioPacket;
use crate::config::AudioFormat;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
use gst::prelude::*; use gst::prelude::*;
use gst::*; use gst::*;
use std::sync::mpsc::{sync_channel, SyncSender}; use std::sync::mpsc::{sync_channel, SyncSender};
use std::{io, thread}; use std::{io, thread};
use zerocopy::*; use zerocopy::AsBytes;
#[allow(dead_code)] #[allow(dead_code)]
pub struct GstreamerSink { pub struct GstreamerSink {
tx: SyncSender<Vec<u8>>, tx: SyncSender<Vec<u8>>,
pipeline: gst::Pipeline, pipeline: gst::Pipeline,
format: AudioFormat,
} }
impl Open for GstreamerSink { impl Open for GstreamerSink {
fn open(device: Option<String>) -> GstreamerSink { fn open(device: Option<String>, format: AudioFormat) -> Self {
gst::init().expect("Failed to init gstreamer!"); info!("Using GStreamer sink with format: {:?}", format);
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 "#; 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_rest = r#" ! audioconvert ! autoaudiosink"#;
let pipeline_str: String = match device { let pipeline_str: String = match device {
Some(x) => format!("{}{}", pipeline_str_preamble, x), 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 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 let pipeline = pipelinee
.dynamic_cast::<gst::Pipeline>() .dynamic_cast::<gst::Pipeline>()
.expect("Couldn't cast pipeline element at runtime!"); .expect("couldn't cast pipeline element at runtime!");
let bus = pipeline.get_bus().expect("Couldn't get bus from pipeline"); let bus = pipeline.get_bus().expect("couldn't get bus from pipeline");
let mainloop = glib::MainLoop::new(None, false); let mainloop = glib::MainLoop::new(None, false);
let appsrce: gst::Element = pipeline let appsrce: gst::Element = pipeline
.get_by_name("appsrc0") .get_by_name("appsrc0")
.expect("Couldn't get appsrc from pipeline"); .expect("couldn't get appsrc from pipeline");
let appsrc: gst_app::AppSrc = appsrce let appsrc: gst_app::AppSrc = appsrce
.dynamic_cast::<gst_app::AppSrc>() .dynamic_cast::<gst_app::AppSrc>()
.expect("Couldn't cast AppSrc element at runtime!"); .expect("couldn't cast AppSrc element at runtime!");
let bufferpool = gst::BufferPool::new(); 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(); 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 bufferpool
.set_config(conf) .set_config(conf)
.expect("Couldn't configure the buffer pool"); .expect("couldn't configure the buffer pool");
bufferpool bufferpool
.set_active(true) .set_active(true)
.expect("Couldn't activate buffer pool"); .expect("couldn't activate buffer pool");
let (tx, rx) = sync_channel::<Vec<u8>>(128); let (tx, rx) = sync_channel::<Vec<u8>>(64 * sample_size);
thread::spawn(move || { thread::spawn(move || {
for data in rx { for data in rx {
let buffer = bufferpool.acquire_buffer(None); let buffer = bufferpool.acquire_buffer(None);
@ -57,7 +74,7 @@ impl Open for GstreamerSink {
mutbuf.set_size(data.len()); mutbuf.set_size(data.len());
mutbuf mutbuf
.copy_from_slice(0, data.as_bytes()) .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); let _eat = appsrc.push_buffer(okbuffer);
} }
} }
@ -83,33 +100,32 @@ impl Open for GstreamerSink {
glib::Continue(true) glib::Continue(true)
}) })
.expect("Failed to add bus watch"); .expect("failed to add bus watch");
thread_mainloop.run(); thread_mainloop.run();
}); });
pipeline pipeline
.set_state(gst::State::Playing) .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, tx: tx,
pipeline: pipeline, pipeline: pipeline,
format: format,
} }
} }
} }
impl Sink for GstreamerSink { impl Sink for GstreamerSink {
fn start(&mut self) -> io::Result<()> { start_stop_noop!();
Ok(()) sink_as_bytes!();
} }
fn stop(&mut self) -> io::Result<()> {
Ok(()) impl SinkAsBytes for GstreamerSink {
} fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> {
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
// Copy expensively (in to_vec()) to avoid thread synchronization // Copy expensively (in to_vec()) to avoid thread synchronization
let deighta: &[u8] = packet.samples().as_bytes();
self.tx self.tx
.send(deighta.to_vec()) .send(data.to_vec())
.expect("tx send failed in write function"); .expect("tx send failed in write function");
Ok(()) Ok(())
} }

View file

@ -1,5 +1,7 @@
use super::{Open, Sink}; use super::{Open, Sink};
use crate::audio::AudioPacket; use crate::audio::AudioPacket;
use crate::config::AudioFormat;
use crate::player::NUM_CHANNELS;
use jack::{ use jack::{
AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope, AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope,
}; };
@ -7,20 +9,18 @@ use std::io;
use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
pub struct JackSink { pub struct JackSink {
send: SyncSender<i16>, send: SyncSender<f32>,
// We have to keep hold of this object, or the Sink can't play...
#[allow(dead_code)]
active_client: AsyncClient<(), JackData>, active_client: AsyncClient<(), JackData>,
} }
pub struct JackData { pub struct JackData {
rec: Receiver<i16>, rec: Receiver<f32>,
port_l: Port<AudioOut>, port_l: Port<AudioOut>,
port_r: Port<AudioOut>, port_r: Port<AudioOut>,
} }
fn pcm_to_f32(sample: i16) -> f32 {
sample as f32 / 32768.0
}
impl ProcessHandler for JackData { impl ProcessHandler for JackData {
fn process(&mut self, _: &Client, ps: &ProcessScope) -> Control { fn process(&mut self, _: &Client, ps: &ProcessScope) -> Control {
// get output port buffers // get output port buffers
@ -33,16 +33,19 @@ impl ProcessHandler for JackData {
let buf_size = buf_r.len(); let buf_size = buf_r.len();
for i in 0..buf_size { for i in 0..buf_size {
buf_r[i] = pcm_to_f32(queue_iter.next().unwrap_or(0)); buf_r[i] = queue_iter.next().unwrap_or(0.0);
buf_l[i] = pcm_to_f32(queue_iter.next().unwrap_or(0)); buf_l[i] = queue_iter.next().unwrap_or(0.0);
} }
Control::Continue Control::Continue
} }
} }
impl Open for JackSink { impl Open for JackSink {
fn open(client_name: Option<String>) -> JackSink { fn open(client_name: Option<String>, format: AudioFormat) -> Self {
info!("Using jack sink!"); 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_name = client_name.unwrap_or("librespot".to_string());
let (client, _status) = let (client, _status) =
@ -50,7 +53,7 @@ impl Open for JackSink {
let ch_r = client.register_port("out_0", AudioOut::default()).unwrap(); let ch_r = client.register_port("out_0", AudioOut::default()).unwrap();
let ch_l = client.register_port("out_1", AudioOut::default()).unwrap(); let ch_l = client.register_port("out_1", AudioOut::default()).unwrap();
// buffer for samples from librespot (~10ms) // buffer for samples from librespot (~10ms)
let (tx, rx) = sync_channel(2 * 1024 * 4); let (tx, rx) = sync_channel::<f32>(NUM_CHANNELS as usize * 1024 * AudioFormat::F32.size());
let jack_data = JackData { let jack_data = JackData {
rec: rx, rec: rx,
port_l: ch_l, port_l: ch_l,
@ -58,7 +61,7 @@ impl Open for JackSink {
}; };
let active_client = AsyncClient::new(client, (), jack_data).unwrap(); let active_client = AsyncClient::new(client, (), jack_data).unwrap();
JackSink { Self {
send: tx, send: tx,
active_client: active_client, active_client: active_client,
} }
@ -66,19 +69,13 @@ impl Open for JackSink {
} }
impl Sink for JackSink { impl Sink for JackSink {
fn start(&mut self) -> io::Result<()> { start_stop_noop!();
Ok(())
}
fn stop(&mut self) -> io::Result<()> {
Ok(())
}
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
for s in packet.samples().iter() { for s in packet.samples().iter() {
let res = self.send.send(*s); let res = self.send.send(*s);
if res.is_err() { if res.is_err() {
error!("jackaudio: cannot write to channel"); error!("cannot write to channel");
} }
} }
Ok(()) Ok(())

View file

@ -1,8 +1,9 @@
use crate::audio::AudioPacket; use crate::audio::AudioPacket;
use crate::config::AudioFormat;
use std::io; use std::io;
pub trait Open { pub trait Open {
fn open(_: Option<String>) -> Self; fn open(_: Option<String>, format: AudioFormat) -> Self;
} }
pub trait Sink { pub trait Sink {
@ -11,8 +12,55 @@ pub trait Sink {
fn write(&mut self, packet: &AudioPacket) -> io::Result<()>; fn write(&mut self, packet: &AudioPacket) -> io::Result<()>;
} }
fn mk_sink<S: Sink + Open + 'static>(device: Option<String>) -> Box<dyn Sink> { pub trait SinkAsBytes {
Box::new(S::open(device)) fn write_bytes(&mut self, data: &[u8]) -> io::Result<()>;
}
fn mk_sink<S: Sink + Open + 'static>(device: Option<String>, format: AudioFormat) -> Box<dyn Sink> {
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")] #[cfg(feature = "alsa-backend")]
@ -68,7 +116,10 @@ use self::pipe::StdoutSink;
mod subprocess; mod subprocess;
use self::subprocess::SubprocessSink; use self::subprocess::SubprocessSink;
pub const BACKENDS: &'static [(&'static str, fn(Option<String>) -> Box<dyn Sink>)] = &[ pub const BACKENDS: &'static [(
&'static str,
fn(Option<String>, AudioFormat) -> Box<dyn Sink>,
)] = &[
#[cfg(feature = "alsa-backend")] #[cfg(feature = "alsa-backend")]
("alsa", mk_sink::<AlsaSink>), ("alsa", mk_sink::<AlsaSink>),
#[cfg(feature = "portaudio-backend")] #[cfg(feature = "portaudio-backend")]
@ -92,7 +143,7 @@ pub const BACKENDS: &'static [(&'static str, fn(Option<String>) -> Box<dyn Sink>
("subprocess", mk_sink::<SubprocessSink>), ("subprocess", mk_sink::<SubprocessSink>),
]; ];
pub fn find(name: Option<String>) -> Option<fn(Option<String>) -> Box<dyn Sink>> { pub fn find(name: Option<String>) -> Option<fn(Option<String>, AudioFormat) -> Box<dyn Sink>> {
if let Some(name) = name { if let Some(name) = name {
BACKENDS BACKENDS
.iter() .iter()

View file

@ -1,46 +1,39 @@
use super::{Open, Sink}; use super::{Open, Sink, SinkAsBytes};
use crate::audio::AudioPacket; use crate::audio::AudioPacket;
use crate::config::AudioFormat;
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io::{self, Write}; use std::io::{self, Write};
use std::mem;
use std::slice;
pub struct StdoutSink(Box<dyn Write>); pub struct StdoutSink {
output: Box<dyn Write>,
format: AudioFormat,
}
impl Open for StdoutSink { impl Open for StdoutSink {
fn open(path: Option<String>) -> StdoutSink { fn open(path: Option<String>, format: AudioFormat) -> Self {
if let Some(path) = path { info!("Using pipe sink with format: {:?}", format);
let file = OpenOptions::new().write(true).open(path).unwrap();
StdoutSink(Box::new(file)) let output: Box<dyn Write> = match path {
} else { Some(path) => Box::new(OpenOptions::new().write(true).open(path).unwrap()),
StdoutSink(Box::new(io::stdout())) _ => Box::new(io::stdout()),
};
Self {
output: output,
format: format,
} }
} }
} }
impl Sink for StdoutSink { impl Sink for StdoutSink {
fn start(&mut self) -> io::Result<()> { start_stop_noop!();
Ok(()) sink_as_bytes!();
} }
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::<i16>(),
)
},
AudioPacket::OggData(data) => data,
};
self.0.write_all(data)?;
self.0.flush()?;
impl SinkAsBytes for StdoutSink {
fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> {
self.output.write_all(data)?;
self.output.flush()?;
Ok(()) Ok(())
} }
} }

View file

@ -1,5 +1,7 @@
use super::{Open, Sink}; 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;
use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo}; use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo};
use portaudio_rs::stream::*; use portaudio_rs::stream::*;
@ -7,10 +9,20 @@ use std::io;
use std::process::exit; use std::process::exit;
use std::time::Duration; use std::time::Duration;
pub struct PortAudioSink<'a>( pub enum PortAudioSink<'a> {
Option<portaudio_rs::stream::Stream<'a, i16, i16>>, F32(
StreamParameters<i16>, Option<portaudio_rs::stream::Stream<'a, f32, f32>>,
); StreamParameters<f32>,
),
S32(
Option<portaudio_rs::stream::Stream<'a, i32, i32>>,
StreamParameters<i32>,
),
S16(
Option<portaudio_rs::stream::Stream<'a, i16, i16>>,
StreamParameters<i16>,
),
}
fn output_devices() -> Box<dyn Iterator<Item = (DeviceIndex, DeviceInfo)>> { fn output_devices() -> Box<dyn Iterator<Item = (DeviceIndex, DeviceInfo)>> {
let count = portaudio_rs::device::get_count().unwrap(); let count = portaudio_rs::device::get_count().unwrap();
@ -40,8 +52,11 @@ fn find_output(device: &str) -> Option<DeviceIndex> {
} }
impl<'a> Open for PortAudioSink<'a> { impl<'a> Open for PortAudioSink<'a> {
fn open(device: Option<String>) -> PortAudioSink<'a> { fn open(device: Option<String>, format: AudioFormat) -> PortAudioSink<'a> {
debug!("Using PortAudio sink"); 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(); portaudio_rs::initialize().unwrap();
@ -53,7 +68,7 @@ impl<'a> Open for PortAudioSink<'a> {
Some(device) => find_output(device), Some(device) => find_output(device),
None => get_default_output_index(), 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 info = portaudio_rs::device::get_info(device_idx);
let latency = match info { let latency = match info {
@ -61,46 +76,99 @@ impl<'a> Open for PortAudioSink<'a> {
None => Duration::new(0, 0), None => Duration::new(0, 0),
}; };
let params = StreamParameters { macro_rules! open_sink {
device: device_idx, ($sink: expr, $type: ty) => {{
channel_count: 2, let params = StreamParameters {
suggested_latency: latency, device: device_idx,
data: 0i16, channel_count: NUM_CHANNELS as u32,
}; suggested_latency: latency,
data: 0.0 as $type,
PortAudioSink(None, params) };
$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> { impl<'a> Sink for PortAudioSink<'a> {
fn start(&mut self) -> io::Result<()> { fn start(&mut self) -> io::Result<()> {
if self.0.is_none() { macro_rules! start_sink {
self.0 = Some( (ref mut $stream: ident, ref $parameters: ident) => {{
Stream::open( if $stream.is_none() {
None, *$stream = Some(
Some(self.1), Stream::open(
44100.0, None,
FRAMES_PER_BUFFER_UNSPECIFIED, Some(*$parameters),
StreamFlags::empty(), SAMPLE_RATE as f64,
None, FRAMES_PER_BUFFER_UNSPECIFIED,
) StreamFlags::empty(),
.unwrap(), 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(()) Ok(())
} }
fn stop(&mut self) -> io::Result<()> { fn stop(&mut self) -> io::Result<()> {
self.0.as_mut().unwrap().stop().unwrap(); macro_rules! stop_sink {
self.0 = None; (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(()) Ok(())
} }
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { 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(_) => (), Ok(_) => (),
Err(portaudio_rs::PaError::OutputUnderflowed) => error!("PortAudio write underflow"), Err(portaudio_rs::PaError::OutputUnderflowed) => error!("PortAudio write underflow"),
Err(e) => panic!("PA Error {}", e), Err(e) => panic!("PortAudio error {}", e),
}; };
Ok(()) Ok(())

View file

@ -1,5 +1,7 @@
use super::{Open, Sink}; use super::{Open, Sink, SinkAsBytes};
use crate::audio::AudioPacket; 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_binding::{self as pulse, stream::Direction};
use libpulse_simple_binding::Simple; use libpulse_simple_binding::Simple;
use std::io; use std::io;
@ -11,23 +13,34 @@ pub struct PulseAudioSink {
s: Option<Simple>, s: Option<Simple>,
ss: pulse::sample::Spec, ss: pulse::sample::Spec,
device: Option<String>, device: Option<String>,
format: AudioFormat,
} }
impl Open for PulseAudioSink { impl Open for PulseAudioSink {
fn open(device: Option<String>) -> PulseAudioSink { fn open(device: Option<String>, format: AudioFormat) -> Self {
debug!("Using PulseAudio sink"); 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 { let ss = pulse::sample::Spec {
format: pulse::sample::Format::S16le, format: pulse_format,
channels: 2, // stereo channels: NUM_CHANNELS,
rate: 44100, rate: SAMPLE_RATE,
}; };
debug_assert!(ss.is_valid()); debug_assert!(ss.is_valid());
PulseAudioSink { Self {
s: None, s: None,
ss: ss, ss: ss,
device: device, device: device,
format: format,
} }
} }
} }
@ -66,19 +79,13 @@ impl Sink for PulseAudioSink {
Ok(()) Ok(())
} }
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { sink_as_bytes!();
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,
)
};
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(()), Ok(_) => Ok(()),
Err(e) => Err(io::Error::new( Err(e) => Err(io::Error::new(
io::ErrorKind::BrokenPipe, io::ErrorKind::BrokenPipe,
@ -88,7 +95,7 @@ impl Sink for PulseAudioSink {
} else { } else {
Err(io::Error::new( Err(io::Error::new(
io::ErrorKind::NotConnected, io::ErrorKind::NotConnected,
"Not connected to pulseaudio", "Not connected to PulseAudio",
)) ))
} }
} }

View file

@ -1,34 +1,96 @@
use super::{Open, Sink}; use super::{Open, Sink};
extern crate cpal; extern crate cpal;
extern crate rodio; 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 cpal::traits::{DeviceTrait, HostTrait};
use std::process::exit; use std::process::exit;
use std::{io, thread, time}; use std::{io, thread, time};
pub struct RodioSink { // most code is shared between RodioSink and JackRodioSink
rodio_sink: rodio::Sink, macro_rules! rodio_sink {
// We have to keep hold of this object, or the Sink can't play... ($name: ident) => {
#[allow(dead_code)] pub struct $name {
stream: rodio::OutputStream, 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<String>, 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( #[cfg(all(
feature = "rodiojack-backend", feature = "rodiojack-backend",
any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd") any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd")
))] ))]
pub struct JackRodioSink { rodio_sink!(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,
}
fn list_formats(ref device: &rodio::Device) { fn list_formats(ref device: &rodio::Device) {
let default_fmt = match device.default_output_config() { let default_fmt = match device.default_output_config() {
Ok(fmt) => cpal::SupportedStreamConfig::from(fmt), Ok(fmt) => cpal::SupportedStreamConfig::from(fmt),
Err(e) => { Err(e) => {
warn!("Error getting default rodio::Sink config: {}", e); warn!("Error getting default Rodio output config: {}", e);
return; return;
} }
}; };
@ -38,13 +100,13 @@ fn list_formats(ref device: &rodio::Device) {
let mut output_configs = match device.supported_output_configs() { let mut output_configs = match device.supported_output_configs() {
Ok(f) => f.peekable(), Ok(f) => f.peekable(),
Err(e) => { Err(e) => {
warn!("Error getting supported rodio::Sink configs: {}", e); warn!("Error getting supported Rodio output configs: {}", e);
return; return;
} }
}; };
if output_configs.peek().is_some() { if output_configs.peek().is_some() {
debug!(" Available configs:"); debug!(" Available output configs:");
for format in output_configs { for format in output_configs {
debug!(" {:?}", format); debug!(" {:?}", format);
} }
@ -54,13 +116,13 @@ fn list_formats(ref device: &rodio::Device) {
fn list_outputs(ref host: &cpal::Host) { fn list_outputs(ref host: &cpal::Host) {
let default_device = get_default_device(host); let default_device = get_default_device(host);
let default_device_name = default_device.name().expect("cannot get output name"); 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); list_formats(&default_device);
println!("Other Available Audio Devices:"); println!("Other available audio devices:");
let found_devices = host.output_devices().expect(&format!( 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() host.id()
)); ));
for device in found_devices { for device in found_devices {
@ -86,7 +148,7 @@ fn match_device(ref host: &cpal::Host, device: Option<String>) -> rodio::Device
} }
let found_devices = host.output_devices().expect(&format!( 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() host.id()
)); ));
for d in found_devices { for d in found_devices {
@ -102,22 +164,14 @@ fn match_device(ref host: &cpal::Host, device: Option<String>) -> rodio::Device
} }
impl Open for RodioSink { impl Open for RodioSink {
fn open(device: Option<String>) -> RodioSink { fn open(device: Option<String>, format: AudioFormat) -> RodioSink {
let host = cpal::default_host(); let host = cpal::default_host();
debug!("Using rodio sink with cpal host: {:?}", host.id()); info!(
"Using Rodio sink with format {:?} and cpal host: {:?}",
let rodio_device = match_device(&host, device); format,
debug!("Using cpal device"); host.id()
let stream = rodio::OutputStream::try_from_device(&rodio_device) );
.expect("Couldn't open output stream."); Self::open_sink(&host, device, format)
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,
}
} }
} }
@ -126,89 +180,18 @@ impl Open for RodioSink {
any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd") any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd")
))] ))]
impl Open for JackRodioSink { impl Open for JackRodioSink {
fn open(device: Option<String>) -> JackRodioSink { fn open(device: Option<String>, format: AudioFormat) -> JackRodioSink {
let host = cpal::host_from_id( let host = cpal::host_from_id(
cpal::available_hosts() cpal::available_hosts()
.into_iter() .into_iter()
.find(|id| *id == cpal::HostId::Jack) .find(|id| *id == cpal::HostId::Jack)
.expect("Jack Host not found"), .expect("JACK host not found"),
) )
.expect("Jack Host not found"); .expect("JACK host not found");
debug!("Using jack rodio sink with cpal Jack host"); info!(
"Using JACK Rodio sink with format {:?} and cpal JACK host",
let rodio_device = match_device(&host, device); format
debug!("Using cpal device"); );
let stream = rodio::OutputStream::try_from_device(&rodio_device) Self::open_sink(&host, device, format)
.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(())
} }
} }

View file

@ -1,57 +1,112 @@
use super::{Open, Sink}; 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 sdl2::audio::{AudioQueue, AudioSpecDesired};
use std::{io, thread, time}; use std::{io, thread, time};
type Channel = i16; pub enum SdlSink {
F32(AudioQueue<f32>),
pub struct SdlSink { S32(AudioQueue<i32>),
queue: AudioQueue<Channel>, S16(AudioQueue<i16>),
} }
impl Open for SdlSink { impl Open for SdlSink {
fn open(device: Option<String>) -> SdlSink { fn open(device: Option<String>, format: AudioFormat) -> Self {
debug!("Using SDL sink"); info!("Using SDL sink with format: {:?}", format);
if device.is_some() { 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 ctx = sdl2::init().expect("could not initialize SDL");
let audio = ctx.audio().expect("Could not init SDL audio subsystem"); let audio = ctx
.audio()
.expect("could not initialize SDL audio subsystem");
let desired_spec = AudioSpecDesired { let desired_spec = AudioSpecDesired {
freq: Some(44_100), freq: Some(SAMPLE_RATE as i32),
channels: Some(2), channels: Some(NUM_CHANNELS),
samples: None, 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 { impl Sink for SdlSink {
fn start(&mut self) -> io::Result<()> { fn start(&mut self) -> io::Result<()> {
self.queue.clear(); macro_rules! start_sink {
self.queue.resume(); ($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(()) Ok(())
} }
fn stop(&mut self) -> io::Result<()> { fn stop(&mut self) -> io::Result<()> {
self.queue.pause(); macro_rules! stop_sink {
self.queue.clear(); ($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(()) Ok(())
} }
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
while self.queue.size() > (2 * 2 * 44_100) { macro_rules! drain_sink {
// sleep and wait for sdl thread to drain the queue a bit ($queue: expr, $size: expr) => {{
thread::sleep(time::Duration::from_millis(10)); // 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(()) Ok(())
} }
} }

View file

@ -1,22 +1,25 @@
use super::{Open, Sink}; use super::{Open, Sink, SinkAsBytes};
use crate::audio::AudioPacket; use crate::audio::AudioPacket;
use crate::config::AudioFormat;
use shell_words::split; use shell_words::split;
use std::io::{self, Write}; use std::io::{self, Write};
use std::mem;
use std::process::{Child, Command, Stdio}; use std::process::{Child, Command, Stdio};
use std::slice;
pub struct SubprocessSink { pub struct SubprocessSink {
shell_command: String, shell_command: String,
child: Option<Child>, child: Option<Child>,
format: AudioFormat,
} }
impl Open for SubprocessSink { impl Open for SubprocessSink {
fn open(shell_command: Option<String>) -> SubprocessSink { fn open(shell_command: Option<String>, format: AudioFormat) -> Self {
info!("Using subprocess sink with format: {:?}", format);
if let Some(shell_command) = shell_command { if let Some(shell_command) = shell_command {
SubprocessSink { SubprocessSink {
shell_command: shell_command, shell_command: shell_command,
child: None, child: None,
format: format,
} }
} else { } else {
panic!("subprocess sink requires specifying a shell command"); panic!("subprocess sink requires specifying a shell command");
@ -44,16 +47,15 @@ impl Sink for SubprocessSink {
Ok(()) Ok(())
} }
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { sink_as_bytes!();
let data: &[u8] = unsafe { }
slice::from_raw_parts(
packet.samples().as_ptr() as *const u8, impl SinkAsBytes for SubprocessSink {
packet.samples().len() * mem::size_of::<i16>(), fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> {
)
};
if let Some(child) = &mut self.child { if let Some(child) = &mut self.child {
let child_stdin = child.stdin.as_mut().unwrap(); let child_stdin = child.stdin.as_mut().unwrap();
child_stdin.write_all(data)?; child_stdin.write_all(data)?;
child_stdin.flush()?;
} }
Ok(()) Ok(())
} }

View file

@ -1,3 +1,6 @@
use crate::audio::i24;
use std::convert::TryFrom;
use std::mem;
use std::str::FromStr; use std::str::FromStr;
#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)]
@ -11,17 +14,58 @@ impl FromStr for Bitrate {
type Err = (); type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
match s { match s {
"96" => Ok(Bitrate::Bitrate96), "96" => Ok(Self::Bitrate96),
"160" => Ok(Bitrate::Bitrate160), "160" => Ok(Self::Bitrate160),
"320" => Ok(Bitrate::Bitrate320), "320" => Ok(Self::Bitrate320),
_ => Err(()), _ => Err(()),
} }
} }
} }
impl Default for Bitrate { impl Default for Bitrate {
fn default() -> Bitrate { fn default() -> Self {
Bitrate::Bitrate160 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<Self, Self::Error> {
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::<i24>(),
Self::S16 => mem::size_of::<i16>(),
_ => mem::size_of::<i32>(), // S32 and S24 are both stored in i32
}
} }
} }
@ -35,16 +79,39 @@ impl FromStr for NormalisationType {
type Err = (); type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
match s { match s {
"album" => Ok(NormalisationType::Album), "album" => Ok(Self::Album),
"track" => Ok(NormalisationType::Track), "track" => Ok(Self::Track),
_ => Err(()), _ => Err(()),
} }
} }
} }
impl Default for NormalisationType { impl Default for NormalisationType {
fn default() -> NormalisationType { fn default() -> Self {
NormalisationType::Album Self::Album
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum NormalisationMethod {
Basic,
Dynamic,
}
impl FromStr for NormalisationMethod {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
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 bitrate: Bitrate,
pub normalisation: bool, pub normalisation: bool,
pub normalisation_type: NormalisationType, pub normalisation_type: NormalisationType,
pub normalisation_method: NormalisationMethod,
pub normalisation_pregain: f32, pub normalisation_pregain: f32,
pub normalisation_threshold: f32,
pub normalisation_attack: f32,
pub normalisation_release: f32,
pub normalisation_knee: f32,
pub gapless: bool, pub gapless: bool,
pub passthrough: bool, pub passthrough: bool,
} }
@ -64,7 +136,12 @@ impl Default for PlayerConfig {
bitrate: Bitrate::default(), bitrate: Bitrate::default(),
normalisation: false, normalisation: false,
normalisation_type: NormalisationType::default(), normalisation_type: NormalisationType::default(),
normalisation_method: NormalisationMethod::default(),
normalisation_pregain: 0.0, normalisation_pregain: 0.0,
normalisation_threshold: -1.0,
normalisation_attack: 0.005,
normalisation_release: 0.1,
normalisation_knee: 1.0,
gapless: true, gapless: true,
passthrough: false, passthrough: false,
} }

View file

@ -12,7 +12,7 @@ pub trait Mixer: Send {
} }
pub trait AudioFilter { pub trait AudioFilter {
fn modify_stream(&self, data: &mut [i16]); fn modify_stream(&self, data: &mut [f32]);
} }
#[cfg(feature = "alsa-backend")] #[cfg(feature = "alsa-backend")]

View file

@ -35,11 +35,12 @@ struct SoftVolumeApplier {
} }
impl AudioFilter for 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; let volume = self.volume.load(Ordering::Relaxed) as u16;
if volume != 0xFFFF { if volume != 0xFFFF {
let volume_factor = volume as f64 / 0xFFFF as f64;
for x in data.iter_mut() { for x in data.iter_mut() {
*x = (*x as i32 * volume as i32 / 0xFFFF) as i16; *x = (*x as f64 * volume_factor) as f32;
} }
} }
} }

View file

@ -9,7 +9,7 @@ use std::mem;
use std::thread; use std::thread;
use std::time::{Duration, Instant}; 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::session::Session;
use librespot_core::spotify_id::SpotifyId; use librespot_core::spotify_id::SpotifyId;
@ -25,7 +25,12 @@ use crate::audio_backend::Sink;
use crate::metadata::{AudioItem, FileFormat}; use crate::metadata::{AudioItem, FileFormat};
use crate::mixer::AudioFilter; 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 PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000;
const DB_VOLTAGE_RATIO: f32 = 20.0;
pub struct Player { pub struct Player {
commands: Option<futures::sync::mpsc::UnboundedSender<PlayerCommand>>, commands: Option<futures::sync::mpsc::UnboundedSender<PlayerCommand>>,
@ -54,6 +59,13 @@ struct PlayerInternal {
sink_event_callback: Option<SinkEventCallback>, sink_event_callback: Option<SinkEventCallback>,
audio_filter: Option<Box<dyn AudioFilter + Send>>, audio_filter: Option<Box<dyn AudioFilter + Send>>,
event_senders: Vec<futures::sync::mpsc::UnboundedSender<PlayerEvent>>, event_senders: Vec<futures::sync::mpsc::UnboundedSender<PlayerEvent>>,
limiter_active: bool,
limiter_attack_counter: u32,
limiter_release_counter: u32,
limiter_peak_sample: f32,
limiter_factor: f32,
limiter_strength: f32,
} }
enum PlayerCommand { enum PlayerCommand {
@ -185,7 +197,7 @@ impl PlayerEvent {
pub type PlayerEventChannel = futures::sync::mpsc::UnboundedReceiver<PlayerEvent>; pub type PlayerEventChannel = futures::sync::mpsc::UnboundedReceiver<PlayerEvent>;
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
struct NormalisationData { pub struct NormalisationData {
track_gain_db: f32, track_gain_db: f32,
track_peak: f32, track_peak: f32,
album_gain_db: f32, album_gain_db: f32,
@ -193,6 +205,14 @@ struct NormalisationData {
} }
impl 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<T: Read + Seek>(mut file: T) -> Result<NormalisationData> { fn parse_from_file<T: Read + Seek>(mut file: T) -> Result<NormalisationData> {
const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144; const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144;
file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET)) 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::Album => [data.album_gain_db, data.album_peak],
NormalisationType::Track => [data.track_gain_db, data.track_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 { let normalisation_power = gain_db + config.normalisation_pregain;
warn!("Reducing normalisation factor to prevent clipping. Please add negative pregain to avoid."); let mut normalisation_factor = Self::db_to_ratio(normalisation_power);
normalisation_factor = 1.0 / gain_peak;
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 Data: {:?}", data);
debug!("Normalisation Type: {:?}", config.normalisation_type); 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 normalisation_factor
} }
@ -262,6 +306,13 @@ impl Player {
sink_event_callback: None, sink_event_callback: None,
audio_filter: audio_filter, audio_filter: audio_filter,
event_senders: [event_sender].to_vec(), 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. // 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 !self.config.passthrough {
if let Some(ref packet) = packet { if let Some(ref packet) = packet {
*stream_position_pcm = *stream_position_pcm = *stream_position_pcm
*stream_position_pcm + (packet.samples().len() / 2) as u64; + (packet.samples().len() / NUM_CHANNELS as usize) as u64;
let stream_position_millis = let stream_position_millis =
Self::position_pcm_to_ms(*stream_position_pcm); Self::position_pcm_to_ms(*stream_position_pcm);
@ -1113,9 +1164,116 @@ impl PlayerInternal {
editor.modify_stream(data) editor.modify_stream(data)
} }
if self.config.normalisation && normalisation_factor != 1.0 { if self.config.normalisation
for x in data.iter_mut() { && (normalisation_factor != 1.0
*x = (*x as f32 * normalisation_factor) as i16; || 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( fn start_playback(
&mut self, &mut self,
track_id: SpotifyId, track_id: SpotifyId,

View file

@ -2,6 +2,7 @@ use futures::sync::mpsc::UnboundedReceiver;
use futures::{Async, Future, Poll, Stream}; use futures::{Async, Future, Poll, Stream};
use log::{error, info, trace, warn}; use log::{error, info, trace, warn};
use sha1::{Digest, Sha1}; use sha1::{Digest, Sha1};
use std::convert::TryFrom;
use std::env; use std::env;
use std::io::{stderr, Write}; use std::io::{stderr, Write};
use std::mem; use std::mem;
@ -22,13 +23,17 @@ use librespot::core::version;
use librespot::connect::discovery::{discovery, DiscoveryStream}; use librespot::connect::discovery::{discovery, DiscoveryStream};
use librespot::connect::spirc::{Spirc, SpircTask}; use librespot::connect::spirc::{Spirc, SpircTask};
use librespot::playback::audio_backend::{self, Sink, BACKENDS}; 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::mixer::{self, Mixer, MixerConfig};
use librespot::playback::player::{Player, PlayerEvent}; use librespot::playback::player::{NormalisationData, Player, PlayerEvent};
mod player_event_handler; mod player_event_handler;
use crate::player_event_handler::{emit_sink_event, run_program_on_events}; use crate::player_event_handler::{emit_sink_event, run_program_on_events};
const MILLIS: f32 = 1000.0;
fn device_id(name: &str) -> String { fn device_id(name: &str) -> String {
hex::encode(Sha1::digest(name.as_bytes())) hex::encode(Sha1::digest(name.as_bytes()))
} }
@ -83,7 +88,8 @@ fn print_version() {
#[derive(Clone)] #[derive(Clone)]
struct Setup { struct Setup {
backend: fn(Option<String>) -> Box<dyn Sink>, format: AudioFormat,
backend: fn(Option<String>, AudioFormat) -> Box<dyn Sink>,
device: Option<String>, device: Option<String>,
mixer: fn(Option<MixerConfig>) -> Box<dyn Mixer>, mixer: fn(Option<MixerConfig>) -> Box<dyn Mixer>,
@ -147,6 +153,12 @@ fn setup(args: &[String]) -> Setup {
"Audio device to use. Use '?' to list options if using portaudio or alsa", "Audio device to use. Use '?' to list options if using portaudio or alsa",
"DEVICE", "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("", "mixer", "Mixer to use (alsa or softvol)", "MIXER")
.optopt( .optopt(
"m", "m",
@ -188,6 +200,12 @@ fn setup(args: &[String]) -> Setup {
"enable-volume-normalisation", "enable-volume-normalisation",
"Play all tracks at the same volume", "Play all tracks at the same volume",
) )
.optopt(
"",
"normalisation-method",
"Specify the normalisation method to use - [basic, dynamic]. Default is dynamic.",
"NORMALISATION_METHOD",
)
.optopt( .optopt(
"", "",
"normalisation-gain-type", "normalisation-gain-type",
@ -200,6 +218,30 @@ fn setup(args: &[String]) -> Setup {
"Pregain (dB) applied by volume normalisation", "Pregain (dB) applied by volume normalisation",
"PREGAIN", "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( .optopt(
"", "",
"volume-ctrl", "volume-ctrl",
@ -260,9 +302,15 @@ fn setup(args: &[String]) -> Setup {
let backend = audio_backend::find(backend_name).expect("Invalid backend"); 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"); let device = matches.opt_str("device");
if device == Some("?".into()) { if device == Some("?".into()) {
backend(device); backend(device, format);
exit(0); exit(0);
} }
@ -390,15 +438,47 @@ fn setup(args: &[String]) -> Setup {
NormalisationType::from_str(gain_type).expect("Invalid normalisation type") NormalisationType::from_str(gain_type).expect("Invalid normalisation type")
}) })
.unwrap_or(NormalisationType::default()); .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 { PlayerConfig {
bitrate: bitrate, bitrate: bitrate,
gapless: !matches.opt_present("disable-gapless"), gapless: !matches.opt_present("disable-gapless"),
normalisation: matches.opt_present("enable-volume-normalisation"), normalisation: matches.opt_present("enable-volume-normalisation"),
normalisation_method: normalisation_method,
normalisation_type: gain_type, normalisation_type: gain_type,
normalisation_pregain: matches normalisation_pregain: matches
.opt_str("normalisation-pregain") .opt_str("normalisation-pregain")
.map(|pregain| pregain.parse::<f32>().expect("Invalid pregain float value")) .map(|pregain| pregain.parse::<f32>().expect("Invalid pregain float value"))
.unwrap_or(PlayerConfig::default().normalisation_pregain), .unwrap_or(PlayerConfig::default().normalisation_pregain),
normalisation_threshold: NormalisationData::db_to_ratio(
matches
.opt_str("normalisation-threshold")
.map(|threshold| {
threshold
.parse::<f32>()
.expect("Invalid threshold float value")
})
.unwrap_or(PlayerConfig::default().normalisation_threshold),
),
normalisation_attack: matches
.opt_str("normalisation-attack")
.map(|attack| attack.parse::<f32>().expect("Invalid attack float value"))
.unwrap_or(PlayerConfig::default().normalisation_attack * MILLIS)
/ MILLIS,
normalisation_release: matches
.opt_str("normalisation-release")
.map(|release| release.parse::<f32>().expect("Invalid release float value"))
.unwrap_or(PlayerConfig::default().normalisation_release * MILLIS)
/ MILLIS,
normalisation_knee: matches
.opt_str("normalisation-knee")
.map(|knee| knee.parse::<f32>().expect("Invalid knee float value"))
.unwrap_or(PlayerConfig::default().normalisation_knee),
passthrough, passthrough,
} }
}; };
@ -428,6 +508,7 @@ fn setup(args: &[String]) -> Setup {
let enable_discovery = !matches.opt_present("disable-discovery"); let enable_discovery = !matches.opt_present("disable-discovery");
Setup { Setup {
format: format,
backend: backend, backend: backend,
cache: cache, cache: cache,
session_config: session_config, session_config: session_config,
@ -449,7 +530,8 @@ struct Main {
player_config: PlayerConfig, player_config: PlayerConfig,
session_config: SessionConfig, session_config: SessionConfig,
connect_config: ConnectConfig, connect_config: ConnectConfig,
backend: fn(Option<String>) -> Box<dyn Sink>, format: AudioFormat,
backend: fn(Option<String>, AudioFormat) -> Box<dyn Sink>,
device: Option<String>, device: Option<String>,
mixer: fn(Option<MixerConfig>) -> Box<dyn Mixer>, mixer: fn(Option<MixerConfig>) -> Box<dyn Mixer>,
mixer_config: MixerConfig, mixer_config: MixerConfig,
@ -479,6 +561,7 @@ impl Main {
session_config: setup.session_config, session_config: setup.session_config,
player_config: setup.player_config, player_config: setup.player_config,
connect_config: setup.connect_config, connect_config: setup.connect_config,
format: setup.format,
backend: setup.backend, backend: setup.backend,
device: setup.device, device: setup.device,
mixer: setup.mixer, mixer: setup.mixer,
@ -558,11 +641,12 @@ impl Future for Main {
let connect_config = self.connect_config.clone(); let connect_config = self.connect_config.clone();
let audio_filter = mixer.get_audio_filter(); let audio_filter = mixer.get_audio_filter();
let format = self.format;
let backend = self.backend; let backend = self.backend;
let device = self.device.clone(); let device = self.device.clone();
let (player, event_channel) = let (player, event_channel) =
Player::new(player_config, session.clone(), audio_filter, move || { Player::new(player_config, session.clone(), audio_filter, move || {
(backend)(device) (backend)(device, format)
}); });
if self.emit_sink_events { if self.emit_sink_events {