diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c20fe1c..f06445e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,61 +4,81 @@ name: test on: push: branches: [master, dev] - paths: ['**.rs', '**.toml', '**.lock', '**.yml'] + paths: + [ + "**.rs", + "Cargo.toml", + "/Cargo.lock", + "/rustfmt.toml", + "/.github/workflows", + ] pull_request: - branches: [master, dev] - paths: ['**.rs', '**.toml', '**.lock', '**.yml'] + paths: + [ + "**.rs", + "Cargo.toml", + "/Cargo.lock", + "/rustfmt.toml", + "/.github/workflows", + ] + schedule: + # Run CI every week + - cron: "00 01 * * 0" + +env: + RUST_BACKTRACE: 1 jobs: fmt: - name: 'Rust: format check' - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - # Only run the formatting check for stable - include: - - os: ubuntu-latest - toolchain: stable - steps: - - name: Checkout code - uses: actions/checkout@v2 - - name: Install toolchain - uses: actions-rs/toolchain@v1 - with: - # Use default profile to get rustfmt - profile: default - toolchain: ${{ matrix.toolchain }} - override: true - - run: cargo fmt --verbose --all -- --check - - test: - needs: fmt - runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.experimental }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest] - toolchain: - - 1.42.0 # MSRV (Minimum supported rust version) - - stable - - beta - experimental: [false] - # Ignore failures in nightly, not ideal, but necessary - include: - - os: ubuntu-latest - toolchain: nightly - experimental: true + name: rustfmt + runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: rustfmt + - run: cargo fmt --all -- --check + + test-linux: + needs: fmt + name: cargo +${{ matrix.toolchain }} build (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.experimental }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest] + toolchain: + - 1.45 # MSRV (Minimum supported rust version) + - stable + - beta + experimental: [false] + # Ignore failures in nightly + include: + - os: ubuntu-latest + toolchain: nightly + experimental: true + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Install toolchain uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: ${{ matrix.toolchain }} override: true + + - name: Get Rustc version + id: get-rustc-version + run: echo "::set-output name=version::$(rustc -V)" + shell: bash + - name: Cache Rust dependencies uses: actions/cache@v2 with: @@ -67,21 +87,65 @@ jobs: ~/.cargo/registry/cache ~/.cargo/git target - key: ${{ runner.os }}-build-${{ hashFiles('**/Cargo.lock') }} + key: ${{ runner.os }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('**/Cargo.lock') }} + - name: Install developer package dependencies - run: sudo apt-get update && sudo apt-get install libpulse-dev portaudio19-dev libasound2-dev libsdl2-dev gstreamer1.0-dev libgstreamer-plugins-base1.0-dev - - run: cargo build --locked --no-default-features - - run: cargo build --locked --examples - - run: cargo build --locked --no-default-features --features "with-tremor" - - run: cargo build --locked --no-default-features --features "with-vorbis" - - run: cargo build --locked --no-default-features --features "alsa-backend" - - run: cargo build --locked --no-default-features --features "portaudio-backend" - - run: cargo build --locked --no-default-features --features "pulseaudio-backend" - - run: cargo build --locked --no-default-features --features "jackaudio-backend" - - run: cargo build --locked --no-default-features --features "rodiojack-backend" - - run: cargo build --locked --no-default-features --features "rodio-backend" - - run: cargo build --locked --no-default-features --features "sdl-backend" - - run: cargo build --locked --no-default-features --features "gstreamer-backend" + run: sudo apt-get update && sudo apt-get install libpulse-dev portaudio19-dev libasound2-dev libsdl2-dev gstreamer1.0-dev libgstreamer-plugins-base1.0-dev libavahi-compat-libdnssd-dev + + - run: cargo build --workspace --examples + - run: cargo test --workspace + + - run: cargo install cargo-hack + - run: cargo hack --workspace --remove-dev-deps + - run: cargo build -p librespot-core --no-default-features + - run: cargo build -p librespot-core + - run: cargo hack build --each-feature -p librespot-audio + - run: cargo build -p librespot-connect + - run: cargo build -p librespot-connect --no-default-features --features with-dns-sd + - run: cargo hack build --locked --each-feature + + test-windows: + needs: fmt + name: cargo build (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [windows-latest] + toolchain: [stable] + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.toolchain }} + profile: minimal + override: true + + - name: Get Rustc version + id: get-rustc-version + run: echo "::set-output name=version::$(rustc -V)" + shell: bash + + - name: Cache Rust dependencies + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git + target + key: ${{ runner.os }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('**/Cargo.lock') }} + + - run: cargo build --workspace --examples + - run: cargo test --workspace + + - run: cargo install cargo-hack + - run: cargo hack --workspace --remove-dev-deps + - run: cargo build --no-default-features + - run: cargo build test-cross-arm: needs: fmt @@ -97,6 +161,7 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 + - name: Install toolchain uses: actions-rs/toolchain@v1 with: @@ -104,6 +169,12 @@ jobs: target: ${{ matrix.target }} toolchain: ${{ matrix.toolchain }} override: true + + - name: Get Rustc version + id: get-rustc-version + run: echo "::set-output name=version::$(rustc -V)" + shell: bash + - name: Cache Rust dependencies uses: actions/cache@v2 with: @@ -112,7 +183,7 @@ jobs: ~/.cargo/registry/cache ~/.cargo/git target - key: ${{ runner.os }}-build-${{ hashFiles('**/Cargo.lock') }} + key: ${{ runner.os }}-${{ matrix.target }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('**/Cargo.lock') }} - name: Install cross run: cargo install cross || true - name: Build diff --git a/COMPILING.md b/COMPILING.md index 40eefb3..539dc69 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -13,7 +13,7 @@ curl https://sh.rustup.rs -sSf | sh Follow any prompts it gives you to install Rust. Once that’s done, Rust's standard tools should be setup and ready to use. -*Note: The current minimum required Rust version at the time of writing is 1.40.0, you can find the current minimum version specified in the `.github/workflow/test.yml` file.* +*Note: The current minimum required Rust version at the time of writing is 1.45, you can find the current minimum version specified in the `.github/workflow/test.yml` file.* #### Additional Rust tools - `rustfmt` To ensure a consistent codebase, we utilise [`rustfmt`](https://github.com/rust-lang/rustfmt), which is installed by default with `rustup` these days, else it can be installed manually with: diff --git a/Cargo.lock b/Cargo.lock index 7908dde..5077859 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,27 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "addr2line" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" - -[[package]] -name = "adler32" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" - [[package]] name = "aes" version = "0.6.0" @@ -77,9 +56,9 @@ dependencies = [ [[package]] name = "alsa" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb213f6b3e4b1480a60931ca2035794aa67b73103d254715b1db7b70dcb3c934" +checksum = "75c4da790adcb2ce5e758c064b4f3ec17a30349f9961d3e5e6c9688b052a9e18" dependencies = [ "alsa-sys", "bitflags", @@ -103,12 +82,6 @@ version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1" -[[package]] -name = "ascii" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" - [[package]] name = "async-trait" version = "0.1.42" @@ -137,26 +110,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" -[[package]] -name = "backtrace" -version = "0.3.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc" -dependencies = [ - "addr2line", - "cfg-if 1.0.0", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "base-x" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b" - [[package]] name = "base64" version = "0.13.0" @@ -236,6 +189,9 @@ name = "cc" version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" +dependencies = [ + "jobserver", +] [[package]] name = "cesu8" @@ -273,16 +229,10 @@ dependencies = [ "libc", "num-integer", "num-traits", - "time 0.1.43", + "time", "winapi", ] -[[package]] -name = "chunked_transfer" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" - [[package]] name = "cipher" version = "0.2.5" @@ -303,19 +253,6 @@ dependencies = [ "libloading 0.7.0", ] -[[package]] -name = "combine" -version = "3.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" -dependencies = [ - "ascii", - "byteorder", - "either", - "memchr", - "unreachable", -] - [[package]] name = "combine" version = "4.5.2" @@ -326,39 +263,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "const_fn" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6" - -[[package]] -name = "cookie" -version = "0.14.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784ad0fbab4f3e9cef09f20e0aea6000ae08d2cb98ac4c0abc53df18803d702f" -dependencies = [ - "percent-encoding", - "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", - "log", - "publicsuffix", - "serde", - "serde_json", - "time 0.2.25", - "url", -] - [[package]] name = "core-foundation-sys" version = "0.6.2" @@ -367,9 +271,9 @@ checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" [[package]] name = "coreaudio-rs" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f229761965dad3e9b11081668a6ea00f1def7aa46062321b5ec245b834f6e491" +checksum = "11894b20ebfe1ff903cbdc52259693389eea03b94918a2def2c30c3bf227ad88" dependencies = [ "bitflags", "coreaudio-sys", @@ -386,15 +290,15 @@ dependencies = [ [[package]] name = "cpal" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05631e2089dfa5d3b6ea1cfbbfd092e2ee5deeb69698911bc976b28b746d3657" +checksum = "8351ddf2aaa3c583fa388029f8b3d26f3c7035a20911fdd5f2e2ed7ab57dad25" dependencies = [ "alsa", "core-foundation-sys", "coreaudio-rs", "jack", - "jni 0.17.0", + "jni", "js-sys", "lazy_static", "libc", @@ -404,7 +308,7 @@ dependencies = [ "nix", "oboe", "parking_lot", - "stdweb 0.1.3", + "stdweb", "thiserror", "web-sys", "winapi", @@ -416,15 +320,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634" -[[package]] -name = "crc32fast" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" -dependencies = [ - "cfg-if 1.0.0", -] - [[package]] name = "crypto-mac" version = "0.10.0" @@ -499,12 +394,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "discard" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" - [[package]] name = "dns-sd" version = "0.1.3" @@ -534,39 +423,6 @@ dependencies = [ "termcolor", ] -[[package]] -name = "error-chain" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" -dependencies = [ - "backtrace", - "version_check", -] - -[[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", - "winapi", -] - [[package]] name = "fnv" version = "1.0.7" @@ -711,12 +567,6 @@ dependencies = [ "wasi", ] -[[package]] -name = "gimli" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" - [[package]] name = "glib" version = "0.10.3" @@ -890,7 +740,7 @@ dependencies = [ "http", "mime", "sha-1", - "time 0.1.43", + "time", ] [[package]] @@ -1088,9 +938,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" [[package]] name = "jack" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c1871c91fa65aa328f3bedbaa54a6e5d1de009264684c153eb708ba933aa6f5" +checksum = "2deb4974bd7e6b2fb7784f27fa13d819d11292b3b004dce0185ec08163cf686a" dependencies = [ "bitflags", "jack-sys", @@ -1100,9 +950,9 @@ dependencies = [ [[package]] name = "jack-sys" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d6ab7ada402b6a27912a2b86504be62a48c58313c886fe72a059127acb4d7" +checksum = "57983f0d72dfecf2b719ed39bc9cacd85194e1a94cb3f9146009eff9856fef41" dependencies = [ "lazy_static", "libc", @@ -1111,29 +961,15 @@ dependencies = [ [[package]] name = "jni" -version = "0.14.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1981310da491a4f0f815238097d0d43d8072732b5ae5f8bd0d8eadf5bf245402" +checksum = "24967112a1e4301ca5342ea339763613a37592b8a6ce6cf2e4494537c7a42faf" dependencies = [ "cesu8", - "combine 3.8.1", - "error-chain", - "jni-sys", - "log", - "walkdir", -] - -[[package]] -name = "jni" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36bcc950632e48b86da402c5c077590583da5ac0d480103611d5374e7c967a3c" -dependencies = [ - "cesu8", - "combine 4.5.2", - "error-chain", + "combine", "jni-sys", "log", + "thiserror", "walkdir", ] @@ -1143,6 +979,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.47" @@ -1177,27 +1022,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.86" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c" - -[[package]] -name = "libflate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389de7875e06476365974da3e7ff85d55f1972188ccd9f6020dd7c8156e17914" -dependencies = [ - "adler32", - "crc32fast", - "libflate_lz77", - "rle-decode-fast", -] - -[[package]] -name = "libflate_lz77" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3286f09f7d4926fc486334f28d8d2e6ebe4f7f9994494b6dab27ddfad2c9b11b" +checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" [[package]] name = "libloading" @@ -1239,9 +1066,9 @@ dependencies = [ [[package]] name = "libpulse-binding" -version = "2.23.0" +version = "2.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2405f806801527dfb3d2b6d48a282cdebe9a1b41b0652e0d7b5bad81dbc700e" +checksum = "db951f37898e19a6785208e3a290261e0f1a8e086716be596aaad684882ca8e3" dependencies = [ "bitflags", "libc", @@ -1325,6 +1152,7 @@ dependencies = [ "tempfile", "tokio", "vorbis", + "zerocopy", ] [[package]] @@ -1335,6 +1163,7 @@ dependencies = [ "base64", "block-modes", "dns-sd", + "form_urlencoded", "futures-core", "futures-util", "hmac", @@ -1344,7 +1173,6 @@ dependencies = [ "librespot-playback", "librespot-protocol", "log", - "num-bigint", "protobuf", "rand", "serde", @@ -1364,6 +1192,7 @@ dependencies = [ "byteorder", "bytes", "env_logger", + "form_urlencoded", "futures-core", "futures-util", "hmac", @@ -1507,16 +1336,6 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" -[[package]] -name = "miniz_oxide" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d" -dependencies = [ - "adler", - "autocfg", -] - [[package]] name = "mio" version = "0.7.9" @@ -1557,9 +1376,9 @@ dependencies = [ [[package]] name = "ndk" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb167c1febed0a496639034d0c76b3b74263636045db5489eee52143c246e73" +checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab" dependencies = [ "jni-sys", "ndk-sys", @@ -1569,9 +1388,9 @@ dependencies = [ [[package]] name = "ndk-glue" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdf399b8b7a39c6fb153c4ec32c72fd5fe789df24a647f229c239aa7adb15241" +checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385" dependencies = [ "lazy_static", "libc", @@ -1602,15 +1421,14 @@ checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" [[package]] name = "nix" -version = "0.15.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2e0b4f3320ed72aaedb9a5ac838690a8047c7b275da22711fddff4f8a14229" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" dependencies = [ "bitflags", "cc", - "cfg-if 0.1.10", + "cfg-if 1.0.0", "libc", - "void", ] [[package]] @@ -1634,13 +1452,14 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e9a41747ae4633fce5adffb4d2e81ffc5e89593cb19917f8fb2cc5ff76507bf" +checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512" dependencies = [ "autocfg", "num-integer", "num-traits", + "rand", ] [[package]] @@ -1696,9 +1515,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.4.3" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca565a7df06f3d4b485494f25ba05da1435950f4dc263440eda7a6fa9b8e36e4" +checksum = "226b45a5c2ac4dd696ed30fa6b94b057ad909c7b7fc2e0d0808192bced894066" dependencies = [ "derivative", "num_enum_derive", @@ -1706,9 +1525,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.4.3" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffa5a33ddddfee04c0283a7653987d634e880347e96b5b2ed64de07efb59db9d" +checksum = "1c0fd9eba1d5db0994a239e09c1be402d35622277e35468ba891aa5e3188ce7e" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1716,19 +1535,13 @@ dependencies = [ "syn", ] -[[package]] -name = "object" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" - [[package]] name = "oboe" -version = "0.3.1" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aadc2b0867bdbb9a81c4d99b9b682958f49dbea1295a81d2f646cca2afdd9fc" +checksum = "4cfb2390bddb9546c0f7448fd1d2abdd39e6075206f960991eb28c7fa7f126c4" dependencies = [ - "jni 0.14.0", + "jni", "ndk", "ndk-glue", "num-derive", @@ -1738,11 +1551,11 @@ dependencies = [ [[package]] name = "oboe-sys" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ff7a51600eabe34e189eec5c995a62f151d8d97e5fbca39e87ca738bb99b82" +checksum = "fe069264d082fc820dfa172f79be3f2e088ecfece9b1c47b0c9fd838d2bef103" dependencies = [ - "fetch_unroll", + "cc", ] [[package]] @@ -1989,28 +1802,6 @@ dependencies = [ "protobuf-codegen", ] -[[package]] -name = "publicsuffix" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bbaa49075179162b49acac1c6aa45fb4dafb5f13cf6794276d77bc7fd95757b" -dependencies = [ - "error-chain", - "idna", - "lazy_static", - "regex", - "url", -] - -[[package]] -name = "qstring" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" -dependencies = [ - "percent-encoding", -] - [[package]] name = "quick-error" version = "1.2.3" @@ -2102,32 +1893,11 @@ dependencies = [ "winapi", ] -[[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", -] - -[[package]] -name = "rle-decode-fast" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cabe4fa914dec5870285fa7f71f602645da47c486e68486d2b4ceb4a343e90ac" - [[package]] name = "rodio" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9683532495146e98878d4948fa1a1953f584cd923f2a5f5c26b7a8701b56943" +checksum = "b65c2eda643191f6d1bb12ea323a9db8d9ba95374e9be3780b5a9fb5cfb8520f" dependencies = [ "cpal", ] @@ -2142,47 +1912,19 @@ dependencies = [ "winapi", ] -[[package]] -name = "rustc-demangle" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" - [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" -[[package]] -name = "rustc_version" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" -dependencies = [ - "semver 0.9.0", -] - [[package]] name = "rustc_version" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" dependencies = [ - "semver 0.11.0", -] - -[[package]] -name = "rustls" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "064fd21ff87c6e87ed4506e68beb42459caa4a0e2eb144932e6776768556980b" -dependencies = [ - "base64", - "log", - "ring", - "sct", - "webpki", + "semver", ] [[package]] @@ -2206,16 +1948,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" -[[package]] -name = "sct" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "sdl2" version = "0.34.3" @@ -2230,39 +1962,24 @@ dependencies = [ [[package]] name = "sdl2-sys" -version = "0.34.3" +version = "0.34.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d81feded049b9c14eceb4a4f6d596a98cebbd59abdba949c5552a015466d33" +checksum = "4cb164f53dbcad111de976bbf1f3083d3fcdeda88da9cfa281c70822720ee3da" dependencies = [ "cfg-if 0.1.10", "libc", "version-compare", ] -[[package]] -name = "semver" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -dependencies = [ - "semver-parser 0.7.0", -] - [[package]] name = "semver" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" dependencies = [ - "semver-parser 0.10.2", + "semver-parser", ] -[[package]] -name = "semver-parser" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" - [[package]] name = "semver-parser" version = "0.10.2" @@ -2316,12 +2033,6 @@ dependencies = [ "opaque-debug", ] -[[package]] -name = "sha1" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" - [[package]] name = "shannon" version = "0.2.0" @@ -2375,76 +2086,12 @@ dependencies = [ "winapi", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "standback" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2beb4d1860a61f571530b3f855a1b538d0200f7871c63331ecd6f17b1f014f8" -dependencies = [ - "version_check", -] - [[package]] name = "stdweb" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" -[[package]] -name = "stdweb" -version = "0.4.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" -dependencies = [ - "discard", - "rustc_version 0.2.3", - "stdweb-derive", - "stdweb-internal-macros", - "stdweb-internal-runtime", - "wasm-bindgen", -] - -[[package]] -name = "stdweb-derive" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "serde_derive", - "syn", -] - -[[package]] -name = "stdweb-internal-macros" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" -dependencies = [ - "base-x", - "proc-macro2", - "quote", - "serde", - "serde_derive", - "serde_json", - "sha1", - "syn", -] - -[[package]] -name = "stdweb-internal-runtime" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" - [[package]] name = "strsim" version = "0.9.3" @@ -2513,17 +2160,6 @@ dependencies = [ "version-compare", ] -[[package]] -name = "tar" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0bcfbd6a598361fda270d82469fff3d65089dc33e175c9a131f7b4cd395f228" -dependencies = [ - "filetime", - "libc", - "xattr", -] - [[package]] name = "tempfile" version = "3.2.0" @@ -2586,44 +2222,6 @@ dependencies = [ "winapi", ] -[[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", -] - -[[package]] -name = "time-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1" -dependencies = [ - "proc-macro-hack", - "time-macros-impl", -] - -[[package]] -name = "time-macros-impl" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" -dependencies = [ - "proc-macro-hack", - "proc-macro2", - "quote", - "standback", - "syn", -] - [[package]] name = "tinyvec" version = "1.1.1" @@ -2783,40 +2381,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" -[[package]] -name = "unreachable" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" -dependencies = [ - "void", -] - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - -[[package]] -name = "ureq" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "294b85ef5dbc3670a72e82a89971608a1fcc4ed5c7c5a2895230d31a95f0569b" -dependencies = [ - "base64", - "chunked_transfer", - "cookie", - "cookie_store", - "log", - "once_cell", - "qstring", - "rustls", - "url", - "webpki", - "webpki-roots", -] - [[package]] name = "url" version = "2.2.1" @@ -2846,7 +2410,7 @@ checksum = "e7141e445af09c8919f1d5f8a20dae0b20c3b57a45dee0d5823c6ed5d237f15a" dependencies = [ "bitflags", "chrono", - "rustc_version 0.3.3", + "rustc_version", ] [[package]] @@ -2861,12 +2425,6 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" -[[package]] -name = "void" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" - [[package]] name = "vorbis" version = "0.0.14" @@ -2995,25 +2553,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "webpki-roots" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82015b7e0b8bad8185994674a13a93306bea76cf5a16c5a181382fd3a5ec2376" -dependencies = [ - "webpki", -] - [[package]] name = "winapi" version = "0.3.9" @@ -3045,15 +2584,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "xattr" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" -dependencies = [ - "libc", -] - [[package]] name = "zerocopy" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 8f3e44c..14e33a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ version = "0.1.6" [dependencies.librespot-core] path = "core" version = "0.1.6" +features = ["apresolve"] [dependencies.librespot-metadata] path = "metadata" @@ -58,8 +59,6 @@ url = "2.1" sha-1 = "0.9" [features] -apresolve = ["librespot-core/apresolve"] - alsa-backend = ["librespot-playback/alsa-backend"] portaudio-backend = ["librespot-playback/portaudio-backend"] pulseaudio-backend = ["librespot-playback/pulseaudio-backend"] @@ -71,11 +70,10 @@ gstreamer-backend = ["librespot-playback/gstreamer-backend"] with-tremor = ["librespot-audio/with-tremor"] with-vorbis = ["librespot-audio/with-vorbis"] -with-lewton = ["librespot-audio/with-lewton"] -# with-dns-sd = ["librespot-connect/with-dns-sd"] +with-dns-sd = ["librespot-connect/with-dns-sd"] -default = ["rodio-backend", "apresolve", "with-lewton"] +default = ["rodio-backend"] [package.metadata.deb] maintainer = "librespot-org" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index d8c0eea..a3dfbe7 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -15,17 +15,17 @@ aes-ctr = "0.6" byteorder = "1.4" bytes = "1.0" cfg-if = "1" +lewton = "0.10" log = "0.4" futures-util = { version = "0.3", default_features = false } ogg = "0.8" tempfile = "3.1" -tokio = { version = "1", features = ["sync"] } +tokio = { version = "1", features = ["sync", "macros"] } +zerocopy = "0.3" -lewton = { version = "0.10", optional = true } -librespot-tremor = { version = "0.2.0", optional = true } -vorbis = { version ="0.0.14", optional = true } +librespot-tremor = { version = "0.2", optional = true } +vorbis = { version ="0.0", optional = true } [features] -with-lewton = ["lewton"] with-tremor = ["librespot-tremor"] with-vorbis = ["vorbis"] diff --git a/audio/src/convert.rs b/audio/src/convert.rs new file mode 100644 index 0000000..450910b --- /dev/null +++ b/audio/src/convert.rs @@ -0,0 +1,56 @@ +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 fn to_s32(samples: &[f32]) -> Vec { + convert_samples_to!(i32, samples) +} + +pub fn to_s24(samples: &[f32]) -> Vec { + convert_samples_to!(i32, samples, 8) +} + +pub fn to_s24_3(samples: &[f32]) -> Vec { + to_s32(samples) + .iter() + .map(|sample| i24::pcm_from_i32(*sample)) + .collect() +} + +pub fn to_s16(samples: &[f32]) -> Vec { + convert_samples_to!(i16, samples) +} diff --git a/audio/src/lewton_decoder.rs b/audio/src/lewton_decoder.rs index 698cc64..528d934 100644 --- a/audio/src/lewton_decoder.rs +++ b/audio/src/lewton_decoder.rs @@ -35,8 +35,11 @@ where use lewton::OggReadError::NoCapturePatternFound; use lewton::VorbisError::{BadAudio, OggError}; loop { - match self.0.read_dec_packet_itl() { - Ok(Some(packet)) => return Ok(Some(AudioPacket::Samples(packet))), + match self + .0 + .read_dec_packet_generic::>() + { + Ok(Some(packet)) => return Ok(Some(AudioPacket::Samples(packet.samples))), Ok(None) => return Ok(None), Err(BadAudio(AudioIsHeader)) => (), diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 80f1097..b587f03 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -3,27 +3,19 @@ #[macro_use] extern crate log; +pub mod convert; mod decrypt; mod fetch; use cfg_if::cfg_if; -#[cfg(any( - all(feature = "with-lewton", feature = "with-tremor"), - all(feature = "with-vorbis", feature = "with-tremor"), - all(feature = "with-lewton", feature = "with-vorbis") -))] -compile_error!("Cannot use two decoders at the same time."); - cfg_if! { - if #[cfg(feature = "with-lewton")] { - mod lewton_decoder; - pub use lewton_decoder::{VorbisDecoder, VorbisError}; - } else if #[cfg(any(feature = "with-tremor", feature = "with-vorbis"))] { + if #[cfg(any(feature = "with-tremor", feature = "with-vorbis"))] { mod libvorbis_decoder; pub use crate::libvorbis_decoder::{VorbisDecoder, VorbisError}; } else { - compile_error!("Must choose a vorbis decoder."); + mod lewton_decoder; + pub use lewton_decoder::{VorbisDecoder, VorbisError}; } } @@ -41,12 +33,12 @@ pub use fetch::{ use std::fmt; pub enum AudioPacket { - Samples(Vec), + Samples(Vec), OggData(Vec), } impl AudioPacket { - pub fn samples(&self) -> &[i16] { + pub fn samples(&self) -> &[f32] { match self { AudioPacket::Samples(s) => s, AudioPacket::OggData(_) => panic!("can't return OggData on samples"), diff --git a/audio/src/libvorbis_decoder.rs b/audio/src/libvorbis_decoder.rs index eeef8ab..6f9a68a 100644 --- a/audio/src/libvorbis_decoder.rs +++ b/audio/src/libvorbis_decoder.rs @@ -37,7 +37,18 @@ where fn next_packet(&mut self) -> Result, AudioError> { loop { match self.0.packets().next() { - Some(Ok(packet)) => return Ok(Some(AudioPacket::Samples(packet.data))), + Some(Ok(packet)) => { + // Losslessly represent [-32768, 32767] to [-1.0, 1.0] while maintaining DC linearity. + return Ok(Some(AudioPacket::Samples( + packet + .data + .iter() + .map(|sample| { + ((*sample as f64 + 0.5) / (std::i16::MAX as f64 + 0.5)) as f32 + }) + .collect(), + ))); + } None => return Ok(None), Some(Err(vorbis::VorbisError::Hole)) => (), diff --git a/audio/src/passthrough_decoder.rs b/audio/src/passthrough_decoder.rs index 25802e4..e064cba 100644 --- a/audio/src/passthrough_decoder.rs +++ b/audio/src/passthrough_decoder.rs @@ -5,75 +5,32 @@ use std::fmt; use std::io::{Read, Seek}; use std::time::{SystemTime, UNIX_EPOCH}; -fn write_headers( - rdr: &mut PacketReader, - wtr: &mut PacketWriter>, -) -> Result { - let mut stream_serial: u32 = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as u32; - - // search for ident, comment, setup - get_header(1, rdr, wtr, &mut stream_serial, PacketWriteEndInfo::EndPage)?; - get_header( - 3, - rdr, - wtr, - &mut stream_serial, - PacketWriteEndInfo::NormalPacket, - )?; - get_header(5, rdr, wtr, &mut stream_serial, PacketWriteEndInfo::EndPage)?; - - // remove un-needed packets - rdr.delete_unread_packets(); - Ok(stream_serial) -} - -fn get_header( - code: u8, - rdr: &mut PacketReader, - wtr: &mut PacketWriter>, - stream_serial: &mut u32, - info: PacketWriteEndInfo, -) -> Result +fn get_header(code: u8, rdr: &mut PacketReader) -> Result, PassthroughError> where T: Read + Seek, { let pck: Packet = rdr.read_packet_expected()?; - // set a unique serial number - if pck.stream_serial() != 0 { - *stream_serial = pck.stream_serial(); - } - let pkt_type = pck.data[0]; debug!("Vorbis header type{}", &pkt_type); - // all headers are mandatory if pkt_type != code { return Err(PassthroughError(OggReadError::InvalidData)); } - // headers keep original granule number - let absgp_page = pck.absgp_page(); - wtr.write_packet( - pck.data.into_boxed_slice(), - *stream_serial, - info, - absgp_page, - ) - .unwrap(); - - Ok(*stream_serial) + Ok(pck.data.into_boxed_slice()) } pub struct PassthroughDecoder { rdr: PacketReader, wtr: PacketWriter>, - lastgp_page: Option, - absgp_page: u64, + eos: bool, + bos: bool, + ofsgp_page: u64, stream_serial: u32, + ident: Box<[u8]>, + comment: Box<[u8]>, + setup: Box<[u8]>, } pub struct PassthroughError(ogg::OggReadError); @@ -82,17 +39,31 @@ impl PassthroughDecoder { /// Constructs a new Decoder from a given implementation of `Read + Seek`. pub fn new(rdr: R) -> Result { let mut rdr = PacketReader::new(rdr); - let mut wtr = PacketWriter::new(Vec::new()); + let stream_serial = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_millis() as u32; - let stream_serial = write_headers(&mut rdr, &mut wtr)?; info!("Starting passthrough track with serial {}", stream_serial); + // search for ident, comment, setup + let ident = get_header(1, &mut rdr)?; + let comment = get_header(3, &mut rdr)?; + let setup = get_header(5, &mut rdr)?; + + // remove un-needed packets + rdr.delete_unread_packets(); + Ok(PassthroughDecoder { rdr, - wtr, - lastgp_page: Some(0), - absgp_page: 0, + wtr: PacketWriter::new(Vec::new()), + ofsgp_page: 0, stream_serial, + ident, + comment, + setup, + eos: false, + bos: false, }) } } @@ -100,52 +71,94 @@ impl PassthroughDecoder { impl AudioDecoder for PassthroughDecoder { fn seek(&mut self, ms: i64) -> Result<(), AudioError> { info!("Seeking to {}", ms); - self.lastgp_page = match ms { - 0 => Some(0), - _ => None, - }; + + // add an eos to previous stream if missing + if self.bos && !self.eos { + match self.rdr.read_packet() { + Ok(Some(pck)) => { + let absgp_page = pck.absgp_page() - self.ofsgp_page; + self.wtr + .write_packet( + pck.data.into_boxed_slice(), + self.stream_serial, + PacketWriteEndInfo::EndStream, + absgp_page, + ) + .unwrap(); + } + _ => warn! {"Cannot write EoS after seeking"}, + }; + } + + self.eos = false; + self.bos = false; + self.ofsgp_page = 0; + self.stream_serial += 1; // hard-coded to 44.1 kHz match self.rdr.seek_absgp(None, (ms * 44100 / 1000) as u64) { - Ok(_) => Ok(()), + Ok(_) => { + // need to set some offset for next_page() + let pck = self.rdr.read_packet().unwrap().unwrap(); + self.ofsgp_page = pck.absgp_page(); + debug!("Seek to offset page {}", self.ofsgp_page); + Ok(()) + } Err(err) => Err(AudioError::PassthroughError(err.into())), } } fn next_packet(&mut self) -> Result, AudioError> { - let mut skip = self.lastgp_page.is_none(); + // write headers if we are (re)starting + if !self.bos { + self.wtr + .write_packet( + self.ident.clone(), + self.stream_serial, + PacketWriteEndInfo::EndPage, + 0, + ) + .unwrap(); + self.wtr + .write_packet( + self.comment.clone(), + self.stream_serial, + PacketWriteEndInfo::NormalPacket, + 0, + ) + .unwrap(); + self.wtr + .write_packet( + self.setup.clone(), + self.stream_serial, + PacketWriteEndInfo::EndPage, + 0, + ) + .unwrap(); + self.bos = true; + debug!("Wrote Ogg headers"); + } + loop { let pck = match self.rdr.read_packet() { Ok(Some(pck)) => pck, - Ok(None) | Err(OggReadError::NoCapturePatternFound) => { info!("end of streaming"); return Ok(None); } - Err(err) => return Err(AudioError::PassthroughError(err.into())), }; let pckgp_page = pck.absgp_page(); - let lastgp_page = self.lastgp_page.get_or_insert(pckgp_page); - // consume packets till next page to get a granule reference - if skip { - if *lastgp_page == pckgp_page { - debug!("skipping packet"); - continue; - } - skip = false; - info!("skipped at {}", pckgp_page); + // skip till we have audio and a calculable granule position + if pckgp_page == 0 || pckgp_page == self.ofsgp_page { + continue; } - // now we can calculate absolute granule - self.absgp_page += pckgp_page - *lastgp_page; - self.lastgp_page = Some(pckgp_page); - // set packet type let inf = if pck.last_in_stream() { - self.lastgp_page = Some(0); + self.eos = true; PacketWriteEndInfo::EndStream } else if pck.last_in_page() { PacketWriteEndInfo::EndPage @@ -158,7 +171,7 @@ impl AudioDecoder for PassthroughDecoder { pck.data.into_boxed_slice(), self.stream_serial, inf, - self.absgp_page, + pckgp_page - self.ofsgp_page, ) .unwrap(); diff --git a/connect/Cargo.toml b/connect/Cargo.toml index b03de88..689efd3 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -11,12 +11,13 @@ edition = "2018" aes-ctr = "0.6" base64 = "0.13" block-modes = "0.7" +form_urlencoded = "1.0" futures-core = "0.3" futures-util = { version = "0.3", default_features = false } hmac = "0.10" hyper = { version = "0.14", features = ["server", "http1", "tcp"] } +libmdns = "0.6" log = "0.4" -num-bigint = "0.3" protobuf = "~2.14.0" rand = "0.8" serde = { version = "1.0", features = ["derive"] } @@ -27,7 +28,6 @@ tokio-stream = { version = "0.1" } url = "2.1" dns-sd = { version = "0.1.3", optional = true } -libmdns = { version = "0.6", optional = true } [dependencies.librespot-core] path = "../core" @@ -42,7 +42,5 @@ path = "../protocol" version = "0.1.6" [features] -with-libmdns = ["libmdns"] with-dns-sd = ["dns-sd"] -default = ["with-libmdns"] diff --git a/connect/src/discovery.rs b/connect/src/discovery.rs index 7bb36a2..383035c 100644 --- a/connect/src/discovery.rs +++ b/connect/src/discovery.rs @@ -5,7 +5,6 @@ use futures_core::Stream; use hmac::{Hmac, Mac, NewMac}; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Method, Request, Response, StatusCode}; -use num_bigint::BigUint; use serde_json::json; use sha1::{Digest, Sha1}; use tokio::sync::{mpsc, oneshot}; @@ -15,8 +14,7 @@ use dns_sd::DNSService; use librespot_core::authentication::Credentials; use librespot_core::config::ConnectConfig; -use librespot_core::diffie_hellman::{DH_GENERATOR, DH_PRIME}; -use librespot_core::util; +use librespot_core::diffie_hellman::DhLocalKeys; use std::borrow::Cow; use std::collections::BTreeMap; @@ -34,8 +32,7 @@ struct Discovery(Arc); struct DiscoveryInner { config: ConnectConfig, device_id: String, - private_key: BigUint, - public_key: BigUint, + keys: DhLocalKeys, tx: mpsc::UnboundedSender, } @@ -46,15 +43,10 @@ impl Discovery { ) -> (Discovery, mpsc::UnboundedReceiver) { let (tx, rx) = mpsc::unbounded_channel(); - let key_data = util::rand_vec(&mut rand::thread_rng(), 95); - let private_key = BigUint::from_bytes_be(&key_data); - let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME); - let discovery = Discovery(Arc::new(DiscoveryInner { config, device_id, - private_key, - public_key, + keys: DhLocalKeys::random(&mut rand::thread_rng()), tx, })); @@ -62,8 +54,7 @@ impl Discovery { } fn handle_get_info(&self, _: BTreeMap, Cow<'_, str>>) -> Response { - let public_key = self.0.public_key.to_bytes_be(); - let public_key = base64::encode(&public_key); + let public_key = base64::encode(&self.0.keys.public_key()); let result = json!({ "status": 101, @@ -98,16 +89,16 @@ impl Discovery { let encrypted_blob = base64::decode(encrypted_blob.as_bytes()).unwrap(); - let client_key = base64::decode(client_key.as_bytes()).unwrap(); - let client_key = BigUint::from_bytes_be(&client_key); - - let shared_key = util::powm(&client_key, &self.0.private_key, &DH_PRIME); + let shared_key = self + .0 + .keys + .shared_secret(&base64::decode(client_key.as_bytes()).unwrap()); let iv = &encrypted_blob[0..16]; let encrypted = &encrypted_blob[16..encrypted_blob.len() - 20]; let cksum = &encrypted_blob[encrypted_blob.len() - 20..encrypted_blob.len()]; - let base_key = Sha1::digest(&shared_key.to_bytes_be()); + let base_key = Sha1::digest(&shared_key); let base_key = &base_key[..16]; let checksum_key = { diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index f111e54..4fcb025 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -7,7 +7,6 @@ use crate::core::config::{ConnectConfig, VolumeCtrl}; use crate::core::mercury::{MercuryError, MercurySender}; use crate::core::session::Session; use crate::core::spotify_id::{SpotifyAudioType, SpotifyId, SpotifyIdError}; -use crate::core::util::url_encode; use crate::core::util::SeqGenerator; use crate::core::version; use crate::playback::mixer::Mixer; @@ -106,7 +105,7 @@ fn initial_state() -> State { fn initial_device_state(config: ConnectConfig) -> DeviceState { { let mut msg = DeviceState::new(); - msg.set_sw_version(version::version_string()); + msg.set_sw_version(version::VERSION_STRING.to_string()); msg.set_is_active(false); msg.set_can_play(true); msg.set_volume(0); @@ -244,6 +243,10 @@ fn volume_to_mixer(volume: u16, volume_ctrl: &VolumeCtrl) -> u16 { } } +fn url_encode(bytes: impl AsRef<[u8]>) -> String { + form_urlencoded::byte_serialize(bytes.as_ref()).collect() +} + impl Spirc { pub fn new( config: ConnectConfig, @@ -256,7 +259,7 @@ impl Spirc { let ident = session.device_id().to_owned(); // Uri updated in response to issue #288 - debug!("canonical_username: {}", url_encode(&session.username())); + debug!("canonical_username: {}", &session.username()); let uri = format!("hm://remote/user/{}/", url_encode(&session.username())); let subscription = Box::pin( diff --git a/core/Cargo.toml b/core/Cargo.toml index 7aabb1b..29f4f33 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -17,6 +17,7 @@ aes = "0.6" base64 = "0.13" byteorder = "1.4" bytes = "1.0" +form_urlencoded = "1.0" futures-core = { version = "0.3", default-features = false } futures-util = { version = "0.3", default-features = false, features = ["alloc", "bilock", "unstable", "sink"] } hmac = "0.10" @@ -25,7 +26,7 @@ http = "0.2" hyper = { version = "0.14", optional = true, features = ["client", "tcp", "http1"] } hyper-proxy = { version = "0.9.1", optional = true, default-features = false } log = "0.4" -num-bigint = "0.3" +num-bigint = { version = "0.4", features = ["rand"] } num-integer = "0.1" num-traits = "0.2" once_cell = "1.5.2" diff --git a/core/build.rs b/core/build.rs index 0fc2933..8e61c91 100644 --- a/core/build.rs +++ b/core/build.rs @@ -12,5 +12,6 @@ fn main() { .take(8) .map(char::from) .collect(); - println!("cargo:rustc-env=VERGEN_BUILD_ID={}", build_id); + + println!("cargo:rustc-env=LIBRESPOT_BUILD_ID={}", build_id); } diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index c954dab..b11e275 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -11,7 +11,7 @@ use super::AP_FALLBACK; const APRESOLVE_ENDPOINT: &str = "http://apresolve.spotify.com:80"; #[derive(Clone, Debug, Deserialize)] -struct APResolveData { +struct ApResolveData { ap_list: Vec, } @@ -41,7 +41,7 @@ async fn try_apresolve( }; let body = hyper::body::to_bytes(response.into_body()).await?; - let data: APResolveData = serde_json::from_slice(body.as_ref())?; + let data: ApResolveData = serde_json::from_slice(body.as_ref())?; let ap = if ap_port.is_some() || proxy.is_some() { data.ap_list.into_iter().find_map(|ap| { @@ -66,3 +66,26 @@ pub async fn apresolve(proxy: Option<&Url>, ap_port: Option) -> String { AP_FALLBACK.into() }) } + +#[cfg(test)] +mod test { + use std::net::ToSocketAddrs; + + use super::try_apresolve; + + #[tokio::test] + async fn test_apresolve() { + let ap = try_apresolve(None, None).await.unwrap(); + + // Assert that the result contains a valid host and port + ap.to_socket_addrs().unwrap().next().unwrap(); + } + + #[tokio::test] + async fn test_apresolve_port_443() { + let ap = try_apresolve(None, Some(443)).await.unwrap(); + + let port = ap.to_socket_addrs().unwrap().next().unwrap().port(); + assert_eq!(port, 443); + } +} diff --git a/core/src/authentication.rs b/core/src/authentication.rs index 2992abc..db787bb 100644 --- a/core/src/authentication.rs +++ b/core/src/authentication.rs @@ -31,13 +31,10 @@ impl Credentials { /// ### Example /// ```rust /// use librespot_core::authentication::Credentials; - /// + /// /// let creds = Credentials::with_password("my account", "my password"); /// ``` - pub fn with_password( - username: impl Into, - password: impl Into, - ) -> Credentials { + pub fn with_password(username: impl Into, password: impl Into) -> Credentials { Credentials { username: username.into(), auth_type: AuthenticationType::AUTHENTICATION_USER_PASS, diff --git a/core/src/config.rs b/core/src/config.rs index 2692464..9c70c25 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -14,7 +14,7 @@ impl Default for SessionConfig { fn default() -> SessionConfig { let device_id = uuid::Uuid::new_v4().to_hyphenated().to_string(); SessionConfig { - user_agent: crate::version::version_string(), + user_agent: crate::version::VERSION_STRING.to_string(), device_id, proxy: None, ap_port: None, @@ -29,9 +29,9 @@ pub enum DeviceType { Tablet = 2, Smartphone = 3, Speaker = 4, - TV = 5, - AVR = 6, - STB = 7, + Tv = 5, + Avr = 6, + Stb = 7, AudioDongle = 8, GameConsole = 9, CastAudio = 10, @@ -54,9 +54,9 @@ impl FromStr for DeviceType { "tablet" => Ok(Tablet), "smartphone" => Ok(Smartphone), "speaker" => Ok(Speaker), - "tv" => Ok(TV), - "avr" => Ok(AVR), - "stb" => Ok(STB), + "tv" => Ok(Tv), + "avr" => Ok(Avr), + "stb" => Ok(Stb), "audiodongle" => Ok(AudioDongle), "gameconsole" => Ok(GameConsole), "castaudio" => Ok(CastAudio), @@ -80,9 +80,9 @@ impl fmt::Display for DeviceType { Tablet => f.write_str("Tablet"), Smartphone => f.write_str("Smartphone"), Speaker => f.write_str("Speaker"), - TV => f.write_str("TV"), - AVR => f.write_str("AVR"), - STB => f.write_str("STB"), + Tv => f.write_str("TV"), + Avr => f.write_str("AVR"), + Stb => f.write_str("STB"), AudioDongle => f.write_str("AudioDongle"), GameConsole => f.write_str("GameConsole"), CastAudio => f.write_str("CastAudio"), diff --git a/core/src/connection/codec.rs b/core/src/connection/codec.rs index ead07b6..299220f 100644 --- a/core/src/connection/codec.rs +++ b/core/src/connection/codec.rs @@ -13,7 +13,7 @@ enum DecodeState { Payload(u8, usize), } -pub struct APCodec { +pub struct ApCodec { encode_nonce: u32, encode_cipher: Shannon, @@ -22,9 +22,9 @@ pub struct APCodec { decode_state: DecodeState, } -impl APCodec { - pub fn new(send_key: &[u8], recv_key: &[u8]) -> APCodec { - APCodec { +impl ApCodec { + pub fn new(send_key: &[u8], recv_key: &[u8]) -> ApCodec { + ApCodec { encode_nonce: 0, encode_cipher: Shannon::new(send_key), @@ -35,7 +35,7 @@ impl APCodec { } } -impl Encoder<(u8, Vec)> for APCodec { +impl Encoder<(u8, Vec)> for ApCodec { type Error = io::Error; fn encode(&mut self, item: (u8, Vec), buf: &mut BytesMut) -> io::Result<()> { @@ -60,7 +60,7 @@ impl Encoder<(u8, Vec)> for APCodec { } } -impl Decoder for APCodec { +impl Decoder for ApCodec { type Item = (u8, Bytes); type Error = io::Error; diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index 67a786e..6f802ab 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -1,22 +1,21 @@ use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use hmac::{Hmac, Mac, NewMac}; use protobuf::{self, Message}; -use rand::thread_rng; +use rand::{thread_rng, RngCore}; use sha1::Sha1; use std::io; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio_util::codec::{Decoder, Framed}; -use super::codec::APCodec; -use crate::diffie_hellman::DHLocalKeys; +use super::codec::ApCodec; +use crate::diffie_hellman::DhLocalKeys; use crate::protocol; use crate::protocol::keyexchange::{APResponseMessage, ClientHello, ClientResponsePlaintext}; -use crate::util; pub async fn handshake( mut connection: T, -) -> io::Result> { - let local_keys = DHLocalKeys::random(&mut thread_rng()); +) -> io::Result> { + let local_keys = DhLocalKeys::random(&mut thread_rng()); let gc = local_keys.public_key(); let mut accumulator = client_hello(&mut connection, gc).await?; let message: APResponseMessage = recv_packet(&mut connection, &mut accumulator).await?; @@ -29,7 +28,7 @@ pub async fn handshake( let shared_secret = local_keys.shared_secret(&remote_key); let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator); - let codec = APCodec::new(&send_key, &recv_key); + let codec = ApCodec::new(&send_key, &recv_key); client_response(&mut connection, challenge).await?; @@ -40,6 +39,9 @@ async fn client_hello(connection: &mut T, gc: Vec) -> io::Result> where T: AsyncWrite + Unpin, { + let mut client_nonce = vec![0; 0x10]; + thread_rng().fill_bytes(&mut client_nonce); + let mut packet = ClientHello::new(); packet .mut_build_info() @@ -59,7 +61,7 @@ where .mut_login_crypto_hello() .mut_diffie_hellman() .set_server_keys_known(1); - packet.set_client_nonce(util::rand_vec(&mut thread_rng(), 0x10)); + packet.set_client_nonce(client_nonce); packet.set_padding(vec![0x1e]); let mut buffer = vec![0, 4]; diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index ab35366..d8a4012 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -1,7 +1,7 @@ mod codec; mod handshake; -pub use self::codec::APCodec; +pub use self::codec::ApCodec; pub use self::handshake::handshake; use std::io::{self, ErrorKind}; @@ -19,7 +19,7 @@ use crate::protocol::keyexchange::{APLoginFailed, ErrorCode}; use crate::proxytunnel; use crate::version; -pub type Transport = Framed; +pub type Transport = Framed; fn login_error_message(code: &ErrorCode) -> &'static str { pub use ErrorCode::*; @@ -131,13 +131,13 @@ pub async fn authenticate( .mut_system_info() .set_system_information_string(format!( "librespot_{}_{}", - version::short_sha(), - version::build_id() + version::SHA_SHORT, + version::BUILD_ID )); packet .mut_system_info() .set_device_id(device_id.to_string()); - packet.set_version_string(version::version_string()); + packet.set_version_string(version::VERSION_STRING.to_string()); let cmd = 0xab; let data = packet.write_to_bytes().unwrap(); diff --git a/core/src/diffie_hellman.rs b/core/src/diffie_hellman.rs index 8544809..57caa02 100644 --- a/core/src/diffie_hellman.rs +++ b/core/src/diffie_hellman.rs @@ -1,11 +1,11 @@ -use num_bigint::BigUint; +use num_bigint::{BigUint, RandBigInt}; +use num_integer::Integer; +use num_traits::{One, Zero}; use once_cell::sync::Lazy; -use rand::Rng; +use rand::{CryptoRng, Rng}; -use crate::util; - -pub static DH_GENERATOR: Lazy = Lazy::new(|| BigUint::from_bytes_be(&[0x02])); -pub static DH_PRIME: Lazy = Lazy::new(|| { +static DH_GENERATOR: Lazy = Lazy::new(|| BigUint::from_bytes_be(&[0x02])); +static DH_PRIME: Lazy = Lazy::new(|| { BigUint::from_bytes_be(&[ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, @@ -17,19 +17,33 @@ pub static DH_PRIME: Lazy = Lazy::new(|| { ]) }); -pub struct DHLocalKeys { +fn powm(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint { + let mut base = base.clone(); + let mut exp = exp.clone(); + let mut result: BigUint = One::one(); + + while !exp.is_zero() { + if exp.is_odd() { + result = (result * &base) % modulus; + } + exp >>= 1; + base = (&base * &base) % modulus; + } + + result +} + +pub struct DhLocalKeys { private_key: BigUint, public_key: BigUint, } -impl DHLocalKeys { - pub fn random(rng: &mut R) -> DHLocalKeys { - let key_data = util::rand_vec(rng, 95); +impl DhLocalKeys { + pub fn random(rng: &mut R) -> DhLocalKeys { + let private_key = rng.gen_biguint(95 * 8); + let public_key = powm(&DH_GENERATOR, &private_key, &DH_PRIME); - let private_key = BigUint::from_bytes_be(&key_data); - let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME); - - DHLocalKeys { + DhLocalKeys { private_key, public_key, } @@ -40,7 +54,7 @@ impl DHLocalKeys { } pub fn shared_secret(&self, remote_key: &[u8]) -> Vec { - let shared_key = util::powm( + let shared_key = powm( &BigUint::from_bytes_be(remote_key), &self.private_key, &DH_PRIME, diff --git a/core/src/lib.rs b/core/src/lib.rs index 320967f..bb3e21d 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -14,12 +14,14 @@ pub mod cache; pub mod channel; pub mod config; mod connection; +#[doc(hidden)] pub mod diffie_hellman; pub mod keymaster; pub mod mercury; mod proxytunnel; pub mod session; pub mod spotify_id; +#[doc(hidden)] pub mod util; pub mod version; diff --git a/core/src/mercury/mod.rs b/core/src/mercury/mod.rs index 1a68e15..ef04e98 100644 --- a/core/src/mercury/mod.rs +++ b/core/src/mercury/mod.rs @@ -10,7 +10,6 @@ use bytes::Bytes; use tokio::sync::{mpsc, oneshot}; use crate::protocol; -use crate::util::url_encode; use crate::util::SeqGenerator; mod types; @@ -82,7 +81,7 @@ impl MercuryManager { pub fn get>(&self, uri: T) -> MercuryFuture { self.request(MercuryRequest { - method: MercuryMethod::GET, + method: MercuryMethod::Get, uri: uri.into(), content_type: None, payload: Vec::new(), @@ -91,7 +90,7 @@ impl MercuryManager { pub fn send>(&self, uri: T, data: Vec) -> MercuryFuture { self.request(MercuryRequest { - method: MercuryMethod::SEND, + method: MercuryMethod::Send, uri: uri.into(), content_type: None, payload: vec![data], @@ -109,7 +108,7 @@ impl MercuryManager { { let uri = uri.into(); let request = self.request(MercuryRequest { - method: MercuryMethod::SUB, + method: MercuryMethod::Sub, uri: uri.clone(), content_type: None, payload: Vec::new(), @@ -199,7 +198,7 @@ impl MercuryManager { let header: protocol::mercury::Header = protobuf::parse_from_bytes(&header_data).unwrap(); let response = MercuryResponse { - uri: url_encode(header.get_uri()), + uri: header.get_uri().to_string(), status_code: header.get_status_code(), payload: pending.parts, }; @@ -214,8 +213,21 @@ impl MercuryManager { } else if cmd == 0xb5 { self.lock(|inner| { let mut found = false; + + // TODO: This is just a workaround to make utf-8 encoded usernames work. + // A better solution would be to use an uri struct and urlencode it directly + // before sending while saving the subscription under its unencoded form. + let mut uri_split = response.uri.split('/'); + + let encoded_uri = std::iter::once(uri_split.next().unwrap().to_string()) + .chain(uri_split.map(|component| { + form_urlencoded::byte_serialize(component.as_bytes()).collect::() + })) + .collect::>() + .join("/"); + inner.subscriptions.retain(|&(ref prefix, ref sub)| { - if response.uri.starts_with(prefix) { + if encoded_uri.starts_with(prefix) { found = true; // if send fails, remove from list of subs diff --git a/core/src/mercury/types.rs b/core/src/mercury/types.rs index 57cedce..402a954 100644 --- a/core/src/mercury/types.rs +++ b/core/src/mercury/types.rs @@ -6,10 +6,10 @@ use crate::protocol; #[derive(Debug, PartialEq, Eq)] pub enum MercuryMethod { - GET, - SUB, - UNSUB, - SEND, + Get, + Sub, + Unsub, + Send, } #[derive(Debug)] @@ -33,10 +33,10 @@ pub struct MercuryError; impl ToString for MercuryMethod { fn to_string(&self) -> String { match *self { - MercuryMethod::GET => "GET", - MercuryMethod::SUB => "SUB", - MercuryMethod::UNSUB => "UNSUB", - MercuryMethod::SEND => "SEND", + MercuryMethod::Get => "GET", + MercuryMethod::Sub => "SUB", + MercuryMethod::Unsub => "UNSUB", + MercuryMethod::Send => "SEND", } .to_owned() } @@ -45,9 +45,9 @@ impl ToString for MercuryMethod { impl MercuryMethod { pub fn command(&self) -> u8 { match *self { - MercuryMethod::GET | MercuryMethod::SEND => 0xb2, - MercuryMethod::SUB => 0xb3, - MercuryMethod::UNSUB => 0xb4, + MercuryMethod::Get | MercuryMethod::Send => 0xb2, + MercuryMethod::Sub => 0xb3, + MercuryMethod::Unsub => 0xb4, } } } diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index 09d1097..801c6ac 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -18,9 +18,9 @@ impl From<&str> for SpotifyAudioType { } } -impl Into<&str> for SpotifyAudioType { - fn into(self) -> &'static str { - match self { +impl From for &str { + fn from(audio_type: SpotifyAudioType) -> &'static str { + match audio_type { SpotifyAudioType::Track => "track", SpotifyAudioType::Podcast => "episode", SpotifyAudioType::NonPlayable => "unknown", diff --git a/core/src/util.rs b/core/src/util.rs index c55d960..df9ea71 100644 --- a/core/src/util.rs +++ b/core/src/util.rs @@ -1,50 +1,4 @@ -use num_bigint::BigUint; -use num_integer::Integer; -use num_traits::{One, Zero}; -use rand::Rng; use std::mem; -use std::ops::{Mul, Rem, Shr}; - -pub fn rand_vec(rng: &mut G, size: usize) -> Vec { - ::std::iter::repeat(()) - .map(|()| rng.gen()) - .take(size) - .collect() -} - -pub fn url_encode(inp: &str) -> String { - let mut encoded = String::new(); - - for c in inp.as_bytes().iter() { - match *c as char { - 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' | ':' | '/' => { - encoded.push(*c as char) - } - c => encoded.push_str(format!("%{:02X}", c as u32).as_str()), - }; - } - - encoded -} - -pub fn powm(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint { - let mut base = base.clone(); - let mut exp = exp.clone(); - let mut result: BigUint = One::one(); - - while !exp.is_zero() { - if exp.is_odd() { - result = result.mul(&base).rem(modulus); - } - exp = exp.shr(1); - base = (&base).mul(&base).rem(modulus); - } - - result -} - -pub trait ReadSeek: ::std::io::Read + ::std::io::Seek {} -impl ReadSeek for T {} pub trait Seq { fn next(&self) -> Self; diff --git a/core/src/version.rs b/core/src/version.rs index cd7fa04..ef55346 100644 --- a/core/src/version.rs +++ b/core/src/version.rs @@ -1,44 +1,17 @@ -pub fn version_string() -> String { - format!("librespot-{}", short_sha()) -} +/// Version string of the form "librespot-" +pub const VERSION_STRING: &str = concat!("librespot-", env!("VERGEN_SHA_SHORT")); -// Generate a timestamp representing now (UTC) in RFC3339 format. -pub fn now() -> &'static str { - env!("VERGEN_BUILD_TIMESTAMP") -} +/// Generate a timestamp string representing the build date (UTC). +pub const BUILD_DATE: &str = env!("VERGEN_BUILD_DATE"); -// Generate a timstamp string representing now (UTC). -pub fn short_now() -> &'static str { - env!("VERGEN_BUILD_DATE") -} +/// Short sha of the latest git commit. +pub const SHA_SHORT: &str = env!("VERGEN_SHA_SHORT"); -// Generate a SHA string -pub fn sha() -> &'static str { - env!("VERGEN_SHA") -} +/// Date of the latest git commit. +pub const COMMIT_DATE: &str = env!("VERGEN_COMMIT_DATE"); -// Generate a short SHA string -pub fn short_sha() -> &'static str { - env!("VERGEN_SHA_SHORT") -} +/// Librespot crate version. +pub const SEMVER: &str = env!("CARGO_PKG_VERSION"); -// Generate the commit date string -pub fn commit_date() -> &'static str { - env!("VERGEN_COMMIT_DATE") -} - -// Generate the target triple string -pub fn target() -> &'static str { - env!("VERGEN_TARGET_TRIPLE") -} - -// Generate a semver string -pub fn semver() -> &'static str { - // env!("VERGEN_SEMVER") - env!("CARGO_PKG_VERSION") -} - -// Generate a random build id. -pub fn build_id() -> &'static str { - env!("VERGEN_BUILD_ID") -} +/// A random build id. +pub const BUILD_ID: &str = env!("LIBRESPOT_BUILD_ID"); diff --git a/core/tests/connect.rs b/core/tests/connect.rs index b7bc29f..4f1dbe6 100644 --- a/core/tests/connect.rs +++ b/core/tests/connect.rs @@ -1,34 +1,18 @@ -use librespot_core::*; +use librespot_core::authentication::Credentials; +use librespot_core::config::SessionConfig; +use librespot_core::session::Session; -// TODO: test is broken -// #[cfg(test)] -// mod tests { -// use super::*; -// // Test AP Resolve -// use apresolve::apresolve_or_fallback; -// #[tokio::test] -// async fn test_ap_resolve() { -// env_logger::init(); -// let ap = apresolve_or_fallback(&None, &None).await; -// println!("AP: {:?}", ap); -// } +#[tokio::test] +async fn test_connection() { + let result = Session::connect( + SessionConfig::default(), + Credentials::with_password("test", "test"), + None, + ) + .await; -// // Test connect -// use authentication::Credentials; -// use config::SessionConfig; -// #[tokio::test] -// async fn test_connection() -> Result<(), Box> { -// println!("Running connection test"); -// let ap = apresolve_or_fallback(&None, &None).await; -// let credentials = Credentials::with_password(String::from("test"), String::from("test")); -// let session_config = SessionConfig::default(); -// let proxy = None; - -// println!("Connecting to AP \"{}\"", ap); -// let mut connection = connection::connect(ap, &proxy).await?; -// let rc = connection::authenticate(&mut connection, credentials, &session_config.device_id) -// .await?; -// println!("Authenticated as \"{}\"", rc.username); -// Ok(()) -// } -// } + match result { + Ok(_) => panic!("Authentication succeeded despite of bad credentials."), + Err(e) => assert_eq!(e.to_string(), "Login failed with reason: Bad credentials"), + }; +} diff --git a/examples/get_token.rs b/examples/get_token.rs index 15b97bc..636155e 100644 --- a/examples/get_token.rs +++ b/examples/get_token.rs @@ -20,7 +20,9 @@ async fn main() { println!("Connecting.."); let credentials = Credentials::with_password(&args[1], &args[2]); - let session = Session::connect(session_config, credentials, None).await.unwrap(); + let session = Session::connect(session_config, credentials, None) + .await + .unwrap(); println!( "Token: {:#?}", diff --git a/examples/play.rs b/examples/play.rs index 9b1988a..d6c7196 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -4,14 +4,15 @@ use librespot::core::authentication::Credentials; use librespot::core::config::SessionConfig; use librespot::core::session::Session; use librespot::core::spotify_id::SpotifyId; -use librespot::playback::config::PlayerConfig; use librespot::playback::audio_backend; +use librespot::playback::config::{AudioFormat, PlayerConfig}; use librespot::playback::player::Player; #[tokio::main] async fn main() { let session_config = SessionConfig::default(); let player_config = PlayerConfig::default(); + let audio_format = AudioFormat::default(); let args: Vec<_> = env::args().collect(); if args.len() != 4 { @@ -25,10 +26,12 @@ async fn main() { let backend = audio_backend::find(None).unwrap(); println!("Connecting .."); - let session = Session::connect(session_config, credentials, None).await.unwrap(); + let session = Session::connect(session_config, credentials, None) + .await + .unwrap(); let (mut player, _) = Player::new(player_config, session, None, move || { - backend(None) + backend(None, audio_format) }); player.load(track, true, 0); diff --git a/examples/playlist_tracks.rs b/examples/playlist_tracks.rs index 7bd95ae..e96938c 100644 --- a/examples/playlist_tracks.rs +++ b/examples/playlist_tracks.rs @@ -25,7 +25,9 @@ async fn main() { let plist_uri = SpotifyId::from_base62(uri_parts[2]).unwrap(); - let session = Session::connect(session_config, credentials, None).await.unwrap(); + let session = Session::connect(session_config, credentials, None) + .await + .unwrap(); let plist = Playlist::get(&session, plist_uri).await.unwrap(); println!("{:?}", plist); diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 03675de..b8736db 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -25,17 +25,17 @@ byteorder = "1.4" shell-words = "1.0.0" tokio = { version = "1", features = ["sync"] } -alsa = { version = "0.4", optional = true } +alsa = { version = "0.5", optional = true } portaudio-rs = { version = "0.3", optional = true } -libpulse-binding = { version = "2.13", optional = true, default-features = false } -libpulse-simple-binding = { version = "2.13", optional = true, default-features = false } +libpulse-binding = { version = "2", optional = true, default-features = false } +libpulse-simple-binding = { version = "2", optional = true, default-features = false } jack = { version = "0.6", optional = true } libc = { version = "0.2", optional = true } -sdl2 = { version = "0.34", optional = true } +sdl2 = { version = "0.34.3", optional = true } gstreamer = { version = "0.16", optional = true } gstreamer-app = { version = "0.16", optional = true } glib = { version = "0.10", optional = true } -zerocopy = { version = "0.3", optional = true } +zerocopy = { version = "0.3" } # Rodio dependencies rodio = { version = "0.13", optional = true, default-features = false } @@ -50,4 +50,4 @@ jackaudio-backend = ["jack"] rodio-backend = ["rodio", "cpal", "thiserror"] rodiojack-backend = ["rodio", "cpal/jack", "thiserror"] sdl-backend = ["sdl2"] -gstreamer-backend = ["gstreamer", "gstreamer-app", "glib", "zerocopy"] +gstreamer-backend = ["gstreamer", "gstreamer-app", "glib"] diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index bf7b137..54fed31 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -1,5 +1,7 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLES_PER_SECOND, SAMPLE_RATE}; use alsa::device_name::HintIter; use alsa::pcm::{Access, Format, Frames, HwParams, PCM}; use alsa::{Direction, Error, ValueOr}; @@ -8,13 +10,14 @@ use std::ffi::CString; use std::io; use std::process::exit; -const PREFERED_PERIOD_SIZE: Frames = 5512; // Period of roughly 125ms +const BUFFERED_LATENCY: f32 = 0.125; // seconds const BUFFERED_PERIODS: Frames = 4; pub struct AlsaSink { pcm: Option, + format: AudioFormat, device: String, - buffer: Vec, + buffer: Vec, } fn list_outputs() { @@ -34,23 +37,27 @@ fn list_outputs() { } } -fn open_device(dev_name: &str) -> Result<(PCM, Frames), Box> { +fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, Frames), Box> { let pcm = PCM::new(dev_name, Direction::Playback, false)?; - let mut period_size = PREFERED_PERIOD_SIZE; + let alsa_format = match format { + AudioFormat::F32 => Format::float(), + AudioFormat::S32 => Format::s32(), + AudioFormat::S24 => Format::s24(), + AudioFormat::S24_3 => Format::S243LE, + AudioFormat::S16 => Format::s16(), + }; + // http://www.linuxjournal.com/article/6735?page=0,1#N0x19ab2890.0x19ba78d8 // latency = period_size * periods / (rate * bytes_per_frame) - // For 16 Bit stereo data, one frame has a length of four bytes. - // 500ms = buffer_size / (44100 * 4) - // buffer_size_bytes = 0.5 * 44100 / 4 - // buffer_size_frames = 0.5 * 44100 = 22050 + // For stereo samples encoded as 32-bit float, one frame has a length of eight bytes. + let mut period_size = ((SAMPLES_PER_SECOND * format.size() as u32) as f32 + * (BUFFERED_LATENCY / BUFFERED_PERIODS as f32)) as Frames; { - // Set hardware parameters: 44100 Hz / Stereo / 16 bit let hwp = HwParams::any(&pcm)?; - hwp.set_access(Access::RWInterleaved)?; - hwp.set_format(Format::s16())?; - hwp.set_rate(44100, ValueOr::Nearest)?; - hwp.set_channels(2)?; + hwp.set_format(alsa_format)?; + hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest)?; + hwp.set_channels(NUM_CHANNELS as u32)?; period_size = hwp.set_period_size_near(period_size, ValueOr::Greater)?; hwp.set_buffer_size_near(period_size * BUFFERED_PERIODS)?; pcm.hw_params(&hwp)?; @@ -64,12 +71,12 @@ fn open_device(dev_name: &str) -> Result<(PCM, Frames), Box> { } impl Open for AlsaSink { - fn open(device: Option) -> AlsaSink { - info!("Using alsa sink"); + fn open(device: Option, format: AudioFormat) -> Self { + info!("Using Alsa sink with format: {:?}", format); let name = match device.as_ref().map(AsRef::as_ref) { Some("?") => { - println!("Listing available alsa outputs"); + println!("Listing available Alsa outputs:"); list_outputs(); exit(0) } @@ -78,8 +85,9 @@ impl Open for AlsaSink { } .to_string(); - AlsaSink { + Self { pcm: None, + format, device: name, buffer: vec![], } @@ -89,12 +97,14 @@ impl Open for AlsaSink { impl Sink for AlsaSink { fn start(&mut self) -> io::Result<()> { if self.pcm.is_none() { - let pcm = open_device(&self.device); + let pcm = open_device(&self.device, self.format); match pcm { Ok((p, period_size)) => { self.pcm = Some(p); // Create a buffer for all samples for a full period - self.buffer = Vec::with_capacity((period_size * 2) as usize); + self.buffer = Vec::with_capacity( + period_size as usize * BUFFERED_PERIODS as usize * self.format.size(), + ); } Err(e) => { error!("Alsa error PCM open {}", e); @@ -111,23 +121,22 @@ impl Sink for AlsaSink { fn stop(&mut self) -> io::Result<()> { { - let pcm = self.pcm.as_mut().unwrap(); // Write any leftover data in the period buffer // before draining the actual buffer - let io = pcm.io_i16().unwrap(); - match io.writei(&self.buffer[..]) { - Ok(_) => (), - Err(err) => pcm.try_recover(err, false).unwrap(), - } + self.write_bytes(&[]).expect("could not flush buffer"); + let pcm = self.pcm.as_mut().unwrap(); pcm.drain().unwrap(); } self.pcm = None; Ok(()) } - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + sink_as_bytes!(); +} + +impl SinkAsBytes for AlsaSink { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { let mut processed_data = 0; - let data = packet.samples(); while processed_data < data.len() { let data_to_buffer = min( self.buffer.capacity() - self.buffer.len(), @@ -137,12 +146,7 @@ impl Sink for AlsaSink { .extend_from_slice(&data[processed_data..processed_data + data_to_buffer]); processed_data += data_to_buffer; if self.buffer.len() == self.buffer.capacity() { - let pcm = self.pcm.as_mut().unwrap(); - let io = pcm.io_i16().unwrap(); - match io.writei(&self.buffer) { - Ok(_) => (), - Err(err) => pcm.try_recover(err, false).unwrap(), - } + self.write_buf(); self.buffer.clear(); } } @@ -150,3 +154,14 @@ impl Sink for AlsaSink { Ok(()) } } + +impl AlsaSink { + fn write_buf(&mut self) { + 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(), + }; + } +} diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 4678bfb..93b718d 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -1,8 +1,13 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; + +use gstreamer as gst; +use gstreamer_app as gst_app; + use gst::prelude::*; -use gst::*; -use zerocopy::*; +use zerocopy::AsBytes; use std::sync::mpsc::{sync_channel, SyncSender}; use std::{io, thread}; @@ -11,12 +16,27 @@ use std::{io, thread}; pub struct GstreamerSink { tx: SyncSender>, pipeline: gst::Pipeline, + format: AudioFormat, } impl Open for GstreamerSink { - fn open(device: Option) -> GstreamerSink { - gst::init().expect("Failed to init gstreamer!"); - let pipeline_str_preamble = r#"appsrc caps="audio/x-raw,format=S16LE,layout=interleaved,channels=2,rate=44100" block=true max-bytes=4096 name=appsrc0 "#; + fn open(device: Option, format: AudioFormat) -> Self { + info!("Using GStreamer sink with format: {:?}", format); + gst::init().expect("failed to init GStreamer!"); + + // GStreamer calls S24 and S24_3 different from the rest of the world + let gst_format = match format { + AudioFormat::S24 => "S24_32".to_string(), + AudioFormat::S24_3 => "S24".to_string(), + _ => format!("{:?}", format), + }; + let sample_size = format.size(); + let gst_bytes = 2048 * sample_size; + + let pipeline_str_preamble = format!( + "appsrc caps=\"audio/x-raw,format={}LE,layout=interleaved,channels={},rate={}\" block=true max-bytes={} name=appsrc0 ", + gst_format, NUM_CHANNELS, SAMPLE_RATE, gst_bytes + ); let pipeline_str_rest = r#" ! audioconvert ! autoaudiosink"#; let pipeline_str: String = match device { Some(x) => format!("{}{}", pipeline_str_preamble, x), @@ -28,38 +48,37 @@ impl Open for GstreamerSink { let pipelinee = gst::parse_launch(&*pipeline_str).expect("Couldn't launch pipeline; likely a GStreamer issue or an error in the pipeline string you specified in the 'device' argument to librespot."); let pipeline = pipelinee .dynamic_cast::() - .expect("Couldn't cast pipeline element at runtime!"); - let bus = pipeline.get_bus().expect("Couldn't get bus from pipeline"); + .expect("couldn't cast pipeline element at runtime!"); + let bus = pipeline.get_bus().expect("couldn't get bus from pipeline"); let mainloop = glib::MainLoop::new(None, false); let appsrce: gst::Element = pipeline .get_by_name("appsrc0") - .expect("Couldn't get appsrc from pipeline"); + .expect("couldn't get appsrc from pipeline"); let appsrc: gst_app::AppSrc = appsrce .dynamic_cast::() - .expect("Couldn't cast AppSrc element at runtime!"); + .expect("couldn't cast AppSrc element at runtime!"); let bufferpool = gst::BufferPool::new(); - let appsrc_caps = appsrc.get_caps().expect("Couldn't get appsrc caps"); + let appsrc_caps = appsrc.get_caps().expect("couldn't get appsrc caps"); let mut conf = bufferpool.get_config(); - conf.set_params(Some(&appsrc_caps), 8192, 0, 0); + conf.set_params(Some(&appsrc_caps), 4096 * sample_size as u32, 0, 0); bufferpool .set_config(conf) - .expect("Couldn't configure the buffer pool"); + .expect("couldn't configure the buffer pool"); bufferpool .set_active(true) - .expect("Couldn't activate buffer pool"); + .expect("couldn't activate buffer pool"); - let (tx, rx) = sync_channel::>(128); + let (tx, rx) = sync_channel::>(64 * sample_size); thread::spawn(move || { for data in rx { let buffer = bufferpool.acquire_buffer(None); - if !buffer.is_err() { - let mut okbuffer = buffer.unwrap(); - let mutbuf = okbuffer.make_mut(); + if let Ok(mut buffer) = buffer { + let mutbuf = buffer.make_mut(); mutbuf.set_size(data.len()); mutbuf .copy_from_slice(0, data.as_bytes()) .expect("Failed to copy from slice"); - let _eat = appsrc.push_buffer(okbuffer); + let _eat = appsrc.push_buffer(buffer); } } }); @@ -69,8 +88,8 @@ impl Open for GstreamerSink { let watch_mainloop = thread_mainloop.clone(); bus.add_watch(move |_, msg| { match msg.view() { - MessageView::Eos(..) => watch_mainloop.quit(), - MessageView::Error(err) => { + gst::MessageView::Eos(..) => watch_mainloop.quit(), + gst::MessageView::Error(err) => { println!( "Error from {:?}: {} ({:?})", err.get_src().map(|s| s.get_path_string()), @@ -84,30 +103,32 @@ impl Open for GstreamerSink { glib::Continue(true) }) - .expect("Failed to add bus watch"); + .expect("failed to add bus watch"); thread_mainloop.run(); }); pipeline .set_state(gst::State::Playing) - .expect("Unable to set the pipeline to the `Playing` state"); + .expect("unable to set the pipeline to the `Playing` state"); - GstreamerSink { tx, pipeline } + Self { + tx, + pipeline, + format, + } } } impl Sink for GstreamerSink { - fn start(&mut self) -> io::Result<()> { - Ok(()) - } - fn stop(&mut self) -> io::Result<()> { - Ok(()) - } - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + start_stop_noop!(); + sink_as_bytes!(); +} + +impl SinkAsBytes for GstreamerSink { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { // Copy expensively (in to_vec()) to avoid thread synchronization - let deighta: &[u8] = packet.samples().as_bytes(); self.tx - .send(deighta.to_vec()) + .send(data.to_vec()) .expect("tx send failed in write function"); Ok(()) } diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index e659d54..aca2edd 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -1,5 +1,7 @@ use super::{Open, Sink}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; +use crate::player::NUM_CHANNELS; use jack::{ AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope, }; @@ -7,20 +9,18 @@ use std::io; use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; pub struct JackSink { - send: SyncSender, + send: SyncSender, + // We have to keep hold of this object, or the Sink can't play... + #[allow(dead_code)] active_client: AsyncClient<(), JackData>, } pub struct JackData { - rec: Receiver, + rec: Receiver, port_l: Port, port_r: Port, } -fn pcm_to_f32(sample: i16) -> f32 { - sample as f32 / 32768.0 -} - impl ProcessHandler for JackData { fn process(&mut self, _: &Client, ps: &ProcessScope) -> Control { // get output port buffers @@ -33,24 +33,27 @@ impl ProcessHandler for JackData { let buf_size = buf_r.len(); for i in 0..buf_size { - buf_r[i] = pcm_to_f32(queue_iter.next().unwrap_or(0)); - buf_l[i] = pcm_to_f32(queue_iter.next().unwrap_or(0)); + buf_r[i] = queue_iter.next().unwrap_or(0.0); + buf_l[i] = queue_iter.next().unwrap_or(0.0); } Control::Continue } } impl Open for JackSink { - fn open(client_name: Option) -> JackSink { - info!("Using jack sink!"); + fn open(client_name: Option, format: AudioFormat) -> Self { + if format != AudioFormat::F32 { + warn!("JACK currently does not support {:?} output", format); + } + info!("Using JACK sink with format {:?}", AudioFormat::F32); - let client_name = client_name.unwrap_or("librespot".to_string()); + let client_name = client_name.unwrap_or_else(|| "librespot".to_string()); let (client, _status) = Client::new(&client_name[..], ClientOptions::NO_START_SERVER).unwrap(); let ch_r = client.register_port("out_0", AudioOut::default()).unwrap(); let ch_l = client.register_port("out_1", AudioOut::default()).unwrap(); // buffer for samples from librespot (~10ms) - let (tx, rx) = sync_channel(2 * 1024 * 4); + let (tx, rx) = sync_channel::(NUM_CHANNELS as usize * 1024 * AudioFormat::F32.size()); let jack_data = JackData { rec: rx, port_l: ch_l, @@ -58,7 +61,7 @@ impl Open for JackSink { }; let active_client = AsyncClient::new(client, (), jack_data).unwrap(); - JackSink { + Self { send: tx, active_client, } @@ -66,19 +69,13 @@ impl Open for JackSink { } impl Sink for JackSink { - fn start(&mut self) -> io::Result<()> { - Ok(()) - } - - fn stop(&mut self) -> io::Result<()> { - Ok(()) - } + start_stop_noop!(); fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { for s in packet.samples().iter() { let res = self.send.send(*s); if res.is_err() { - error!("jackaudio: cannot write to channel"); + error!("cannot write to channel"); } } Ok(()) diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index c816a6d..72659f1 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -1,8 +1,9 @@ use crate::audio::AudioPacket; +use crate::config::AudioFormat; use std::io; pub trait Open { - fn open(_: Option) -> Self; + fn open(_: Option, format: AudioFormat) -> Self; } pub trait Sink { @@ -11,10 +12,57 @@ pub trait Sink { fn write(&mut self, packet: &AudioPacket) -> io::Result<()>; } -pub type SinkBuilder = fn(Option) -> Box; +pub type SinkBuilder = fn(Option, AudioFormat) -> Box; -fn mk_sink(device: Option) -> Box { - Box::new(S::open(device)) +pub trait SinkAsBytes { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()>; +} + +fn mk_sink(device: Option, format: AudioFormat) -> Box { + Box::new(S::open(device, format)) +} + +// reuse code for various backends +macro_rules! sink_as_bytes { + () => { + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { + use crate::audio::convert::{self, i24}; + 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] = &convert::to_s32(samples); + self.write_bytes(samples_s32.as_bytes()) + } + AudioFormat::S24 => { + let samples_s24: &[i32] = &convert::to_s24(samples); + self.write_bytes(samples_s24.as_bytes()) + } + AudioFormat::S24_3 => { + let samples_s24_3: &[i24] = &convert::to_s24_3(samples); + self.write_bytes(samples_s24_3.as_bytes()) + } + AudioFormat::S16 => { + let samples_s16: &[i16] = &convert::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")] diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 9fcd09f..4c6f27c 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -1,46 +1,36 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; use std::fs::OpenOptions; use std::io::{self, Write}; -use std::mem; -use std::slice; -pub struct StdoutSink(Box); +pub struct StdoutSink { + output: Box, + format: AudioFormat, +} impl Open for StdoutSink { - fn open(path: Option) -> StdoutSink { - if let Some(path) = path { - let file = OpenOptions::new().write(true).open(path).unwrap(); - StdoutSink(Box::new(file)) - } else { - StdoutSink(Box::new(io::stdout())) - } + fn open(path: Option, format: AudioFormat) -> Self { + info!("Using pipe sink with format: {:?}", format); + + let output: Box = match path { + Some(path) => Box::new(OpenOptions::new().write(true).open(path).unwrap()), + _ => Box::new(io::stdout()), + }; + + Self { output, format } } } impl Sink for StdoutSink { - fn start(&mut self) -> io::Result<()> { - Ok(()) - } - - fn stop(&mut self) -> io::Result<()> { - Ok(()) - } - - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - let data: &[u8] = match packet { - AudioPacket::Samples(data) => unsafe { - slice::from_raw_parts( - data.as_ptr() as *const u8, - data.len() * mem::size_of::(), - ) - }, - AudioPacket::OggData(data) => data, - }; - - self.0.write_all(data)?; - self.0.flush()?; + start_stop_noop!(); + sink_as_bytes!(); +} +impl SinkAsBytes for StdoutSink { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { + self.output.write_all(data)?; + self.output.flush()?; Ok(()) } } diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 0e25021..234a9af 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -1,16 +1,27 @@ use super::{Open, Sink}; -use crate::audio::AudioPacket; -use portaudio_rs; +use crate::audio::{convert, AudioPacket}; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo}; use portaudio_rs::stream::*; use std::io; use std::process::exit; use std::time::Duration; -pub struct PortAudioSink<'a>( - Option>, - StreamParameters, -); +pub enum PortAudioSink<'a> { + F32( + Option>, + StreamParameters, + ), + S32( + Option>, + StreamParameters, + ), + S16( + Option>, + StreamParameters, + ), +} fn output_devices() -> Box> { let count = portaudio_rs::device::get_count().unwrap(); @@ -40,8 +51,11 @@ fn find_output(device: &str) -> Option { } impl<'a> Open for PortAudioSink<'a> { - fn open(device: Option) -> PortAudioSink<'a> { - debug!("Using PortAudio sink"); + fn open(device: Option, format: AudioFormat) -> PortAudioSink<'a> { + info!("Using PortAudio sink with format: {:?}", format); + + warn!("This backend is known to panic on several platforms."); + warn!("Consider using some other backend, or better yet, contributing a fix."); portaudio_rs::initialize().unwrap(); @@ -53,7 +67,7 @@ impl<'a> Open for PortAudioSink<'a> { Some(device) => find_output(device), None => get_default_output_index(), } - .expect("Could not find device"); + .expect("could not find device"); let info = portaudio_rs::device::get_info(device_idx); let latency = match info { @@ -61,46 +75,99 @@ impl<'a> Open for PortAudioSink<'a> { None => Duration::new(0, 0), }; - let params = StreamParameters { - device: device_idx, - channel_count: 2, - suggested_latency: latency, - data: 0i16, - }; - - PortAudioSink(None, params) + macro_rules! open_sink { + ($sink: expr, $type: ty) => {{ + let params = StreamParameters { + device: device_idx, + channel_count: NUM_CHANNELS as u32, + suggested_latency: latency, + data: 0.0 as $type, + }; + $sink(None, params) + }}; + } + match format { + AudioFormat::F32 => open_sink!(Self::F32, f32), + AudioFormat::S32 => open_sink!(Self::S32, i32), + AudioFormat::S16 => open_sink!(Self::S16, i16), + _ => { + unimplemented!("PortAudio currently does not support {:?} output", format) + } + } } } impl<'a> Sink for PortAudioSink<'a> { fn start(&mut self) -> io::Result<()> { - if self.0.is_none() { - self.0 = Some( - Stream::open( - None, - Some(self.1), - 44100.0, - FRAMES_PER_BUFFER_UNSPECIFIED, - StreamFlags::empty(), - None, - ) - .unwrap(), - ); + macro_rules! start_sink { + (ref mut $stream: ident, ref $parameters: ident) => {{ + if $stream.is_none() { + *$stream = Some( + Stream::open( + None, + Some(*$parameters), + SAMPLE_RATE as f64, + FRAMES_PER_BUFFER_UNSPECIFIED, + StreamFlags::empty(), + None, + ) + .unwrap(), + ); + } + $stream.as_mut().unwrap().start().unwrap() + }}; } - self.0.as_mut().unwrap().start().unwrap(); + match self { + Self::F32(stream, parameters) => start_sink!(ref mut stream, ref parameters), + Self::S32(stream, parameters) => start_sink!(ref mut stream, ref parameters), + Self::S16(stream, parameters) => start_sink!(ref mut stream, ref parameters), + }; + Ok(()) } + fn stop(&mut self) -> io::Result<()> { - self.0.as_mut().unwrap().stop().unwrap(); - self.0 = None; + macro_rules! stop_sink { + (ref mut $stream: ident) => {{ + $stream.as_mut().unwrap().stop().unwrap(); + *$stream = None; + }}; + } + match self { + Self::F32(stream, _parameters) => stop_sink!(ref mut stream), + Self::S32(stream, _parameters) => stop_sink!(ref mut stream), + Self::S16(stream, _parameters) => stop_sink!(ref mut stream), + }; + Ok(()) } + fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - match self.0.as_mut().unwrap().write(packet.samples()) { + macro_rules! write_sink { + (ref mut $stream: expr, $samples: expr) => { + $stream.as_mut().unwrap().write($samples) + }; + } + + let samples = packet.samples(); + let result = match self { + Self::F32(stream, _parameters) => { + write_sink!(ref mut stream, samples) + } + Self::S32(stream, _parameters) => { + let samples_s32: &[i32] = &convert::to_s32(samples); + write_sink!(ref mut stream, samples_s32) + } + Self::S16(stream, _parameters) => { + let samples_s16: &[i16] = &convert::to_s16(samples); + write_sink!(ref mut stream, samples_s16) + } + }; + match result { Ok(_) => (), Err(portaudio_rs::PaError::OutputUnderflowed) => error!("PortAudio write underflow"), - Err(e) => panic!("PA Error {}", e), + Err(e) => panic!("PortAudio error {}", e), }; Ok(()) diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index bc2be90..b165c0b 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -1,5 +1,7 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use libpulse_binding::{self as pulse, stream::Direction}; use libpulse_simple_binding::Simple; use std::io; @@ -11,23 +13,34 @@ pub struct PulseAudioSink { s: Option, ss: pulse::sample::Spec, device: Option, + format: AudioFormat, } impl Open for PulseAudioSink { - fn open(device: Option) -> PulseAudioSink { - debug!("Using PulseAudio sink"); + fn open(device: Option, format: AudioFormat) -> Self { + info!("Using PulseAudio sink with format: {:?}", format); + + // PulseAudio calls S24 and S24_3 different from the rest of the world + let pulse_format = match format { + AudioFormat::F32 => pulse::sample::Format::F32le, + AudioFormat::S32 => pulse::sample::Format::S32le, + AudioFormat::S24 => pulse::sample::Format::S24_32le, + AudioFormat::S24_3 => pulse::sample::Format::S24le, + AudioFormat::S16 => pulse::sample::Format::S16le, + }; let ss = pulse::sample::Spec { - format: pulse::sample::Format::S16le, - channels: 2, // stereo - rate: 44100, + format: pulse_format, + channels: NUM_CHANNELS, + rate: SAMPLE_RATE, }; debug_assert!(ss.is_valid()); - PulseAudioSink { + Self { s: None, ss, device, + format, } } } @@ -66,19 +79,13 @@ impl Sink for PulseAudioSink { Ok(()) } - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - if let Some(s) = &self.s { - // SAFETY: An i16 consists of two bytes, so that the given slice can be interpreted - // as a byte array of double length. Each byte pointer is validly aligned, and so - // is the newly created slice. - let d: &[u8] = unsafe { - std::slice::from_raw_parts( - packet.samples().as_ptr() as *const u8, - packet.samples().len() * 2, - ) - }; + sink_as_bytes!(); +} - match s.write(d) { +impl SinkAsBytes for PulseAudioSink { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { + if let Some(s) = &self.s { + match s.write(data) { Ok(_) => Ok(()), Err(e) => Err(io::Error::new( io::ErrorKind::BrokenPipe, @@ -88,7 +95,7 @@ impl Sink for PulseAudioSink { } else { Err(io::Error::new( io::ErrorKind::NotConnected, - "Not connected to pulseaudio", + "Not connected to PulseAudio", )) } } diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 338dfbb..65436a3 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -1,12 +1,13 @@ use std::process::exit; -use std::{convert::Infallible, sync::mpsc}; use std::{io, thread, time}; use cpal::traits::{DeviceTrait, HostTrait}; use thiserror::Error; use super::Sink; -use crate::audio::AudioPacket; +use crate::audio::{convert, AudioPacket}; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; #[cfg(all( feature = "rodiojack-backend", @@ -15,15 +16,16 @@ use crate::audio::AudioPacket; compile_error!("Rodio JACK backend is currently only supported on linux."); #[cfg(feature = "rodio-backend")] -pub fn mk_rodio(device: Option) -> Box { - Box::new(open(cpal::default_host(), device)) +pub fn mk_rodio(device: Option, format: AudioFormat) -> Box { + Box::new(open(cpal::default_host(), device, format)) } #[cfg(feature = "rodiojack-backend")] -pub fn mk_rodiojack(device: Option) -> Box { +pub fn mk_rodiojack(device: Option, format: AudioFormat) -> Box { Box::new(open( cpal::host_from_id(cpal::HostId::Jack).unwrap(), device, + format, )) } @@ -43,8 +45,8 @@ pub enum RodioError { pub struct RodioSink { rodio_sink: rodio::Sink, - // will produce a TryRecvError on the receiver side when it is dropped. - _close_tx: mpsc::SyncSender, + format: AudioFormat, + _stream: rodio::OutputStream, } fn list_formats(device: &rodio::Device) { @@ -149,52 +151,54 @@ fn create_sink( Ok((sink, stream)) } -pub fn open(host: cpal::Host, device: Option) -> RodioSink { - debug!("Using rodio sink with cpal host: {}", host.id().name()); +pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> RodioSink { + debug!( + "Using rodio sink with format {:?} and cpal host: {}", + format, + host.id().name() + ); - let (sink_tx, sink_rx) = mpsc::sync_channel(1); - let (close_tx, close_rx) = mpsc::sync_channel(1); - - std::thread::spawn(move || match create_sink(&host, device) { - Ok((sink, stream)) => { - sink_tx.send(Ok(sink)).unwrap(); - - close_rx.recv().unwrap_err(); // This will fail as soon as the sender is dropped - debug!("drop rodio::OutputStream"); - drop(stream); + match format { + AudioFormat::F32 => { + #[cfg(target_os = "linux")] + warn!("Rodio output to Alsa is known to cause garbled sound, consider using `--backend alsa`") } - Err(e) => { - sink_tx.send(Err(e)).unwrap(); - } - }); + AudioFormat::S16 => (), + _ => unimplemented!("Rodio currently only supports F32 and S16 formats"), + } - // Instead of the second `unwrap`, better error handling could be introduced - let sink = sink_rx.recv().unwrap().unwrap(); + let (sink, stream) = create_sink(&host, device).unwrap(); debug!("Rodio sink was created"); RodioSink { rodio_sink: sink, - _close_tx: close_tx, + format, + _stream: stream, } } 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(()) - } + start_stop_noop!(); fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - let source = rodio::buffer::SamplesBuffer::new(2, 44100, packet.samples()); - self.rodio_sink.append(source); + 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] = &convert::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: diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 47cd225..2956653 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -1,57 +1,112 @@ use super::{Open, Sink}; -use crate::audio::AudioPacket; +use crate::audio::{convert, AudioPacket}; +use crate::config::AudioFormat; +use crate::player::{NUM_CHANNELS, SAMPLE_RATE}; use sdl2::audio::{AudioQueue, AudioSpecDesired}; use std::{io, thread, time}; -type Channel = i16; - -pub struct SdlSink { - queue: AudioQueue, +pub enum SdlSink { + F32(AudioQueue), + S32(AudioQueue), + S16(AudioQueue), } impl Open for SdlSink { - fn open(device: Option) -> SdlSink { - debug!("Using SDL sink"); + fn open(device: Option, format: AudioFormat) -> Self { + info!("Using SDL sink with format: {:?}", format); if device.is_some() { - panic!("SDL sink does not support specifying a device name"); + warn!("SDL sink does not support specifying a device name"); } - let ctx = sdl2::init().expect("Could not init SDL"); - let audio = ctx.audio().expect("Could not init SDL audio subsystem"); + let ctx = sdl2::init().expect("could not initialize SDL"); + let audio = ctx + .audio() + .expect("could not initialize SDL audio subsystem"); let desired_spec = AudioSpecDesired { - freq: Some(44_100), - channels: Some(2), + freq: Some(SAMPLE_RATE as i32), + channels: Some(NUM_CHANNELS), samples: None, }; - let queue = audio - .open_queue(None, &desired_spec) - .expect("Could not open SDL audio device"); - SdlSink { queue } + macro_rules! open_sink { + ($sink: expr, $type: ty) => {{ + let queue: AudioQueue<$type> = audio + .open_queue(None, &desired_spec) + .expect("could not open SDL audio device"); + $sink(queue) + }}; + } + match format { + AudioFormat::F32 => open_sink!(Self::F32, f32), + AudioFormat::S32 => open_sink!(Self::S32, i32), + AudioFormat::S16 => open_sink!(Self::S16, i16), + _ => { + unimplemented!("SDL currently does not support {:?} output", format) + } + } } } impl Sink for SdlSink { fn start(&mut self) -> io::Result<()> { - self.queue.clear(); - self.queue.resume(); + macro_rules! start_sink { + ($queue: expr) => {{ + $queue.clear(); + $queue.resume(); + }}; + } + match self { + Self::F32(queue) => start_sink!(queue), + Self::S32(queue) => start_sink!(queue), + Self::S16(queue) => start_sink!(queue), + }; Ok(()) } fn stop(&mut self) -> io::Result<()> { - self.queue.pause(); - self.queue.clear(); + macro_rules! stop_sink { + ($queue: expr) => {{ + $queue.pause(); + $queue.clear(); + }}; + } + match self { + Self::F32(queue) => stop_sink!(queue), + Self::S32(queue) => stop_sink!(queue), + Self::S16(queue) => stop_sink!(queue), + }; Ok(()) } fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - while self.queue.size() > (2 * 2 * 44_100) { - // sleep and wait for sdl thread to drain the queue a bit - thread::sleep(time::Duration::from_millis(10)); + macro_rules! drain_sink { + ($queue: expr, $size: expr) => {{ + // sleep and wait for sdl thread to drain the queue a bit + while $queue.size() > (NUM_CHANNELS as u32 * $size as u32 * SAMPLE_RATE) { + thread::sleep(time::Duration::from_millis(10)); + } + }}; } - self.queue.queue(packet.samples()); + + let samples = packet.samples(); + match self { + Self::F32(queue) => { + drain_sink!(queue, AudioFormat::F32.size()); + queue.queue(samples) + } + Self::S32(queue) => { + let samples_s32: &[i32] = &convert::to_s32(samples); + drain_sink!(queue, AudioFormat::S32.size()); + queue.queue(samples_s32) + } + Self::S16(queue) => { + let samples_s16: &[i16] = &convert::to_s16(samples); + drain_sink!(queue, AudioFormat::S16.size()); + queue.queue(samples_s16) + } + }; Ok(()) } } diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index 9b24e21..b4af1b4 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -1,24 +1,26 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkAsBytes}; use crate::audio::AudioPacket; - +use crate::config::AudioFormat; use shell_words::split; use std::io::{self, Write}; -use std::mem; use std::process::{Child, Command, Stdio}; -use std::slice; pub struct SubprocessSink { shell_command: String, child: Option, + format: AudioFormat, } impl Open for SubprocessSink { - fn open(shell_command: Option) -> SubprocessSink { + fn open(shell_command: Option, format: AudioFormat) -> Self { + info!("Using subprocess sink with format: {:?}", format); + if let Some(shell_command) = shell_command { SubprocessSink { shell_command, child: None, + format, } } else { panic!("subprocess sink requires specifying a shell command"); @@ -46,16 +48,15 @@ impl Sink for SubprocessSink { Ok(()) } - fn write(&mut self, packet: &AudioPacket) -> io::Result<()> { - let data: &[u8] = unsafe { - slice::from_raw_parts( - packet.samples().as_ptr() as *const u8, - packet.samples().len() * mem::size_of::(), - ) - }; + sink_as_bytes!(); +} + +impl SinkAsBytes for SubprocessSink { + fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { if let Some(child) = &mut self.child { let child_stdin = child.stdin.as_mut().unwrap(); child_stdin.write_all(data)?; + child_stdin.flush()?; } Ok(()) } diff --git a/playback/src/config.rs b/playback/src/config.rs index 31f6362..f8f0289 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,3 +1,6 @@ +use crate::audio::convert::i24; +use std::convert::TryFrom; +use std::mem; use std::str::FromStr; #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] @@ -11,17 +14,58 @@ impl FromStr for Bitrate { type Err = (); fn from_str(s: &str) -> Result { match s { - "96" => Ok(Bitrate::Bitrate96), - "160" => Ok(Bitrate::Bitrate160), - "320" => Ok(Bitrate::Bitrate320), + "96" => Ok(Self::Bitrate96), + "160" => Ok(Self::Bitrate160), + "320" => Ok(Self::Bitrate320), _ => Err(()), } } } impl Default for Bitrate { - fn default() -> Bitrate { - Bitrate::Bitrate160 + fn default() -> Self { + Self::Bitrate160 + } +} + +#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] +pub enum AudioFormat { + F32, + S32, + S24, + S24_3, + S16, +} + +impl TryFrom<&String> for AudioFormat { + type Error = (); + fn try_from(s: &String) -> Result { + match s.to_uppercase().as_str() { + "F32" => Ok(Self::F32), + "S32" => Ok(Self::S32), + "S24" => Ok(Self::S24), + "S24_3" => Ok(Self::S24_3), + "S16" => Ok(Self::S16), + _ => Err(()), + } + } +} + +impl Default for AudioFormat { + fn default() -> Self { + Self::S16 + } +} + +impl AudioFormat { + // not used by all backends + #[allow(dead_code)] + pub fn size(&self) -> usize { + match self { + Self::S24_3 => mem::size_of::(), + Self::S16 => mem::size_of::(), + _ => mem::size_of::(), // S32 and S24 are both stored in i32 + } } } @@ -35,16 +79,39 @@ impl FromStr for NormalisationType { type Err = (); fn from_str(s: &str) -> Result { match s { - "album" => Ok(NormalisationType::Album), - "track" => Ok(NormalisationType::Track), + "album" => Ok(Self::Album), + "track" => Ok(Self::Track), _ => Err(()), } } } impl Default for NormalisationType { - fn default() -> NormalisationType { - NormalisationType::Album + fn default() -> Self { + Self::Album + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum NormalisationMethod { + Basic, + Dynamic, +} + +impl FromStr for NormalisationMethod { + type Err = (); + fn from_str(s: &str) -> Result { + match s { + "basic" => Ok(Self::Basic), + "dynamic" => Ok(Self::Dynamic), + _ => Err(()), + } + } +} + +impl Default for NormalisationMethod { + fn default() -> Self { + Self::Dynamic } } @@ -53,7 +120,12 @@ pub struct PlayerConfig { pub bitrate: Bitrate, pub normalisation: bool, pub normalisation_type: NormalisationType, + pub normalisation_method: NormalisationMethod, pub normalisation_pregain: f32, + pub normalisation_threshold: f32, + pub normalisation_attack: f32, + pub normalisation_release: f32, + pub normalisation_knee: f32, pub gapless: bool, pub passthrough: bool, } @@ -64,7 +136,12 @@ impl Default for PlayerConfig { bitrate: Bitrate::default(), normalisation: false, normalisation_type: NormalisationType::default(), + normalisation_method: NormalisationMethod::default(), normalisation_pregain: 0.0, + normalisation_threshold: -1.0, + normalisation_attack: 0.005, + normalisation_release: 0.1, + normalisation_knee: 1.0, gapless: true, passthrough: false, } diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index d9dbe31..5e0a963 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -33,13 +33,12 @@ impl AlsaMixer { let mixer = alsa::mixer::Mixer::new(&config.card, false)?; let sid = alsa::mixer::SelemId::new(&config.mixer, config.index); - let selem = mixer.find_selem(&sid).expect( - format!( + let selem = mixer.find_selem(&sid).unwrap_or_else(|| { + panic!( "Couldn't find simple mixer control for {},{}", &config.mixer, &config.index, ) - .as_str(), - ); + }); let (min, max) = selem.get_playback_volume_range(); let (min_db, max_db) = selem.get_playback_db_range(); let hw_mix = selem diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index 9d9a817..af41c6f 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -12,7 +12,7 @@ pub trait Mixer: Send { } pub trait AudioFilter { - fn modify_stream(&self, data: &mut [i16]); + fn modify_stream(&self, data: &mut [f32]); } #[cfg(feature = "alsa-backend")] diff --git a/playback/src/mixer/softmixer.rs b/playback/src/mixer/softmixer.rs index 28e1cf5..ec8ed6b 100644 --- a/playback/src/mixer/softmixer.rs +++ b/playback/src/mixer/softmixer.rs @@ -35,11 +35,12 @@ struct SoftVolumeApplier { } impl AudioFilter for SoftVolumeApplier { - fn modify_stream(&self, data: &mut [i16]) { + fn modify_stream(&self, data: &mut [f32]) { let volume = self.volume.load(Ordering::Relaxed) as u16; if volume != 0xFFFF { + let volume_factor = volume as f64 / 0xFFFF as f64; for x in data.iter_mut() { - *x = (*x as i32 * volume as i32 / 0xFFFF) as i16; + *x = (*x as f64 * volume_factor) as f32; } } } diff --git a/playback/src/player.rs b/playback/src/player.rs index 0d2380e..3f0778f 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -18,15 +18,19 @@ use crate::audio::{ READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS, }; use crate::audio_backend::Sink; -use crate::config::NormalisationType; -use crate::config::{Bitrate, PlayerConfig}; +use crate::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}; use crate::core::session::Session; use crate::core::spotify_id::SpotifyId; use crate::core::util::SeqGenerator; use crate::metadata::{AudioItem, FileFormat}; use crate::mixer::AudioFilter; +pub const SAMPLE_RATE: u32 = 44100; +pub const NUM_CHANNELS: u8 = 2; +pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE as u32 * NUM_CHANNELS as u32; + const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; +const DB_VOLTAGE_RATIO: f32 = 20.0; pub struct Player { commands: Option>, @@ -50,11 +54,18 @@ struct PlayerInternal { state: PlayerState, preload: PlayerPreload, - sink: Box, + sink: Box, sink_status: SinkStatus, sink_event_callback: Option, audio_filter: Option>, event_senders: Vec>, + + limiter_active: bool, + limiter_attack_counter: u32, + limiter_release_counter: u32, + limiter_peak_sample: f32, + limiter_factor: f32, + limiter_strength: f32, } enum PlayerCommand { @@ -186,7 +197,7 @@ impl PlayerEvent { pub type PlayerEventChannel = mpsc::UnboundedReceiver; #[derive(Clone, Copy, Debug)] -struct NormalisationData { +pub struct NormalisationData { track_gain_db: f32, track_peak: f32, album_gain_db: f32, @@ -194,6 +205,14 @@ struct NormalisationData { } impl NormalisationData { + pub fn db_to_ratio(db: f32) -> f32 { + f32::powf(10.0, db / DB_VOLTAGE_RATIO) + } + + pub fn ratio_to_db(ratio: f32) -> f32 { + ratio.log10() * DB_VOLTAGE_RATIO + } + fn parse_from_file(mut file: T) -> io::Result { const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144; file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET))?; @@ -218,17 +237,41 @@ impl NormalisationData { NormalisationType::Album => [data.album_gain_db, data.album_peak], NormalisationType::Track => [data.track_gain_db, data.track_peak], }; - let mut normalisation_factor = - f32::powf(10.0, (gain_db + config.normalisation_pregain) / 20.0); - if normalisation_factor * gain_peak > 1.0 { - warn!("Reducing normalisation factor to prevent clipping. Please add negative pregain to avoid."); - normalisation_factor = 1.0 / gain_peak; + let normalisation_power = gain_db + config.normalisation_pregain; + let mut normalisation_factor = Self::db_to_ratio(normalisation_power); + + if normalisation_factor * gain_peak > config.normalisation_threshold { + let limited_normalisation_factor = config.normalisation_threshold / gain_peak; + let limited_normalisation_power = Self::ratio_to_db(limited_normalisation_factor); + + if config.normalisation_method == NormalisationMethod::Basic { + warn!("Limiting gain to {:.2} for the duration of this track to stay under normalisation threshold.", limited_normalisation_power); + normalisation_factor = limited_normalisation_factor; + } else { + warn!( + "This track will at its peak be subject to {:.2} dB of dynamic limiting.", + normalisation_power - limited_normalisation_power + ); + } + + warn!("Please lower pregain to avoid."); } debug!("Normalisation Data: {:?}", data); debug!("Normalisation Type: {:?}", config.normalisation_type); - debug!("Applied normalisation factor: {}", normalisation_factor); + debug!( + "Normalisation Threshold: {:.1}", + Self::ratio_to_db(config.normalisation_threshold) + ); + debug!("Normalisation Method: {:?}", config.normalisation_method); + debug!("Normalisation Factor: {}", normalisation_factor); + + if config.normalisation_method == NormalisationMethod::Dynamic { + debug!("Normalisation Attack: {:?}", config.normalisation_attack); + debug!("Normalisation Release: {:?}", config.normalisation_release); + debug!("Normalisation Knee: {:?}", config.normalisation_knee); + } normalisation_factor } @@ -242,7 +285,7 @@ impl Player { sink_builder: F, ) -> (Player, PlayerEventChannel) where - F: FnOnce() -> Box + Send + 'static, + F: FnOnce() -> Box + Send + 'static, { let (cmd_tx, cmd_rx) = mpsc::unbounded_channel(); let (event_sender, event_receiver) = mpsc::unbounded_channel(); @@ -262,6 +305,13 @@ impl Player { sink_event_callback: None, audio_filter, event_senders: [event_sender].to_vec(), + + limiter_active: false, + limiter_attack_counter: 0, + limiter_release_counter: 0, + limiter_peak_sample: 0.0, + limiter_factor: 1.0, + limiter_strength: 0.0, }; // While PlayerInternal is written as a future, it still contains blocking code. @@ -888,7 +938,8 @@ impl Future for PlayerInternal { if !passthrough { if let Some(ref packet) = packet { - *stream_position_pcm += (packet.samples().len() / 2) as u64; + *stream_position_pcm += + (packet.samples().len() / NUM_CHANNELS as usize) as u64; let stream_position_millis = Self::position_pcm_to_ms(*stream_position_pcm); @@ -1110,10 +1161,115 @@ impl PlayerInternal { } if self.config.normalisation - && f32::abs(normalisation_factor - 1.0) > f32::EPSILON + && (f32::abs(normalisation_factor - 1.0) < f32::EPSILON + || self.config.normalisation_method != NormalisationMethod::Basic) { - for x in data.iter_mut() { - *x = (*x as f32 * normalisation_factor) as i16; + 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; + } } } } @@ -1144,6 +1300,15 @@ impl PlayerInternal { } } + fn reset_limiter(&mut self) { + self.limiter_active = false; + self.limiter_release_counter = 0; + self.limiter_attack_counter = 0; + self.limiter_peak_sample = 0.0; + self.limiter_factor = 1.0; + self.limiter_strength = 0.0; + } + fn start_playback( &mut self, track_id: SpotifyId, diff --git a/src/main.rs b/src/main.rs index 5b445b2..78e7e2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,13 +12,16 @@ use librespot::core::config::{ConnectConfig, DeviceType, SessionConfig, VolumeCt use librespot::core::session::Session; use librespot::core::version; use librespot::playback::audio_backend::{self, Sink, BACKENDS}; -use librespot::playback::config::{Bitrate, NormalisationType, PlayerConfig}; +use librespot::playback::config::{ + AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, +}; use librespot::playback::mixer::{self, Mixer, MixerConfig}; -use librespot::playback::player::Player; +use librespot::playback::player::{NormalisationData, Player}; mod player_event_handler; use player_event_handler::{emit_sink_event, run_program_on_events}; +use std::convert::TryFrom; use std::path::Path; use std::process::exit; use std::str::FromStr; @@ -28,6 +31,8 @@ use std::{ pin::Pin, }; +const MILLIS: f32 = 1000.0; + fn device_id(name: &str) -> String { hex::encode(Sha1::digest(name.as_bytes())) } @@ -93,9 +98,20 @@ pub fn get_credentials Option>( } } +fn print_version() { + println!( + "librespot {semver} {sha} (Built on {build_date}, Build ID: {build_id})", + semver = version::SEMVER, + sha = version::SHA_SHORT, + build_date = version::BUILD_DATE, + build_id = version::BUILD_ID + ); +} + #[derive(Clone)] struct Setup { - backend: fn(Option) -> Box, + format: AudioFormat, + backend: fn(Option, AudioFormat) -> Box, device: Option, mixer: fn(Option) -> Box, @@ -112,7 +128,7 @@ struct Setup { emit_sink_events: bool, } -fn setup(args: &[String]) -> Setup { +fn get_setup(args: &[String]) -> Setup { let mut opts = getopts::Options::new(); opts.optopt( "c", @@ -125,7 +141,7 @@ fn setup(args: &[String]) -> Setup { "Path to a directory where system files (credentials, volume) will be cached. Can be different from cache option value", "SYTEMCACHE", ).optflag("", "disable-audio-cache", "Disable caching of the audio data.") - .reqopt("n", "name", "Device name", "NAME") + .optopt("n", "name", "Device name", "NAME") .optopt("", "device-type", "Displayed device type", "DEVICE_TYPE") .optopt( "b", @@ -141,6 +157,7 @@ fn setup(args: &[String]) -> Setup { ) .optflag("", "emit-sink-events", "Run program set by --onevent before sink is opened and after it is closed.") .optflag("v", "verbose", "Enable verbose output") + .optflag("V", "version", "Display librespot version string") .optopt("u", "username", "Username to sign in with", "USERNAME") .optopt("p", "password", "Password", "PASSWORD") .optopt("", "proxy", "HTTP proxy to use when connecting", "PROXY") @@ -158,6 +175,12 @@ fn setup(args: &[String]) -> Setup { "Audio device to use. Use '?' to list options if using portaudio or alsa", "DEVICE", ) + .optopt( + "", + "format", + "Output format (F32, S32, S24, S24_3 or S16). Defaults to S16", + "FORMAT", + ) .optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER") .optopt( "m", @@ -199,6 +222,12 @@ fn setup(args: &[String]) -> Setup { "enable-volume-normalisation", "Play all tracks at the same volume", ) + .optopt( + "", + "normalisation-method", + "Specify the normalisation method to use - [basic, dynamic]. Default is dynamic.", + "NORMALISATION_METHOD", + ) .optopt( "", "normalisation-gain-type", @@ -211,6 +240,30 @@ fn setup(args: &[String]) -> Setup { "Pregain (dB) applied by volume normalisation", "PREGAIN", ) + .optopt( + "", + "normalisation-threshold", + "Threshold (dBFS) to prevent clipping. Default is -1.0.", + "THRESHOLD", + ) + .optopt( + "", + "normalisation-attack", + "Attack time (ms) in which the dynamic limiter is reducing gain. Default is 5.", + "ATTACK", + ) + .optopt( + "", + "normalisation-release", + "Release or decay time (ms) in which the dynamic limiter is restoring gain. Default is 100.", + "RELEASE", + ) + .optopt( + "", + "normalisation-knee", + "Knee steepness of the dynamic limiter. Default is 1.0.", + "KNEE", + ) .optopt( "", "volume-ctrl", @@ -241,15 +294,20 @@ fn setup(args: &[String]) -> Setup { } }; + if matches.opt_present("version") { + print_version(); + exit(0); + } + let verbose = matches.opt_present("verbose"); setup_logging(verbose); info!( - "librespot {} ({}). Built on {}. Build ID: {}", - version::short_sha(), - version::commit_date(), - version::short_now(), - version::build_id() + "librespot {semver} {sha} (Built on {build_date}, Build ID: {build_id})", + semver = version::SEMVER, + sha = version::SHA_SHORT, + build_date = version::BUILD_DATE, + build_id = version::BUILD_ID ); let backend_name = matches.opt_str("backend"); @@ -260,9 +318,15 @@ fn setup(args: &[String]) -> Setup { let backend = audio_backend::find(backend_name).expect("Invalid backend"); + let format = matches + .opt_str("format") + .as_ref() + .map(|format| AudioFormat::try_from(format).expect("Invalid output format")) + .unwrap_or_default(); + let device = matches.opt_str("device"); if device == Some("?".into()) { - backend(device); + backend(device, format); exit(0); } @@ -329,7 +393,9 @@ fn setup(args: &[String]) -> Setup { .map(|port| port.parse::().unwrap()) .unwrap_or(0); - let name = matches.opt_str("name").unwrap(); + let name = matches + .opt_str("name") + .unwrap_or_else(|| "Librespot".to_string()); let credentials = { let cached_credentials = cache.as_ref().and_then(Cache::credentials); @@ -352,7 +418,7 @@ fn setup(args: &[String]) -> Setup { let device_id = device_id(&name); SessionConfig { - user_agent: version::version_string(), + user_agent: version::VERSION_STRING.to_string(), device_id, proxy: matches.opt_str("proxy").or_else(|| std::env::var("http_proxy").ok()).map( |s| { @@ -392,15 +458,48 @@ fn setup(args: &[String]) -> Setup { NormalisationType::from_str(gain_type).expect("Invalid normalisation type") }) .unwrap_or_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_default(); + PlayerConfig { bitrate, gapless: !matches.opt_present("disable-gapless"), normalisation: matches.opt_present("enable-volume-normalisation"), + normalisation_method, normalisation_type: gain_type, normalisation_pregain: matches .opt_str("normalisation-pregain") .map(|pregain| pregain.parse::().expect("Invalid pregain float value")) .unwrap_or(PlayerConfig::default().normalisation_pregain), + normalisation_threshold: NormalisationData::db_to_ratio( + matches + .opt_str("normalisation-threshold") + .map(|threshold| { + threshold + .parse::() + .expect("Invalid threshold float value") + }) + .unwrap_or(PlayerConfig::default().normalisation_threshold), + ), + normalisation_attack: matches + .opt_str("normalisation-attack") + .map(|attack| attack.parse::().expect("Invalid attack float value")) + .unwrap_or(PlayerConfig::default().normalisation_attack * MILLIS) + / MILLIS, + normalisation_release: matches + .opt_str("normalisation-release") + .map(|release| release.parse::().expect("Invalid release float value")) + .unwrap_or(PlayerConfig::default().normalisation_release * MILLIS) + / MILLIS, + normalisation_knee: matches + .opt_str("normalisation-knee") + .map(|knee| knee.parse::().expect("Invalid knee float value")) + .unwrap_or(PlayerConfig::default().normalisation_knee), passthrough, } }; @@ -430,6 +529,7 @@ fn setup(args: &[String]) -> Setup { let enable_discovery = !matches.opt_present("disable-discovery"); Setup { + format, backend, cache, session_config, @@ -453,7 +553,7 @@ async fn main() { } let args: Vec = std::env::args().collect(); - let setupp = setup(&args); + let setup = get_setup(&args); let mut last_credentials = None; let mut spirc: Option = None; @@ -463,23 +563,23 @@ async fn main() { let mut discovery = None; let mut connecting: Pin>> = Box::pin(future::pending()); - if setupp.enable_discovery { - let config = setupp.connect_config.clone(); - let device_id = setupp.session_config.device_id.clone(); + if setup.enable_discovery { + let config = setup.connect_config.clone(); + let device_id = setup.session_config.device_id.clone(); discovery = Some( - librespot_connect::discovery::discovery(config, device_id, setupp.zeroconf_port) + librespot_connect::discovery::discovery(config, device_id, setup.zeroconf_port) .unwrap(), ); } - if let Some(credentials) = setupp.credentials { + if let Some(credentials) = setup.credentials { last_credentials = Some(credentials.clone()); connecting = Box::pin( Session::connect( - setupp.session_config.clone(), + setup.session_config.clone(), credentials, - setupp.cache.clone(), + setup.cache.clone(), ) .fuse(), ); @@ -502,9 +602,9 @@ async fn main() { } connecting = Box::pin(Session::connect( - setupp.session_config.clone(), + setup.session_config.clone(), credentials, - setupp.cache.clone(), + setup.cache.clone(), ).fuse()); }, None => { @@ -515,21 +615,22 @@ async fn main() { }, session = &mut connecting, if !connecting.is_terminated() => match session { Ok(session) => { - let mixer_config = setupp.mixer_config.clone(); - let mixer = (setupp.mixer)(Some(mixer_config)); - let player_config = setupp.player_config.clone(); - let connect_config = setupp.connect_config.clone(); + let mixer_config = setup.mixer_config.clone(); + let mixer = (setup.mixer)(Some(mixer_config)); + let player_config = setup.player_config.clone(); + let connect_config = setup.connect_config.clone(); let audio_filter = mixer.get_audio_filter(); - let backend = setupp.backend; - let device = setupp.device.clone(); + let format = setup.format; + let backend = setup.backend; + let device = setup.device.clone(); let (player, event_channel) = Player::new(player_config, session.clone(), audio_filter, move || { - (backend)(device) + (backend)(device, format) }); - if setupp.emit_sink_events { - if let Some(player_event_program) = setupp.player_event_program.clone() { + if setup.emit_sink_events { + if let Some(player_event_program) = setup.player_event_program.clone() { player.set_sink_event_callback(Some(Box::new(move |sink_status| { match emit_sink_event(sink_status, &player_event_program) { Ok(e) if e.success() => (), @@ -575,16 +676,16 @@ async fn main() { auto_connect_times.push(Instant::now()); connecting = Box::pin(Session::connect( - setupp.session_config.clone(), + setup.session_config.clone(), credentials, - setupp.cache.clone(), + setup.cache.clone(), ).fuse()); } } }, event = async { player_event_channel.as_mut().unwrap().recv().await }, if player_event_channel.is_some() => match event { Some(event) => { - if let Some(program) = &setupp.player_event_program { + if let Some(program) = &setup.player_event_program { if let Some(child) = run_program_on_events(event, program) { let mut child = child.expect("program failed to start");