mirror of
https://github.com/ivabus/lonelyradio
synced 2024-11-21 15:45:09 +03:00
Compare commits
2 commits
adf4b7bb1c
...
d43dcde7a2
Author | SHA1 | Date | |
---|---|---|---|
d43dcde7a2 | |||
29338f32e3 |
30 changed files with 2006 additions and 2782 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -50,7 +50,6 @@ fastlane/test_output
|
|||
### SwiftPackageManager ###
|
||||
Packages
|
||||
xcuserdata
|
||||
*.xcodeproj
|
||||
|
||||
### Xcode ###
|
||||
# Xcode
|
||||
|
|
1856
Cargo.lock
generated
1856
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
32
Cargo.toml
32
Cargo.toml
|
@ -10,20 +10,17 @@ members = [
|
|||
[package]
|
||||
name = "lonelyradio"
|
||||
description = "TCP radio for lonely ones"
|
||||
version = "0.6.1"
|
||||
version = "0.7.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
||||
repository = "https://github.com/ivabus/lonelyradio"
|
||||
|
||||
[dependencies]
|
||||
lonelyradio_types = { version = "0.6.0", path = "./lonelyradio_types" }
|
||||
lonelyradio_types = { version = "0.7.0", path = "./lonelyradio_types" }
|
||||
rand = "0.8.5"
|
||||
clap = { version = "4.4.18", features = ["derive"] }
|
||||
tokio = { version = "1.35.1", features = [
|
||||
"sync",
|
||||
"fs",
|
||||
"io-util",
|
||||
"net",
|
||||
"rt-multi-thread",
|
||||
"rt",
|
||||
|
@ -42,14 +39,27 @@ lofty = "0.18.2"
|
|||
async-stream = "0.3.5"
|
||||
tokio-stream = { version = "0.1.15", features = ["sync"] }
|
||||
futures-util = "0.3.30"
|
||||
samplerate = "0.2.4"
|
||||
once_cell = "1.19.0"
|
||||
flacenc = { version = "0.4.0", default-features = false }
|
||||
image = "0.25.1"
|
||||
image = { version = "0.25.1", default-features = false, features = ["png", "jpeg", "bmp"]}
|
||||
xspf = "0.4.0"
|
||||
url = "2.5.2"
|
||||
samplerate = "0.2.4"
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0.98"
|
||||
# Optional encoders
|
||||
flacenc = { version = "0.4.0", default-features = false, optional = true }
|
||||
alac-encoder = { version = "0.3.0", optional = true }
|
||||
vorbis_rs = {version = "0.5.4", optional = true }
|
||||
|
||||
[profile.release]
|
||||
[features]
|
||||
default = ["all-lossless", "all-lossy"]
|
||||
all-lossless = ["alac", "flac"]
|
||||
all-lossy = ["vorbis"]
|
||||
alac = ["dep:alac-encoder"]
|
||||
flac = ["dep:flacenc"]
|
||||
vorbis = ["dep:vorbis_rs"]
|
||||
|
||||
[profile.distribute]
|
||||
inherits = "release"
|
||||
opt-level = 3
|
||||
strip = true
|
||||
lto = "fat"
|
||||
|
|
53
README.md
53
README.md
|
@ -1,27 +1,33 @@
|
|||
# lonelyradio
|
||||
# lonelyradio Music Streamer
|
||||
|
||||
Broadcast lossless audio over the internet.
|
||||
Shuffles through your [XSPF playlists](https://www.xspf.org) or your entire library.
|
||||
|
||||
Decodes audio streams using [symphonia](https://github.com/pdeljanov/Symphonia).
|
||||
Decodes audio streams using [symphonia](https://github.com/pdeljanov/Symphonia) (supported [decoders](https://github.com/pdeljanov/Symphonia?tab=readme-ov-file#codecs-decoders) and [demuxers](https://github.com/pdeljanov/Symphonia?tab=readme-ov-file#formats-demuxers))
|
||||
|
||||
Optionally transcodes audio into and from FLAC using [flacenc-rs](https://github.com/yotarok/flacenc-rs/) and [claxon](https://github.com/ruuda/claxon).
|
||||
Streams music using [FLAC](https://crates.io/crates/flacenc), [ALAC](https://crates.io/crates/alac-encoder), [Vorbis](https://crates.io/crates/vorbis_rs) or raw PCM on client’s requests.
|
||||
|
||||
## Install server
|
||||
### Install server
|
||||
|
||||
```shell
|
||||
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.6.1 lonelyradio
|
||||
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.7.0 lonelyradio
|
||||
```
|
||||
|
||||
## Run
|
||||
### Run
|
||||
|
||||
```
|
||||
lonelyradio <MUSIC_FOLDER>
|
||||
```
|
||||
|
||||
All files (recursively) will be shuffled and played back. Public log will be displayed to stdout, private to stderr.
|
||||
All files (recursively) will be shuffled and played back. Log will be displayed to stdout.
|
||||
|
||||
Look into `--help` for detailed info
|
||||
|
||||
#### Playlists
|
||||
|
||||
Specify a directory with playlists with `--playlist-dir`. lonelyradio will scan them on startup and play them on clients’ requests.
|
||||
|
||||
Only the `<location>` and (playlist's) element would be used and only `file://` is supported.
|
||||
|
||||
### Clients
|
||||
|
||||
#### monoclient-x
|
||||
|
@ -37,42 +43,37 @@ Look into `--help` for detailed info
|
|||
|
||||
[monoclient-s](./monoclient-s) is a GUI player for lonelyradio built with [Slint](https://slint.dev)
|
||||
|
||||
|
||||
##### Install
|
||||
|
||||
```shell
|
||||
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.6.1 monoclient-s
|
||||
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.7.0 monoclient-s
|
||||
```
|
||||
|
||||
You may need to install some dependencies for Slint.
|
||||
|
||||
Desktop integration will be added later.
|
||||
|
||||
##### Build
|
||||
|
||||
```
|
||||
cargo build -p monoclient-s
|
||||
```
|
||||
|
||||
You may need to install some dependencies for Slint.
|
||||
|
||||
#### monoclient
|
||||
|
||||
[monoclient](./monoclient) is a CLI player for lonelyradio that uses [monolib](./monolib)
|
||||
|
||||
##### Install monoclient
|
||||
|
||||
```shell
|
||||
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.7.0 monoclient
|
||||
```
|
||||
|
||||
#### Usage
|
||||
|
||||
```shell
|
||||
monoclient <SERVER>:<PORT>
|
||||
```
|
||||
|
||||
##### Install monoclient
|
||||
|
||||
```shell
|
||||
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.6.1 monoclient
|
||||
```
|
||||
Look into `--help` for detailed info on usage.
|
||||
|
||||
# Other things
|
||||
|
||||
[monoloader](./monoloader) is a tool, that allows you to download individual audio tracks from lonelyradio-compatible servers.
|
||||
[monoloader](./monoloader) is a tool that allows you to download individual audio tracks from lonelyradio-compatible servers.
|
||||
|
||||
[monolib](./monolib) provides a C API compatible with lonelyradio for creating custom clients.
|
||||
|
||||
|
@ -82,10 +83,6 @@ The full protocol specification will be available later. If you would like to le
|
|||
|
||||
As lonelyradio has not yet reached its first major release, the API may (and will) break at any point.
|
||||
|
||||
### Microphone server
|
||||
|
||||
Experimental (and uncompatible with versions 0.6+) server (lonelyradio-compatible) for streaming audio from your microphone is available in the [microserve](./microserve) crate.
|
||||
|
||||
## License
|
||||
|
||||
lonelyradio, monolib and monoclient, as well as all other crates in this repository, are licensed under the terms of the [MIT license](./LICENSE).
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
[package]
|
||||
name = "lonelyradio_types"
|
||||
edition = "2021"
|
||||
version = "0.7.0"
|
||||
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
||||
description = "Shared types for lonelyradio"
|
||||
license = "MIT"
|
||||
version = "0.6.0"
|
||||
edition = "2021"
|
||||
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
||||
repository = "https://github.com/ivabus/lonelyradio"
|
||||
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
serde_bytes = "0.11.15"
|
||||
serde = {version = "1.0.209", features = ["derive"]}
|
||||
serde_bytes = {version = "0.11.15"}
|
||||
|
|
|
@ -1,9 +1,39 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const HELLO_MAGIC: u64 = 0x104e1374d10;
|
||||
pub const HELLO_MAGIC: &[u8; 8] = b"lonelyra";
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||
pub enum Message {
|
||||
pub enum Request {
|
||||
// Just play what server wants you to give
|
||||
#[serde(rename = "p", alias = "Play")]
|
||||
Play(Settings),
|
||||
#[serde(rename = "lpl", alias = "ListPlaylist")]
|
||||
ListPlaylist,
|
||||
#[serde(rename = "ppl", alias = "PlayPlaylist")]
|
||||
PlayPlaylist(String, Settings),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||
pub struct PlaylistResponce {
|
||||
pub playlists: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||
pub enum RequestResult {
|
||||
Ok,
|
||||
Playlist(PlaylistResponce),
|
||||
Error(RequestError),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||
pub enum RequestError {
|
||||
NoSuchPlaylist,
|
||||
WrongCoverSize,
|
||||
UnsupportedEncoder,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||
pub enum PlayMessage {
|
||||
T(TrackMetadata),
|
||||
F(FragmentMetadata),
|
||||
}
|
||||
|
@ -11,10 +41,10 @@ pub enum Message {
|
|||
#[repr(C)]
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||
pub struct Settings {
|
||||
#[serde(rename = "e")]
|
||||
#[serde(rename = "e", alias = "encoder")]
|
||||
pub encoder: Encoder,
|
||||
|
||||
#[serde(rename = "co")]
|
||||
#[serde(rename = "co", alias = "cover")]
|
||||
pub cover: i32,
|
||||
}
|
||||
|
||||
|
@ -22,6 +52,9 @@ pub struct Settings {
|
|||
pub struct ServerCapabilities {
|
||||
#[serde(rename = "e")]
|
||||
pub encoders: Vec<Encoder>,
|
||||
// Will be used in the next updates
|
||||
//#[serde(rename = "ar")]
|
||||
//pub available_requests: Vec<Request>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||
|
@ -42,22 +75,47 @@ pub struct TrackMetadata {
|
|||
pub album: String,
|
||||
#[serde(rename = "mar")]
|
||||
pub artist: String,
|
||||
#[serde(rename = "co")]
|
||||
#[serde(with = "serde_bytes")]
|
||||
|
||||
#[serde(
|
||||
rename = "co",
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "serde_bytes",
|
||||
default = "none"
|
||||
)]
|
||||
pub cover: Option<Vec<u8>>,
|
||||
|
||||
pub id: u8,
|
||||
}
|
||||
|
||||
// WavPack, Opus and Aac are currently unimplemented.
|
||||
#[repr(u8)]
|
||||
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Encoder {
|
||||
Pcm16 = 0,
|
||||
PcmFloat = 1,
|
||||
Flac = 2,
|
||||
Alac = 3,
|
||||
WavPack = 4,
|
||||
Opus = 5,
|
||||
Aac = 6,
|
||||
Vorbis = 7,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||
pub struct FragmentMetadata {
|
||||
// In bytes
|
||||
#[serde(rename = "l")]
|
||||
// In bytes or samples, depends on encoder: Pcm* - samples, any compressed - bytes
|
||||
#[serde(rename = "le")]
|
||||
pub length: u64,
|
||||
|
||||
#[serde(
|
||||
rename = "mc",
|
||||
skip_serializing_if = "Option::is_none",
|
||||
with = "serde_bytes",
|
||||
default = "none"
|
||||
)]
|
||||
pub magic_cookie: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
fn none() -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
|
|
886
microserve/Cargo.lock
generated
886
microserve/Cargo.lock
generated
|
@ -1,886 +0,0 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alsa"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37fe60779335388a88c01ac6c3be40304d1e349de3ada3b15f7808bb90fa9dce"
|
||||
dependencies = [
|
||||
"alsa-sys",
|
||||
"bitflags 2.5.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alsa-sys"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.69.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools",
|
||||
"lazy_static",
|
||||
"lazycell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"rustc-hash",
|
||||
"shlex",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cesu8"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
|
||||
|
||||
[[package]]
|
||||
name = "cexpr"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "clang-sys"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"libc",
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
|
||||
|
||||
[[package]]
|
||||
name = "coreaudio-rs"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation-sys",
|
||||
"coreaudio-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coreaudio-sys"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f01585027057ff5f0a5bf276174ae4c1594a2c5bde93d5f46a016d76270f5a9"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpal"
|
||||
version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
|
||||
dependencies = [
|
||||
"alsa",
|
||||
"core-foundation-sys",
|
||||
"coreaudio-rs",
|
||||
"dasp_sample",
|
||||
"jni",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"mach2",
|
||||
"ndk",
|
||||
"ndk-context",
|
||||
"oboe",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dasp_sample"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
|
||||
dependencies = [
|
||||
"cesu8",
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"thiserror",
|
||||
"walkdir",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "lazycell"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.155"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||
|
||||
[[package]]
|
||||
name = "lonelyradio_types"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
|
||||
|
||||
[[package]]
|
||||
name = "microserve"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cpal",
|
||||
"lonelyradio_types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "microserve-session"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"ndk-sys",
|
||||
"num_enum",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk-context"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
|
||||
|
||||
[[package]]
|
||||
name = "ndk-sys"
|
||||
version = "0.5.0+25.2.9519653"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691"
|
||||
dependencies = [
|
||||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845"
|
||||
dependencies = [
|
||||
"num_enum_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum_derive"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oboe"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb"
|
||||
dependencies = [
|
||||
"jni",
|
||||
"ndk",
|
||||
"ndk-context",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"oboe-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oboe-sys"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
|
||||
dependencies = [
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.202"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.202"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ad3dee41f36859875573074334c200d1add8e4a87bb37113ebd31d926b7b11f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.54.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.54.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
|
||||
dependencies = [
|
||||
"windows-result",
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "749f0da9cc72d82e600d8d2e44cadd0b9eedb9038f71a1c58556ac1c5791813b"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
|
||||
dependencies = [
|
||||
"windows-targets 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.42.2",
|
||||
"windows_aarch64_msvc 0.42.2",
|
||||
"windows_i686_gnu 0.42.2",
|
||||
"windows_i686_msvc 0.42.2",
|
||||
"windows_x86_64_gnu 0.42.2",
|
||||
"windows_x86_64_gnullvm 0.42.2",
|
||||
"windows_x86_64_msvc 0.42.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.5",
|
||||
"windows_aarch64_msvc 0.52.5",
|
||||
"windows_i686_gnu 0.52.5",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.5",
|
||||
"windows_x86_64_gnu 0.52.5",
|
||||
"windows_x86_64_gnullvm 0.52.5",
|
||||
"windows_x86_64_msvc 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
|
@ -1,24 +0,0 @@
|
|||
[package]
|
||||
name = "microserve"
|
||||
version = "0.5.0"
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
||||
repository = "https://github.com/ivabus/lonelyradio"
|
||||
|
||||
|
||||
[dependencies]
|
||||
cpal = "0.15.3"
|
||||
lonelyradio_types = { path = "../lonelyradio_types" }
|
||||
once_cell = "1.19.0"
|
||||
queues = "1.1.0"
|
||||
rmp-serde = "1.3.0"
|
||||
tokio = { version = "1.35.1", features = [
|
||||
"sync",
|
||||
"fs",
|
||||
"io-util",
|
||||
"net",
|
||||
"rt-multi-thread",
|
||||
"rt",
|
||||
"macros",
|
||||
] }
|
|
@ -1,96 +0,0 @@
|
|||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use lonelyradio_types::{FragmentMetadata, TrackMetadata};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::VecDeque;
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::RwLock;
|
||||
use std::time::Duration;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
static QUEUE: Lazy<Arc<RwLock<VecDeque<Vec<i16>>>>> =
|
||||
Lazy::new(|| Arc::new(RwLock::new(VecDeque::new())));
|
||||
|
||||
static START_INDEX: Mutex<usize> = Mutex::new(0);
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tokio::spawn(listen_mic());
|
||||
println!("Started buffering");
|
||||
let listener = TcpListener::bind("0.0.0.0:5894").await.unwrap();
|
||||
std::thread::sleep(Duration::from_secs(5));
|
||||
tokio::spawn(update_start());
|
||||
println!("Accepting connections");
|
||||
loop {
|
||||
let (socket, _) = listener.accept().await.unwrap();
|
||||
let socket = socket.into_std().unwrap();
|
||||
tokio::spawn(stream(socket));
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_start() {
|
||||
loop {
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
*START_INDEX.lock().unwrap() = QUEUE.read().unwrap().len() - 5;
|
||||
}
|
||||
}
|
||||
|
||||
async fn stream(mut s: std::net::TcpStream) {
|
||||
println!("Playing for {}", s.peer_addr().unwrap());
|
||||
let md = lonelyradio_types::Message::T(TrackMetadata {
|
||||
cover: None,
|
||||
encoder: lonelyradio_types::Encoder::Pcm,
|
||||
track_length_secs: 0,
|
||||
track_length_frac: 0.0,
|
||||
channels: 1,
|
||||
sample_rate: 44100,
|
||||
title: "microserve instance".to_string(),
|
||||
album: "".to_string(),
|
||||
artist: "".to_string(),
|
||||
});
|
||||
s.write_all(rmp_serde::to_vec(&md).unwrap().as_slice()).unwrap();
|
||||
let mut ind = *START_INDEX.lock().unwrap();
|
||||
dbg!(ind);
|
||||
loop {
|
||||
let front = QUEUE.read().unwrap()[ind].clone();
|
||||
ind += 1;
|
||||
let md = lonelyradio_types::Message::F(FragmentMetadata {
|
||||
length: front.len() as u64,
|
||||
});
|
||||
s.write_all(rmp_serde::to_vec(&md).unwrap().as_slice()).unwrap();
|
||||
|
||||
if s.write_all(unsafe { front.as_slice().align_to::<u8>().1 }).is_err() {
|
||||
return;
|
||||
};
|
||||
while ind >= QUEUE.read().unwrap().len() - 5 {
|
||||
std::thread::sleep(Duration::from_millis(100))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn listen_mic() {
|
||||
let host = cpal::default_host();
|
||||
|
||||
let device = host.default_input_device().unwrap();
|
||||
let config = device.default_input_config().unwrap();
|
||||
let stream = match config.sample_format() {
|
||||
cpal::SampleFormat::F32 => device.build_input_stream(
|
||||
&config.into(),
|
||||
move |data: &[f32], _: &_| {
|
||||
let samples = data.iter().map(|x| (*x * 32767.0) as i16).collect();
|
||||
QUEUE.write().unwrap().push_back(samples);
|
||||
},
|
||||
|e| eprintln!("Error while reading: {}", e),
|
||||
None,
|
||||
),
|
||||
_ => {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
.unwrap();
|
||||
loop {
|
||||
stream.play().unwrap();
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
}
|
|
@ -1,17 +1,13 @@
|
|||
[package]
|
||||
name = "monoclient-s"
|
||||
description = "Client for lonelyradio built with Slint"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
slint = { version = "1.6", features = ["backend-android-activity-06"] }
|
||||
monolib = { path = "../monolib", version = "0.6.0" }
|
||||
lonelyradio_types = { version = "0.6.0", path = "../lonelyradio_types" }
|
||||
zune-jpeg = "0.4.11"
|
||||
|
||||
[lib]
|
||||
crate-type = [ "cdylib" ]
|
||||
slint = { version = "1.6" }
|
||||
monolib = { path = "../monolib", version = "0.7.1" }
|
||||
zune-jpeg = "0.4.13"
|
||||
|
||||
[package.metadata.bundle]
|
||||
name = "monoclient-s"
|
||||
|
|
|
@ -1,227 +0,0 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use monolib::State;
|
||||
use slint::{Image, Rgb8Pixel, Rgba8Pixel, SharedPixelBuffer, Weak};
|
||||
|
||||
slint::slint! {
|
||||
import { AboutSlint, Button, VerticalBox, GroupBox, Slider } from "std-widgets.slint";
|
||||
export component MainWindow inherits Window {
|
||||
max-height: self.preferred-height;
|
||||
callback play;
|
||||
callback stop;
|
||||
callback next;
|
||||
callback change_volume(float);
|
||||
callback text_edited;
|
||||
|
||||
in-out property <string> addr: address.text;
|
||||
in-out property <string> mtitle: "";
|
||||
in-out property <string> malbum: "";
|
||||
in-out property <string> martist: "";
|
||||
in-out property <float> volume: svolume.value;
|
||||
in-out property <bool> start_enabled: false;
|
||||
in-out property <bool> playing: false;
|
||||
in-out property <bool> paused: false;
|
||||
in property <image> cover: @image-url("lonelyradio.png");
|
||||
|
||||
title: "monoclient-s";
|
||||
min-width: 192px;
|
||||
max-width: 768px;
|
||||
VerticalBox {
|
||||
alignment: center;
|
||||
spacing: 0px;
|
||||
|
||||
Image {
|
||||
source: cover;
|
||||
max-height: 192px;
|
||||
max-width: 192px;
|
||||
min-height: 192px;
|
||||
min-width: 192px;
|
||||
}
|
||||
|
||||
GroupBox{
|
||||
max-width: 768px;
|
||||
address := TextInput {
|
||||
text: "";
|
||||
horizontal-alignment: center;
|
||||
height: 1.25rem;
|
||||
|
||||
accepted => {
|
||||
self.clear_focus()
|
||||
}
|
||||
|
||||
edited => {
|
||||
text_edited()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerticalLayout {
|
||||
max-width: 512px;
|
||||
|
||||
VerticalLayout {
|
||||
spacing: 4px;
|
||||
Button {
|
||||
max-width: 256px;
|
||||
text: playing ? (paused ? "Play" : "Pause") : "Start";
|
||||
enabled: start_enabled || playing;
|
||||
clicked => {
|
||||
play()
|
||||
}
|
||||
}
|
||||
HorizontalLayout {
|
||||
spacing: 4px;
|
||||
max-width: 256px;
|
||||
Button {
|
||||
text: "Stop";
|
||||
enabled: playing && !paused;
|
||||
clicked => {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
Button {
|
||||
text: "Next";
|
||||
enabled: playing && !paused;
|
||||
clicked => {
|
||||
next()
|
||||
}
|
||||
}
|
||||
}
|
||||
svolume := Slider {
|
||||
value: 255;
|
||||
maximum: 255;
|
||||
changed(f) => {
|
||||
change_volume(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerticalLayout {
|
||||
padding: 4px;
|
||||
tartist := Text {
|
||||
height: 1.25rem;
|
||||
font-weight: 600;
|
||||
text: martist;
|
||||
overflow: elide;
|
||||
}
|
||||
talbum := Text {
|
||||
height: 1.25rem;
|
||||
text: malbum;
|
||||
overflow: elide;
|
||||
}
|
||||
ttitle := Text {
|
||||
height: 1.25rem;
|
||||
text: mtitle;
|
||||
overflow: elide;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn start_playback(window_weak: Weak<MainWindow>) {
|
||||
let window = window_weak.upgrade().unwrap();
|
||||
let addr = window.get_addr().to_string();
|
||||
let handle = std::thread::spawn(move || {
|
||||
monolib::run(
|
||||
&addr,
|
||||
lonelyradio_types::Settings {
|
||||
encoder: lonelyradio_types::Encoder::Flac,
|
||||
cover: 512,
|
||||
},
|
||||
)
|
||||
});
|
||||
std::thread::sleep(Duration::from_millis(166));
|
||||
if handle.is_finished() {
|
||||
window.set_playing(false);
|
||||
return;
|
||||
}
|
||||
window.set_playing(true);
|
||||
window.set_paused(false);
|
||||
while monolib::get_metadata().is_none() {}
|
||||
monolib::set_volume(window.get_volume() as u8);
|
||||
}
|
||||
|
||||
pub fn _main() {
|
||||
let window = MainWindow::new().unwrap();
|
||||
|
||||
let window_weak = window.as_weak();
|
||||
window.on_text_edited(move || {
|
||||
let window = window_weak.upgrade().unwrap();
|
||||
let addr = window.get_addr().to_string();
|
||||
window.set_start_enabled(addr.contains(':'));
|
||||
});
|
||||
|
||||
let window_weak = window.as_weak();
|
||||
window.on_play(move || match monolib::get_state() {
|
||||
State::NotStarted => start_playback(window_weak.clone()),
|
||||
State::Paused => {
|
||||
let window = window_weak.upgrade().unwrap();
|
||||
window.set_paused(false);
|
||||
monolib::toggle();
|
||||
}
|
||||
State::Resetting => {}
|
||||
State::Playing => {
|
||||
let window = window_weak.upgrade().unwrap();
|
||||
window.set_paused(true);
|
||||
monolib::toggle()
|
||||
}
|
||||
});
|
||||
|
||||
let window_weak = window.as_weak();
|
||||
window.on_next(move || {
|
||||
monolib::stop();
|
||||
start_playback(window_weak.clone())
|
||||
});
|
||||
let window_weak = window.as_weak();
|
||||
window.on_stop(move || {
|
||||
let window = window_weak.upgrade().unwrap();
|
||||
window.set_playing(false);
|
||||
window.set_martist("".into());
|
||||
window.set_malbum("".into());
|
||||
window.set_mtitle("".into());
|
||||
window.set_cover(Image::from_rgba8(SharedPixelBuffer::<Rgba8Pixel>::new(1, 1)));
|
||||
monolib::stop();
|
||||
});
|
||||
window.on_change_volume(move |vol| monolib::set_volume(vol as u8));
|
||||
let window_weak = window.as_weak();
|
||||
std::thread::spawn(move || loop {
|
||||
let window = window_weak.clone();
|
||||
while monolib::get_metadata().is_none() {
|
||||
std::thread::sleep(Duration::from_millis(25))
|
||||
}
|
||||
let md = monolib::get_metadata().unwrap();
|
||||
let _md = md.clone();
|
||||
if let Some(jpeg) = md.cover {
|
||||
let mut decoder = zune_jpeg::JpegDecoder::new(jpeg);
|
||||
decoder.decode_headers().unwrap();
|
||||
let (w, h) = decoder.dimensions().unwrap();
|
||||
let decoded = decoder.decode().unwrap();
|
||||
let mut pixel_buffer = SharedPixelBuffer::<Rgb8Pixel>::new(w as u32, h as u32);
|
||||
pixel_buffer.make_mut_bytes().copy_from_slice(&decoded);
|
||||
window
|
||||
.upgrade_in_event_loop(|win| {
|
||||
let image = Image::from_rgb8(pixel_buffer);
|
||||
win.set_cover(image);
|
||||
})
|
||||
.unwrap();
|
||||
} else {
|
||||
window
|
||||
.upgrade_in_event_loop(|win| {
|
||||
win.set_cover(Image::from_rgba8(SharedPixelBuffer::<Rgba8Pixel>::new(1, 1)));
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
slint::invoke_from_event_loop(move || {
|
||||
let window = window.unwrap();
|
||||
window.set_martist(md.artist.clone().into());
|
||||
window.set_malbum(md.album.clone().into());
|
||||
window.set_mtitle(md.title.clone().into());
|
||||
})
|
||||
.unwrap();
|
||||
while monolib::get_metadata() == Some(_md.clone()) {
|
||||
std::thread::sleep(Duration::from_millis(100))
|
||||
}
|
||||
});
|
||||
window.run().unwrap();
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
mod app;
|
||||
#[cfg(target_os = "andoid")]
|
||||
#[no_mangle]
|
||||
fn android_main(app: slint::android::AndroidApp) {
|
||||
slint::android::init(app).unwrap();
|
||||
app::_main();
|
||||
}
|
|
@ -1,4 +1,262 @@
|
|||
mod app;
|
||||
fn main() {
|
||||
app::_main()
|
||||
use std::time::Duration;
|
||||
|
||||
use monolib::State;
|
||||
use slint::{
|
||||
Image, ModelRc, Rgb8Pixel, Rgba8Pixel, SharedPixelBuffer, SharedString, VecModel, Weak,
|
||||
};
|
||||
|
||||
slint::slint! {
|
||||
import { ComboBox, Button, VerticalBox, GroupBox, Slider } from "std-widgets.slint";
|
||||
export component MainWindow inherits Window {
|
||||
max-height: self.preferred-height;
|
||||
callback play;
|
||||
callback stop;
|
||||
callback next;
|
||||
callback refreshp;
|
||||
callback change_volume(float);
|
||||
callback text_edited;
|
||||
|
||||
in-out property <string> addr: address.text;
|
||||
in-out property <string> mtitle: "";
|
||||
in-out property <string> malbum: "";
|
||||
in-out property <string> martist: "";
|
||||
in-out property <float> volume: svolume.value;
|
||||
in-out property <bool> start_enabled: false;
|
||||
in-out property <bool> playing: false;
|
||||
in-out property <bool> paused: false;
|
||||
in property <image> cover: @image-url("lonelyradio.png");
|
||||
in property <[string]> playlists: ["All tracks"];
|
||||
in-out property <string> selected_playlist: selected.current-value;
|
||||
|
||||
title: "monoclient-s";
|
||||
min-width: 192px;
|
||||
max-width: 768px;
|
||||
VerticalBox {
|
||||
alignment: center;
|
||||
spacing: 0px;
|
||||
|
||||
Image {
|
||||
source: cover;
|
||||
max-height: 192px;
|
||||
max-width: 192px;
|
||||
min-height: 192px;
|
||||
min-width: 192px;
|
||||
}
|
||||
|
||||
GroupBox{
|
||||
max-width: 768px;
|
||||
address := TextInput {
|
||||
text: "";
|
||||
horizontal-alignment: center;
|
||||
height: 1.25rem;
|
||||
|
||||
accepted => {
|
||||
self.clear_focus()
|
||||
}
|
||||
|
||||
edited => {
|
||||
text_edited();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerticalLayout {
|
||||
max-width: 512px;
|
||||
|
||||
VerticalLayout {
|
||||
spacing: 4px;
|
||||
Button {
|
||||
max-width: 256px;
|
||||
text: playing ? (paused ? "Play" : "Pause") : "Start";
|
||||
enabled: start_enabled || playing;
|
||||
clicked => {
|
||||
play()
|
||||
}
|
||||
}
|
||||
HorizontalLayout {
|
||||
spacing: 4px;
|
||||
max-width: 256px;
|
||||
Button {
|
||||
text: "Stop";
|
||||
enabled: playing && !paused;
|
||||
clicked => {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
Button {
|
||||
text: "Next";
|
||||
enabled: playing && !paused;
|
||||
clicked => {
|
||||
next()
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalLayout {
|
||||
selected := ComboBox {
|
||||
model: playlists;
|
||||
current-value: "All tracks";
|
||||
selected() => {
|
||||
self.clear_focus()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
svolume := Slider {
|
||||
value: 255;
|
||||
maximum: 255;
|
||||
changed(f) => {
|
||||
change_volume(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerticalLayout {
|
||||
padding: 4px;
|
||||
tartist := Text {
|
||||
height: 1.25rem;
|
||||
font-weight: 600;
|
||||
text: martist;
|
||||
overflow: elide;
|
||||
}
|
||||
talbum := Text {
|
||||
height: 1.25rem;
|
||||
text: malbum;
|
||||
overflow: elide;
|
||||
}
|
||||
ttitle := Text {
|
||||
height: 1.25rem;
|
||||
text: mtitle;
|
||||
overflow: elide;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn start_playback(window_weak: Weak<MainWindow>) {
|
||||
let window = window_weak.upgrade().unwrap();
|
||||
let addr = window.get_addr().to_string();
|
||||
let playlist = window.get_selected_playlist();
|
||||
let handle = std::thread::spawn(move || {
|
||||
monolib::run(
|
||||
&addr,
|
||||
lonelyradio_types::Settings {
|
||||
encoder: lonelyradio_types::Encoder::Flac,
|
||||
cover: 512,
|
||||
},
|
||||
if playlist == "All tracks" {
|
||||
""
|
||||
} else {
|
||||
&playlist
|
||||
},
|
||||
)
|
||||
});
|
||||
std::thread::sleep(Duration::from_millis(166));
|
||||
if handle.is_finished() {
|
||||
window.set_playing(false);
|
||||
return;
|
||||
}
|
||||
window.set_playing(true);
|
||||
window.set_paused(false);
|
||||
while monolib::get_metadata().is_none() {}
|
||||
monolib::set_volume(window.get_volume() as u8);
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
let window = MainWindow::new().unwrap();
|
||||
|
||||
let window_weak = window.as_weak();
|
||||
window.on_text_edited(move || {
|
||||
let window = window_weak.upgrade().unwrap();
|
||||
let addr = window.get_addr().to_string();
|
||||
if addr.contains(':') {
|
||||
window.set_start_enabled(true);
|
||||
|
||||
let playlists = match monolib::list_playlists(&window.get_addr()) {
|
||||
Some(v) => [vec!["All tracks".to_string()], v].concat(),
|
||||
None => vec!["All tracks".to_string()],
|
||||
};
|
||||
window.set_playlists(ModelRc::new(VecModel::from(
|
||||
playlists.iter().map(SharedString::from).collect::<Vec<_>>(),
|
||||
)));
|
||||
} else {
|
||||
window.set_start_enabled(false);
|
||||
}
|
||||
});
|
||||
|
||||
let window_weak = window.as_weak();
|
||||
window.on_play(move || match monolib::get_state() {
|
||||
State::NotStarted => start_playback(window_weak.clone()),
|
||||
State::Paused => {
|
||||
let window = window_weak.upgrade().unwrap();
|
||||
window.set_paused(false);
|
||||
monolib::toggle();
|
||||
}
|
||||
State::Resetting => {}
|
||||
State::Playing => {
|
||||
let window = window_weak.upgrade().unwrap();
|
||||
window.set_paused(true);
|
||||
monolib::toggle()
|
||||
}
|
||||
});
|
||||
|
||||
let window_weak = window.as_weak();
|
||||
window.on_next(move || {
|
||||
monolib::stop();
|
||||
start_playback(window_weak.clone())
|
||||
});
|
||||
let window_weak = window.as_weak();
|
||||
window.on_stop(move || {
|
||||
let window = window_weak.upgrade().unwrap();
|
||||
window.set_playing(false);
|
||||
window.set_martist("".into());
|
||||
window.set_malbum("".into());
|
||||
window.set_mtitle("".into());
|
||||
window.set_cover(Image::from_rgba8(SharedPixelBuffer::<Rgba8Pixel>::new(1, 1)));
|
||||
monolib::stop();
|
||||
});
|
||||
window.on_change_volume(move |vol| monolib::set_volume(vol as u8));
|
||||
|
||||
let window_weak = window.as_weak();
|
||||
std::thread::spawn(move || loop {
|
||||
let window = window_weak.clone();
|
||||
while monolib::get_metadata().is_none() {
|
||||
std::thread::sleep(Duration::from_millis(25))
|
||||
}
|
||||
let md = monolib::get_metadata().unwrap();
|
||||
let _md = md.clone();
|
||||
if let Some(jpeg) = md.cover {
|
||||
let mut decoder = zune_jpeg::JpegDecoder::new(jpeg);
|
||||
decoder.decode_headers().unwrap();
|
||||
let (w, h) = decoder.dimensions().unwrap();
|
||||
let decoded = decoder.decode().unwrap();
|
||||
let mut pixel_buffer = SharedPixelBuffer::<Rgb8Pixel>::new(w as u32, h as u32);
|
||||
pixel_buffer.make_mut_bytes().copy_from_slice(&decoded);
|
||||
window
|
||||
.upgrade_in_event_loop(|win| {
|
||||
let image = Image::from_rgb8(pixel_buffer);
|
||||
win.set_cover(image);
|
||||
})
|
||||
.unwrap();
|
||||
} else {
|
||||
window
|
||||
.upgrade_in_event_loop(|win| {
|
||||
win.set_cover(Image::from_rgba8(SharedPixelBuffer::<Rgba8Pixel>::new(1, 1)));
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
slint::invoke_from_event_loop(move || {
|
||||
let window = window.unwrap();
|
||||
window.set_martist(md.artist.clone().into());
|
||||
window.set_malbum(md.album.clone().into());
|
||||
window.set_mtitle(md.title.clone().into());
|
||||
})
|
||||
.unwrap();
|
||||
while monolib::get_metadata() == Some(_md.clone()) {
|
||||
std::thread::sleep(Duration::from_millis(100))
|
||||
}
|
||||
});
|
||||
window.run().unwrap();
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ struct Cover {
|
|||
self.cover = PlatformImage(cgImage: CGImage(jpegDataProviderSource: CGDataProvider(data: data)!, decode: nil, shouldInterpolate: false, intent: CGColorRenderingIntent.absoluteColorimetric)!).preparingForDisplay()!
|
||||
#endif
|
||||
// deallocating memory
|
||||
c_drop(cov.bytes, Int(cov.length))
|
||||
c_drop(cov.bytes, UInt(Int(cov.length)))
|
||||
print(self.cover.size)
|
||||
|
||||
} else {
|
||||
|
|
|
@ -29,6 +29,11 @@ enum EncoderType: UInt8 {
|
|||
case PCM16 = 0
|
||||
case PCMFloat = 1
|
||||
case FLAC = 2
|
||||
case Alac = 3
|
||||
//WavPack = 4,
|
||||
//Opus = 5,
|
||||
//Aac = 6,
|
||||
case Vorbis = 7
|
||||
}
|
||||
|
||||
enum CoverSize: Int32 {
|
||||
|
@ -40,110 +45,166 @@ enum CoverSize: Int32 {
|
|||
case NoCover = -1
|
||||
}
|
||||
|
||||
struct PlayList: Identifiable, Hashable {
|
||||
var id: Int
|
||||
|
||||
var name: String
|
||||
}
|
||||
|
||||
|
||||
struct Settings {
|
||||
var encoder: EncoderType = EncoderType.FLAC
|
||||
var cover_size: CoverSize = CoverSize.High/*
|
||||
init(enc: EncoderType, cov: CoverSize) {
|
||||
encoder = enc
|
||||
cover_size = cov
|
||||
}*/
|
||||
init(enc: EncoderType, cov: CoverSize) {
|
||||
encoder = enc
|
||||
cover_size = cov
|
||||
}*/
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
typealias MyStack = HStack
|
||||
#else
|
||||
typealias MyStack = VStack
|
||||
#endif
|
||||
|
||||
struct Player: View {
|
||||
|
||||
let timer_state = Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()
|
||||
let timer_meta = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
|
||||
let timer_playlists = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
|
||||
@State var metadata: Metadata = Metadata(title: "", album: "", artist: "")
|
||||
@State var prev_meta: Metadata = Metadata(title: "", album: "", artist: "")
|
||||
@State var cover: Cover = Cover(cover: PlatformImage())
|
||||
@State var state: PlayerState = PlayerState.NotStarted
|
||||
@State var settings: Settings = Settings.init()
|
||||
@State var playlists: [PlayList] = [PlayList(id: 0, name: "All tracks")]
|
||||
@State var playlist: PlayList = PlayList(id: 0, name: "All tracks")
|
||||
@AppStorage("ContentView.server") var server: String = ""
|
||||
|
||||
var body: some View {
|
||||
|
||||
VStack(alignment: .center) {
|
||||
|
||||
MyStack(alignment: .center) {
|
||||
|
||||
VStack(alignment: .center) {
|
||||
|
||||
#if os(macOS)
|
||||
Image(nsImage: cover.cover)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
|
||||
.frame(width: 256.0, height: 256.0)
|
||||
.clipShape(.rect(cornerRadius: 24))
|
||||
.shadow(radius: 16)
|
||||
.padding(16)
|
||||
Image(nsImage: cover.cover)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
|
||||
.frame(width: 256.0, height: 256.0)
|
||||
.clipShape(.rect(cornerRadius: 24))
|
||||
.shadow(radius: 16)
|
||||
.padding(16)
|
||||
#else
|
||||
Image(uiImage: cover.cover)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
|
||||
.frame(width: 256.0, height: 256.0)
|
||||
.clipShape(.rect(cornerRadius: 24))
|
||||
.shadow(radius: 16)
|
||||
.padding(16)
|
||||
Image(uiImage: cover.cover)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(minWidth: 256, maxWidth: 256, minHeight: 256, maxHeight: 256)
|
||||
.frame(width: 256.0, height: 256.0)
|
||||
.clipShape(.rect(cornerRadius: 24))
|
||||
.shadow(radius: 16)
|
||||
.padding(16)
|
||||
#endif
|
||||
|
||||
VStack(alignment: .center){
|
||||
Text(metadata.title).bold()
|
||||
|
||||
Text(metadata.album)
|
||||
|
||||
Text(metadata.artist)
|
||||
}.frame(minHeight: 64)
|
||||
|
||||
TextField(
|
||||
"Server",
|
||||
text: $server,
|
||||
onCommit: {
|
||||
#if os(macOS)
|
||||
DispatchQueue.main.async {
|
||||
NSApp.keyWindow?.makeFirstResponder(nil)
|
||||
VStack(alignment: .center){
|
||||
Text(metadata.title).bold()
|
||||
|
||||
Text(metadata.album)
|
||||
|
||||
Text(metadata.artist)
|
||||
}.frame(minHeight: 64).onReceive(timer_state) { _ in
|
||||
metadata.update()
|
||||
if prev_meta != metadata {
|
||||
prev_meta = metadata
|
||||
cover.update()
|
||||
}
|
||||
let image = cover.cover
|
||||
let mediaArtwork = MPMediaItemArtwork(boundsSize: image.size) { (size: CGSize) -> PlatformImage in
|
||||
return image
|
||||
}
|
||||
|
||||
|
||||
#if os(macOS)
|
||||
MPNowPlayingInfoCenter.default().playbackState = state == PlayerState.Playing ? .playing : .paused
|
||||
#endif
|
||||
|
||||
let nowPlayingInfo: [String: Any] = [
|
||||
MPMediaItemPropertyArtist: metadata.artist,
|
||||
MPMediaItemPropertyAlbumTitle: metadata.album,
|
||||
MPMediaItemPropertyTitle: metadata.title,
|
||||
MPMediaItemPropertyArtwork: mediaArtwork,
|
||||
MPNowPlayingInfoPropertyIsLiveStream: true,
|
||||
MPMediaItemPropertyPlaybackDuration: c_get_metadata_length(),
|
||||
|
||||
]
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
||||
}
|
||||
)
|
||||
.disableAutocorrection(true)
|
||||
.frame(width: 256)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(16)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Button(action: stop){
|
||||
Image(systemName: "stop.fill").padding(4).frame(width: 32, height: 24)
|
||||
}
|
||||
.disabled(state == PlayerState.NotStarted)
|
||||
.buttonStyle(.bordered)
|
||||
.font(.system(size: 20))
|
||||
.buttonBorderShape(.capsule)
|
||||
|
||||
Button(action: play){
|
||||
Image(systemName: state == PlayerState.NotStarted ? "infinity.circle" : (state == PlayerState.Playing) ? "pause.circle.fill" : "play.circle" )
|
||||
.font(.system(size: 30))
|
||||
.padding(4)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.buttonBorderShape(.capsule)
|
||||
TextField(
|
||||
"Server",
|
||||
text: $server,
|
||||
onCommit: {
|
||||
#if os(macOS)
|
||||
DispatchQueue.main.async {
|
||||
NSApp.keyWindow?.makeFirstResponder(nil)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
)
|
||||
.disableAutocorrection(true)
|
||||
.frame(width: 256)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(16)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Button(action: next){
|
||||
Image(systemName: "forward.end.fill").padding(4).frame(width: 32, height: 24)
|
||||
}.disabled(state == PlayerState.NotStarted)
|
||||
HStack(spacing: 8) {
|
||||
Button(action: stop){
|
||||
Image(systemName: "stop.fill").padding(4).frame(width: 32, height: 24)
|
||||
}
|
||||
.disabled(state == PlayerState.NotStarted)
|
||||
.buttonStyle(.bordered)
|
||||
.font(.system(size: 20))
|
||||
.buttonBorderShape(.capsule)
|
||||
}
|
||||
Menu {
|
||||
Picker("Encoder", selection: $settings.encoder) {
|
||||
Text("PCM (s16)")
|
||||
.tag(EncoderType.PCM16)
|
||||
Text("PCM (f32)")
|
||||
.tag(EncoderType.PCMFloat)
|
||||
Text("FLAC (s24)")
|
||||
.tag(EncoderType.FLAC)
|
||||
}.pickerStyle(.menu)
|
||||
|
||||
Picker("Cover size", selection: $settings.cover_size) {
|
||||
|
||||
Button(action: play){
|
||||
Image(systemName: state == PlayerState.NotStarted ? "infinity.circle" : (state == PlayerState.Playing) ? "pause.circle.fill" : "play.circle" )
|
||||
.font(.system(size: 30))
|
||||
.padding(4)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.buttonBorderShape(.capsule)
|
||||
|
||||
Button(action: next){
|
||||
Image(systemName: "forward.end.fill").padding(4).frame(width: 32, height: 24)
|
||||
}.disabled(state == PlayerState.NotStarted)
|
||||
.buttonStyle(.bordered)
|
||||
.font(.system(size: 20))
|
||||
.buttonBorderShape(.capsule)
|
||||
}.onReceive(timer_state) { _ in
|
||||
state.update()
|
||||
}
|
||||
Menu {
|
||||
Picker("Playlist", selection: $playlist) {
|
||||
ForEach ($playlists) { pl in
|
||||
Text(pl.wrappedValue.name).tag(pl.wrappedValue)
|
||||
}
|
||||
|
||||
}.pickerStyle(.menu)
|
||||
|
||||
Picker("Encoder", selection: $settings.encoder) {
|
||||
Text("PCM (s16)")
|
||||
.tag(EncoderType.PCM16)
|
||||
Text("PCM (f32)")
|
||||
.tag(EncoderType.PCMFloat)
|
||||
Text("FLAC (s24)")
|
||||
.tag(EncoderType.FLAC)
|
||||
Text("ALAC (s16)")
|
||||
.tag(EncoderType.Alac)
|
||||
Text("Vorbis (lossy)")
|
||||
.tag(EncoderType.Vorbis)
|
||||
}.pickerStyle(.menu)
|
||||
|
||||
Picker("Cover size", selection: $settings.cover_size) {
|
||||
Text("Original")
|
||||
.tag(CoverSize.Full)
|
||||
Text("High (768)")
|
||||
|
@ -157,77 +218,58 @@ struct Player: View {
|
|||
Text("No cover")
|
||||
.tag(CoverSize.NoCover)
|
||||
}.pickerStyle(.menu)
|
||||
} label: {
|
||||
Label("Settings", systemImage: "gearshape")
|
||||
.padding(16)
|
||||
}.frame(maxWidth: 128)
|
||||
}
|
||||
.padding(32)
|
||||
.onReceive(timer_state) { _ in
|
||||
state.update()
|
||||
|
||||
#if os(macOS)
|
||||
MPNowPlayingInfoCenter.default().playbackState = state == PlayerState.Playing ? .playing : .paused
|
||||
#endif
|
||||
|
||||
}
|
||||
.onReceive(timer_meta) { _ in
|
||||
metadata.update()
|
||||
if prev_meta != metadata || metadata.album == "" || cover.cover == PlatformImage() {
|
||||
prev_meta = metadata
|
||||
cover.update()
|
||||
}
|
||||
let image = cover.cover
|
||||
let mediaArtwork = MPMediaItemArtwork(boundsSize: image.size) { (size: CGSize) -> PlatformImage in
|
||||
return image
|
||||
} label: {
|
||||
Label("Settings", systemImage: "gearshape")
|
||||
.padding(16)
|
||||
}.frame(maxWidth: 128)
|
||||
}
|
||||
|
||||
let nowPlayingInfo: [String: Any] = [
|
||||
MPMediaItemPropertyArtist: metadata.artist,
|
||||
MPMediaItemPropertyAlbumTitle: metadata.album,
|
||||
MPMediaItemPropertyTitle: metadata.title,
|
||||
MPMediaItemPropertyArtwork: mediaArtwork,
|
||||
MPNowPlayingInfoPropertyIsLiveStream: true,
|
||||
MPMediaItemPropertyPlaybackDuration: c_get_metadata_length(),
|
||||
|
||||
]
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
||||
|
||||
}
|
||||
.onAppear() {
|
||||
.padding(32)
|
||||
.onReceive(timer_playlists) { _ in
|
||||
var id = -1
|
||||
playlists = (["All tracks"] + String(cString: c_list_playlists(server)).components(separatedBy: "\n")).map({elem in
|
||||
if elem.isEmpty {
|
||||
return PlayList(id: -1, name: elem)
|
||||
}
|
||||
id += 1;
|
||||
return PlayList(id: id, name: elem)
|
||||
}).filter({elem in elem.id != -1})
|
||||
}
|
||||
.onAppear() {
|
||||
#if os(iOS)
|
||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||
#endif
|
||||
MPRemoteCommandCenter.shared().previousTrackCommand.isEnabled = false
|
||||
MPRemoteCommandCenter.shared().nextTrackCommand.isEnabled = true
|
||||
MPRemoteCommandCenter.shared().skipForwardCommand.isEnabled = false
|
||||
MPRemoteCommandCenter.shared().skipBackwardCommand.isEnabled = false
|
||||
MPRemoteCommandCenter.shared().pauseCommand.addTarget(handler: { _ in
|
||||
if state != PlayerState.Paused {
|
||||
MPRemoteCommandCenter.shared().previousTrackCommand.isEnabled = false
|
||||
MPRemoteCommandCenter.shared().nextTrackCommand.isEnabled = true
|
||||
MPRemoteCommandCenter.shared().skipForwardCommand.isEnabled = false
|
||||
MPRemoteCommandCenter.shared().skipBackwardCommand.isEnabled = false
|
||||
MPRemoteCommandCenter.shared().pauseCommand.addTarget(handler: { _ in
|
||||
if state != PlayerState.Paused {
|
||||
play()
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
})
|
||||
MPRemoteCommandCenter.shared().playCommand.addTarget(handler: { _ in
|
||||
if state != PlayerState.Playing {
|
||||
play()
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
})
|
||||
|
||||
MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget(handler: {_ in
|
||||
play()
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
})
|
||||
MPRemoteCommandCenter.shared().playCommand.addTarget(handler: { _ in
|
||||
if state != PlayerState.Playing {
|
||||
play()
|
||||
}
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
})
|
||||
|
||||
MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget(handler: {_ in
|
||||
play()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
})
|
||||
|
||||
MPRemoteCommandCenter.shared().nextTrackCommand.addTarget(handler: {_ in
|
||||
next()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
})
|
||||
|
||||
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
})
|
||||
|
||||
MPRemoteCommandCenter.shared().nextTrackCommand.addTarget(handler: {_ in
|
||||
next()
|
||||
return MPRemoteCommandHandlerStatus.success
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
.animation(.spring, value: UUID())
|
||||
}
|
||||
.animation(.spring, value: UUID())
|
||||
}
|
||||
|
||||
|
||||
|
@ -236,7 +278,8 @@ struct Player: View {
|
|||
func play() {
|
||||
switch state {
|
||||
case PlayerState.NotStarted: do {
|
||||
#if os(iOS)
|
||||
/*#if os(macOS)
|
||||
#else*/
|
||||
let audioSession = AVAudioSession.sharedInstance()
|
||||
do {
|
||||
try audioSession.setCategory(
|
||||
|
@ -246,9 +289,9 @@ struct Player: View {
|
|||
} catch {
|
||||
print("Failed to set the audio session configuration")
|
||||
}
|
||||
#endif
|
||||
/*#endif*/
|
||||
Thread.detachNewThread {
|
||||
c_start(server, CSettings(encoder: settings.encoder.rawValue, cover: settings.cover_size.rawValue))
|
||||
c_start(server, CSettings(encoder: settings.encoder.rawValue, cover: settings.cover_size.rawValue), playlist.name == "All tracks" ? "" : playlist.name)
|
||||
}
|
||||
}
|
||||
default: do {
|
||||
|
|
|
@ -25,11 +25,12 @@ struct monoclient_xApp: App {
|
|||
CommandGroup(replacing: CommandGroupPlacement.newItem) {
|
||||
}
|
||||
}
|
||||
.defaultSize(width: 256, height: 512)
|
||||
#else
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
.defaultSize(width: 256, height: 512)
|
||||
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
[package]
|
||||
name = "monoclient"
|
||||
license = "MIT"
|
||||
version = "0.6.1"
|
||||
version = "0.7.0"
|
||||
edition = "2021"
|
||||
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
||||
repository = "https://github.com/ivabus/lonelyradio"
|
||||
|
||||
[dependencies]
|
||||
monolib = { version = "0.6.0", path = "../monolib" }
|
||||
clap = { version = "4.4.18", features = ["derive"] }
|
||||
crossterm = "0.27.0"
|
||||
lonelyradio_types = { version = "0.6.0", path = "../lonelyradio_types" }
|
||||
|
||||
monolib = { version = "0.7.1", path = "../monolib" }
|
||||
clap = { version = "4.5.16", features = ["derive"] }
|
||||
crossterm = "0.28.1"
|
||||
|
|
|
@ -3,7 +3,7 @@ use crossterm::cursor::MoveToColumn;
|
|||
use crossterm::event::{poll, read, Event};
|
||||
use crossterm::style::Print;
|
||||
use crossterm::terminal::{Clear, ClearType};
|
||||
use lonelyradio_types::{Encoder, Settings};
|
||||
use monolib::lonelyradio_types::{Encoder, Settings};
|
||||
use std::io::stdout;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Instant;
|
||||
|
@ -17,6 +17,12 @@ struct Args {
|
|||
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
|
||||
#[arg(short, long, default_value = "")]
|
||||
playlist: String,
|
||||
|
||||
#[arg(short, long)]
|
||||
list: bool,
|
||||
}
|
||||
|
||||
const HELP: &str = r#"Keybinds:
|
||||
|
@ -37,17 +43,29 @@ macro_rules! verbose {
|
|||
fn main() {
|
||||
let args = Args::parse();
|
||||
VERBOSE.set(args.verbose).unwrap();
|
||||
if args.list {
|
||||
println!(
|
||||
"Available playlists: {}",
|
||||
match monolib::list_playlists(&args.address) {
|
||||
Some(s) => format!("{:?}", s),
|
||||
None => String::from("None"),
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
std::thread::spawn(move || {
|
||||
monolib::run(
|
||||
&args.address,
|
||||
Settings {
|
||||
encoder: Encoder::PcmFloat,
|
||||
encoder: Encoder::Flac,
|
||||
cover: -1,
|
||||
},
|
||||
&args.playlist,
|
||||
)
|
||||
});
|
||||
while monolib::get_metadata().is_none() {}
|
||||
let mut md = monolib::get_metadata().unwrap();
|
||||
let mut next_md = md.clone();
|
||||
verbose!("md: {:?}", md);
|
||||
let mut track_start = Instant::now();
|
||||
let mut seconds_past = 0;
|
||||
|
@ -63,7 +81,6 @@ fn main() {
|
|||
))
|
||||
)
|
||||
.unwrap();
|
||||
let mut next_md = md.clone();
|
||||
crossterm::terminal::enable_raw_mode().unwrap();
|
||||
loop {
|
||||
if let Ok(true) = poll(std::time::Duration::from_micros(1)) {
|
||||
|
@ -116,8 +133,8 @@ fn main() {
|
|||
}
|
||||
}
|
||||
}
|
||||
if monolib::get_metadata().unwrap() != md
|
||||
//&& track_length <= (Instant::now() - track_start).as_secs_f64()
|
||||
if next_md != md
|
||||
&& md.track_length_secs as f64 <= (Instant::now() - track_start).as_secs_f64()
|
||||
{
|
||||
md = next_md.clone();
|
||||
verbose!("md: {:?}", md);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "monolib"
|
||||
version = "0.6.0"
|
||||
version = "0.7.1"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "A library implementing the lonely radio audio streaming protocol"
|
||||
|
@ -12,11 +12,25 @@ name = "monolib"
|
|||
crate-type = ["cdylib", "staticlib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
rodio = { version = "0.17.3", default-features = false }
|
||||
rodio = { version = "0.19.0", default-features = false }
|
||||
byteorder = "1.5.0"
|
||||
rmp-serde = "1.1.2"
|
||||
lonelyradio_types = { version = "0.6.0", path = "../lonelyradio_types" }
|
||||
claxon = "0.4.3"
|
||||
lonelyradio_types = { version = "0.7.0", path = "../lonelyradio_types" }
|
||||
anyhow = "1.0.86"
|
||||
|
||||
# Optional decoders
|
||||
claxon = { version = "0.4.3", optional = true }
|
||||
symphonia-codec-alac = {version = "0.5.4", optional = true }
|
||||
symphonia-core = {version = "0.5.4", optional = true }
|
||||
vorbis_rs = {version = "0.5.4", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["all-lossless", "all-lossy"]
|
||||
all-lossless = ["alac", "flac"]
|
||||
all-lossy = ["vorbis"]
|
||||
alac = ["dep:symphonia-codec-alac", "dep:symphonia-core"]
|
||||
flac = ["dep:claxon"]
|
||||
vorbis = ["dep:vorbis_rs"]
|
||||
|
||||
[package.metadata.xcframework]
|
||||
include-dir = "src"
|
||||
|
@ -24,4 +38,5 @@ lib-type = "cdylib"
|
|||
zip = false
|
||||
macOS = true
|
||||
iOS = true
|
||||
#tvOS = true
|
||||
simulators = true
|
||||
|
|
|
@ -9,7 +9,8 @@ A library implementing the lonely radio audio streaming protocol
|
|||
## Examples
|
||||
|
||||
- [CLI](../monoclient)
|
||||
- [SwiftUI](../platform/swiftui)
|
||||
- [SwiftUI](../monoclient-x)
|
||||
- [Slint](../monoclient-s)
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
@ -11,39 +11,52 @@ pub struct CTrackMetadata {
|
|||
pub artist: *mut c_char,
|
||||
}
|
||||
|
||||
pub const ENCODER_PCM16: u8 = 0;
|
||||
pub const ENCODER_PCMFLOAT: u8 = 1;
|
||||
pub const ENCODER_FLAC: u8 = 2;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CSettings {
|
||||
/// See lonelyradio_types -> Encoder
|
||||
/// See lonelyradio_types for numeric representation -> Encoder
|
||||
pub encoder: u8,
|
||||
pub cover: i32,
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||
pub extern "C" fn c_start(server: *const c_char, settings: CSettings) {
|
||||
/// Starts audio playback using rodio
|
||||
/// Play without playlist => playlist = ""
|
||||
pub extern "C" fn c_start(server: *const c_char, settings: CSettings, playlist: *const c_char) {
|
||||
let serv = unsafe { CStr::from_ptr(server) };
|
||||
let playlist = unsafe { CStr::from_ptr(playlist) };
|
||||
run(
|
||||
match serv.to_str() {
|
||||
Ok(s) => s,
|
||||
_ => "",
|
||||
},
|
||||
serv.to_str().unwrap_or_default(),
|
||||
Settings {
|
||||
encoder: match settings.encoder {
|
||||
0 => Encoder::Pcm16,
|
||||
1 => Encoder::PcmFloat,
|
||||
2 => Encoder::Flac,
|
||||
3 => Encoder::Alac,
|
||||
7 => Encoder::Vorbis,
|
||||
_ => return,
|
||||
},
|
||||
cover: settings.cover,
|
||||
},
|
||||
playlist.to_str().unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||
/// Playlists separated by '\n'
|
||||
pub extern "C" fn c_list_playlists(server: *const c_char) -> *mut c_char {
|
||||
let serv = unsafe { CStr::from_ptr(server) };
|
||||
let playlists = list_playlists(serv.to_str().unwrap_or_default());
|
||||
CString::new(match playlists {
|
||||
None => "".to_string(),
|
||||
Some(s) => s.join("\n"),
|
||||
})
|
||||
.unwrap()
|
||||
.into_raw()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn c_toggle() {
|
||||
toggle()
|
||||
|
|
99
monolib/src/decode.rs
Normal file
99
monolib/src/decode.rs
Normal file
|
@ -0,0 +1,99 @@
|
|||
use std::io::{Cursor, Read};
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
use lonelyradio_types::{Encoder, FragmentMetadata, TrackMetadata};
|
||||
use symphonia_core::{
|
||||
audio::SampleBuffer,
|
||||
codecs::{Decoder, CODEC_TYPE_ALAC},
|
||||
formats::Packet,
|
||||
};
|
||||
|
||||
pub(crate) fn decode(
|
||||
mut stream: impl ReadBytesExt,
|
||||
md: &TrackMetadata,
|
||||
fmd: &FragmentMetadata,
|
||||
) -> anyhow::Result<Vec<f32>> {
|
||||
let mut samples = vec![];
|
||||
match md.encoder {
|
||||
Encoder::Pcm16 => {
|
||||
let mut samples_i16 = vec![0; fmd.length as usize / 2];
|
||||
stream.read_i16_into::<LittleEndian>(&mut samples_i16)?;
|
||||
samples
|
||||
.append(&mut samples_i16.iter().map(|sample| *sample as f32 / 32767.0).collect());
|
||||
}
|
||||
Encoder::PcmFloat => {
|
||||
let mut samples_f32 = vec![0f32; fmd.length as usize / 4];
|
||||
stream.read_f32_into::<LittleEndian>(&mut samples_f32)?;
|
||||
samples.append(&mut samples_f32);
|
||||
}
|
||||
Encoder::Flac => {
|
||||
#[cfg(feature = "alac")]
|
||||
{
|
||||
let take = std::io::Read::by_ref(&mut stream).take(fmd.length);
|
||||
let mut reader = claxon::FlacReader::new(take)?;
|
||||
samples.append(
|
||||
&mut reader
|
||||
.samples()
|
||||
.map(|x| x.unwrap_or(0) as f32 / 32768.0 / 256.0)
|
||||
.collect::<Vec<f32>>(),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "flac"))]
|
||||
{
|
||||
unimplemented!("flac decoding is disabled in library")
|
||||
}
|
||||
}
|
||||
Encoder::Alac => {
|
||||
#[cfg(feature = "alac")]
|
||||
{
|
||||
let mut buf = vec![];
|
||||
std::io::Read::by_ref(&mut stream).take(fmd.length).read_to_end(&mut buf)?;
|
||||
let mut reader = symphonia_codec_alac::AlacDecoder::try_new(
|
||||
symphonia_core::codecs::CodecParameters::default()
|
||||
.for_codec(CODEC_TYPE_ALAC)
|
||||
.with_extra_data(fmd.magic_cookie.clone().unwrap().into_boxed_slice()),
|
||||
&symphonia_core::codecs::DecoderOptions {
|
||||
verify: false,
|
||||
},
|
||||
)?;
|
||||
let decoded = reader.decode(&Packet::new_from_slice(0, 0, 0, &buf))?;
|
||||
let mut byte_buf =
|
||||
SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec());
|
||||
byte_buf.copy_interleaved_ref(decoded);
|
||||
samples.extend(byte_buf.samples());
|
||||
}
|
||||
#[cfg(not(feature = "alac"))]
|
||||
{
|
||||
unimplemented!("alac decoding is disabled in library")
|
||||
}
|
||||
}
|
||||
Encoder::Vorbis => {
|
||||
#[cfg(feature = "vorbis")]
|
||||
{
|
||||
let mut buf = vec![];
|
||||
std::io::Read::by_ref(&mut stream).take(fmd.length).read_to_end(&mut buf)?;
|
||||
let mut decoder = vorbis_rs::VorbisDecoder::new(Cursor::new(buf))?;
|
||||
let mut interleaved = vec![];
|
||||
|
||||
while let Some(decoded_block) = decoder.decode_audio_block()? {
|
||||
let s = decoded_block.samples();
|
||||
interleaved.resize(s[0].len() * s.len(), 0f32);
|
||||
for (ind, channel) in s.iter().enumerate() {
|
||||
for (samind, sample) in channel.iter().enumerate() {
|
||||
interleaved[ind + samind * md.channels as usize] = *sample;
|
||||
}
|
||||
}
|
||||
samples.extend(interleaved);
|
||||
interleaved = vec![];
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "vorbis"))]
|
||||
{
|
||||
unimplemented!("vorbis decoding is disabled in library")
|
||||
}
|
||||
}
|
||||
Encoder::Aac | Encoder::Opus | Encoder::WavPack => unimplemented!(),
|
||||
};
|
||||
Ok(samples)
|
||||
}
|
|
@ -5,8 +5,9 @@
|
|||
//! extern crate monolib;
|
||||
//! use std::thread::{sleep, spawn};
|
||||
//! use std::time::Duration;
|
||||
//! use monolib::lonelyradio_types::{Settings, Encoder};
|
||||
//!
|
||||
//! spawn(|| monolib::run("someserver:someport"));
|
||||
//! spawn(|| monolib::run("someserver:someport", Settings {encoder: Encoder::Flac, cover: -1}, "my_playlist"));
|
||||
//! while monolib::get_metadata().is_none() {}
|
||||
//! let seconds = md.length / md.sample_rate as u64 / 2;
|
||||
//! println!("Playing: {} - {} - {} ({}:{:02})", md.artist, md.album, md.title, seconds / 60, seconds % 60);
|
||||
|
@ -17,19 +18,36 @@
|
|||
/// Functions, providing C-like API
|
||||
pub mod c;
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use lonelyradio_types::{Encoder, Message, ServerCapabilities, Settings, TrackMetadata};
|
||||
pub use lonelyradio_types;
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use decode::decode;
|
||||
use lonelyradio_types::{
|
||||
Encoder, PlayMessage, Request, RequestResult, ServerCapabilities, Settings, TrackMetadata,
|
||||
};
|
||||
use rodio::buffer::SamplesBuffer;
|
||||
use rodio::{OutputStream, Sink};
|
||||
use std::error::Error;
|
||||
use std::io::{Read, Write};
|
||||
use std::io::Write;
|
||||
use std::net::TcpStream;
|
||||
use std::sync::atomic::AtomicU8;
|
||||
use std::sync::RwLock;
|
||||
use std::time::Instant;
|
||||
|
||||
mod decode;
|
||||
|
||||
const CACHE_SIZE_PCM: usize = 32;
|
||||
const CACHE_SIZE_COMPRESSED: usize = 2;
|
||||
const CACHE_SIZE_COMPRESSED: usize = 4;
|
||||
|
||||
const SUPPORTED_DECODERS: &[Encoder] = &[
|
||||
Encoder::Pcm16,
|
||||
Encoder::PcmFloat,
|
||||
#[cfg(feature = "flac")]
|
||||
Encoder::Flac,
|
||||
#[cfg(feature = "alac")]
|
||||
Encoder::Alac,
|
||||
#[cfg(feature = "vorbis")]
|
||||
Encoder::Vorbis,
|
||||
];
|
||||
|
||||
static SINK: RwLock<Option<Sink>> = RwLock::new(None);
|
||||
static VOLUME: AtomicU8 = AtomicU8::new(255);
|
||||
|
@ -144,139 +162,146 @@ pub fn set_volume(volume: u8) {
|
|||
}
|
||||
|
||||
/// Download track as samples
|
||||
pub fn get_track(server: &str, mut settings: Settings) -> Option<(TrackMetadata, Vec<i16>)> {
|
||||
let mut connection = unwrap(TcpStream::connect(server))?;
|
||||
unwrap(connection.write_u64::<LittleEndian>(lonelyradio_types::HELLO_MAGIC))?;
|
||||
let capabilities: ServerCapabilities = unwrap(rmp_serde::from_read(&mut connection))?;
|
||||
pub fn get_track(
|
||||
server: &str,
|
||||
mut settings: Settings,
|
||||
playlist: &str,
|
||||
) -> anyhow::Result<(TrackMetadata, Vec<f32>)> {
|
||||
let mut connection = TcpStream::connect(server)?;
|
||||
connection.write_all(lonelyradio_types::HELLO_MAGIC)?;
|
||||
let capabilities: ServerCapabilities = rmp_serde::from_read(&mut connection)?;
|
||||
if !capabilities.encoders.contains(&settings.encoder) {
|
||||
settings.encoder = Encoder::Pcm16
|
||||
}
|
||||
unwrap(connection.write_all(&rmp_serde::to_vec_named(&settings).unwrap()))?;
|
||||
|
||||
let mut stream = connection;
|
||||
let request = if playlist.is_empty() {
|
||||
Request::Play(settings)
|
||||
} else {
|
||||
Request::PlayPlaylist(playlist.to_string(), settings)
|
||||
};
|
||||
connection.write_all(&rmp_serde::to_vec_named(&request).unwrap())?;
|
||||
|
||||
let response: RequestResult = rmp_serde::from_read(&connection)?;
|
||||
if let RequestResult::Error(e) = response {
|
||||
bail!("{e:?}")
|
||||
}
|
||||
|
||||
let mut samples = vec![];
|
||||
let mut md: Option<TrackMetadata> = None;
|
||||
|
||||
loop {
|
||||
let recv_md: Message = rmp_serde::from_read(&mut stream).expect("Failed to parse message");
|
||||
let recv_md: PlayMessage = rmp_serde::from_read(&mut connection)?;
|
||||
match recv_md {
|
||||
Message::T(tmd) => {
|
||||
PlayMessage::T(tmd) => {
|
||||
if md.is_some() {
|
||||
break;
|
||||
}
|
||||
md = Some(tmd);
|
||||
}
|
||||
Message::F(fmd) => match md.as_ref().unwrap().encoder {
|
||||
Encoder::Pcm16 => {
|
||||
let mut buf = vec![0; fmd.length as usize];
|
||||
stream.read_i16_into::<LittleEndian>(&mut buf).unwrap();
|
||||
samples.append(&mut buf);
|
||||
}
|
||||
Encoder::PcmFloat => unimplemented!(),
|
||||
Encoder::Flac => {
|
||||
let take = std::io::Read::by_ref(&mut stream).take(fmd.length);
|
||||
let mut reader = claxon::FlacReader::new(take).unwrap();
|
||||
samples.append(
|
||||
&mut reader.samples().map(|x| x.unwrap_or(0) as i16).collect::<Vec<i16>>(),
|
||||
);
|
||||
}
|
||||
},
|
||||
PlayMessage::F(fmd) => {
|
||||
samples.extend(decode(&mut connection, md.as_ref().unwrap(), &fmd)?)
|
||||
}
|
||||
}
|
||||
}
|
||||
md.map(|md| (md, samples))
|
||||
|
||||
if let Some(md) = md {
|
||||
Ok((md, samples))
|
||||
} else {
|
||||
bail!("No metadata")
|
||||
}
|
||||
}
|
||||
|
||||
fn unwrap<T, E: Error>(thing: Result<T, E>) -> Option<T> {
|
||||
if thing.is_err() {
|
||||
*STATE.write().unwrap() = State::NotStarted;
|
||||
pub fn list_playlists(server: &str) -> Option<Vec<String>> {
|
||||
let mut connection = TcpStream::connect(server).ok()?;
|
||||
connection.write_all(lonelyradio_types::HELLO_MAGIC).ok()?;
|
||||
let _: ServerCapabilities = rmp_serde::from_read(&mut connection).ok()?;
|
||||
connection.write_all(&rmp_serde::to_vec_named(&Request::ListPlaylist).ok()?).ok()?;
|
||||
let res: RequestResult = rmp_serde::from_read(connection).ok()?;
|
||||
match res {
|
||||
RequestResult::Playlist(plist) => Some(plist.playlists),
|
||||
_ => None,
|
||||
}
|
||||
thing.ok()
|
||||
}
|
||||
|
||||
/// Starts playing at "server:port"
|
||||
pub fn run(server: &str, settings: Settings) {
|
||||
let _ = _run(server, settings);
|
||||
pub fn run(server: &str, settings: Settings, playlist: &str) {
|
||||
let result = _run(server, settings, playlist);
|
||||
if let Err(e) = result {
|
||||
println!("{:?}", e);
|
||||
*STATE.write().unwrap() = State::NotStarted;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn _run(server: &str, settings: Settings) -> Option<()> {
|
||||
let mut settings = settings;
|
||||
pub(crate) fn _run(server: &str, mut settings: Settings, playlist: &str) -> anyhow::Result<()> {
|
||||
if !SUPPORTED_DECODERS.contains(&settings.encoder) {
|
||||
eprintln!(
|
||||
"monolib was built without support for {:?}, falling back to Pcm16",
|
||||
settings.encoder
|
||||
);
|
||||
settings.encoder = Encoder::Pcm16
|
||||
}
|
||||
let mut state = STATE.write().unwrap();
|
||||
if *state == State::Playing || *state == State::Paused {
|
||||
return None;
|
||||
return Ok(());
|
||||
}
|
||||
*state = State::Playing;
|
||||
drop(state);
|
||||
|
||||
let mut connection = unwrap(TcpStream::connect(server))?;
|
||||
unwrap(connection.write_u64::<LittleEndian>(lonelyradio_types::HELLO_MAGIC))?;
|
||||
let capabilities: ServerCapabilities = unwrap(rmp_serde::from_read(&mut connection))?;
|
||||
let mut connection = TcpStream::connect(server).context("failed to connect to the server")?;
|
||||
connection.write_all(lonelyradio_types::HELLO_MAGIC)?;
|
||||
let capabilities: ServerCapabilities = rmp_serde::from_read(&mut connection)?;
|
||||
if !capabilities.encoders.contains(&settings.encoder) {
|
||||
settings.encoder = Encoder::Pcm16
|
||||
}
|
||||
unwrap(connection.write_all(&rmp_serde::to_vec_named(&settings).unwrap()))?;
|
||||
|
||||
let request = if playlist.is_empty() {
|
||||
Request::Play(settings)
|
||||
} else {
|
||||
Request::PlayPlaylist(playlist.to_string(), settings)
|
||||
};
|
||||
connection.write_all(&rmp_serde::to_vec_named(&request).unwrap())?;
|
||||
|
||||
let response: RequestResult = rmp_serde::from_read(&connection).unwrap();
|
||||
if let RequestResult::Error(e) = response {
|
||||
bail!("{:?}", e)
|
||||
}
|
||||
let mut stream = connection;
|
||||
|
||||
let mut sink = SINK.write().unwrap();
|
||||
let (_stream, stream_handle) = unwrap(OutputStream::try_default())?;
|
||||
let (_stream, stream_handle) =
|
||||
OutputStream::try_default().context("failed to determine audio device")?;
|
||||
|
||||
// Can't reuse old sink for some reason
|
||||
let audio_sink = Sink::try_new(&stream_handle).unwrap();
|
||||
let audio_sink = Sink::try_new(&stream_handle).context("failed to create audio sink")?;
|
||||
*sink = Some(audio_sink);
|
||||
drop(sink);
|
||||
|
||||
let mut samples = Vec::with_capacity(8192);
|
||||
loop {
|
||||
let recv_md: Message = rmp_serde::from_read(&mut stream).expect("Failed to parse message");
|
||||
let recv_md: PlayMessage =
|
||||
rmp_serde::from_read(&mut stream).expect("Failed to parse message");
|
||||
match recv_md {
|
||||
Message::T(tmd) => {
|
||||
PlayMessage::T(tmd) => {
|
||||
// No metadata shift
|
||||
if watching_sleep_until_end() {
|
||||
_stop();
|
||||
return None;
|
||||
return Ok(());
|
||||
}
|
||||
let mut md = MD.write().unwrap();
|
||||
*md = Some(tmd.clone());
|
||||
|
||||
drop(md);
|
||||
}
|
||||
Message::F(fmd) => {
|
||||
PlayMessage::F(fmd) => {
|
||||
while *STATE.read().unwrap() == State::Paused {
|
||||
std::thread::sleep(std::time::Duration::from_secs_f32(0.25))
|
||||
}
|
||||
if *STATE.read().unwrap() == State::Resetting {
|
||||
_stop();
|
||||
return None;
|
||||
return Ok(());
|
||||
}
|
||||
match MD.read().unwrap().as_ref().unwrap().encoder {
|
||||
Encoder::Pcm16 => {
|
||||
let mut samples_i16 = vec![0; fmd.length as usize / 2];
|
||||
if stream.read_i16_into::<LittleEndian>(&mut samples_i16).is_err() {
|
||||
return None;
|
||||
};
|
||||
samples.append(
|
||||
&mut samples_i16
|
||||
.iter()
|
||||
.map(|sample| *sample as f32 / 32767.0)
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
Encoder::PcmFloat => {
|
||||
let mut samples_f32 = vec![0f32; fmd.length as usize / 4];
|
||||
if stream.read_f32_into::<LittleEndian>(&mut samples_f32).is_err() {
|
||||
return None;
|
||||
};
|
||||
samples.append(&mut samples_f32);
|
||||
}
|
||||
Encoder::Flac => {
|
||||
let take = std::io::Read::by_ref(&mut stream).take(fmd.length);
|
||||
let mut reader = claxon::FlacReader::new(take).unwrap();
|
||||
samples.append(
|
||||
&mut reader
|
||||
.samples()
|
||||
.map(|x| x.unwrap_or(0) as f32 / 32768.0 / 256.0)
|
||||
.collect::<Vec<f32>>(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
samples.extend(decode(&mut stream, &MD.read().unwrap().clone().unwrap(), &fmd)?);
|
||||
|
||||
// Synchronizing with sink
|
||||
let sink = SINK.read().unwrap();
|
||||
|
@ -284,8 +309,12 @@ pub fn _run(server: &str, settings: Settings) -> Option<()> {
|
|||
let md = _md.as_ref().unwrap().clone();
|
||||
drop(_md);
|
||||
if let Some(sink) = sink.as_ref() {
|
||||
while (sink.len() >= CACHE_SIZE_PCM && md.encoder != Encoder::Flac)
|
||||
|| (sink.len() >= CACHE_SIZE_COMPRESSED && md.encoder == Encoder::Flac)
|
||||
while (sink.len() >= CACHE_SIZE_PCM
|
||||
&& md.encoder == Encoder::Pcm16
|
||||
&& md.encoder == Encoder::PcmFloat)
|
||||
|| (sink.len() >= CACHE_SIZE_COMPRESSED
|
||||
&& md.encoder != Encoder::Pcm16
|
||||
&& md.encoder != Encoder::PcmFloat)
|
||||
{
|
||||
// Sleeping exactly one buffer and watching for reset signal
|
||||
if watching_sleep(
|
||||
|
@ -297,7 +326,7 @@ pub fn _run(server: &str, settings: Settings) -> Option<()> {
|
|||
/ 4.0,
|
||||
) {
|
||||
_stop();
|
||||
return None;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
sink.append(SamplesBuffer::new(
|
||||
|
|
|
@ -1,23 +1,45 @@
|
|||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef struct CSettings {
|
||||
/**
|
||||
* See lonelyradio_types for numeric representation -> Encoder
|
||||
*/
|
||||
uint8_t encoder;
|
||||
int32_t cover;
|
||||
} CSettings;
|
||||
|
||||
typedef struct CImageJpeg {
|
||||
uint32_t length;
|
||||
uint8_t *bytes;
|
||||
} CImageJpeg;
|
||||
|
||||
typedef struct CSettings {
|
||||
/**
|
||||
* See lonelyradio_types -> Encoder
|
||||
*/
|
||||
uint8_t encoder;
|
||||
int32_t cover;
|
||||
} CSettings;
|
||||
/**
|
||||
* Starts audio playback using rodio
|
||||
* Play without playlist => playlist = ""
|
||||
*/
|
||||
void c_start(const char *server, struct CSettings settings, const char *playlist);
|
||||
|
||||
void c_drop(uint8_t *ptr, size_t count);
|
||||
/**
|
||||
* Playlists separated by '\n'
|
||||
*/
|
||||
char *c_list_playlists(const char *server);
|
||||
|
||||
void c_toggle(void);
|
||||
|
||||
void c_stop(void);
|
||||
|
||||
char c_get_state(void);
|
||||
|
||||
char *c_get_metadata_artist(void);
|
||||
|
||||
char *c_get_metadata_album(void);
|
||||
|
||||
char *c_get_metadata_title(void);
|
||||
|
||||
float c_get_metadata_length(void);
|
||||
|
||||
/**
|
||||
* # Safety
|
||||
|
@ -25,18 +47,8 @@ void c_drop(uint8_t *ptr, size_t count);
|
|||
*/
|
||||
struct CImageJpeg c_get_cover_jpeg(void);
|
||||
|
||||
char *c_get_metadata_album(void);
|
||||
|
||||
char *c_get_metadata_artist(void);
|
||||
|
||||
float c_get_metadata_length(void);
|
||||
|
||||
char *c_get_metadata_title(void);
|
||||
|
||||
char c_get_state(void);
|
||||
|
||||
void c_start(const char *server, struct CSettings settings);
|
||||
|
||||
void c_stop(void);
|
||||
|
||||
void c_toggle(void);
|
||||
/**
|
||||
* # Safety
|
||||
* None
|
||||
*/
|
||||
void c_drop(uint8_t *ptr, uintptr_t count);
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
../target
|
|
@ -1,12 +1,11 @@
|
|||
[package]
|
||||
name = "monoloader"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
monolib = { path = "../monolib" }
|
||||
monolib = { version = "0.7.1", path = "../monolib" }
|
||||
clap = { version = "4.4.18", features = ["derive"] }
|
||||
hound = "3.5.1"
|
||||
lonelyradio_types = { version = "0.6.0", path = "../lonelyradio_types" }
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use clap::Parser;
|
||||
use lonelyradio_types::Settings;
|
||||
use monolib::lonelyradio_types::Settings;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
|
@ -9,34 +9,53 @@ struct Args {
|
|||
|
||||
#[arg(long)]
|
||||
xor_key_file: Option<PathBuf>,
|
||||
|
||||
#[arg(short, long, default_value = "")]
|
||||
playlist: String,
|
||||
|
||||
#[arg(short, long)]
|
||||
list: bool,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
if args.list {
|
||||
println!(
|
||||
"Available playlists: {}",
|
||||
match monolib::list_playlists(&args.address) {
|
||||
Some(s) => format!("{:?}", s),
|
||||
None => String::from("None"),
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let (md, samples) = monolib::get_track(
|
||||
&args.address,
|
||||
Settings {
|
||||
encoder: lonelyradio_types::Encoder::Pcm16,
|
||||
encoder: monolib::lonelyradio_types::Encoder::Flac,
|
||||
cover: -1,
|
||||
},
|
||||
&args.playlist,
|
||||
)
|
||||
.unwrap();
|
||||
println!(
|
||||
"Downloaded: {} - {} - {} ({} MB)",
|
||||
"Downloaded: {} - {} - {} ({:?}, {} MiB)",
|
||||
md.artist,
|
||||
md.album,
|
||||
md.title,
|
||||
samples.len() as f32 * 2.0 / 1024.0 / 1024.0
|
||||
md.encoder,
|
||||
samples.len() as f32 / 256.0 / 1024.0
|
||||
);
|
||||
let spec = hound::WavSpec {
|
||||
channels: md.channels,
|
||||
sample_rate: md.sample_rate,
|
||||
bits_per_sample: 16,
|
||||
sample_format: hound::SampleFormat::Int,
|
||||
bits_per_sample: 32,
|
||||
sample_format: hound::SampleFormat::Float,
|
||||
};
|
||||
let mut writer =
|
||||
hound::WavWriter::create(format!("{} - {}.wav", md.artist, md.title), spec).unwrap();
|
||||
let mut writer_i16 = writer.get_i16_writer(samples.len() as u32);
|
||||
samples.iter().for_each(|s| writer_i16.write_sample(*s));
|
||||
writer_i16.flush().unwrap();
|
||||
samples.iter().for_each(|s| writer.write_sample(*s).unwrap());
|
||||
writer.flush().unwrap();
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ use symphonia::core::units::Time;
|
|||
|
||||
use crate::Args;
|
||||
|
||||
pub async fn get_meta(file_path: &Path) -> (u16, u32, Time) {
|
||||
pub async fn get_meta(file_path: &Path, encoder_wants: u32) -> (u16, u32, Time) {
|
||||
let file = Box::new(std::fs::File::open(file_path).unwrap());
|
||||
let mut hint = Hint::new();
|
||||
hint.with_extension(file_path.extension().unwrap().to_str().unwrap());
|
||||
|
@ -65,11 +65,26 @@ pub async fn get_meta(file_path: &Path) -> (u16, u32, Time) {
|
|||
}
|
||||
let args = Args::parse();
|
||||
|
||||
(channels, get_resampling_rate(&sample_rate, &args.max_samplerate), track_length)
|
||||
(
|
||||
channels,
|
||||
if args.no_resampling && encoder_wants == 0 {
|
||||
sample_rate
|
||||
} else {
|
||||
get_resampling_rate(
|
||||
&sample_rate,
|
||||
&if encoder_wants != 0 {
|
||||
args.max_samplerate.min(encoder_wants)
|
||||
} else {
|
||||
args.max_samplerate
|
||||
},
|
||||
)
|
||||
},
|
||||
track_length,
|
||||
)
|
||||
}
|
||||
|
||||
/// Getting samples
|
||||
pub fn decode_file_stream(file_path: PathBuf) -> impl Stream<Item = Vec<f32>> {
|
||||
pub fn decode_file_stream(file_path: PathBuf, encoder_wants: u32) -> impl Stream<Item = Vec<f32>> {
|
||||
let args = Args::parse();
|
||||
let file = Box::new(std::fs::File::open(&file_path).unwrap());
|
||||
let mut hint = Hint::new();
|
||||
|
@ -93,7 +108,7 @@ pub fn decode_file_stream(file_path: PathBuf) -> impl Stream<Item = Vec<f32>> {
|
|||
.expect("no supported audio tracks");
|
||||
|
||||
let mut decoder = symphonia::default::get_codecs()
|
||||
.make(track.codec_params.clone().with_max_frames_per_packet(65536), &Default::default())
|
||||
.make(&track.codec_params, &Default::default())
|
||||
.expect("unsupported codec");
|
||||
let track_id = track.id;
|
||||
stream! {
|
||||
|
@ -109,30 +124,30 @@ pub fn decode_file_stream(file_path: PathBuf) -> impl Stream<Item = Vec<f32>> {
|
|||
|
||||
match decoder.decode(&packet) {
|
||||
Ok(decoded) => {
|
||||
if decoded.spec().rate > args.max_samplerate {
|
||||
let output_rate = get_resampling_rate(&decoded.spec().rate, &if encoder_wants != 0 {
|
||||
args.max_samplerate.min(encoder_wants)
|
||||
} else {
|
||||
args.max_samplerate
|
||||
});
|
||||
if decoded.spec().rate > output_rate && (!args.no_resampling || encoder_wants != 0) {
|
||||
let spec = *decoded.spec();
|
||||
let mut byte_buf =
|
||||
SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec());
|
||||
byte_buf.copy_interleaved_ref(decoded);
|
||||
let output_rate = get_resampling_rate(&spec.rate,&args.max_samplerate);
|
||||
|
||||
// About Samplerate struct:
|
||||
// We are downsampling, not upsampling, so we should be fine
|
||||
yield (
|
||||
if output_rate == spec.rate {
|
||||
byte_buf.samples().to_vec()
|
||||
} else {
|
||||
samplerate::convert(
|
||||
spec.rate,
|
||||
args.max_samplerate,
|
||||
spec.channels.count(),
|
||||
samplerate::ConverterType::Linear,
|
||||
byte_buf.samples(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
);
|
||||
|
||||
yield (if output_rate == spec.rate {
|
||||
byte_buf.samples().to_vec()
|
||||
} else {
|
||||
samplerate::convert(
|
||||
spec.rate,
|
||||
output_rate,
|
||||
spec.channels.count(),
|
||||
samplerate::ConverterType::Linear,
|
||||
byte_buf.samples(),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
} else {
|
||||
let mut byte_buf =
|
||||
SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec());
|
||||
|
|
131
src/encode.rs
131
src/encode.rs
|
@ -1,52 +1,133 @@
|
|||
use flacenc::{component::BitRepr, error::Verify, source::MemSource};
|
||||
use lonelyradio_types::Encoder;
|
||||
|
||||
// Return: 0 - encoded bytes, 1 - magic cookie (for alac only)
|
||||
#[allow(unused_variables)]
|
||||
pub fn encode(
|
||||
codec: Encoder,
|
||||
mut samples: Vec<f32>,
|
||||
sample_rate: u32,
|
||||
channels: u16,
|
||||
) -> Option<Vec<u8>> {
|
||||
) -> Option<(Vec<u8>, Option<Vec<u8>>)> {
|
||||
match codec {
|
||||
Encoder::Pcm16 => {
|
||||
#[allow(unused_mut)]
|
||||
let mut samples = samples.iter_mut().map(|x| (*x * 32768.0) as i16).collect::<Vec<_>>();
|
||||
// Launching lonelyradio on the router moment
|
||||
if cfg!(target_endian = "big") {
|
||||
#[cfg(target_endian = "big")]
|
||||
{
|
||||
samples.iter_mut().for_each(|sample| {
|
||||
*sample = sample.to_le();
|
||||
});
|
||||
}
|
||||
// Sowwy about that
|
||||
let (_, samples, _) = unsafe { samples.align_to::<u8>() };
|
||||
Some(samples.to_vec())
|
||||
Some((samples.to_vec(), None))
|
||||
}
|
||||
Encoder::PcmFloat => {
|
||||
// Launching lonelyradio on the router moment
|
||||
// Sowwy about that
|
||||
let samples = samples.iter().map(|x| x.to_bits()).collect::<Vec<u32>>();
|
||||
let (_, samples, _) = unsafe { samples.align_to::<u8>() };
|
||||
Some(samples.to_vec())
|
||||
Some((samples.to_vec(), None))
|
||||
}
|
||||
Encoder::Flac => {
|
||||
let encoded = flacenc::encode_with_fixed_block_size(
|
||||
&flacenc::config::Encoder::default().into_verified().unwrap(),
|
||||
MemSource::from_samples(
|
||||
// I'm crying (It's just a burning memory)
|
||||
&samples
|
||||
.iter()
|
||||
.map(|x| (*x as f64 * 32768.0 * 256.0) as i32)
|
||||
.collect::<Vec<i32>>(),
|
||||
channels as usize,
|
||||
24,
|
||||
sample_rate as usize,
|
||||
),
|
||||
256,
|
||||
)
|
||||
.unwrap();
|
||||
#[cfg(feature = "flac")]
|
||||
{
|
||||
use flacenc::{component::BitRepr, error::Verify, source::MemSource};
|
||||
|
||||
let mut sink = flacenc::bitsink::ByteSink::new();
|
||||
encoded.write(&mut sink).unwrap();
|
||||
Some(sink.as_slice().to_vec())
|
||||
let encoded = flacenc::encode_with_fixed_block_size(
|
||||
&flacenc::config::Encoder::default().into_verified().unwrap(),
|
||||
MemSource::from_samples(
|
||||
&samples
|
||||
.iter()
|
||||
.map(|x| (*x as f64 * 32768.0 * 256.0) as i32)
|
||||
.collect::<Vec<i32>>(),
|
||||
channels as usize,
|
||||
24,
|
||||
sample_rate as usize,
|
||||
),
|
||||
256,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut sink = flacenc::bitsink::ByteSink::new();
|
||||
encoded.write(&mut sink).unwrap();
|
||||
Some((sink.as_slice().to_vec(), None))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "flac"))]
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
Encoder::Alac => {
|
||||
#[cfg(feature = "alac")]
|
||||
{
|
||||
use alac_encoder::{AlacEncoder, FormatDescription};
|
||||
|
||||
let samples = samples.iter_mut().map(|x| (*x * 32768.0) as i16).collect::<Vec<_>>();
|
||||
let (_, samples, _) = unsafe { samples.align_to::<u8>() };
|
||||
|
||||
let input_format =
|
||||
FormatDescription::pcm::<i16>(sample_rate as f64, channels as u32);
|
||||
let output_format = FormatDescription::alac(
|
||||
sample_rate as f64,
|
||||
samples.len() as u32,
|
||||
channels as u32,
|
||||
);
|
||||
|
||||
// Initialize the encoder
|
||||
let mut encoder = AlacEncoder::new(&output_format);
|
||||
|
||||
// Allocate a buffer for the encoder to write chunks to.
|
||||
let mut output = vec![0u8; output_format.max_packet_size()];
|
||||
let size = encoder.encode(&input_format, samples, &mut output);
|
||||
|
||||
// Here you can do whatever you want with the result:
|
||||
Some((Vec::from(&output[0..size]), Some(encoder.magic_cookie())))
|
||||
}
|
||||
#[cfg(not(feature = "alac"))]
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
Encoder::Vorbis => {
|
||||
#[cfg(feature = "vorbis")]
|
||||
{
|
||||
use std::num::{NonZeroU32, NonZeroU8};
|
||||
let out: Vec<u8> = vec![];
|
||||
let mut encoder = vorbis_rs::VorbisEncoderBuilder::new(
|
||||
NonZeroU32::new(sample_rate).unwrap(),
|
||||
NonZeroU8::new(channels as u8).unwrap(),
|
||||
out,
|
||||
)
|
||||
.unwrap()
|
||||
.bitrate_management_strategy(
|
||||
vorbis_rs::VorbisBitrateManagementStrategy::ConstrainedAbr {
|
||||
// I will think about clients asking about bitrate later, now it's just
|
||||
// "enough" 128 kib/s
|
||||
maximum_bitrate: NonZeroU32::new(192 * 1024).unwrap(),
|
||||
},
|
||||
)
|
||||
.build()
|
||||
.unwrap();
|
||||
let mut samples_channels = vec![];
|
||||
for i in 0..channels as usize {
|
||||
samples_channels.push(
|
||||
samples[i..]
|
||||
.iter()
|
||||
.step_by(channels as usize)
|
||||
.copied()
|
||||
.collect::<Vec<f32>>(),
|
||||
);
|
||||
}
|
||||
encoder.encode_audio_block(samples_channels).unwrap();
|
||||
Some((encoder.finish().unwrap(), None))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "vorbis"))]
|
||||
{
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
Encoder::Aac | Encoder::Opus | Encoder::WavPack => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
|
226
src/main.rs
226
src/main.rs
|
@ -1,9 +1,9 @@
|
|||
mod decode;
|
||||
mod encode;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use std::io::Read;
|
||||
use std::net::TcpStream;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
@ -13,18 +13,22 @@ use clap::Parser;
|
|||
use encode::encode;
|
||||
use futures_util::pin_mut;
|
||||
use futures_util::StreamExt;
|
||||
use image::io::Reader as ImageReader;
|
||||
use image::ImageReader;
|
||||
use lofty::Accessor;
|
||||
use lofty::TaggedFileExt;
|
||||
use lonelyradio_types::Encoder;
|
||||
use lonelyradio_types::Request;
|
||||
use lonelyradio_types::RequestResult;
|
||||
use lonelyradio_types::ServerCapabilities;
|
||||
use lonelyradio_types::Settings;
|
||||
use lonelyradio_types::{FragmentMetadata, Message, TrackMetadata};
|
||||
use lonelyradio_types::{FragmentMetadata, PlayMessage, TrackMetadata};
|
||||
use rand::prelude::*;
|
||||
use std::io::Write;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio_stream::Stream;
|
||||
use url::Url;
|
||||
use walkdir::DirEntry;
|
||||
use xspf::Playlist;
|
||||
|
||||
use crate::decode::decode_file_stream;
|
||||
use crate::decode::get_meta;
|
||||
|
@ -38,14 +42,6 @@ struct Args {
|
|||
#[arg(short, default_value = "0.0.0.0:5894")]
|
||||
address: String,
|
||||
|
||||
/// Enable "public" log (without sensitive information)
|
||||
#[arg(short, long)]
|
||||
public_log: bool,
|
||||
|
||||
/// Process all samples to -1 or 1
|
||||
#[arg(short, long)]
|
||||
war: bool,
|
||||
|
||||
/// Resample all tracks, which samplerate exceeds N
|
||||
#[arg(short, long, default_value = "96000")]
|
||||
max_samplerate: u32,
|
||||
|
@ -57,54 +53,65 @@ struct Args {
|
|||
/// Size of artwork (-1 for no artwork, 0 for original, N for NxN)
|
||||
#[arg(long, default_value = "96000")]
|
||||
artwork: i32,
|
||||
|
||||
#[arg(long)]
|
||||
playlist_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
const SUPPORTED_ENCODERS: [Encoder; 3] = [Encoder::Pcm16, Encoder::PcmFloat, Encoder::Flac];
|
||||
const SUPPORTED_ENCODERS: &[Encoder] = &[
|
||||
Encoder::Pcm16,
|
||||
Encoder::PcmFloat,
|
||||
#[cfg(feature = "flac")]
|
||||
Encoder::Flac,
|
||||
#[cfg(feature = "alac")]
|
||||
Encoder::Alac,
|
||||
#[cfg(feature = "vorbis")]
|
||||
Encoder::Vorbis,
|
||||
];
|
||||
|
||||
async fn stream_track(
|
||||
samples_stream: impl Stream<Item = Vec<f32>>,
|
||||
war: bool,
|
||||
md: TrackMetadata,
|
||||
s: &mut TcpStream,
|
||||
mut s: impl Write,
|
||||
) -> bool {
|
||||
pin_mut!(samples_stream);
|
||||
|
||||
let _md = md.clone();
|
||||
|
||||
if s.write_all(rmp_serde::encode::to_vec_named(&Message::T(_md)).unwrap().as_slice()).is_err() {
|
||||
if s.write_all(rmp_serde::encode::to_vec_named(&PlayMessage::T(_md)).unwrap().as_slice())
|
||||
.is_err()
|
||||
{
|
||||
return true;
|
||||
};
|
||||
|
||||
// Why chunks?
|
||||
// flacenc is broken on low amount of samples (Symphonia's AIFF decoder returns
|
||||
// ~2304 samples per packet (on bo en's tracks), instead of usual ~8192 on any
|
||||
// other lossless decoder)
|
||||
// Different codecs have different quality on different audio lenghts
|
||||
while let Some(mut _samples) = samples_stream
|
||||
.as_mut()
|
||||
.chunks(match md.encoder {
|
||||
Encoder::Pcm16 => 1,
|
||||
Encoder::PcmFloat => 1,
|
||||
Encoder::Flac => 16,
|
||||
Encoder::Alac => 32,
|
||||
Encoder::Vorbis => 64,
|
||||
Encoder::Aac | Encoder::Opus | Encoder::WavPack => unimplemented!(),
|
||||
})
|
||||
.next()
|
||||
.await
|
||||
{
|
||||
let mut _samples = _samples.concat();
|
||||
if war {
|
||||
_samples.iter_mut().for_each(|sample| {
|
||||
*sample = sample.signum();
|
||||
});
|
||||
}
|
||||
|
||||
match md.encoder {
|
||||
Encoder::Pcm16 => {
|
||||
let _md = Message::F(FragmentMetadata {
|
||||
let _md = PlayMessage::F(FragmentMetadata {
|
||||
length: _samples.len() as u64 * 2,
|
||||
magic_cookie: None,
|
||||
});
|
||||
if s.write_all(rmp_serde::to_vec(&_md).unwrap().as_slice()).is_err() {
|
||||
return true;
|
||||
}
|
||||
if s.write_all(
|
||||
&encode(Encoder::Pcm16, _samples, md.sample_rate, md.channels).unwrap(),
|
||||
&encode(Encoder::Pcm16, _samples, md.sample_rate, md.channels).unwrap().0,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
|
@ -112,24 +119,27 @@ async fn stream_track(
|
|||
}
|
||||
}
|
||||
Encoder::PcmFloat => {
|
||||
let _md = Message::F(FragmentMetadata {
|
||||
let _md = PlayMessage::F(FragmentMetadata {
|
||||
length: _samples.len() as u64 * 4,
|
||||
magic_cookie: None,
|
||||
});
|
||||
if s.write_all(rmp_serde::to_vec(&_md).unwrap().as_slice()).is_err() {
|
||||
return true;
|
||||
}
|
||||
if s.write_all(
|
||||
&encode(Encoder::PcmFloat, _samples, md.sample_rate, md.channels).unwrap(),
|
||||
&encode(Encoder::PcmFloat, _samples, md.sample_rate, md.channels).unwrap().0,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Encoder::Flac => {
|
||||
let encoded = encode(Encoder::Flac, _samples, md.sample_rate, md.channels).unwrap();
|
||||
let _md = Message::F(FragmentMetadata {
|
||||
Encoder::Flac | Encoder::Alac | Encoder::Vorbis => {
|
||||
let (encoded, magic_cookie) =
|
||||
encode(md.encoder, _samples, md.sample_rate, md.channels).unwrap();
|
||||
let _md = PlayMessage::F(FragmentMetadata {
|
||||
length: encoded.as_slice().len() as u64,
|
||||
magic_cookie,
|
||||
});
|
||||
if s.write_all(rmp_serde::to_vec(&_md).unwrap().as_slice()).is_err() {
|
||||
return true;
|
||||
|
@ -138,11 +148,39 @@ async fn stream_track(
|
|||
return true;
|
||||
}
|
||||
}
|
||||
Encoder::Aac | Encoder::Opus | Encoder::WavPack => unimplemented!(),
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn get_playlists(dir: impl AsRef<Path>) -> Option<HashMap<String, Arc<Vec<PathBuf>>>> {
|
||||
let mut map: HashMap<String, Arc<Vec<PathBuf>>> = HashMap::new();
|
||||
for playlist in walkdir::WalkDir::new(dir)
|
||||
.into_iter()
|
||||
.filter_entry(is_not_hidden)
|
||||
.filter_map(|v| v.ok())
|
||||
.map(|x| x.into_path())
|
||||
.filter(|x| x.is_file())
|
||||
{
|
||||
let mut name = playlist.file_name().unwrap().to_str().unwrap().to_string();
|
||||
let parsed = Playlist::read_file(playlist).unwrap();
|
||||
if let Some(ref n) = parsed.title {
|
||||
name = n.clone();
|
||||
}
|
||||
let tracklist = parsed
|
||||
.track_list
|
||||
.iter()
|
||||
.flat_map(|x| x.location.iter().flat_map(|l| Url::parse(l.as_str()).ok()))
|
||||
.filter(|x| x.scheme() == "file")
|
||||
.map(|x| x.to_file_path().unwrap())
|
||||
.filter(|x| track_valid(x))
|
||||
.collect();
|
||||
map.insert(name, Arc::new(tracklist));
|
||||
}
|
||||
Some(map)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let args = Args::parse();
|
||||
|
@ -156,17 +194,24 @@ async fn main() {
|
|||
.filter(|x| track_valid(x))
|
||||
.collect::<Vec<PathBuf>>(),
|
||||
);
|
||||
let playlists: Option<HashMap<String, Arc<Vec<PathBuf>>>> = match args.playlist_dir.as_ref() {
|
||||
None => None,
|
||||
Some(dir) => get_playlists(dir),
|
||||
};
|
||||
loop {
|
||||
let (socket, _) = listener.accept().await.unwrap();
|
||||
let mut s = socket.into_std().unwrap();
|
||||
s.set_nonblocking(false).unwrap();
|
||||
|
||||
let mut hello = [0u8; 8];
|
||||
if s.read_exact(&mut hello).is_err() {
|
||||
continue;
|
||||
}
|
||||
if hello != lonelyradio_types::HELLO_MAGIC.to_le_bytes() {
|
||||
|
||||
if &hello != lonelyradio_types::HELLO_MAGIC {
|
||||
continue;
|
||||
}
|
||||
|
||||
if s.write_all(
|
||||
&rmp_serde::to_vec_named(&ServerCapabilities {
|
||||
encoders: SUPPORTED_ENCODERS.to_vec(),
|
||||
|
@ -177,17 +222,82 @@ async fn main() {
|
|||
{
|
||||
continue;
|
||||
};
|
||||
let settings: Settings = match rmp_serde::from_read(&s) {
|
||||
Ok(s) => s,
|
||||
_ => continue,
|
||||
s.flush().unwrap();
|
||||
|
||||
let request: Request = match rmp_serde::from_read(&s) {
|
||||
Ok(r) => r,
|
||||
Err(_) => {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if settings.cover < -1 {
|
||||
continue;
|
||||
|
||||
match request {
|
||||
Request::Play(settings) => {
|
||||
if s.write_all(&rmp_serde::to_vec_named(&check_settings(&settings)).unwrap())
|
||||
.is_err()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
tokio::spawn(stream(s, tracklist.clone(), settings));
|
||||
}
|
||||
Request::ListPlaylist => match playlists {
|
||||
None => {
|
||||
s.write_all(
|
||||
&rmp_serde::to_vec_named(&RequestResult::Playlist(
|
||||
lonelyradio_types::PlaylistResponce {
|
||||
playlists: vec![],
|
||||
},
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
Some(ref playlists) => {
|
||||
s.write_all(
|
||||
&rmp_serde::to_vec_named(&RequestResult::Playlist(
|
||||
lonelyradio_types::PlaylistResponce {
|
||||
playlists: playlists.keys().cloned().collect(),
|
||||
},
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
},
|
||||
|
||||
Request::PlayPlaylist(playlist, settings) => {
|
||||
if playlists.is_none() || playlists.as_ref().unwrap().get(&playlist).is_none() {
|
||||
s.write_all(
|
||||
&rmp_serde::to_vec_named(&RequestResult::Error(
|
||||
lonelyradio_types::RequestError::NoSuchPlaylist,
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
continue;
|
||||
}
|
||||
if s.write_all(&rmp_serde::to_vec_named(&check_settings(&settings)).unwrap())
|
||||
.is_err()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let tracklist = playlists.as_ref().unwrap().get(&playlist).unwrap().clone();
|
||||
tokio::spawn(stream(s, tracklist, settings));
|
||||
}
|
||||
}
|
||||
tokio::spawn(stream(s, tracklist.clone(), settings));
|
||||
}
|
||||
}
|
||||
|
||||
fn check_settings(settings: &Settings) -> RequestResult {
|
||||
if settings.cover < -1 {
|
||||
return RequestResult::Error(lonelyradio_types::RequestError::WrongCoverSize);
|
||||
}
|
||||
if !SUPPORTED_ENCODERS.contains(&settings.encoder) {
|
||||
return RequestResult::Error(lonelyradio_types::RequestError::UnsupportedEncoder);
|
||||
}
|
||||
RequestResult::Ok
|
||||
}
|
||||
|
||||
fn is_not_hidden(entry: &DirEntry) -> bool {
|
||||
entry.file_name().to_str().map(|s| entry.depth() == 0 || !s.starts_with('.')).unwrap_or(false)
|
||||
}
|
||||
|
@ -212,9 +322,13 @@ fn track_valid(track: &Path) -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
async fn stream(mut s: TcpStream, tracklist: Arc<Vec<PathBuf>>, settings: Settings) {
|
||||
async fn stream(mut s: impl Write, tracklist: Arc<Vec<PathBuf>>, settings: Settings) {
|
||||
let args = Args::parse();
|
||||
|
||||
let encoder_wants = match settings.encoder {
|
||||
Encoder::Opus | Encoder::Vorbis | Encoder::Aac => 48000,
|
||||
Encoder::Flac => 96000,
|
||||
_ => 0,
|
||||
};
|
||||
loop {
|
||||
let track = tracklist.choose(&mut thread_rng()).unwrap().clone();
|
||||
|
||||
|
@ -255,43 +369,19 @@ async fn stream(mut s: TcpStream, tracklist: Arc<Vec<PathBuf>>, settings: Settin
|
|||
};
|
||||
};
|
||||
let track_message = format!("{} - {} - {}", &artist, &album, &title);
|
||||
eprintln!(
|
||||
"[{}] {} to {}:{}{} ({:?})",
|
||||
Local::now().to_rfc3339(),
|
||||
track_message,
|
||||
s.peer_addr().unwrap().ip(),
|
||||
s.peer_addr().unwrap().port(),
|
||||
if args.war {
|
||||
" with WAR.rs"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
settings.encoder
|
||||
);
|
||||
println!("[{}] {} ({:?})", Local::now().to_rfc3339(), track_message, settings.encoder);
|
||||
|
||||
if args.public_log {
|
||||
println!(
|
||||
"[{}] {} to {}{}",
|
||||
Local::now().to_rfc3339(),
|
||||
track.to_str().unwrap(),
|
||||
s.peer_addr().unwrap().port(),
|
||||
if args.war {
|
||||
" with WAR.rs"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
}
|
||||
let (channels, sample_rate, time) = get_meta(track.as_path()).await;
|
||||
let stream = decode_file_stream(track);
|
||||
let (channels, sample_rate, time) = get_meta(track.as_path(), encoder_wants).await;
|
||||
let stream = decode_file_stream(track, encoder_wants);
|
||||
let id = thread_rng().gen();
|
||||
if stream_track(
|
||||
stream,
|
||||
args.war,
|
||||
TrackMetadata {
|
||||
track_length_frac: time.frac as f32,
|
||||
track_length_secs: time.seconds,
|
||||
encoder: settings.encoder,
|
||||
cover: cover.join().unwrap(),
|
||||
id,
|
||||
album,
|
||||
artist,
|
||||
title,
|
||||
|
|
Loading…
Reference in a new issue