Compare commits

..

No commits in common. "d43dcde7a2b8980d4b1c8c98d25d6be423e30fdf" and "adf4b7bb1c6f21ea708d9430781408f17fd1fb9d" have entirely different histories.

30 changed files with 2792 additions and 2016 deletions

1
.gitignore vendored
View file

@ -50,6 +50,7 @@ fastlane/test_output
### SwiftPackageManager ###
Packages
xcuserdata
*.xcodeproj
### Xcode ###
# Xcode

1862
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -10,17 +10,20 @@ members = [
[package]
name = "lonelyradio"
description = "TCP radio for lonely ones"
version = "0.7.0"
version = "0.6.1"
edition = "2021"
license = "MIT"
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
repository = "https://github.com/ivabus/lonelyradio"
[dependencies]
lonelyradio_types = { version = "0.7.0", path = "./lonelyradio_types" }
lonelyradio_types = { version = "0.6.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",
@ -39,27 +42,14 @@ lofty = "0.18.2"
async-stream = "0.3.5"
tokio-stream = { version = "0.1.15", features = ["sync"] }
futures-util = "0.3.30"
once_cell = "1.19.0"
image = { version = "0.25.1", default-features = false, features = ["png", "jpeg", "bmp"]}
xspf = "0.4.0"
url = "2.5.2"
samplerate = "0.2.4"
once_cell = "1.19.0"
flacenc = { version = "0.4.0", default-features = false }
image = "0.25.1"
# 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 }
[build-dependencies]
cc = "1.0.98"
[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"
[profile.release]
opt-level = 3
strip = true
lto = "fat"

View file

@ -1,33 +1,27 @@
# lonelyradio Music Streamer
# lonelyradio
Shuffles through your [XSPF playlists](https://www.xspf.org) or your entire library.
Broadcast lossless audio over the internet.
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))
Decodes audio streams using [symphonia](https://github.com/pdeljanov/Symphonia).
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 clients requests.
Optionally transcodes audio into and from FLAC using [flacenc-rs](https://github.com/yotarok/flacenc-rs/) and [claxon](https://github.com/ruuda/claxon).
### Install server
## Install server
```shell
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.7.0 lonelyradio
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.6.1 lonelyradio
```
### Run
## Run
```
lonelyradio <MUSIC_FOLDER>
```
All files (recursively) will be shuffled and played back. Log will be displayed to stdout.
All files (recursively) will be shuffled and played back. Public log will be displayed to stdout, private to stderr.
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
@ -43,37 +37,42 @@ Only the `<location>` and (playlist's) element would be used and only `file://`
[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.7.0 monoclient-s
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.6.1 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>
```
Look into `--help` for detailed info on usage.
##### Install monoclient
```shell
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.6.1 monoclient
```
# 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.
@ -83,6 +82,10 @@ 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).

View file

@ -1,13 +1,12 @@
[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.209", features = ["derive"]}
serde_bytes = {version = "0.11.15"}
serde = { version = "1.0.197", features = ["derive"] }
serde_bytes = "0.11.15"

View file

@ -1,39 +1,9 @@
use serde::{Deserialize, Serialize};
pub const HELLO_MAGIC: &[u8; 8] = b"lonelyra";
pub const HELLO_MAGIC: u64 = 0x104e1374d10;
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
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 {
pub enum Message {
T(TrackMetadata),
F(FragmentMetadata),
}
@ -41,10 +11,10 @@ pub enum PlayMessage {
#[repr(C)]
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct Settings {
#[serde(rename = "e", alias = "encoder")]
#[serde(rename = "e")]
pub encoder: Encoder,
#[serde(rename = "co", alias = "cover")]
#[serde(rename = "co")]
pub cover: i32,
}
@ -52,9 +22,6 @@ 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)]
@ -75,47 +42,22 @@ pub struct TrackMetadata {
pub album: String,
#[serde(rename = "mar")]
pub artist: String,
#[serde(
rename = "co",
skip_serializing_if = "Option::is_none",
with = "serde_bytes",
default = "none"
)]
#[serde(rename = "co")]
#[serde(with = "serde_bytes")]
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 or samples, depends on encoder: Pcm* - samples, any compressed - bytes
#[serde(rename = "le")]
// In bytes
#[serde(rename = "l")]
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 Normal file
View file

@ -0,0 +1,886 @@
# 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",
]

24
microserve/Cargo.toml Normal file
View file

@ -0,0 +1,24 @@
[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",
] }

96
microserve/src/main.rs Normal file
View file

@ -0,0 +1,96 @@
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));
}
}

View file

@ -1,13 +1,17 @@
[package]
name = "monoclient-s"
description = "Client for lonelyradio built with Slint"
version = "0.7.0"
version = "0.6.0"
edition = "2021"
[dependencies]
slint = { version = "1.6" }
monolib = { path = "../monolib", version = "0.7.1" }
zune-jpeg = "0.4.13"
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" ]
[package.metadata.bundle]
name = "monoclient-s"

227
monoclient-s/src/app.rs Normal file
View file

@ -0,0 +1,227 @@
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();
}

7
monoclient-s/src/lib.rs Normal file
View file

@ -0,0 +1,7 @@
mod app;
#[cfg(target_os = "andoid")]
#[no_mangle]
fn android_main(app: slint::android::AndroidApp) {
slint::android::init(app).unwrap();
app::_main();
}

View file

@ -1,262 +1,4 @@
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();
mod app;
fn main() {
app::_main()
}

View file

@ -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, UInt(Int(cov.length)))
c_drop(cov.bytes, Int(cov.length))
print(self.cover.size)
} else {

View file

@ -29,11 +29,6 @@ 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 {
@ -45,166 +40,110 @@ 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_playlists = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
let timer_meta = Timer.publish(every: 0.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 {
MyStack(alignment: .center) {
VStack(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)
#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)
#endif
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
}
TextField(
"Server",
text: $server,
onCommit: {
#if os(macOS)
DispatchQueue.main.async {
NSApp.keyWindow?.makeFirstResponder(nil)
}
#endif
}
)
.disableAutocorrection(true)
.frame(width: 256)
.textFieldStyle(.roundedBorder)
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)
.multilineTextAlignment(.center)
#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)
#endif
VStack(alignment: .center){
Text(metadata.title).bold()
HStack(spacing: 8) {
Button(action: stop){
Image(systemName: "stop.fill").padding(4).frame(width: 32, height: 24)
Text(metadata.album)
Text(metadata.artist)
}.frame(minHeight: 64)
TextField(
"Server",
text: $server,
onCommit: {
#if os(macOS)
DispatchQueue.main.async {
NSApp.keyWindow?.makeFirstResponder(nil)
}
.disabled(state == PlayerState.NotStarted)
#endif
}
)
.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)
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)
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) {
}
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) {
Text("Original")
.tag(CoverSize.Full)
Text("High (768)")
@ -218,58 +157,77 @@ struct Player: View {
Text("No cover")
.tag(CoverSize.NoCover)
}.pickerStyle(.menu)
} label: {
Label("Settings", systemImage: "gearshape")
.padding(16)
}.frame(maxWidth: 128)
} 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
}
.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()
#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 {
play()
}
return MPRemoteCommandHandlerStatus.success
})
MPRemoteCommandCenter.shared().playCommand.addTarget(handler: { _ in
if state != PlayerState.Playing {
play()
}
return MPRemoteCommandHandlerStatus.success
})
let nowPlayingInfo: [String: Any] = [
MPMediaItemPropertyArtist: metadata.artist,
MPMediaItemPropertyAlbumTitle: metadata.album,
MPMediaItemPropertyTitle: metadata.title,
MPMediaItemPropertyArtwork: mediaArtwork,
MPNowPlayingInfoPropertyIsLiveStream: true,
MPMediaItemPropertyPlaybackDuration: c_get_metadata_length(),
MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget(handler: {_ in
play()
return MPRemoteCommandHandlerStatus.success
})
MPRemoteCommandCenter.shared().nextTrackCommand.addTarget(handler: {_ in
next()
return MPRemoteCommandHandlerStatus.success
})
}
.animation(.spring, value: UUID())
]
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
.onAppear() {
#if os(iOS)
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 {
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
})
}
.animation(.spring, value: UUID())
}
@ -278,8 +236,7 @@ struct Player: View {
func play() {
switch state {
case PlayerState.NotStarted: do {
/*#if os(macOS)
#else*/
#if os(iOS)
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(
@ -289,9 +246,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), playlist.name == "All tracks" ? "" : playlist.name)
c_start(server, CSettings(encoder: settings.encoder.rawValue, cover: settings.cover_size.rawValue))
}
}
default: do {

View file

@ -25,12 +25,11 @@ struct monoclient_xApp: App {
CommandGroup(replacing: CommandGroupPlacement.newItem) {
}
}
.defaultSize(width: 256, height: 512)
#else
WindowGroup {
ContentView()
}
.defaultSize(width: 256, height: 512)
#endif
}
}

View file

@ -1,12 +1,14 @@
[package]
name = "monoclient"
license = "MIT"
version = "0.7.0"
version = "0.6.1"
edition = "2021"
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
repository = "https://github.com/ivabus/lonelyradio"
[dependencies]
monolib = { version = "0.7.1", path = "../monolib" }
clap = { version = "4.5.16", features = ["derive"] }
crossterm = "0.28.1"
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" }

View file

@ -3,7 +3,7 @@ use crossterm::cursor::MoveToColumn;
use crossterm::event::{poll, read, Event};
use crossterm::style::Print;
use crossterm::terminal::{Clear, ClearType};
use monolib::lonelyradio_types::{Encoder, Settings};
use lonelyradio_types::{Encoder, Settings};
use std::io::stdout;
use std::sync::OnceLock;
use std::time::Instant;
@ -17,12 +17,6 @@ struct Args {
#[arg(short, long)]
verbose: bool,
#[arg(short, long, default_value = "")]
playlist: String,
#[arg(short, long)]
list: bool,
}
const HELP: &str = r#"Keybinds:
@ -43,29 +37,17 @@ 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::Flac,
encoder: Encoder::PcmFloat,
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;
@ -81,6 +63,7 @@ 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)) {
@ -133,8 +116,8 @@ fn main() {
}
}
}
if next_md != md
&& md.track_length_secs as f64 <= (Instant::now() - track_start).as_secs_f64()
if monolib::get_metadata().unwrap() != md
//&& track_length <= (Instant::now() - track_start).as_secs_f64()
{
md = next_md.clone();
verbose!("md: {:?}", md);

View file

@ -1,6 +1,6 @@
[package]
name = "monolib"
version = "0.7.1"
version = "0.6.0"
edition = "2021"
license = "MIT"
description = "A library implementing the lonely radio audio streaming protocol"
@ -12,25 +12,11 @@ name = "monolib"
crate-type = ["cdylib", "staticlib", "rlib"]
[dependencies]
rodio = { version = "0.19.0", default-features = false }
rodio = { version = "0.17.3", default-features = false }
byteorder = "1.5.0"
rmp-serde = "1.1.2"
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"]
lonelyradio_types = { version = "0.6.0", path = "../lonelyradio_types" }
claxon = "0.4.3"
[package.metadata.xcframework]
include-dir = "src"
@ -38,5 +24,4 @@ lib-type = "cdylib"
zip = false
macOS = true
iOS = true
#tvOS = true
simulators = true

View file

@ -9,8 +9,7 @@ A library implementing the lonely radio audio streaming protocol
## Examples
- [CLI](../monoclient)
- [SwiftUI](../monoclient-x)
- [Slint](../monoclient-s)
- [SwiftUI](../platform/swiftui)
## License

View file

@ -11,52 +11,39 @@ 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 for numeric representation -> Encoder
/// See lonelyradio_types -> Encoder
pub encoder: u8,
pub cover: i32,
}
#[no_mangle]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
/// 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) {
pub extern "C" fn c_start(server: *const c_char, settings: CSettings) {
let serv = unsafe { CStr::from_ptr(server) };
let playlist = unsafe { CStr::from_ptr(playlist) };
run(
serv.to_str().unwrap_or_default(),
match serv.to_str() {
Ok(s) => s,
_ => "",
},
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()

View file

@ -1,99 +0,0 @@
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)
}

View file

@ -5,9 +5,8 @@
//! extern crate monolib;
//! use std::thread::{sleep, spawn};
//! use std::time::Duration;
//! use monolib::lonelyradio_types::{Settings, Encoder};
//!
//! spawn(|| monolib::run("someserver:someport", Settings {encoder: Encoder::Flac, cover: -1}, "my_playlist"));
//! spawn(|| monolib::run("someserver:someport"));
//! 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);
@ -18,36 +17,19 @@
/// Functions, providing C-like API
pub mod c;
pub use lonelyradio_types;
use anyhow::{bail, Context};
use decode::decode;
use lonelyradio_types::{
Encoder, PlayMessage, Request, RequestResult, ServerCapabilities, Settings, TrackMetadata,
};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use lonelyradio_types::{Encoder, Message, ServerCapabilities, Settings, TrackMetadata};
use rodio::buffer::SamplesBuffer;
use rodio::{OutputStream, Sink};
use std::io::Write;
use std::error::Error;
use std::io::{Read, 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 = 4;
const SUPPORTED_DECODERS: &[Encoder] = &[
Encoder::Pcm16,
Encoder::PcmFloat,
#[cfg(feature = "flac")]
Encoder::Flac,
#[cfg(feature = "alac")]
Encoder::Alac,
#[cfg(feature = "vorbis")]
Encoder::Vorbis,
];
const CACHE_SIZE_COMPRESSED: usize = 2;
static SINK: RwLock<Option<Sink>> = RwLock::new(None);
static VOLUME: AtomicU8 = AtomicU8::new(255);
@ -162,146 +144,139 @@ pub fn set_volume(volume: u8) {
}
/// Download track as samples
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)?;
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))?;
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)?;
if let RequestResult::Error(e) = response {
bail!("{e:?}")
}
let mut stream = connection;
let mut samples = vec![];
let mut md: Option<TrackMetadata> = None;
loop {
let recv_md: PlayMessage = rmp_serde::from_read(&mut connection)?;
let recv_md: Message = rmp_serde::from_read(&mut stream).expect("Failed to parse message");
match recv_md {
PlayMessage::T(tmd) => {
Message::T(tmd) => {
if md.is_some() {
break;
}
md = Some(tmd);
}
PlayMessage::F(fmd) => {
samples.extend(decode(&mut connection, md.as_ref().unwrap(), &fmd)?)
}
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>>(),
);
}
},
}
}
if let Some(md) = md {
Ok((md, samples))
} else {
bail!("No metadata")
}
md.map(|md| (md, samples))
}
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,
fn unwrap<T, E: Error>(thing: Result<T, E>) -> Option<T> {
if thing.is_err() {
*STATE.write().unwrap() = State::NotStarted;
}
thing.ok()
}
/// Starts playing at "server:port"
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) {
let _ = _run(server, 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
}
pub fn _run(server: &str, settings: Settings) -> Option<()> {
let mut settings = settings;
let mut state = STATE.write().unwrap();
if *state == State::Playing || *state == State::Paused {
return Ok(());
return None;
}
*state = State::Playing;
drop(state);
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)?;
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))?;
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) =
OutputStream::try_default().context("failed to determine audio device")?;
let (_stream, stream_handle) = unwrap(OutputStream::try_default())?;
// Can't reuse old sink for some reason
let audio_sink = Sink::try_new(&stream_handle).context("failed to create audio sink")?;
let audio_sink = Sink::try_new(&stream_handle).unwrap();
*sink = Some(audio_sink);
drop(sink);
let mut samples = Vec::with_capacity(8192);
loop {
let recv_md: PlayMessage =
rmp_serde::from_read(&mut stream).expect("Failed to parse message");
let recv_md: Message = rmp_serde::from_read(&mut stream).expect("Failed to parse message");
match recv_md {
PlayMessage::T(tmd) => {
Message::T(tmd) => {
// No metadata shift
if watching_sleep_until_end() {
_stop();
return Ok(());
return None;
}
let mut md = MD.write().unwrap();
*md = Some(tmd.clone());
drop(md);
}
PlayMessage::F(fmd) => {
Message::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 Ok(());
return None;
}
samples.extend(decode(&mut stream, &MD.read().unwrap().clone().unwrap(), &fmd)?);
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>>(),
);
}
};
// Synchronizing with sink
let sink = SINK.read().unwrap();
@ -309,12 +284,8 @@ pub(crate) fn _run(server: &str, mut settings: Settings, playlist: &str) -> anyh
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::Pcm16
&& md.encoder == Encoder::PcmFloat)
|| (sink.len() >= CACHE_SIZE_COMPRESSED
&& md.encoder != Encoder::Pcm16
&& md.encoder != Encoder::PcmFloat)
while (sink.len() >= CACHE_SIZE_PCM && md.encoder != Encoder::Flac)
|| (sink.len() >= CACHE_SIZE_COMPRESSED && md.encoder == Encoder::Flac)
{
// Sleeping exactly one buffer and watching for reset signal
if watching_sleep(
@ -326,7 +297,7 @@ pub(crate) fn _run(server: &str, mut settings: Settings, playlist: &str) -> anyh
/ 4.0,
) {
_stop();
return Ok(());
return None;
}
}
sink.append(SamplesBuffer::new(

View file

@ -1,45 +1,23 @@
#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;
/**
* Starts audio playback using rodio
* Play without playlist => playlist = ""
*/
void c_start(const char *server, struct CSettings settings, const char *playlist);
typedef struct CSettings {
/**
* See lonelyradio_types -> Encoder
*/
uint8_t encoder;
int32_t cover;
} CSettings;
/**
* 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);
void c_drop(uint8_t *ptr, size_t count);
/**
* # Safety
@ -47,8 +25,18 @@ float c_get_metadata_length(void);
*/
struct CImageJpeg c_get_cover_jpeg(void);
/**
* # Safety
* None
*/
void c_drop(uint8_t *ptr, uintptr_t count);
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);

1
monolib/src/target Symbolic link
View file

@ -0,0 +1 @@
../target

View file

@ -1,11 +1,12 @@
[package]
name = "monoloader"
version = "0.7.0"
version = "0.6.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
monolib = { version = "0.7.1", path = "../monolib" }
monolib = { path = "../monolib" }
clap = { version = "4.4.18", features = ["derive"] }
hound = "3.5.1"
lonelyradio_types = { version = "0.6.0", path = "../lonelyradio_types" }

View file

@ -1,5 +1,5 @@
use clap::Parser;
use monolib::lonelyradio_types::Settings;
use lonelyradio_types::Settings;
use std::path::PathBuf;
#[derive(Parser)]
@ -9,53 +9,34 @@ 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: monolib::lonelyradio_types::Encoder::Flac,
encoder: lonelyradio_types::Encoder::Pcm16,
cover: -1,
},
&args.playlist,
)
.unwrap();
println!(
"Downloaded: {} - {} - {} ({:?}, {} MiB)",
"Downloaded: {} - {} - {} ({} MB)",
md.artist,
md.album,
md.title,
md.encoder,
samples.len() as f32 / 256.0 / 1024.0
samples.len() as f32 * 2.0 / 1024.0 / 1024.0
);
let spec = hound::WavSpec {
channels: md.channels,
sample_rate: md.sample_rate,
bits_per_sample: 32,
sample_format: hound::SampleFormat::Float,
bits_per_sample: 16,
sample_format: hound::SampleFormat::Int,
};
let mut writer =
hound::WavWriter::create(format!("{} - {}.wav", md.artist, md.title), spec).unwrap();
samples.iter().for_each(|s| writer.write_sample(*s).unwrap());
writer.flush().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();
}

View file

@ -11,7 +11,7 @@ use symphonia::core::units::Time;
use crate::Args;
pub async fn get_meta(file_path: &Path, encoder_wants: u32) -> (u16, u32, Time) {
pub async fn get_meta(file_path: &Path) -> (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,26 +65,11 @@ pub async fn get_meta(file_path: &Path, encoder_wants: u32) -> (u16, u32, Time)
}
let args = Args::parse();
(
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,
)
(channels, get_resampling_rate(&sample_rate, &args.max_samplerate), track_length)
}
/// Getting samples
pub fn decode_file_stream(file_path: PathBuf, encoder_wants: u32) -> impl Stream<Item = Vec<f32>> {
pub fn decode_file_stream(file_path: PathBuf) -> 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();
@ -108,7 +93,7 @@ pub fn decode_file_stream(file_path: PathBuf, encoder_wants: u32) -> impl Stream
.expect("no supported audio tracks");
let mut decoder = symphonia::default::get_codecs()
.make(&track.codec_params, &Default::default())
.make(track.codec_params.clone().with_max_frames_per_packet(65536), &Default::default())
.expect("unsupported codec");
let track_id = track.id;
stream! {
@ -124,30 +109,30 @@ pub fn decode_file_stream(file_path: PathBuf, encoder_wants: u32) -> impl Stream
match decoder.decode(&packet) {
Ok(decoded) => {
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) {
if decoded.spec().rate > args.max_samplerate {
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,
output_rate,
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,
args.max_samplerate,
spec.channels.count(),
samplerate::ConverterType::Linear,
byte_buf.samples(),
)
.unwrap()
}
);
} else {
let mut byte_buf =
SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec());

View file

@ -1,133 +1,52 @@
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
#[cfg(target_endian = "big")]
{
if 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(), None))
Some(samples.to_vec())
}
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(), None))
Some(samples.to_vec())
}
Encoder::Flac => {
#[cfg(feature = "flac")]
{
use flacenc::{component::BitRepr, error::Verify, source::MemSource};
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();
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!()
}
let mut sink = flacenc::bitsink::ByteSink::new();
encoded.write(&mut sink).unwrap();
Some(sink.as_slice().to_vec())
}
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!(),
}
}

View file

@ -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,22 +13,18 @@ use clap::Parser;
use encode::encode;
use futures_util::pin_mut;
use futures_util::StreamExt;
use image::ImageReader;
use image::io::Reader as 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, PlayMessage, TrackMetadata};
use lonelyradio_types::{FragmentMetadata, Message, 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;
@ -42,6 +38,14 @@ 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,
@ -53,65 +57,54 @@ 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] = &[
Encoder::Pcm16,
Encoder::PcmFloat,
#[cfg(feature = "flac")]
Encoder::Flac,
#[cfg(feature = "alac")]
Encoder::Alac,
#[cfg(feature = "vorbis")]
Encoder::Vorbis,
];
const SUPPORTED_ENCODERS: [Encoder; 3] = [Encoder::Pcm16, Encoder::PcmFloat, Encoder::Flac];
async fn stream_track(
samples_stream: impl Stream<Item = Vec<f32>>,
war: bool,
md: TrackMetadata,
mut s: impl Write,
s: &mut TcpStream,
) -> bool {
pin_mut!(samples_stream);
let _md = md.clone();
if s.write_all(rmp_serde::encode::to_vec_named(&PlayMessage::T(_md)).unwrap().as_slice())
.is_err()
{
if s.write_all(rmp_serde::encode::to_vec_named(&Message::T(_md)).unwrap().as_slice()).is_err() {
return true;
};
// Why chunks?
// Different codecs have different quality on different audio lenghts
// 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)
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 = PlayMessage::F(FragmentMetadata {
let _md = Message::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().0,
&encode(Encoder::Pcm16, _samples, md.sample_rate, md.channels).unwrap(),
)
.is_err()
{
@ -119,27 +112,24 @@ async fn stream_track(
}
}
Encoder::PcmFloat => {
let _md = PlayMessage::F(FragmentMetadata {
let _md = Message::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().0,
&encode(Encoder::PcmFloat, _samples, md.sample_rate, md.channels).unwrap(),
)
.is_err()
{
return true;
}
}
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 {
Encoder::Flac => {
let encoded = encode(Encoder::Flac, _samples, md.sample_rate, md.channels).unwrap();
let _md = Message::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;
@ -148,39 +138,11 @@ 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();
@ -194,24 +156,17 @@ 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 {
if hello != lonelyradio_types::HELLO_MAGIC.to_le_bytes() {
continue;
}
if s.write_all(
&rmp_serde::to_vec_named(&ServerCapabilities {
encoders: SUPPORTED_ENCODERS.to_vec(),
@ -222,82 +177,17 @@ async fn main() {
{
continue;
};
s.flush().unwrap();
let request: Request = match rmp_serde::from_read(&s) {
Ok(r) => r,
Err(_) => {
continue;
}
let settings: Settings = match rmp_serde::from_read(&s) {
Ok(s) => s,
_ => 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));
}
if settings.cover < -1 {
continue;
}
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)
}
@ -322,13 +212,9 @@ fn track_valid(track: &Path) -> bool {
}
}
async fn stream(mut s: impl Write, tracklist: Arc<Vec<PathBuf>>, settings: Settings) {
async fn stream(mut s: TcpStream, 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();
@ -369,19 +255,43 @@ async fn stream(mut s: impl Write, tracklist: Arc<Vec<PathBuf>>, settings: Setti
};
};
let track_message = format!("{} - {} - {}", &artist, &album, &title);
println!("[{}] {} ({:?})", Local::now().to_rfc3339(), track_message, settings.encoder);
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
);
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 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);
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,