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