Merge pull request #687 from Johannesd3/tokio_migration

Further progress on tokio migration
This commit is contained in:
Sasha Hilton 2021-04-11 01:45:41 +01:00 committed by GitHub
commit f158d230d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1428 additions and 1244 deletions

View file

@ -4,61 +4,81 @@ name: test
on:
push:
branches: [master, dev]
paths: ['**.rs', '**.toml', '**.lock', '**.yml']
paths:
[
"**.rs",
"Cargo.toml",
"/Cargo.lock",
"/rustfmt.toml",
"/.github/workflows",
]
pull_request:
branches: [master, dev]
paths: ['**.rs', '**.toml', '**.lock', '**.yml']
paths:
[
"**.rs",
"Cargo.toml",
"/Cargo.lock",
"/rustfmt.toml",
"/.github/workflows",
]
schedule:
# Run CI every week
- cron: "00 01 * * 0"
env:
RUST_BACKTRACE: 1
jobs:
fmt:
name: 'Rust: format check'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
# Only run the formatting check for stable
include:
- os: ubuntu-latest
toolchain: stable
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
# Use default profile to get rustfmt
profile: default
toolchain: ${{ matrix.toolchain }}
override: true
- run: cargo fmt --verbose --all -- --check
test:
needs: fmt
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
toolchain:
- 1.42.0 # MSRV (Minimum supported rust version)
- stable
- beta
experimental: [false]
# Ignore failures in nightly, not ideal, but necessary
include:
- os: ubuntu-latest
toolchain: nightly
experimental: true
name: rustfmt
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: rustfmt
- run: cargo fmt --all -- --check
test-linux:
needs: fmt
name: cargo +${{ matrix.toolchain }} build (${{ matrix.os }})
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
toolchain:
- 1.45 # MSRV (Minimum supported rust version)
- stable
- beta
experimental: [false]
# Ignore failures in nightly
include:
- os: ubuntu-latest
toolchain: nightly
experimental: true
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.toolchain }}
override: true
- name: Get Rustc version
id: get-rustc-version
run: echo "::set-output name=version::$(rustc -V)"
shell: bash
- name: Cache Rust dependencies
uses: actions/cache@v2
with:
@ -67,21 +87,65 @@ jobs:
~/.cargo/registry/cache
~/.cargo/git
target
key: ${{ runner.os }}-build-${{ hashFiles('**/Cargo.lock') }}
key: ${{ runner.os }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('**/Cargo.lock') }}
- name: Install developer package dependencies
run: sudo apt-get update && sudo apt-get install libpulse-dev portaudio19-dev libasound2-dev libsdl2-dev gstreamer1.0-dev libgstreamer-plugins-base1.0-dev
- run: cargo build --locked --no-default-features
- run: cargo build --locked --examples
- run: cargo build --locked --no-default-features --features "with-tremor"
- run: cargo build --locked --no-default-features --features "with-vorbis"
- run: cargo build --locked --no-default-features --features "alsa-backend"
- run: cargo build --locked --no-default-features --features "portaudio-backend"
- run: cargo build --locked --no-default-features --features "pulseaudio-backend"
- run: cargo build --locked --no-default-features --features "jackaudio-backend"
- run: cargo build --locked --no-default-features --features "rodiojack-backend"
- run: cargo build --locked --no-default-features --features "rodio-backend"
- run: cargo build --locked --no-default-features --features "sdl-backend"
- run: cargo build --locked --no-default-features --features "gstreamer-backend"
run: sudo apt-get update && sudo apt-get install libpulse-dev portaudio19-dev libasound2-dev libsdl2-dev gstreamer1.0-dev libgstreamer-plugins-base1.0-dev libavahi-compat-libdnssd-dev
- run: cargo build --workspace --examples
- run: cargo test --workspace
- run: cargo install cargo-hack
- run: cargo hack --workspace --remove-dev-deps
- run: cargo build -p librespot-core --no-default-features
- run: cargo build -p librespot-core
- run: cargo hack build --each-feature -p librespot-audio
- run: cargo build -p librespot-connect
- run: cargo build -p librespot-connect --no-default-features --features with-dns-sd
- run: cargo hack build --locked --each-feature
test-windows:
needs: fmt
name: cargo build (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [windows-latest]
toolchain: [stable]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.toolchain }}
profile: minimal
override: true
- name: Get Rustc version
id: get-rustc-version
run: echo "::set-output name=version::$(rustc -V)"
shell: bash
- name: Cache Rust dependencies
uses: actions/cache@v2
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git
target
key: ${{ runner.os }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('**/Cargo.lock') }}
- run: cargo build --workspace --examples
- run: cargo test --workspace
- run: cargo install cargo-hack
- run: cargo hack --workspace --remove-dev-deps
- run: cargo build --no-default-features
- run: cargo build
test-cross-arm:
needs: fmt
@ -97,6 +161,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install toolchain
uses: actions-rs/toolchain@v1
with:
@ -104,6 +169,12 @@ jobs:
target: ${{ matrix.target }}
toolchain: ${{ matrix.toolchain }}
override: true
- name: Get Rustc version
id: get-rustc-version
run: echo "::set-output name=version::$(rustc -V)"
shell: bash
- name: Cache Rust dependencies
uses: actions/cache@v2
with:
@ -112,7 +183,7 @@ jobs:
~/.cargo/registry/cache
~/.cargo/git
target
key: ${{ runner.os }}-build-${{ hashFiles('**/Cargo.lock') }}
key: ${{ runner.os }}-${{ matrix.target }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('**/Cargo.lock') }}
- name: Install cross
run: cargo install cross || true
- name: Build

View file

@ -13,7 +13,7 @@ curl https://sh.rustup.rs -sSf | sh
Follow any prompts it gives you to install Rust. Once thats done, Rust's standard tools should be setup and ready to use.
*Note: The current minimum required Rust version at the time of writing is 1.40.0, you can find the current minimum version specified in the `.github/workflow/test.yml` file.*
*Note: The current minimum required Rust version at the time of writing is 1.45, you can find the current minimum version specified in the `.github/workflow/test.yml` file.*
#### Additional Rust tools - `rustfmt`
To ensure a consistent codebase, we utilise [`rustfmt`](https://github.com/rust-lang/rustfmt), which is installed by default with `rustup` these days, else it can be installed manually with:

598
Cargo.lock generated
View file

@ -2,27 +2,6 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "addr2line"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7"
dependencies = [
"gimli",
]
[[package]]
name = "adler"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e"
[[package]]
name = "adler32"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
[[package]]
name = "aes"
version = "0.6.0"
@ -77,9 +56,9 @@ dependencies = [
[[package]]
name = "alsa"
version = "0.4.3"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb213f6b3e4b1480a60931ca2035794aa67b73103d254715b1db7b70dcb3c934"
checksum = "75c4da790adcb2ce5e758c064b4f3ec17a30349f9961d3e5e6c9688b052a9e18"
dependencies = [
"alsa-sys",
"bitflags",
@ -103,12 +82,6 @@ version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1"
[[package]]
name = "ascii"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e"
[[package]]
name = "async-trait"
version = "0.1.42"
@ -137,26 +110,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "backtrace"
version = "0.3.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc"
dependencies = [
"addr2line",
"cfg-if 1.0.0",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
]
[[package]]
name = "base-x"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4521f3e3d031370679b3b140beb36dfe4801b09ac77e30c61941f97df3ef28b"
[[package]]
name = "base64"
version = "0.13.0"
@ -236,6 +189,9 @@ name = "cc"
version = "1.0.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
dependencies = [
"jobserver",
]
[[package]]
name = "cesu8"
@ -273,16 +229,10 @@ dependencies = [
"libc",
"num-integer",
"num-traits",
"time 0.1.43",
"time",
"winapi",
]
[[package]]
name = "chunked_transfer"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
[[package]]
name = "cipher"
version = "0.2.5"
@ -303,19 +253,6 @@ dependencies = [
"libloading 0.7.0",
]
[[package]]
name = "combine"
version = "3.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680"
dependencies = [
"ascii",
"byteorder",
"either",
"memchr",
"unreachable",
]
[[package]]
name = "combine"
version = "4.5.2"
@ -326,39 +263,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "const_fn"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6"
[[package]]
name = "cookie"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "784ad0fbab4f3e9cef09f20e0aea6000ae08d2cb98ac4c0abc53df18803d702f"
dependencies = [
"percent-encoding",
"time 0.2.25",
"version_check",
]
[[package]]
name = "cookie_store"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3818dfca4b0cb5211a659bbcbb94225b7127407b2b135e650d717bfb78ab10d3"
dependencies = [
"cookie",
"idna",
"log",
"publicsuffix",
"serde",
"serde_json",
"time 0.2.25",
"url",
]
[[package]]
name = "core-foundation-sys"
version = "0.6.2"
@ -367,9 +271,9 @@ checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b"
[[package]]
name = "coreaudio-rs"
version = "0.9.1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f229761965dad3e9b11081668a6ea00f1def7aa46062321b5ec245b834f6e491"
checksum = "11894b20ebfe1ff903cbdc52259693389eea03b94918a2def2c30c3bf227ad88"
dependencies = [
"bitflags",
"coreaudio-sys",
@ -386,15 +290,15 @@ dependencies = [
[[package]]
name = "cpal"
version = "0.13.1"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05631e2089dfa5d3b6ea1cfbbfd092e2ee5deeb69698911bc976b28b746d3657"
checksum = "8351ddf2aaa3c583fa388029f8b3d26f3c7035a20911fdd5f2e2ed7ab57dad25"
dependencies = [
"alsa",
"core-foundation-sys",
"coreaudio-rs",
"jack",
"jni 0.17.0",
"jni",
"js-sys",
"lazy_static",
"libc",
@ -404,7 +308,7 @@ dependencies = [
"nix",
"oboe",
"parking_lot",
"stdweb 0.1.3",
"stdweb",
"thiserror",
"web-sys",
"winapi",
@ -416,15 +320,6 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8aebca1129a03dc6dc2b127edd729435bbc4a37e1d5f4d7513165089ceb02634"
[[package]]
name = "crc32fast"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "crypto-mac"
version = "0.10.0"
@ -499,12 +394,6 @@ dependencies = [
"generic-array",
]
[[package]]
name = "discard"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
[[package]]
name = "dns-sd"
version = "0.1.3"
@ -534,39 +423,6 @@ dependencies = [
"termcolor",
]
[[package]]
name = "error-chain"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc"
dependencies = [
"backtrace",
"version_check",
]
[[package]]
name = "fetch_unroll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d44807d562d137f063cbfe209da1c3f9f2fa8375e11166ef495daab7b847f9"
dependencies = [
"libflate",
"tar",
"ureq",
]
[[package]]
name = "filetime"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"winapi",
]
[[package]]
name = "fnv"
version = "1.0.7"
@ -711,12 +567,6 @@ dependencies = [
"wasi",
]
[[package]]
name = "gimli"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce"
[[package]]
name = "glib"
version = "0.10.3"
@ -890,7 +740,7 @@ dependencies = [
"http",
"mime",
"sha-1",
"time 0.1.43",
"time",
]
[[package]]
@ -1088,9 +938,9 @@ checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
[[package]]
name = "jack"
version = "0.6.5"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c1871c91fa65aa328f3bedbaa54a6e5d1de009264684c153eb708ba933aa6f5"
checksum = "2deb4974bd7e6b2fb7784f27fa13d819d11292b3b004dce0185ec08163cf686a"
dependencies = [
"bitflags",
"jack-sys",
@ -1100,9 +950,9 @@ dependencies = [
[[package]]
name = "jack-sys"
version = "0.2.1"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d6ab7ada402b6a27912a2b86504be62a48c58313c886fe72a059127acb4d7"
checksum = "57983f0d72dfecf2b719ed39bc9cacd85194e1a94cb3f9146009eff9856fef41"
dependencies = [
"lazy_static",
"libc",
@ -1111,29 +961,15 @@ dependencies = [
[[package]]
name = "jni"
version = "0.14.0"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1981310da491a4f0f815238097d0d43d8072732b5ae5f8bd0d8eadf5bf245402"
checksum = "24967112a1e4301ca5342ea339763613a37592b8a6ce6cf2e4494537c7a42faf"
dependencies = [
"cesu8",
"combine 3.8.1",
"error-chain",
"jni-sys",
"log",
"walkdir",
]
[[package]]
name = "jni"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36bcc950632e48b86da402c5c077590583da5ac0d480103611d5374e7c967a3c"
dependencies = [
"cesu8",
"combine 4.5.2",
"error-chain",
"combine",
"jni-sys",
"log",
"thiserror",
"walkdir",
]
@ -1143,6 +979,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jobserver"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c71313ebb9439f74b00d9d2dcec36440beaf57a6aa0623068441dd7cd81a7f2"
dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.47"
@ -1177,27 +1022,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.86"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c"
[[package]]
name = "libflate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389de7875e06476365974da3e7ff85d55f1972188ccd9f6020dd7c8156e17914"
dependencies = [
"adler32",
"crc32fast",
"libflate_lz77",
"rle-decode-fast",
]
[[package]]
name = "libflate_lz77"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3286f09f7d4926fc486334f28d8d2e6ebe4f7f9994494b6dab27ddfad2c9b11b"
checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41"
[[package]]
name = "libloading"
@ -1239,9 +1066,9 @@ dependencies = [
[[package]]
name = "libpulse-binding"
version = "2.23.0"
version = "2.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2405f806801527dfb3d2b6d48a282cdebe9a1b41b0652e0d7b5bad81dbc700e"
checksum = "db951f37898e19a6785208e3a290261e0f1a8e086716be596aaad684882ca8e3"
dependencies = [
"bitflags",
"libc",
@ -1325,6 +1152,7 @@ dependencies = [
"tempfile",
"tokio",
"vorbis",
"zerocopy",
]
[[package]]
@ -1335,6 +1163,7 @@ dependencies = [
"base64",
"block-modes",
"dns-sd",
"form_urlencoded",
"futures-core",
"futures-util",
"hmac",
@ -1344,7 +1173,6 @@ dependencies = [
"librespot-playback",
"librespot-protocol",
"log",
"num-bigint",
"protobuf",
"rand",
"serde",
@ -1364,6 +1192,7 @@ dependencies = [
"byteorder",
"bytes",
"env_logger",
"form_urlencoded",
"futures-core",
"futures-util",
"hmac",
@ -1507,16 +1336,6 @@ version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "miniz_oxide"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d"
dependencies = [
"adler",
"autocfg",
]
[[package]]
name = "mio"
version = "0.7.9"
@ -1557,9 +1376,9 @@ dependencies = [
[[package]]
name = "ndk"
version = "0.2.1"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eb167c1febed0a496639034d0c76b3b74263636045db5489eee52143c246e73"
checksum = "8794322172319b972f528bf90c6b467be0079f1fa82780ffb431088e741a73ab"
dependencies = [
"jni-sys",
"ndk-sys",
@ -1569,9 +1388,9 @@ dependencies = [
[[package]]
name = "ndk-glue"
version = "0.2.1"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdf399b8b7a39c6fb153c4ec32c72fd5fe789df24a647f229c239aa7adb15241"
checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385"
dependencies = [
"lazy_static",
"libc",
@ -1602,15 +1421,14 @@ checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d"
[[package]]
name = "nix"
version = "0.15.0"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2e0b4f3320ed72aaedb9a5ac838690a8047c7b275da22711fddff4f8a14229"
checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a"
dependencies = [
"bitflags",
"cc",
"cfg-if 0.1.10",
"cfg-if 1.0.0",
"libc",
"void",
]
[[package]]
@ -1634,13 +1452,14 @@ dependencies = [
[[package]]
name = "num-bigint"
version = "0.3.1"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e9a41747ae4633fce5adffb4d2e81ffc5e89593cb19917f8fb2cc5ff76507bf"
checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
"rand",
]
[[package]]
@ -1696,9 +1515,9 @@ dependencies = [
[[package]]
name = "num_enum"
version = "0.4.3"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca565a7df06f3d4b485494f25ba05da1435950f4dc263440eda7a6fa9b8e36e4"
checksum = "226b45a5c2ac4dd696ed30fa6b94b057ad909c7b7fc2e0d0808192bced894066"
dependencies = [
"derivative",
"num_enum_derive",
@ -1706,9 +1525,9 @@ dependencies = [
[[package]]
name = "num_enum_derive"
version = "0.4.3"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffa5a33ddddfee04c0283a7653987d634e880347e96b5b2ed64de07efb59db9d"
checksum = "1c0fd9eba1d5db0994a239e09c1be402d35622277e35468ba891aa5e3188ce7e"
dependencies = [
"proc-macro-crate",
"proc-macro2",
@ -1716,19 +1535,13 @@ dependencies = [
"syn",
]
[[package]]
name = "object"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4"
[[package]]
name = "oboe"
version = "0.3.1"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aadc2b0867bdbb9a81c4d99b9b682958f49dbea1295a81d2f646cca2afdd9fc"
checksum = "4cfb2390bddb9546c0f7448fd1d2abdd39e6075206f960991eb28c7fa7f126c4"
dependencies = [
"jni 0.14.0",
"jni",
"ndk",
"ndk-glue",
"num-derive",
@ -1738,11 +1551,11 @@ dependencies = [
[[package]]
name = "oboe-sys"
version = "0.3.0"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ff7a51600eabe34e189eec5c995a62f151d8d97e5fbca39e87ca738bb99b82"
checksum = "fe069264d082fc820dfa172f79be3f2e088ecfece9b1c47b0c9fd838d2bef103"
dependencies = [
"fetch_unroll",
"cc",
]
[[package]]
@ -1989,28 +1802,6 @@ dependencies = [
"protobuf-codegen",
]
[[package]]
name = "publicsuffix"
version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bbaa49075179162b49acac1c6aa45fb4dafb5f13cf6794276d77bc7fd95757b"
dependencies = [
"error-chain",
"idna",
"lazy_static",
"regex",
"url",
]
[[package]]
name = "qstring"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e"
dependencies = [
"percent-encoding",
]
[[package]]
name = "quick-error"
version = "1.2.3"
@ -2102,32 +1893,11 @@ dependencies = [
"winapi",
]
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
"cc",
"libc",
"once_cell",
"spin",
"untrusted",
"web-sys",
"winapi",
]
[[package]]
name = "rle-decode-fast"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cabe4fa914dec5870285fa7f71f602645da47c486e68486d2b4ceb4a343e90ac"
[[package]]
name = "rodio"
version = "0.13.0"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9683532495146e98878d4948fa1a1953f584cd923f2a5f5c26b7a8701b56943"
checksum = "b65c2eda643191f6d1bb12ea323a9db8d9ba95374e9be3780b5a9fb5cfb8520f"
dependencies = [
"cpal",
]
@ -2142,47 +1912,19 @@ dependencies = [
"winapi",
]
[[package]]
name = "rustc-demangle"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
dependencies = [
"semver 0.9.0",
]
[[package]]
name = "rustc_version"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
dependencies = [
"semver 0.11.0",
]
[[package]]
name = "rustls"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "064fd21ff87c6e87ed4506e68beb42459caa4a0e2eb144932e6776768556980b"
dependencies = [
"base64",
"log",
"ring",
"sct",
"webpki",
"semver",
]
[[package]]
@ -2206,16 +1948,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "sct"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "sdl2"
version = "0.34.3"
@ -2230,39 +1962,24 @@ dependencies = [
[[package]]
name = "sdl2-sys"
version = "0.34.3"
version = "0.34.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d81feded049b9c14eceb4a4f6d596a98cebbd59abdba949c5552a015466d33"
checksum = "4cb164f53dbcad111de976bbf1f3083d3fcdeda88da9cfa281c70822720ee3da"
dependencies = [
"cfg-if 0.1.10",
"libc",
"version-compare",
]
[[package]]
name = "semver"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
dependencies = [
"semver-parser 0.7.0",
]
[[package]]
name = "semver"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
dependencies = [
"semver-parser 0.10.2",
"semver-parser",
]
[[package]]
name = "semver-parser"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "semver-parser"
version = "0.10.2"
@ -2316,12 +2033,6 @@ dependencies = [
"opaque-debug",
]
[[package]]
name = "sha1"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
[[package]]
name = "shannon"
version = "0.2.0"
@ -2375,76 +2086,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "standback"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2beb4d1860a61f571530b3f855a1b538d0200f7871c63331ecd6f17b1f014f8"
dependencies = [
"version_check",
]
[[package]]
name = "stdweb"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e"
[[package]]
name = "stdweb"
version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5"
dependencies = [
"discard",
"rustc_version 0.2.3",
"stdweb-derive",
"stdweb-internal-macros",
"stdweb-internal-runtime",
"wasm-bindgen",
]
[[package]]
name = "stdweb-derive"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef"
dependencies = [
"proc-macro2",
"quote",
"serde",
"serde_derive",
"syn",
]
[[package]]
name = "stdweb-internal-macros"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11"
dependencies = [
"base-x",
"proc-macro2",
"quote",
"serde",
"serde_derive",
"serde_json",
"sha1",
"syn",
]
[[package]]
name = "stdweb-internal-runtime"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
[[package]]
name = "strsim"
version = "0.9.3"
@ -2513,17 +2160,6 @@ dependencies = [
"version-compare",
]
[[package]]
name = "tar"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0bcfbd6a598361fda270d82469fff3d65089dc33e175c9a131f7b4cd395f228"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]]
name = "tempfile"
version = "3.2.0"
@ -2586,44 +2222,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "time"
version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1195b046942c221454c2539395f85413b33383a067449d78aab2b7b052a142f7"
dependencies = [
"const_fn",
"libc",
"standback",
"stdweb 0.4.20",
"time-macros",
"version_check",
"winapi",
]
[[package]]
name = "time-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "957e9c6e26f12cb6d0dd7fc776bb67a706312e7299aed74c8dd5b17ebb27e2f1"
dependencies = [
"proc-macro-hack",
"time-macros-impl",
]
[[package]]
name = "time-macros-impl"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa"
dependencies = [
"proc-macro-hack",
"proc-macro2",
"quote",
"standback",
"syn",
]
[[package]]
name = "tinyvec"
version = "1.1.1"
@ -2783,40 +2381,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "unreachable"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
dependencies = [
"void",
]
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "ureq"
version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "294b85ef5dbc3670a72e82a89971608a1fcc4ed5c7c5a2895230d31a95f0569b"
dependencies = [
"base64",
"chunked_transfer",
"cookie",
"cookie_store",
"log",
"once_cell",
"qstring",
"rustls",
"url",
"webpki",
"webpki-roots",
]
[[package]]
name = "url"
version = "2.2.1"
@ -2846,7 +2410,7 @@ checksum = "e7141e445af09c8919f1d5f8a20dae0b20c3b57a45dee0d5823c6ed5d237f15a"
dependencies = [
"bitflags",
"chrono",
"rustc_version 0.3.3",
"rustc_version",
]
[[package]]
@ -2861,12 +2425,6 @@ version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
[[package]]
name = "void"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
[[package]]
name = "vorbis"
version = "0.0.14"
@ -2995,25 +2553,6 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webpki"
version = "0.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "webpki-roots"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82015b7e0b8bad8185994674a13a93306bea76cf5a16c5a181382fd3a5ec2376"
dependencies = [
"webpki",
]
[[package]]
name = "winapi"
version = "0.3.9"
@ -3045,15 +2584,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "xattr"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c"
dependencies = [
"libc",
]
[[package]]
name = "zerocopy"
version = "0.3.0"

View file

@ -31,6 +31,7 @@ version = "0.1.6"
[dependencies.librespot-core]
path = "core"
version = "0.1.6"
features = ["apresolve"]
[dependencies.librespot-metadata]
path = "metadata"
@ -58,8 +59,6 @@ url = "2.1"
sha-1 = "0.9"
[features]
apresolve = ["librespot-core/apresolve"]
alsa-backend = ["librespot-playback/alsa-backend"]
portaudio-backend = ["librespot-playback/portaudio-backend"]
pulseaudio-backend = ["librespot-playback/pulseaudio-backend"]
@ -71,11 +70,10 @@ gstreamer-backend = ["librespot-playback/gstreamer-backend"]
with-tremor = ["librespot-audio/with-tremor"]
with-vorbis = ["librespot-audio/with-vorbis"]
with-lewton = ["librespot-audio/with-lewton"]
# with-dns-sd = ["librespot-connect/with-dns-sd"]
with-dns-sd = ["librespot-connect/with-dns-sd"]
default = ["rodio-backend", "apresolve", "with-lewton"]
default = ["rodio-backend"]
[package.metadata.deb]
maintainer = "librespot-org"

View file

@ -15,17 +15,17 @@ aes-ctr = "0.6"
byteorder = "1.4"
bytes = "1.0"
cfg-if = "1"
lewton = "0.10"
log = "0.4"
futures-util = { version = "0.3", default_features = false }
ogg = "0.8"
tempfile = "3.1"
tokio = { version = "1", features = ["sync"] }
tokio = { version = "1", features = ["sync", "macros"] }
zerocopy = "0.3"
lewton = { version = "0.10", optional = true }
librespot-tremor = { version = "0.2.0", optional = true }
vorbis = { version ="0.0.14", optional = true }
librespot-tremor = { version = "0.2", optional = true }
vorbis = { version ="0.0", optional = true }
[features]
with-lewton = ["lewton"]
with-tremor = ["librespot-tremor"]
with-vorbis = ["vorbis"]

56
audio/src/convert.rs Normal file
View 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)
}

View file

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

View file

@ -3,27 +3,19 @@
#[macro_use]
extern crate log;
pub mod convert;
mod decrypt;
mod fetch;
use cfg_if::cfg_if;
#[cfg(any(
all(feature = "with-lewton", feature = "with-tremor"),
all(feature = "with-vorbis", feature = "with-tremor"),
all(feature = "with-lewton", feature = "with-vorbis")
))]
compile_error!("Cannot use two decoders at the same time.");
cfg_if! {
if #[cfg(feature = "with-lewton")] {
mod lewton_decoder;
pub use lewton_decoder::{VorbisDecoder, VorbisError};
} else if #[cfg(any(feature = "with-tremor", feature = "with-vorbis"))] {
if #[cfg(any(feature = "with-tremor", feature = "with-vorbis"))] {
mod libvorbis_decoder;
pub use crate::libvorbis_decoder::{VorbisDecoder, VorbisError};
} else {
compile_error!("Must choose a vorbis decoder.");
mod lewton_decoder;
pub use lewton_decoder::{VorbisDecoder, VorbisError};
}
}
@ -41,12 +33,12 @@ pub use fetch::{
use std::fmt;
pub enum AudioPacket {
Samples(Vec<i16>),
Samples(Vec<f32>),
OggData(Vec<u8>),
}
impl AudioPacket {
pub fn samples(&self) -> &[i16] {
pub fn samples(&self) -> &[f32] {
match self {
AudioPacket::Samples(s) => s,
AudioPacket::OggData(_) => panic!("can't return OggData on samples"),

View file

@ -37,7 +37,18 @@ where
fn next_packet(&mut self) -> Result<Option<AudioPacket>, AudioError> {
loop {
match self.0.packets().next() {
Some(Ok(packet)) => return Ok(Some(AudioPacket::Samples(packet.data))),
Some(Ok(packet)) => {
// Losslessly represent [-32768, 32767] to [-1.0, 1.0] while maintaining DC linearity.
return Ok(Some(AudioPacket::Samples(
packet
.data
.iter()
.map(|sample| {
((*sample as f64 + 0.5) / (std::i16::MAX as f64 + 0.5)) as f32
})
.collect(),
)));
}
None => return Ok(None),
Some(Err(vorbis::VorbisError::Hole)) => (),

View file

@ -5,75 +5,32 @@ use std::fmt;
use std::io::{Read, Seek};
use std::time::{SystemTime, UNIX_EPOCH};
fn write_headers<T: Read + Seek>(
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>
fn get_header<T>(code: u8, rdr: &mut PacketReader<T>) -> Result<Box<[u8]>, PassthroughError>
where
T: Read + Seek,
{
let pck: Packet = rdr.read_packet_expected()?;
// set a unique serial number
if pck.stream_serial() != 0 {
*stream_serial = pck.stream_serial();
}
let pkt_type = pck.data[0];
debug!("Vorbis header type{}", &pkt_type);
// all headers are mandatory
if pkt_type != code {
return Err(PassthroughError(OggReadError::InvalidData));
}
// headers keep original granule number
let absgp_page = pck.absgp_page();
wtr.write_packet(
pck.data.into_boxed_slice(),
*stream_serial,
info,
absgp_page,
)
.unwrap();
Ok(*stream_serial)
Ok(pck.data.into_boxed_slice())
}
pub struct PassthroughDecoder<R: Read + Seek> {
rdr: PacketReader<R>,
wtr: PacketWriter<Vec<u8>>,
lastgp_page: Option<u64>,
absgp_page: u64,
eos: bool,
bos: bool,
ofsgp_page: u64,
stream_serial: u32,
ident: Box<[u8]>,
comment: Box<[u8]>,
setup: Box<[u8]>,
}
pub struct PassthroughError(ogg::OggReadError);
@ -82,17 +39,31 @@ impl<R: Read + Seek> PassthroughDecoder<R> {
/// Constructs a new Decoder from a given implementation of `Read + Seek`.
pub fn new(rdr: R) -> Result<Self, PassthroughError> {
let mut rdr = PacketReader::new(rdr);
let mut wtr = PacketWriter::new(Vec::new());
let stream_serial = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u32;
let stream_serial = write_headers(&mut rdr, &mut wtr)?;
info!("Starting passthrough track with serial {}", stream_serial);
// search for ident, comment, setup
let ident = get_header(1, &mut rdr)?;
let comment = get_header(3, &mut rdr)?;
let setup = get_header(5, &mut rdr)?;
// remove un-needed packets
rdr.delete_unread_packets();
Ok(PassthroughDecoder {
rdr,
wtr,
lastgp_page: Some(0),
absgp_page: 0,
wtr: PacketWriter::new(Vec::new()),
ofsgp_page: 0,
stream_serial,
ident,
comment,
setup,
eos: false,
bos: false,
})
}
}
@ -100,52 +71,94 @@ impl<R: Read + Seek> PassthroughDecoder<R> {
impl<R: Read + Seek> AudioDecoder for PassthroughDecoder<R> {
fn seek(&mut self, ms: i64) -> Result<(), AudioError> {
info!("Seeking to {}", ms);
self.lastgp_page = match ms {
0 => Some(0),
_ => None,
};
// add an eos to previous stream if missing
if self.bos && !self.eos {
match self.rdr.read_packet() {
Ok(Some(pck)) => {
let absgp_page = pck.absgp_page() - self.ofsgp_page;
self.wtr
.write_packet(
pck.data.into_boxed_slice(),
self.stream_serial,
PacketWriteEndInfo::EndStream,
absgp_page,
)
.unwrap();
}
_ => warn! {"Cannot write EoS after seeking"},
};
}
self.eos = false;
self.bos = false;
self.ofsgp_page = 0;
self.stream_serial += 1;
// hard-coded to 44.1 kHz
match self.rdr.seek_absgp(None, (ms * 44100 / 1000) as u64) {
Ok(_) => Ok(()),
Ok(_) => {
// need to set some offset for next_page()
let pck = self.rdr.read_packet().unwrap().unwrap();
self.ofsgp_page = pck.absgp_page();
debug!("Seek to offset page {}", self.ofsgp_page);
Ok(())
}
Err(err) => Err(AudioError::PassthroughError(err.into())),
}
}
fn next_packet(&mut self) -> Result<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 {
let pck = match self.rdr.read_packet() {
Ok(Some(pck)) => pck,
Ok(None) | Err(OggReadError::NoCapturePatternFound) => {
info!("end of streaming");
return Ok(None);
}
Err(err) => return Err(AudioError::PassthroughError(err.into())),
};
let pckgp_page = pck.absgp_page();
let lastgp_page = self.lastgp_page.get_or_insert(pckgp_page);
// consume packets till next page to get a granule reference
if skip {
if *lastgp_page == pckgp_page {
debug!("skipping packet");
continue;
}
skip = false;
info!("skipped at {}", pckgp_page);
// skip till we have audio and a calculable granule position
if pckgp_page == 0 || pckgp_page == self.ofsgp_page {
continue;
}
// now we can calculate absolute granule
self.absgp_page += pckgp_page - *lastgp_page;
self.lastgp_page = Some(pckgp_page);
// set packet type
let inf = if pck.last_in_stream() {
self.lastgp_page = Some(0);
self.eos = true;
PacketWriteEndInfo::EndStream
} else if pck.last_in_page() {
PacketWriteEndInfo::EndPage
@ -158,7 +171,7 @@ impl<R: Read + Seek> AudioDecoder for PassthroughDecoder<R> {
pck.data.into_boxed_slice(),
self.stream_serial,
inf,
self.absgp_page,
pckgp_page - self.ofsgp_page,
)
.unwrap();

View file

@ -11,12 +11,13 @@ edition = "2018"
aes-ctr = "0.6"
base64 = "0.13"
block-modes = "0.7"
form_urlencoded = "1.0"
futures-core = "0.3"
futures-util = { version = "0.3", default_features = false }
hmac = "0.10"
hyper = { version = "0.14", features = ["server", "http1", "tcp"] }
libmdns = "0.6"
log = "0.4"
num-bigint = "0.3"
protobuf = "~2.14.0"
rand = "0.8"
serde = { version = "1.0", features = ["derive"] }
@ -27,7 +28,6 @@ tokio-stream = { version = "0.1" }
url = "2.1"
dns-sd = { version = "0.1.3", optional = true }
libmdns = { version = "0.6", optional = true }
[dependencies.librespot-core]
path = "../core"
@ -42,7 +42,5 @@ path = "../protocol"
version = "0.1.6"
[features]
with-libmdns = ["libmdns"]
with-dns-sd = ["dns-sd"]
default = ["with-libmdns"]

View file

@ -5,7 +5,6 @@ use futures_core::Stream;
use hmac::{Hmac, Mac, NewMac};
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Method, Request, Response, StatusCode};
use num_bigint::BigUint;
use serde_json::json;
use sha1::{Digest, Sha1};
use tokio::sync::{mpsc, oneshot};
@ -15,8 +14,7 @@ use dns_sd::DNSService;
use librespot_core::authentication::Credentials;
use librespot_core::config::ConnectConfig;
use librespot_core::diffie_hellman::{DH_GENERATOR, DH_PRIME};
use librespot_core::util;
use librespot_core::diffie_hellman::DhLocalKeys;
use std::borrow::Cow;
use std::collections::BTreeMap;
@ -34,8 +32,7 @@ struct Discovery(Arc<DiscoveryInner>);
struct DiscoveryInner {
config: ConnectConfig,
device_id: String,
private_key: BigUint,
public_key: BigUint,
keys: DhLocalKeys,
tx: mpsc::UnboundedSender<Credentials>,
}
@ -46,15 +43,10 @@ impl Discovery {
) -> (Discovery, mpsc::UnboundedReceiver<Credentials>) {
let (tx, rx) = mpsc::unbounded_channel();
let key_data = util::rand_vec(&mut rand::thread_rng(), 95);
let private_key = BigUint::from_bytes_be(&key_data);
let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME);
let discovery = Discovery(Arc::new(DiscoveryInner {
config,
device_id,
private_key,
public_key,
keys: DhLocalKeys::random(&mut rand::thread_rng()),
tx,
}));
@ -62,8 +54,7 @@ impl Discovery {
}
fn handle_get_info(&self, _: BTreeMap<Cow<'_, str>, Cow<'_, str>>) -> Response<hyper::Body> {
let public_key = self.0.public_key.to_bytes_be();
let public_key = base64::encode(&public_key);
let public_key = base64::encode(&self.0.keys.public_key());
let result = json!({
"status": 101,
@ -98,16 +89,16 @@ impl Discovery {
let encrypted_blob = base64::decode(encrypted_blob.as_bytes()).unwrap();
let client_key = base64::decode(client_key.as_bytes()).unwrap();
let client_key = BigUint::from_bytes_be(&client_key);
let shared_key = util::powm(&client_key, &self.0.private_key, &DH_PRIME);
let shared_key = self
.0
.keys
.shared_secret(&base64::decode(client_key.as_bytes()).unwrap());
let iv = &encrypted_blob[0..16];
let encrypted = &encrypted_blob[16..encrypted_blob.len() - 20];
let cksum = &encrypted_blob[encrypted_blob.len() - 20..encrypted_blob.len()];
let base_key = Sha1::digest(&shared_key.to_bytes_be());
let base_key = Sha1::digest(&shared_key);
let base_key = &base_key[..16];
let checksum_key = {

View file

@ -7,7 +7,6 @@ use crate::core::config::{ConnectConfig, VolumeCtrl};
use crate::core::mercury::{MercuryError, MercurySender};
use crate::core::session::Session;
use crate::core::spotify_id::{SpotifyAudioType, SpotifyId, SpotifyIdError};
use crate::core::util::url_encode;
use crate::core::util::SeqGenerator;
use crate::core::version;
use crate::playback::mixer::Mixer;
@ -106,7 +105,7 @@ fn initial_state() -> State {
fn initial_device_state(config: ConnectConfig) -> DeviceState {
{
let mut msg = DeviceState::new();
msg.set_sw_version(version::version_string());
msg.set_sw_version(version::VERSION_STRING.to_string());
msg.set_is_active(false);
msg.set_can_play(true);
msg.set_volume(0);
@ -244,6 +243,10 @@ fn volume_to_mixer(volume: u16, volume_ctrl: &VolumeCtrl) -> u16 {
}
}
fn url_encode(bytes: impl AsRef<[u8]>) -> String {
form_urlencoded::byte_serialize(bytes.as_ref()).collect()
}
impl Spirc {
pub fn new(
config: ConnectConfig,
@ -256,7 +259,7 @@ impl Spirc {
let ident = session.device_id().to_owned();
// Uri updated in response to issue #288
debug!("canonical_username: {}", url_encode(&session.username()));
debug!("canonical_username: {}", &session.username());
let uri = format!("hm://remote/user/{}/", url_encode(&session.username()));
let subscription = Box::pin(

View file

@ -17,6 +17,7 @@ aes = "0.6"
base64 = "0.13"
byteorder = "1.4"
bytes = "1.0"
form_urlencoded = "1.0"
futures-core = { version = "0.3", default-features = false }
futures-util = { version = "0.3", default-features = false, features = ["alloc", "bilock", "unstable", "sink"] }
hmac = "0.10"
@ -25,7 +26,7 @@ http = "0.2"
hyper = { version = "0.14", optional = true, features = ["client", "tcp", "http1"] }
hyper-proxy = { version = "0.9.1", optional = true, default-features = false }
log = "0.4"
num-bigint = "0.3"
num-bigint = { version = "0.4", features = ["rand"] }
num-integer = "0.1"
num-traits = "0.2"
once_cell = "1.5.2"

View file

@ -12,5 +12,6 @@ fn main() {
.take(8)
.map(char::from)
.collect();
println!("cargo:rustc-env=VERGEN_BUILD_ID={}", build_id);
println!("cargo:rustc-env=LIBRESPOT_BUILD_ID={}", build_id);
}

View file

@ -11,7 +11,7 @@ use super::AP_FALLBACK;
const APRESOLVE_ENDPOINT: &str = "http://apresolve.spotify.com:80";
#[derive(Clone, Debug, Deserialize)]
struct APResolveData {
struct ApResolveData {
ap_list: Vec<String>,
}
@ -41,7 +41,7 @@ async fn try_apresolve(
};
let body = hyper::body::to_bytes(response.into_body()).await?;
let data: APResolveData = serde_json::from_slice(body.as_ref())?;
let data: ApResolveData = serde_json::from_slice(body.as_ref())?;
let ap = if ap_port.is_some() || proxy.is_some() {
data.ap_list.into_iter().find_map(|ap| {
@ -66,3 +66,26 @@ pub async fn apresolve(proxy: Option<&Url>, ap_port: Option<u16>) -> String {
AP_FALLBACK.into()
})
}
#[cfg(test)]
mod test {
use std::net::ToSocketAddrs;
use super::try_apresolve;
#[tokio::test]
async fn test_apresolve() {
let ap = try_apresolve(None, None).await.unwrap();
// Assert that the result contains a valid host and port
ap.to_socket_addrs().unwrap().next().unwrap();
}
#[tokio::test]
async fn test_apresolve_port_443() {
let ap = try_apresolve(None, Some(443)).await.unwrap();
let port = ap.to_socket_addrs().unwrap().next().unwrap().port();
assert_eq!(port, 443);
}
}

View file

@ -31,13 +31,10 @@ impl Credentials {
/// ### Example
/// ```rust
/// use librespot_core::authentication::Credentials;
///
///
/// let creds = Credentials::with_password("my account", "my password");
/// ```
pub fn with_password(
username: impl Into<String>,
password: impl Into<String>,
) -> Credentials {
pub fn with_password(username: impl Into<String>, password: impl Into<String>) -> Credentials {
Credentials {
username: username.into(),
auth_type: AuthenticationType::AUTHENTICATION_USER_PASS,

View file

@ -14,7 +14,7 @@ impl Default for SessionConfig {
fn default() -> SessionConfig {
let device_id = uuid::Uuid::new_v4().to_hyphenated().to_string();
SessionConfig {
user_agent: crate::version::version_string(),
user_agent: crate::version::VERSION_STRING.to_string(),
device_id,
proxy: None,
ap_port: None,
@ -29,9 +29,9 @@ pub enum DeviceType {
Tablet = 2,
Smartphone = 3,
Speaker = 4,
TV = 5,
AVR = 6,
STB = 7,
Tv = 5,
Avr = 6,
Stb = 7,
AudioDongle = 8,
GameConsole = 9,
CastAudio = 10,
@ -54,9 +54,9 @@ impl FromStr for DeviceType {
"tablet" => Ok(Tablet),
"smartphone" => Ok(Smartphone),
"speaker" => Ok(Speaker),
"tv" => Ok(TV),
"avr" => Ok(AVR),
"stb" => Ok(STB),
"tv" => Ok(Tv),
"avr" => Ok(Avr),
"stb" => Ok(Stb),
"audiodongle" => Ok(AudioDongle),
"gameconsole" => Ok(GameConsole),
"castaudio" => Ok(CastAudio),
@ -80,9 +80,9 @@ impl fmt::Display for DeviceType {
Tablet => f.write_str("Tablet"),
Smartphone => f.write_str("Smartphone"),
Speaker => f.write_str("Speaker"),
TV => f.write_str("TV"),
AVR => f.write_str("AVR"),
STB => f.write_str("STB"),
Tv => f.write_str("TV"),
Avr => f.write_str("AVR"),
Stb => f.write_str("STB"),
AudioDongle => f.write_str("AudioDongle"),
GameConsole => f.write_str("GameConsole"),
CastAudio => f.write_str("CastAudio"),

View file

@ -13,7 +13,7 @@ enum DecodeState {
Payload(u8, usize),
}
pub struct APCodec {
pub struct ApCodec {
encode_nonce: u32,
encode_cipher: Shannon,
@ -22,9 +22,9 @@ pub struct APCodec {
decode_state: DecodeState,
}
impl APCodec {
pub fn new(send_key: &[u8], recv_key: &[u8]) -> APCodec {
APCodec {
impl ApCodec {
pub fn new(send_key: &[u8], recv_key: &[u8]) -> ApCodec {
ApCodec {
encode_nonce: 0,
encode_cipher: Shannon::new(send_key),
@ -35,7 +35,7 @@ impl APCodec {
}
}
impl Encoder<(u8, Vec<u8>)> for APCodec {
impl Encoder<(u8, Vec<u8>)> for ApCodec {
type Error = io::Error;
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 Error = io::Error;

View file

@ -1,22 +1,21 @@
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use hmac::{Hmac, Mac, NewMac};
use protobuf::{self, Message};
use rand::thread_rng;
use rand::{thread_rng, RngCore};
use sha1::Sha1;
use std::io;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio_util::codec::{Decoder, Framed};
use super::codec::APCodec;
use crate::diffie_hellman::DHLocalKeys;
use super::codec::ApCodec;
use crate::diffie_hellman::DhLocalKeys;
use crate::protocol;
use crate::protocol::keyexchange::{APResponseMessage, ClientHello, ClientResponsePlaintext};
use crate::util;
pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
mut connection: T,
) -> io::Result<Framed<T, APCodec>> {
let local_keys = DHLocalKeys::random(&mut thread_rng());
) -> io::Result<Framed<T, ApCodec>> {
let local_keys = DhLocalKeys::random(&mut thread_rng());
let gc = local_keys.public_key();
let mut accumulator = client_hello(&mut connection, gc).await?;
let message: APResponseMessage = recv_packet(&mut connection, &mut accumulator).await?;
@ -29,7 +28,7 @@ pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
let shared_secret = local_keys.shared_secret(&remote_key);
let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator);
let codec = APCodec::new(&send_key, &recv_key);
let codec = ApCodec::new(&send_key, &recv_key);
client_response(&mut connection, challenge).await?;
@ -40,6 +39,9 @@ async fn client_hello<T>(connection: &mut T, gc: Vec<u8>) -> io::Result<Vec<u8>>
where
T: AsyncWrite + Unpin,
{
let mut client_nonce = vec![0; 0x10];
thread_rng().fill_bytes(&mut client_nonce);
let mut packet = ClientHello::new();
packet
.mut_build_info()
@ -59,7 +61,7 @@ where
.mut_login_crypto_hello()
.mut_diffie_hellman()
.set_server_keys_known(1);
packet.set_client_nonce(util::rand_vec(&mut thread_rng(), 0x10));
packet.set_client_nonce(client_nonce);
packet.set_padding(vec![0x1e]);
let mut buffer = vec![0, 4];

View file

@ -1,7 +1,7 @@
mod codec;
mod handshake;
pub use self::codec::APCodec;
pub use self::codec::ApCodec;
pub use self::handshake::handshake;
use std::io::{self, ErrorKind};
@ -19,7 +19,7 @@ use crate::protocol::keyexchange::{APLoginFailed, ErrorCode};
use crate::proxytunnel;
use crate::version;
pub type Transport = Framed<TcpStream, APCodec>;
pub type Transport = Framed<TcpStream, ApCodec>;
fn login_error_message(code: &ErrorCode) -> &'static str {
pub use ErrorCode::*;
@ -131,13 +131,13 @@ pub async fn authenticate(
.mut_system_info()
.set_system_information_string(format!(
"librespot_{}_{}",
version::short_sha(),
version::build_id()
version::SHA_SHORT,
version::BUILD_ID
));
packet
.mut_system_info()
.set_device_id(device_id.to_string());
packet.set_version_string(version::version_string());
packet.set_version_string(version::VERSION_STRING.to_string());
let cmd = 0xab;
let data = packet.write_to_bytes().unwrap();

View file

@ -1,11 +1,11 @@
use num_bigint::BigUint;
use num_bigint::{BigUint, RandBigInt};
use num_integer::Integer;
use num_traits::{One, Zero};
use once_cell::sync::Lazy;
use rand::Rng;
use rand::{CryptoRng, Rng};
use crate::util;
pub static DH_GENERATOR: Lazy<BigUint> = Lazy::new(|| BigUint::from_bytes_be(&[0x02]));
pub static DH_PRIME: Lazy<BigUint> = Lazy::new(|| {
static DH_GENERATOR: Lazy<BigUint> = Lazy::new(|| BigUint::from_bytes_be(&[0x02]));
static DH_PRIME: Lazy<BigUint> = Lazy::new(|| {
BigUint::from_bytes_be(&[
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2,
0x34, 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67,
@ -17,19 +17,33 @@ pub static DH_PRIME: Lazy<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,
public_key: BigUint,
}
impl DHLocalKeys {
pub fn random<R: Rng>(rng: &mut R) -> DHLocalKeys {
let key_data = util::rand_vec(rng, 95);
impl DhLocalKeys {
pub fn random<R: Rng + CryptoRng>(rng: &mut R) -> DhLocalKeys {
let private_key = rng.gen_biguint(95 * 8);
let public_key = powm(&DH_GENERATOR, &private_key, &DH_PRIME);
let private_key = BigUint::from_bytes_be(&key_data);
let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME);
DHLocalKeys {
DhLocalKeys {
private_key,
public_key,
}
@ -40,7 +54,7 @@ impl DHLocalKeys {
}
pub fn shared_secret(&self, remote_key: &[u8]) -> Vec<u8> {
let shared_key = util::powm(
let shared_key = powm(
&BigUint::from_bytes_be(remote_key),
&self.private_key,
&DH_PRIME,

View file

@ -14,12 +14,14 @@ pub mod cache;
pub mod channel;
pub mod config;
mod connection;
#[doc(hidden)]
pub mod diffie_hellman;
pub mod keymaster;
pub mod mercury;
mod proxytunnel;
pub mod session;
pub mod spotify_id;
#[doc(hidden)]
pub mod util;
pub mod version;

View file

@ -10,7 +10,6 @@ use bytes::Bytes;
use tokio::sync::{mpsc, oneshot};
use crate::protocol;
use crate::util::url_encode;
use crate::util::SeqGenerator;
mod types;
@ -82,7 +81,7 @@ impl MercuryManager {
pub fn get<T: Into<String>>(&self, uri: T) -> MercuryFuture<MercuryResponse> {
self.request(MercuryRequest {
method: MercuryMethod::GET,
method: MercuryMethod::Get,
uri: uri.into(),
content_type: None,
payload: Vec::new(),
@ -91,7 +90,7 @@ impl MercuryManager {
pub fn send<T: Into<String>>(&self, uri: T, data: Vec<u8>) -> MercuryFuture<MercuryResponse> {
self.request(MercuryRequest {
method: MercuryMethod::SEND,
method: MercuryMethod::Send,
uri: uri.into(),
content_type: None,
payload: vec![data],
@ -109,7 +108,7 @@ impl MercuryManager {
{
let uri = uri.into();
let request = self.request(MercuryRequest {
method: MercuryMethod::SUB,
method: MercuryMethod::Sub,
uri: uri.clone(),
content_type: None,
payload: Vec::new(),
@ -199,7 +198,7 @@ impl MercuryManager {
let header: protocol::mercury::Header = protobuf::parse_from_bytes(&header_data).unwrap();
let response = MercuryResponse {
uri: url_encode(header.get_uri()),
uri: header.get_uri().to_string(),
status_code: header.get_status_code(),
payload: pending.parts,
};
@ -214,8 +213,21 @@ impl MercuryManager {
} else if cmd == 0xb5 {
self.lock(|inner| {
let mut found = false;
// TODO: This is just a workaround to make utf-8 encoded usernames work.
// A better solution would be to use an uri struct and urlencode it directly
// before sending while saving the subscription under its unencoded form.
let mut uri_split = response.uri.split('/');
let encoded_uri = std::iter::once(uri_split.next().unwrap().to_string())
.chain(uri_split.map(|component| {
form_urlencoded::byte_serialize(component.as_bytes()).collect::<String>()
}))
.collect::<Vec<String>>()
.join("/");
inner.subscriptions.retain(|&(ref prefix, ref sub)| {
if response.uri.starts_with(prefix) {
if encoded_uri.starts_with(prefix) {
found = true;
// if send fails, remove from list of subs

View file

@ -6,10 +6,10 @@ use crate::protocol;
#[derive(Debug, PartialEq, Eq)]
pub enum MercuryMethod {
GET,
SUB,
UNSUB,
SEND,
Get,
Sub,
Unsub,
Send,
}
#[derive(Debug)]
@ -33,10 +33,10 @@ pub struct MercuryError;
impl ToString for MercuryMethod {
fn to_string(&self) -> String {
match *self {
MercuryMethod::GET => "GET",
MercuryMethod::SUB => "SUB",
MercuryMethod::UNSUB => "UNSUB",
MercuryMethod::SEND => "SEND",
MercuryMethod::Get => "GET",
MercuryMethod::Sub => "SUB",
MercuryMethod::Unsub => "UNSUB",
MercuryMethod::Send => "SEND",
}
.to_owned()
}
@ -45,9 +45,9 @@ impl ToString for MercuryMethod {
impl MercuryMethod {
pub fn command(&self) -> u8 {
match *self {
MercuryMethod::GET | MercuryMethod::SEND => 0xb2,
MercuryMethod::SUB => 0xb3,
MercuryMethod::UNSUB => 0xb4,
MercuryMethod::Get | MercuryMethod::Send => 0xb2,
MercuryMethod::Sub => 0xb3,
MercuryMethod::Unsub => 0xb4,
}
}
}

View file

@ -18,9 +18,9 @@ impl From<&str> for SpotifyAudioType {
}
}
impl Into<&str> for SpotifyAudioType {
fn into(self) -> &'static str {
match self {
impl From<SpotifyAudioType> for &str {
fn from(audio_type: SpotifyAudioType) -> &'static str {
match audio_type {
SpotifyAudioType::Track => "track",
SpotifyAudioType::Podcast => "episode",
SpotifyAudioType::NonPlayable => "unknown",

View file

@ -1,50 +1,4 @@
use num_bigint::BigUint;
use num_integer::Integer;
use num_traits::{One, Zero};
use rand::Rng;
use std::mem;
use std::ops::{Mul, Rem, Shr};
pub fn rand_vec<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 {
fn next(&self) -> Self;

View file

@ -1,44 +1,17 @@
pub fn version_string() -> String {
format!("librespot-{}", short_sha())
}
/// Version string of the form "librespot-<sha>"
pub const VERSION_STRING: &str = concat!("librespot-", env!("VERGEN_SHA_SHORT"));
// Generate a timestamp representing now (UTC) in RFC3339 format.
pub fn now() -> &'static str {
env!("VERGEN_BUILD_TIMESTAMP")
}
/// Generate a timestamp string representing the build date (UTC).
pub const BUILD_DATE: &str = env!("VERGEN_BUILD_DATE");
// Generate a timstamp string representing now (UTC).
pub fn short_now() -> &'static str {
env!("VERGEN_BUILD_DATE")
}
/// Short sha of the latest git commit.
pub const SHA_SHORT: &str = env!("VERGEN_SHA_SHORT");
// Generate a SHA string
pub fn sha() -> &'static str {
env!("VERGEN_SHA")
}
/// Date of the latest git commit.
pub const COMMIT_DATE: &str = env!("VERGEN_COMMIT_DATE");
// Generate a short SHA string
pub fn short_sha() -> &'static str {
env!("VERGEN_SHA_SHORT")
}
/// Librespot crate version.
pub const SEMVER: &str = env!("CARGO_PKG_VERSION");
// Generate the commit date string
pub fn commit_date() -> &'static str {
env!("VERGEN_COMMIT_DATE")
}
// Generate the target triple string
pub fn target() -> &'static str {
env!("VERGEN_TARGET_TRIPLE")
}
// Generate a semver string
pub fn semver() -> &'static str {
// env!("VERGEN_SEMVER")
env!("CARGO_PKG_VERSION")
}
// Generate a random build id.
pub fn build_id() -> &'static str {
env!("VERGEN_BUILD_ID")
}
/// A random build id.
pub const BUILD_ID: &str = env!("LIBRESPOT_BUILD_ID");

View file

@ -1,34 +1,18 @@
use librespot_core::*;
use librespot_core::authentication::Credentials;
use librespot_core::config::SessionConfig;
use librespot_core::session::Session;
// TODO: test is broken
// #[cfg(test)]
// mod tests {
// use super::*;
// // Test AP Resolve
// use apresolve::apresolve_or_fallback;
// #[tokio::test]
// async fn test_ap_resolve() {
// env_logger::init();
// let ap = apresolve_or_fallback(&None, &None).await;
// println!("AP: {:?}", ap);
// }
#[tokio::test]
async fn test_connection() {
let result = Session::connect(
SessionConfig::default(),
Credentials::with_password("test", "test"),
None,
)
.await;
// // Test connect
// use authentication::Credentials;
// use config::SessionConfig;
// #[tokio::test]
// async fn test_connection() -> Result<(), Box<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(())
// }
// }
match result {
Ok(_) => panic!("Authentication succeeded despite of bad credentials."),
Err(e) => assert_eq!(e.to_string(), "Login failed with reason: Bad credentials"),
};
}

View file

@ -20,7 +20,9 @@ async fn main() {
println!("Connecting..");
let credentials = Credentials::with_password(&args[1], &args[2]);
let session = Session::connect(session_config, credentials, None).await.unwrap();
let session = Session::connect(session_config, credentials, None)
.await
.unwrap();
println!(
"Token: {:#?}",

View file

@ -4,14 +4,15 @@ use librespot::core::authentication::Credentials;
use librespot::core::config::SessionConfig;
use librespot::core::session::Session;
use librespot::core::spotify_id::SpotifyId;
use librespot::playback::config::PlayerConfig;
use librespot::playback::audio_backend;
use librespot::playback::config::{AudioFormat, PlayerConfig};
use librespot::playback::player::Player;
#[tokio::main]
async fn main() {
let session_config = SessionConfig::default();
let player_config = PlayerConfig::default();
let audio_format = AudioFormat::default();
let args: Vec<_> = env::args().collect();
if args.len() != 4 {
@ -25,10 +26,12 @@ async fn main() {
let backend = audio_backend::find(None).unwrap();
println!("Connecting ..");
let session = Session::connect(session_config, credentials, None).await.unwrap();
let session = Session::connect(session_config, credentials, None)
.await
.unwrap();
let (mut player, _) = Player::new(player_config, session, None, move || {
backend(None)
backend(None, audio_format)
});
player.load(track, true, 0);

View file

@ -25,7 +25,9 @@ async fn main() {
let plist_uri = SpotifyId::from_base62(uri_parts[2]).unwrap();
let session = Session::connect(session_config, credentials, None).await.unwrap();
let session = Session::connect(session_config, credentials, None)
.await
.unwrap();
let plist = Playlist::get(&session, plist_uri).await.unwrap();
println!("{:?}", plist);

View file

@ -25,17 +25,17 @@ byteorder = "1.4"
shell-words = "1.0.0"
tokio = { version = "1", features = ["sync"] }
alsa = { version = "0.4", optional = true }
alsa = { version = "0.5", optional = true }
portaudio-rs = { version = "0.3", optional = true }
libpulse-binding = { version = "2.13", optional = true, default-features = false }
libpulse-simple-binding = { version = "2.13", optional = true, default-features = false }
libpulse-binding = { version = "2", optional = true, default-features = false }
libpulse-simple-binding = { version = "2", optional = true, default-features = false }
jack = { version = "0.6", optional = true }
libc = { version = "0.2", optional = true }
sdl2 = { version = "0.34", optional = true }
sdl2 = { version = "0.34.3", optional = true }
gstreamer = { version = "0.16", optional = true }
gstreamer-app = { version = "0.16", optional = true }
glib = { version = "0.10", optional = true }
zerocopy = { version = "0.3", optional = true }
zerocopy = { version = "0.3" }
# Rodio dependencies
rodio = { version = "0.13", optional = true, default-features = false }
@ -50,4 +50,4 @@ jackaudio-backend = ["jack"]
rodio-backend = ["rodio", "cpal", "thiserror"]
rodiojack-backend = ["rodio", "cpal/jack", "thiserror"]
sdl-backend = ["sdl2"]
gstreamer-backend = ["gstreamer", "gstreamer-app", "glib", "zerocopy"]
gstreamer-backend = ["gstreamer", "gstreamer-app", "glib"]

View file

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

View file

@ -1,8 +1,13 @@
use super::{Open, Sink};
use super::{Open, Sink, SinkAsBytes};
use crate::audio::AudioPacket;
use crate::config::AudioFormat;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
use gstreamer as gst;
use gstreamer_app as gst_app;
use gst::prelude::*;
use gst::*;
use zerocopy::*;
use zerocopy::AsBytes;
use std::sync::mpsc::{sync_channel, SyncSender};
use std::{io, thread};
@ -11,12 +16,27 @@ use std::{io, thread};
pub struct GstreamerSink {
tx: SyncSender<Vec<u8>>,
pipeline: gst::Pipeline,
format: AudioFormat,
}
impl Open for GstreamerSink {
fn open(device: Option<String>) -> GstreamerSink {
gst::init().expect("Failed to init gstreamer!");
let pipeline_str_preamble = r#"appsrc caps="audio/x-raw,format=S16LE,layout=interleaved,channels=2,rate=44100" block=true max-bytes=4096 name=appsrc0 "#;
fn open(device: Option<String>, format: AudioFormat) -> Self {
info!("Using GStreamer sink with format: {:?}", format);
gst::init().expect("failed to init GStreamer!");
// GStreamer calls S24 and S24_3 different from the rest of the world
let gst_format = match format {
AudioFormat::S24 => "S24_32".to_string(),
AudioFormat::S24_3 => "S24".to_string(),
_ => format!("{:?}", format),
};
let sample_size = format.size();
let gst_bytes = 2048 * sample_size;
let pipeline_str_preamble = format!(
"appsrc caps=\"audio/x-raw,format={}LE,layout=interleaved,channels={},rate={}\" block=true max-bytes={} name=appsrc0 ",
gst_format, NUM_CHANNELS, SAMPLE_RATE, gst_bytes
);
let pipeline_str_rest = r#" ! audioconvert ! autoaudiosink"#;
let pipeline_str: String = match device {
Some(x) => format!("{}{}", pipeline_str_preamble, x),
@ -28,38 +48,37 @@ impl Open for GstreamerSink {
let pipelinee = gst::parse_launch(&*pipeline_str).expect("Couldn't launch pipeline; likely a GStreamer issue or an error in the pipeline string you specified in the 'device' argument to librespot.");
let pipeline = pipelinee
.dynamic_cast::<gst::Pipeline>()
.expect("Couldn't cast pipeline element at runtime!");
let bus = pipeline.get_bus().expect("Couldn't get bus from pipeline");
.expect("couldn't cast pipeline element at runtime!");
let bus = pipeline.get_bus().expect("couldn't get bus from pipeline");
let mainloop = glib::MainLoop::new(None, false);
let appsrce: gst::Element = pipeline
.get_by_name("appsrc0")
.expect("Couldn't get appsrc from pipeline");
.expect("couldn't get appsrc from pipeline");
let appsrc: gst_app::AppSrc = appsrce
.dynamic_cast::<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 appsrc_caps = appsrc.get_caps().expect("Couldn't get appsrc caps");
let appsrc_caps = appsrc.get_caps().expect("couldn't get appsrc caps");
let mut conf = bufferpool.get_config();
conf.set_params(Some(&appsrc_caps), 8192, 0, 0);
conf.set_params(Some(&appsrc_caps), 4096 * sample_size as u32, 0, 0);
bufferpool
.set_config(conf)
.expect("Couldn't configure the buffer pool");
.expect("couldn't configure the buffer pool");
bufferpool
.set_active(true)
.expect("Couldn't activate buffer pool");
.expect("couldn't activate buffer pool");
let (tx, rx) = sync_channel::<Vec<u8>>(128);
let (tx, rx) = sync_channel::<Vec<u8>>(64 * sample_size);
thread::spawn(move || {
for data in rx {
let buffer = bufferpool.acquire_buffer(None);
if !buffer.is_err() {
let mut okbuffer = buffer.unwrap();
let mutbuf = okbuffer.make_mut();
if let Ok(mut buffer) = buffer {
let mutbuf = buffer.make_mut();
mutbuf.set_size(data.len());
mutbuf
.copy_from_slice(0, data.as_bytes())
.expect("Failed to copy from slice");
let _eat = appsrc.push_buffer(okbuffer);
let _eat = appsrc.push_buffer(buffer);
}
}
});
@ -69,8 +88,8 @@ impl Open for GstreamerSink {
let watch_mainloop = thread_mainloop.clone();
bus.add_watch(move |_, msg| {
match msg.view() {
MessageView::Eos(..) => watch_mainloop.quit(),
MessageView::Error(err) => {
gst::MessageView::Eos(..) => watch_mainloop.quit(),
gst::MessageView::Error(err) => {
println!(
"Error from {:?}: {} ({:?})",
err.get_src().map(|s| s.get_path_string()),
@ -84,30 +103,32 @@ impl Open for GstreamerSink {
glib::Continue(true)
})
.expect("Failed to add bus watch");
.expect("failed to add bus watch");
thread_mainloop.run();
});
pipeline
.set_state(gst::State::Playing)
.expect("Unable to set the pipeline to the `Playing` state");
.expect("unable to set the pipeline to the `Playing` state");
GstreamerSink { tx, pipeline }
Self {
tx,
pipeline,
format,
}
}
}
impl Sink for GstreamerSink {
fn start(&mut self) -> io::Result<()> {
Ok(())
}
fn stop(&mut self) -> io::Result<()> {
Ok(())
}
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
start_stop_noop!();
sink_as_bytes!();
}
impl SinkAsBytes for GstreamerSink {
fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> {
// Copy expensively (in to_vec()) to avoid thread synchronization
let deighta: &[u8] = packet.samples().as_bytes();
self.tx
.send(deighta.to_vec())
.send(data.to_vec())
.expect("tx send failed in write function");
Ok(())
}

View file

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

View file

@ -1,8 +1,9 @@
use crate::audio::AudioPacket;
use crate::config::AudioFormat;
use std::io;
pub trait Open {
fn open(_: Option<String>) -> Self;
fn open(_: Option<String>, format: AudioFormat) -> Self;
}
pub trait Sink {
@ -11,10 +12,57 @@ pub trait Sink {
fn write(&mut self, packet: &AudioPacket) -> io::Result<()>;
}
pub type SinkBuilder = fn(Option<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> {
Box::new(S::open(device))
pub trait SinkAsBytes {
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")]

View file

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

View file

@ -1,16 +1,27 @@
use super::{Open, Sink};
use crate::audio::AudioPacket;
use portaudio_rs;
use crate::audio::{convert, AudioPacket};
use crate::config::AudioFormat;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo};
use portaudio_rs::stream::*;
use std::io;
use std::process::exit;
use std::time::Duration;
pub struct PortAudioSink<'a>(
Option<portaudio_rs::stream::Stream<'a, i16, i16>>,
StreamParameters<i16>,
);
pub enum PortAudioSink<'a> {
F32(
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)>> {
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> {
fn open(device: Option<String>) -> PortAudioSink<'a> {
debug!("Using PortAudio sink");
fn open(device: Option<String>, format: AudioFormat) -> PortAudioSink<'a> {
info!("Using PortAudio sink with format: {:?}", format);
warn!("This backend is known to panic on several platforms.");
warn!("Consider using some other backend, or better yet, contributing a fix.");
portaudio_rs::initialize().unwrap();
@ -53,7 +67,7 @@ impl<'a> Open for PortAudioSink<'a> {
Some(device) => find_output(device),
None => get_default_output_index(),
}
.expect("Could not find device");
.expect("could not find device");
let info = portaudio_rs::device::get_info(device_idx);
let latency = match info {
@ -61,46 +75,99 @@ impl<'a> Open for PortAudioSink<'a> {
None => Duration::new(0, 0),
};
let params = StreamParameters {
device: device_idx,
channel_count: 2,
suggested_latency: latency,
data: 0i16,
};
PortAudioSink(None, params)
macro_rules! open_sink {
($sink: expr, $type: ty) => {{
let params = StreamParameters {
device: device_idx,
channel_count: NUM_CHANNELS as u32,
suggested_latency: latency,
data: 0.0 as $type,
};
$sink(None, params)
}};
}
match format {
AudioFormat::F32 => open_sink!(Self::F32, f32),
AudioFormat::S32 => open_sink!(Self::S32, i32),
AudioFormat::S16 => open_sink!(Self::S16, i16),
_ => {
unimplemented!("PortAudio currently does not support {:?} output", format)
}
}
}
}
impl<'a> Sink for PortAudioSink<'a> {
fn start(&mut self) -> io::Result<()> {
if self.0.is_none() {
self.0 = Some(
Stream::open(
None,
Some(self.1),
44100.0,
FRAMES_PER_BUFFER_UNSPECIFIED,
StreamFlags::empty(),
None,
)
.unwrap(),
);
macro_rules! start_sink {
(ref mut $stream: ident, ref $parameters: ident) => {{
if $stream.is_none() {
*$stream = Some(
Stream::open(
None,
Some(*$parameters),
SAMPLE_RATE as f64,
FRAMES_PER_BUFFER_UNSPECIFIED,
StreamFlags::empty(),
None,
)
.unwrap(),
);
}
$stream.as_mut().unwrap().start().unwrap()
}};
}
self.0.as_mut().unwrap().start().unwrap();
match self {
Self::F32(stream, parameters) => start_sink!(ref mut stream, ref parameters),
Self::S32(stream, parameters) => start_sink!(ref mut stream, ref parameters),
Self::S16(stream, parameters) => start_sink!(ref mut stream, ref parameters),
};
Ok(())
}
fn stop(&mut self) -> io::Result<()> {
self.0.as_mut().unwrap().stop().unwrap();
self.0 = None;
macro_rules! stop_sink {
(ref mut $stream: ident) => {{
$stream.as_mut().unwrap().stop().unwrap();
*$stream = None;
}};
}
match self {
Self::F32(stream, _parameters) => stop_sink!(ref mut stream),
Self::S32(stream, _parameters) => stop_sink!(ref mut stream),
Self::S16(stream, _parameters) => stop_sink!(ref mut stream),
};
Ok(())
}
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
match self.0.as_mut().unwrap().write(packet.samples()) {
macro_rules! write_sink {
(ref mut $stream: expr, $samples: expr) => {
$stream.as_mut().unwrap().write($samples)
};
}
let samples = packet.samples();
let result = match self {
Self::F32(stream, _parameters) => {
write_sink!(ref mut stream, samples)
}
Self::S32(stream, _parameters) => {
let samples_s32: &[i32] = &convert::to_s32(samples);
write_sink!(ref mut stream, samples_s32)
}
Self::S16(stream, _parameters) => {
let samples_s16: &[i16] = &convert::to_s16(samples);
write_sink!(ref mut stream, samples_s16)
}
};
match result {
Ok(_) => (),
Err(portaudio_rs::PaError::OutputUnderflowed) => error!("PortAudio write underflow"),
Err(e) => panic!("PA Error {}", e),
Err(e) => panic!("PortAudio error {}", e),
};
Ok(())

View file

@ -1,5 +1,7 @@
use super::{Open, Sink};
use super::{Open, Sink, SinkAsBytes};
use crate::audio::AudioPacket;
use crate::config::AudioFormat;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
use libpulse_binding::{self as pulse, stream::Direction};
use libpulse_simple_binding::Simple;
use std::io;
@ -11,23 +13,34 @@ pub struct PulseAudioSink {
s: Option<Simple>,
ss: pulse::sample::Spec,
device: Option<String>,
format: AudioFormat,
}
impl Open for PulseAudioSink {
fn open(device: Option<String>) -> PulseAudioSink {
debug!("Using PulseAudio sink");
fn open(device: Option<String>, format: AudioFormat) -> Self {
info!("Using PulseAudio sink with format: {:?}", format);
// PulseAudio calls S24 and S24_3 different from the rest of the world
let pulse_format = match format {
AudioFormat::F32 => pulse::sample::Format::F32le,
AudioFormat::S32 => pulse::sample::Format::S32le,
AudioFormat::S24 => pulse::sample::Format::S24_32le,
AudioFormat::S24_3 => pulse::sample::Format::S24le,
AudioFormat::S16 => pulse::sample::Format::S16le,
};
let ss = pulse::sample::Spec {
format: pulse::sample::Format::S16le,
channels: 2, // stereo
rate: 44100,
format: pulse_format,
channels: NUM_CHANNELS,
rate: SAMPLE_RATE,
};
debug_assert!(ss.is_valid());
PulseAudioSink {
Self {
s: None,
ss,
device,
format,
}
}
}
@ -66,19 +79,13 @@ impl Sink for PulseAudioSink {
Ok(())
}
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
if let Some(s) = &self.s {
// SAFETY: An i16 consists of two bytes, so that the given slice can be interpreted
// as a byte array of double length. Each byte pointer is validly aligned, and so
// is the newly created slice.
let d: &[u8] = unsafe {
std::slice::from_raw_parts(
packet.samples().as_ptr() as *const u8,
packet.samples().len() * 2,
)
};
sink_as_bytes!();
}
match s.write(d) {
impl SinkAsBytes for PulseAudioSink {
fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> {
if let Some(s) = &self.s {
match s.write(data) {
Ok(_) => Ok(()),
Err(e) => Err(io::Error::new(
io::ErrorKind::BrokenPipe,
@ -88,7 +95,7 @@ impl Sink for PulseAudioSink {
} else {
Err(io::Error::new(
io::ErrorKind::NotConnected,
"Not connected to pulseaudio",
"Not connected to PulseAudio",
))
}
}

View file

@ -1,12 +1,13 @@
use std::process::exit;
use std::{convert::Infallible, sync::mpsc};
use std::{io, thread, time};
use cpal::traits::{DeviceTrait, HostTrait};
use thiserror::Error;
use super::Sink;
use crate::audio::AudioPacket;
use crate::audio::{convert, AudioPacket};
use crate::config::AudioFormat;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
#[cfg(all(
feature = "rodiojack-backend",
@ -15,15 +16,16 @@ use crate::audio::AudioPacket;
compile_error!("Rodio JACK backend is currently only supported on linux.");
#[cfg(feature = "rodio-backend")]
pub fn mk_rodio(device: Option<String>) -> Box<dyn Sink + Send> {
Box::new(open(cpal::default_host(), device))
pub fn mk_rodio(device: Option<String>, format: AudioFormat) -> Box<dyn Sink> {
Box::new(open(cpal::default_host(), device, format))
}
#[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(
cpal::host_from_id(cpal::HostId::Jack).unwrap(),
device,
format,
))
}
@ -43,8 +45,8 @@ pub enum RodioError {
pub struct RodioSink {
rodio_sink: rodio::Sink,
// will produce a TryRecvError on the receiver side when it is dropped.
_close_tx: mpsc::SyncSender<Infallible>,
format: AudioFormat,
_stream: rodio::OutputStream,
}
fn list_formats(device: &rodio::Device) {
@ -149,52 +151,54 @@ fn create_sink(
Ok((sink, stream))
}
pub fn open(host: cpal::Host, device: Option<String>) -> RodioSink {
debug!("Using rodio sink with cpal host: {}", host.id().name());
pub fn open(host: cpal::Host, device: Option<String>, format: AudioFormat) -> RodioSink {
debug!(
"Using rodio sink with format {:?} and cpal host: {}",
format,
host.id().name()
);
let (sink_tx, sink_rx) = mpsc::sync_channel(1);
let (close_tx, close_rx) = mpsc::sync_channel(1);
std::thread::spawn(move || match create_sink(&host, device) {
Ok((sink, stream)) => {
sink_tx.send(Ok(sink)).unwrap();
close_rx.recv().unwrap_err(); // This will fail as soon as the sender is dropped
debug!("drop rodio::OutputStream");
drop(stream);
match format {
AudioFormat::F32 => {
#[cfg(target_os = "linux")]
warn!("Rodio output to Alsa is known to cause garbled sound, consider using `--backend alsa`")
}
Err(e) => {
sink_tx.send(Err(e)).unwrap();
}
});
AudioFormat::S16 => (),
_ => unimplemented!("Rodio currently only supports F32 and S16 formats"),
}
// Instead of the second `unwrap`, better error handling could be introduced
let sink = sink_rx.recv().unwrap().unwrap();
let (sink, stream) = create_sink(&host, device).unwrap();
debug!("Rodio sink was created");
RodioSink {
rodio_sink: sink,
_close_tx: close_tx,
format,
_stream: stream,
}
}
impl Sink for RodioSink {
fn start(&mut self) -> io::Result<()> {
// More similar to an "unpause" than "play". Doesn't undo "stop".
// self.rodio_sink.play();
Ok(())
}
fn stop(&mut self) -> io::Result<()> {
// This will immediately stop playback, but the sink is then unusable.
// We just have to let the current buffer play till the end.
// self.rodio_sink.stop();
Ok(())
}
start_stop_noop!();
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
let source = rodio::buffer::SamplesBuffer::new(2, 44100, packet.samples());
self.rodio_sink.append(source);
let samples = packet.samples();
match self.format {
AudioFormat::F32 => {
let source =
rodio::buffer::SamplesBuffer::new(NUM_CHANNELS as u16, SAMPLE_RATE, samples);
self.rodio_sink.append(source);
}
AudioFormat::S16 => {
let samples_s16: &[i16] = &convert::to_s16(samples);
let source = rodio::buffer::SamplesBuffer::new(
NUM_CHANNELS as u16,
SAMPLE_RATE,
samples_s16,
);
self.rodio_sink.append(source);
}
_ => unreachable!(),
};
// Chunk sizes seem to be about 256 to 3000 ish items long.
// Assuming they're on average 1628 then a half second buffer is:

View file

@ -1,57 +1,112 @@
use super::{Open, Sink};
use crate::audio::AudioPacket;
use crate::audio::{convert, AudioPacket};
use crate::config::AudioFormat;
use crate::player::{NUM_CHANNELS, SAMPLE_RATE};
use sdl2::audio::{AudioQueue, AudioSpecDesired};
use std::{io, thread, time};
type Channel = i16;
pub struct SdlSink {
queue: AudioQueue<Channel>,
pub enum SdlSink {
F32(AudioQueue<f32>),
S32(AudioQueue<i32>),
S16(AudioQueue<i16>),
}
impl Open for SdlSink {
fn open(device: Option<String>) -> SdlSink {
debug!("Using SDL sink");
fn open(device: Option<String>, format: AudioFormat) -> Self {
info!("Using SDL sink with format: {:?}", format);
if device.is_some() {
panic!("SDL sink does not support specifying a device name");
warn!("SDL sink does not support specifying a device name");
}
let ctx = sdl2::init().expect("Could not init SDL");
let audio = ctx.audio().expect("Could not init SDL audio subsystem");
let ctx = sdl2::init().expect("could not initialize SDL");
let audio = ctx
.audio()
.expect("could not initialize SDL audio subsystem");
let desired_spec = AudioSpecDesired {
freq: Some(44_100),
channels: Some(2),
freq: Some(SAMPLE_RATE as i32),
channels: Some(NUM_CHANNELS),
samples: None,
};
let queue = audio
.open_queue(None, &desired_spec)
.expect("Could not open SDL audio device");
SdlSink { queue }
macro_rules! open_sink {
($sink: expr, $type: ty) => {{
let queue: AudioQueue<$type> = audio
.open_queue(None, &desired_spec)
.expect("could not open SDL audio device");
$sink(queue)
}};
}
match format {
AudioFormat::F32 => open_sink!(Self::F32, f32),
AudioFormat::S32 => open_sink!(Self::S32, i32),
AudioFormat::S16 => open_sink!(Self::S16, i16),
_ => {
unimplemented!("SDL currently does not support {:?} output", format)
}
}
}
}
impl Sink for SdlSink {
fn start(&mut self) -> io::Result<()> {
self.queue.clear();
self.queue.resume();
macro_rules! start_sink {
($queue: expr) => {{
$queue.clear();
$queue.resume();
}};
}
match self {
Self::F32(queue) => start_sink!(queue),
Self::S32(queue) => start_sink!(queue),
Self::S16(queue) => start_sink!(queue),
};
Ok(())
}
fn stop(&mut self) -> io::Result<()> {
self.queue.pause();
self.queue.clear();
macro_rules! stop_sink {
($queue: expr) => {{
$queue.pause();
$queue.clear();
}};
}
match self {
Self::F32(queue) => stop_sink!(queue),
Self::S32(queue) => stop_sink!(queue),
Self::S16(queue) => stop_sink!(queue),
};
Ok(())
}
fn write(&mut self, packet: &AudioPacket) -> io::Result<()> {
while self.queue.size() > (2 * 2 * 44_100) {
// sleep and wait for sdl thread to drain the queue a bit
thread::sleep(time::Duration::from_millis(10));
macro_rules! drain_sink {
($queue: expr, $size: expr) => {{
// sleep and wait for sdl thread to drain the queue a bit
while $queue.size() > (NUM_CHANNELS as u32 * $size as u32 * SAMPLE_RATE) {
thread::sleep(time::Duration::from_millis(10));
}
}};
}
self.queue.queue(packet.samples());
let samples = packet.samples();
match self {
Self::F32(queue) => {
drain_sink!(queue, AudioFormat::F32.size());
queue.queue(samples)
}
Self::S32(queue) => {
let samples_s32: &[i32] = &convert::to_s32(samples);
drain_sink!(queue, AudioFormat::S32.size());
queue.queue(samples_s32)
}
Self::S16(queue) => {
let samples_s16: &[i16] = &convert::to_s16(samples);
drain_sink!(queue, AudioFormat::S16.size());
queue.queue(samples_s16)
}
};
Ok(())
}
}

View file

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

View file

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

View file

@ -33,13 +33,12 @@ impl AlsaMixer {
let mixer = alsa::mixer::Mixer::new(&config.card, false)?;
let sid = alsa::mixer::SelemId::new(&config.mixer, config.index);
let selem = mixer.find_selem(&sid).expect(
format!(
let selem = mixer.find_selem(&sid).unwrap_or_else(|| {
panic!(
"Couldn't find simple mixer control for {},{}",
&config.mixer, &config.index,
)
.as_str(),
);
});
let (min, max) = selem.get_playback_volume_range();
let (min_db, max_db) = selem.get_playback_db_range();
let hw_mix = selem

View file

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

View file

@ -35,11 +35,12 @@ struct SoftVolumeApplier {
}
impl AudioFilter for SoftVolumeApplier {
fn modify_stream(&self, data: &mut [i16]) {
fn modify_stream(&self, data: &mut [f32]) {
let volume = self.volume.load(Ordering::Relaxed) as u16;
if volume != 0xFFFF {
let volume_factor = volume as f64 / 0xFFFF as f64;
for x in data.iter_mut() {
*x = (*x as i32 * volume as i32 / 0xFFFF) as i16;
*x = (*x as f64 * volume_factor) as f32;
}
}
}

View file

@ -18,15 +18,19 @@ use crate::audio::{
READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK_SECONDS,
};
use crate::audio_backend::Sink;
use crate::config::NormalisationType;
use crate::config::{Bitrate, PlayerConfig};
use crate::config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig};
use crate::core::session::Session;
use crate::core::spotify_id::SpotifyId;
use crate::core::util::SeqGenerator;
use crate::metadata::{AudioItem, FileFormat};
use crate::mixer::AudioFilter;
pub const SAMPLE_RATE: u32 = 44100;
pub const NUM_CHANNELS: u8 = 2;
pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE as u32 * NUM_CHANNELS as u32;
const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000;
const DB_VOLTAGE_RATIO: f32 = 20.0;
pub struct Player {
commands: Option<mpsc::UnboundedSender<PlayerCommand>>,
@ -50,11 +54,18 @@ struct PlayerInternal {
state: PlayerState,
preload: PlayerPreload,
sink: Box<dyn Sink + Send>,
sink: Box<dyn Sink>,
sink_status: SinkStatus,
sink_event_callback: Option<SinkEventCallback>,
audio_filter: Option<Box<dyn AudioFilter + Send>>,
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 {
@ -186,7 +197,7 @@ impl PlayerEvent {
pub type PlayerEventChannel = mpsc::UnboundedReceiver<PlayerEvent>;
#[derive(Clone, Copy, Debug)]
struct NormalisationData {
pub struct NormalisationData {
track_gain_db: f32,
track_peak: f32,
album_gain_db: f32,
@ -194,6 +205,14 @@ struct NormalisationData {
}
impl NormalisationData {
pub fn db_to_ratio(db: f32) -> f32 {
f32::powf(10.0, db / DB_VOLTAGE_RATIO)
}
pub fn ratio_to_db(ratio: f32) -> f32 {
ratio.log10() * DB_VOLTAGE_RATIO
}
fn parse_from_file<T: Read + Seek>(mut file: T) -> io::Result<NormalisationData> {
const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144;
file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET))?;
@ -218,17 +237,41 @@ impl NormalisationData {
NormalisationType::Album => [data.album_gain_db, data.album_peak],
NormalisationType::Track => [data.track_gain_db, data.track_peak],
};
let mut normalisation_factor =
f32::powf(10.0, (gain_db + config.normalisation_pregain) / 20.0);
if normalisation_factor * gain_peak > 1.0 {
warn!("Reducing normalisation factor to prevent clipping. Please add negative pregain to avoid.");
normalisation_factor = 1.0 / gain_peak;
let normalisation_power = gain_db + config.normalisation_pregain;
let mut normalisation_factor = Self::db_to_ratio(normalisation_power);
if normalisation_factor * gain_peak > config.normalisation_threshold {
let limited_normalisation_factor = config.normalisation_threshold / gain_peak;
let limited_normalisation_power = Self::ratio_to_db(limited_normalisation_factor);
if config.normalisation_method == NormalisationMethod::Basic {
warn!("Limiting gain to {:.2} for the duration of this track to stay under normalisation threshold.", limited_normalisation_power);
normalisation_factor = limited_normalisation_factor;
} else {
warn!(
"This track will at its peak be subject to {:.2} dB of dynamic limiting.",
normalisation_power - limited_normalisation_power
);
}
warn!("Please lower pregain to avoid.");
}
debug!("Normalisation Data: {:?}", data);
debug!("Normalisation Type: {:?}", config.normalisation_type);
debug!("Applied normalisation factor: {}", normalisation_factor);
debug!(
"Normalisation Threshold: {:.1}",
Self::ratio_to_db(config.normalisation_threshold)
);
debug!("Normalisation Method: {:?}", config.normalisation_method);
debug!("Normalisation Factor: {}", normalisation_factor);
if config.normalisation_method == NormalisationMethod::Dynamic {
debug!("Normalisation Attack: {:?}", config.normalisation_attack);
debug!("Normalisation Release: {:?}", config.normalisation_release);
debug!("Normalisation Knee: {:?}", config.normalisation_knee);
}
normalisation_factor
}
@ -242,7 +285,7 @@ impl Player {
sink_builder: F,
) -> (Player, PlayerEventChannel)
where
F: FnOnce() -> Box<dyn Sink + Send> + Send + 'static,
F: FnOnce() -> Box<dyn Sink> + Send + 'static,
{
let (cmd_tx, cmd_rx) = mpsc::unbounded_channel();
let (event_sender, event_receiver) = mpsc::unbounded_channel();
@ -262,6 +305,13 @@ impl Player {
sink_event_callback: None,
audio_filter,
event_senders: [event_sender].to_vec(),
limiter_active: false,
limiter_attack_counter: 0,
limiter_release_counter: 0,
limiter_peak_sample: 0.0,
limiter_factor: 1.0,
limiter_strength: 0.0,
};
// While PlayerInternal is written as a future, it still contains blocking code.
@ -888,7 +938,8 @@ impl Future for PlayerInternal {
if !passthrough {
if let Some(ref packet) = packet {
*stream_position_pcm += (packet.samples().len() / 2) as u64;
*stream_position_pcm +=
(packet.samples().len() / NUM_CHANNELS as usize) as u64;
let stream_position_millis =
Self::position_pcm_to_ms(*stream_position_pcm);
@ -1110,10 +1161,115 @@ impl PlayerInternal {
}
if self.config.normalisation
&& f32::abs(normalisation_factor - 1.0) > f32::EPSILON
&& (f32::abs(normalisation_factor - 1.0) < f32::EPSILON
|| self.config.normalisation_method != NormalisationMethod::Basic)
{
for x in data.iter_mut() {
*x = (*x as f32 * normalisation_factor) as i16;
for sample in data.iter_mut() {
let mut actual_normalisation_factor = normalisation_factor;
if self.config.normalisation_method == NormalisationMethod::Dynamic
{
if self.limiter_active {
// "S"-shaped curve with a configurable knee during attack and release:
// - > 1.0 yields soft knees at start and end, steeper in between
// - 1.0 yields a linear function from 0-100%
// - between 0.0 and 1.0 yields hard knees at start and end, flatter in between
// - 0.0 yields a step response to 50%, causing distortion
// - Rates < 0.0 invert the limiter and are invalid
let mut shaped_limiter_strength = self.limiter_strength;
if shaped_limiter_strength > 0.0
&& shaped_limiter_strength < 1.0
{
shaped_limiter_strength = 1.0
/ (1.0
+ f32::powf(
shaped_limiter_strength
/ (1.0 - shaped_limiter_strength),
-1.0 * self.config.normalisation_knee,
));
}
actual_normalisation_factor =
(1.0 - shaped_limiter_strength) * normalisation_factor
+ shaped_limiter_strength * self.limiter_factor;
};
// Always check for peaks, even when the limiter is already active.
// There may be even higher peaks than we initially targeted.
// Check against the normalisation factor that would be applied normally.
let abs_sample =
((*sample as f64 * normalisation_factor as f64) as f32)
.abs();
if abs_sample > self.config.normalisation_threshold {
self.limiter_active = true;
if self.limiter_release_counter > 0 {
// A peak was encountered while releasing the limiter;
// synchronize with the current release limiter strength.
self.limiter_attack_counter = (((SAMPLES_PER_SECOND
as f32
* self.config.normalisation_release)
- self.limiter_release_counter as f32)
/ (self.config.normalisation_release
/ self.config.normalisation_attack))
as u32;
self.limiter_release_counter = 0;
}
self.limiter_attack_counter =
self.limiter_attack_counter.saturating_add(1);
self.limiter_strength = self.limiter_attack_counter as f32
/ (SAMPLES_PER_SECOND as f32
* self.config.normalisation_attack);
if abs_sample > self.limiter_peak_sample {
self.limiter_peak_sample = abs_sample;
self.limiter_factor =
self.config.normalisation_threshold
/ self.limiter_peak_sample;
}
} else if self.limiter_active {
if self.limiter_attack_counter > 0 {
// Release may start within the attack period, before
// the limiter reached full strength. For that reason
// start the release by synchronizing with the current
// attack limiter strength.
self.limiter_release_counter = (((SAMPLES_PER_SECOND
as f32
* self.config.normalisation_attack)
- self.limiter_attack_counter as f32)
* (self.config.normalisation_release
/ self.config.normalisation_attack))
as u32;
self.limiter_attack_counter = 0;
}
self.limiter_release_counter =
self.limiter_release_counter.saturating_add(1);
if self.limiter_release_counter
> (SAMPLES_PER_SECOND as f32
* self.config.normalisation_release)
as u32
{
self.reset_limiter();
} else {
self.limiter_strength = ((SAMPLES_PER_SECOND as f32
* self.config.normalisation_release)
- self.limiter_release_counter as f32)
/ (SAMPLES_PER_SECOND as f32
* self.config.normalisation_release);
}
}
}
*sample =
(*sample as f64 * actual_normalisation_factor as f64) as f32;
// Extremely sharp attacks, however unlikely, *may* still clip and provide
// undefined results, so strictly enforce output within [-1.0, 1.0].
if *sample < -1.0 {
*sample = -1.0;
} else if *sample > 1.0 {
*sample = 1.0;
}
}
}
}
@ -1144,6 +1300,15 @@ impl PlayerInternal {
}
}
fn reset_limiter(&mut self) {
self.limiter_active = false;
self.limiter_release_counter = 0;
self.limiter_attack_counter = 0;
self.limiter_peak_sample = 0.0;
self.limiter_factor = 1.0;
self.limiter_strength = 0.0;
}
fn start_playback(
&mut self,
track_id: SpotifyId,

View file

@ -12,13 +12,16 @@ use librespot::core::config::{ConnectConfig, DeviceType, SessionConfig, VolumeCt
use librespot::core::session::Session;
use librespot::core::version;
use librespot::playback::audio_backend::{self, Sink, BACKENDS};
use librespot::playback::config::{Bitrate, NormalisationType, PlayerConfig};
use librespot::playback::config::{
AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig,
};
use librespot::playback::mixer::{self, Mixer, MixerConfig};
use librespot::playback::player::Player;
use librespot::playback::player::{NormalisationData, Player};
mod player_event_handler;
use player_event_handler::{emit_sink_event, run_program_on_events};
use std::convert::TryFrom;
use std::path::Path;
use std::process::exit;
use std::str::FromStr;
@ -28,6 +31,8 @@ use std::{
pin::Pin,
};
const MILLIS: f32 = 1000.0;
fn device_id(name: &str) -> String {
hex::encode(Sha1::digest(name.as_bytes()))
}
@ -93,9 +98,20 @@ pub fn get_credentials<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)]
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>,
mixer: fn(Option<MixerConfig>) -> Box<dyn Mixer>,
@ -112,7 +128,7 @@ struct Setup {
emit_sink_events: bool,
}
fn setup(args: &[String]) -> Setup {
fn get_setup(args: &[String]) -> Setup {
let mut opts = getopts::Options::new();
opts.optopt(
"c",
@ -125,7 +141,7 @@ fn setup(args: &[String]) -> Setup {
"Path to a directory where system files (credentials, volume) will be cached. Can be different from cache option value",
"SYTEMCACHE",
).optflag("", "disable-audio-cache", "Disable caching of the audio data.")
.reqopt("n", "name", "Device name", "NAME")
.optopt("n", "name", "Device name", "NAME")
.optopt("", "device-type", "Displayed device type", "DEVICE_TYPE")
.optopt(
"b",
@ -141,6 +157,7 @@ fn setup(args: &[String]) -> Setup {
)
.optflag("", "emit-sink-events", "Run program set by --onevent before sink is opened and after it is closed.")
.optflag("v", "verbose", "Enable verbose output")
.optflag("V", "version", "Display librespot version string")
.optopt("u", "username", "Username to sign in with", "USERNAME")
.optopt("p", "password", "Password", "PASSWORD")
.optopt("", "proxy", "HTTP proxy to use when connecting", "PROXY")
@ -158,6 +175,12 @@ fn setup(args: &[String]) -> Setup {
"Audio device to use. Use '?' to list options if using portaudio or alsa",
"DEVICE",
)
.optopt(
"",
"format",
"Output format (F32, S32, S24, S24_3 or S16). Defaults to S16",
"FORMAT",
)
.optopt("", "mixer", "Mixer to use (alsa or softvol)", "MIXER")
.optopt(
"m",
@ -199,6 +222,12 @@ fn setup(args: &[String]) -> Setup {
"enable-volume-normalisation",
"Play all tracks at the same volume",
)
.optopt(
"",
"normalisation-method",
"Specify the normalisation method to use - [basic, dynamic]. Default is dynamic.",
"NORMALISATION_METHOD",
)
.optopt(
"",
"normalisation-gain-type",
@ -211,6 +240,30 @@ fn setup(args: &[String]) -> Setup {
"Pregain (dB) applied by volume normalisation",
"PREGAIN",
)
.optopt(
"",
"normalisation-threshold",
"Threshold (dBFS) to prevent clipping. Default is -1.0.",
"THRESHOLD",
)
.optopt(
"",
"normalisation-attack",
"Attack time (ms) in which the dynamic limiter is reducing gain. Default is 5.",
"ATTACK",
)
.optopt(
"",
"normalisation-release",
"Release or decay time (ms) in which the dynamic limiter is restoring gain. Default is 100.",
"RELEASE",
)
.optopt(
"",
"normalisation-knee",
"Knee steepness of the dynamic limiter. Default is 1.0.",
"KNEE",
)
.optopt(
"",
"volume-ctrl",
@ -241,15 +294,20 @@ fn setup(args: &[String]) -> Setup {
}
};
if matches.opt_present("version") {
print_version();
exit(0);
}
let verbose = matches.opt_present("verbose");
setup_logging(verbose);
info!(
"librespot {} ({}). Built on {}. Build ID: {}",
version::short_sha(),
version::commit_date(),
version::short_now(),
version::build_id()
"librespot {semver} {sha} (Built on {build_date}, Build ID: {build_id})",
semver = version::SEMVER,
sha = version::SHA_SHORT,
build_date = version::BUILD_DATE,
build_id = version::BUILD_ID
);
let backend_name = matches.opt_str("backend");
@ -260,9 +318,15 @@ fn setup(args: &[String]) -> Setup {
let backend = audio_backend::find(backend_name).expect("Invalid backend");
let format = matches
.opt_str("format")
.as_ref()
.map(|format| AudioFormat::try_from(format).expect("Invalid output format"))
.unwrap_or_default();
let device = matches.opt_str("device");
if device == Some("?".into()) {
backend(device);
backend(device, format);
exit(0);
}
@ -329,7 +393,9 @@ fn setup(args: &[String]) -> Setup {
.map(|port| port.parse::<u16>().unwrap())
.unwrap_or(0);
let name = matches.opt_str("name").unwrap();
let name = matches
.opt_str("name")
.unwrap_or_else(|| "Librespot".to_string());
let credentials = {
let cached_credentials = cache.as_ref().and_then(Cache::credentials);
@ -352,7 +418,7 @@ fn setup(args: &[String]) -> Setup {
let device_id = device_id(&name);
SessionConfig {
user_agent: version::version_string(),
user_agent: version::VERSION_STRING.to_string(),
device_id,
proxy: matches.opt_str("proxy").or_else(|| std::env::var("http_proxy").ok()).map(
|s| {
@ -392,15 +458,48 @@ fn setup(args: &[String]) -> Setup {
NormalisationType::from_str(gain_type).expect("Invalid normalisation type")
})
.unwrap_or_default();
let normalisation_method = matches
.opt_str("normalisation-method")
.as_ref()
.map(|gain_type| {
NormalisationMethod::from_str(gain_type).expect("Invalid normalisation method")
})
.unwrap_or_default();
PlayerConfig {
bitrate,
gapless: !matches.opt_present("disable-gapless"),
normalisation: matches.opt_present("enable-volume-normalisation"),
normalisation_method,
normalisation_type: gain_type,
normalisation_pregain: matches
.opt_str("normalisation-pregain")
.map(|pregain| pregain.parse::<f32>().expect("Invalid pregain float value"))
.unwrap_or(PlayerConfig::default().normalisation_pregain),
normalisation_threshold: NormalisationData::db_to_ratio(
matches
.opt_str("normalisation-threshold")
.map(|threshold| {
threshold
.parse::<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,
}
};
@ -430,6 +529,7 @@ fn setup(args: &[String]) -> Setup {
let enable_discovery = !matches.opt_present("disable-discovery");
Setup {
format,
backend,
cache,
session_config,
@ -453,7 +553,7 @@ async fn main() {
}
let args: Vec<String> = std::env::args().collect();
let setupp = setup(&args);
let setup = get_setup(&args);
let mut last_credentials = None;
let mut spirc: Option<Spirc> = None;
@ -463,23 +563,23 @@ async fn main() {
let mut discovery = None;
let mut connecting: Pin<Box<dyn future::FusedFuture<Output = _>>> = Box::pin(future::pending());
if setupp.enable_discovery {
let config = setupp.connect_config.clone();
let device_id = setupp.session_config.device_id.clone();
if setup.enable_discovery {
let config = setup.connect_config.clone();
let device_id = setup.session_config.device_id.clone();
discovery = Some(
librespot_connect::discovery::discovery(config, device_id, setupp.zeroconf_port)
librespot_connect::discovery::discovery(config, device_id, setup.zeroconf_port)
.unwrap(),
);
}
if let Some(credentials) = setupp.credentials {
if let Some(credentials) = setup.credentials {
last_credentials = Some(credentials.clone());
connecting = Box::pin(
Session::connect(
setupp.session_config.clone(),
setup.session_config.clone(),
credentials,
setupp.cache.clone(),
setup.cache.clone(),
)
.fuse(),
);
@ -502,9 +602,9 @@ async fn main() {
}
connecting = Box::pin(Session::connect(
setupp.session_config.clone(),
setup.session_config.clone(),
credentials,
setupp.cache.clone(),
setup.cache.clone(),
).fuse());
},
None => {
@ -515,21 +615,22 @@ async fn main() {
},
session = &mut connecting, if !connecting.is_terminated() => match session {
Ok(session) => {
let mixer_config = setupp.mixer_config.clone();
let mixer = (setupp.mixer)(Some(mixer_config));
let player_config = setupp.player_config.clone();
let connect_config = setupp.connect_config.clone();
let mixer_config = setup.mixer_config.clone();
let mixer = (setup.mixer)(Some(mixer_config));
let player_config = setup.player_config.clone();
let connect_config = setup.connect_config.clone();
let audio_filter = mixer.get_audio_filter();
let backend = setupp.backend;
let device = setupp.device.clone();
let format = setup.format;
let backend = setup.backend;
let device = setup.device.clone();
let (player, event_channel) =
Player::new(player_config, session.clone(), audio_filter, move || {
(backend)(device)
(backend)(device, format)
});
if setupp.emit_sink_events {
if let Some(player_event_program) = setupp.player_event_program.clone() {
if setup.emit_sink_events {
if let Some(player_event_program) = setup.player_event_program.clone() {
player.set_sink_event_callback(Some(Box::new(move |sink_status| {
match emit_sink_event(sink_status, &player_event_program) {
Ok(e) if e.success() => (),
@ -575,16 +676,16 @@ async fn main() {
auto_connect_times.push(Instant::now());
connecting = Box::pin(Session::connect(
setupp.session_config.clone(),
setup.session_config.clone(),
credentials,
setupp.cache.clone(),
setup.cache.clone(),
).fuse());
}
}
},
event = async { player_event_channel.as_mut().unwrap().recv().await }, if player_event_channel.is_some() => match event {
Some(event) => {
if let Some(program) = &setupp.player_event_program {
if let Some(program) = &setup.player_event_program {
if let Some(child) = run_program_on_events(event, program) {
let mut child = child.expect("program failed to start");