0.5.0: FLAC support, new GUI player, volume settings

Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
This commit is contained in:
Ivan Bushchik 2024-05-20 07:48:32 +03:00
parent 39cc35e16d
commit ff52e14863
No known key found for this signature in database
GPG key ID: 2F16FBF3262E090C
17 changed files with 5101 additions and 408 deletions

3832
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2,15 +2,16 @@
members = [ members = [
"lonelyradio_types", "lonelyradio_types",
"monoclient", "monoclient",
"monoclient-s",
"monolib", "monolib",
"monoloader", "monoloader",
"platform/gtk", "microserve",
] ]
[package] [package]
name = "lonelyradio" name = "lonelyradio"
description = "TCP radio for lonely ones" description = "TCP radio for lonely ones"
version = "0.4.0" version = "0.5.0"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"] authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
@ -42,8 +43,10 @@ async-stream = "0.3.5"
tokio-stream = { version = "0.1.15", features = ["sync"] } tokio-stream = { version = "0.1.15", features = ["sync"] }
futures-util = "0.3.30" futures-util = "0.3.30"
samplerate = "0.2.4" samplerate = "0.2.4"
lonelyradio_types = { path = "./lonelyradio_types" } lonelyradio_types = { version = "0.5.0", path = "./lonelyradio_types" }
once_cell = "1.19.0" once_cell = "1.19.0"
flacenc = { version = "0.4.0", default-features = false }
[profile.release] [profile.release]
opt-level = 3 opt-level = 3
strip = true

View file

@ -1,47 +1,77 @@
# lonelyradio # lonelyradio
> TCP radio for singles Broadcast audio over the internet.
Radio that uses unencrypted TCP socket for broadcasting tagged audio data.
Decodes audio streams using [symphonia](https://github.com/pdeljanov/Symphonia). Decodes audio streams using [symphonia](https://github.com/pdeljanov/Symphonia).
## Install Optionally transcodes audio into and from FLAC using [flacenc-rs](https://github.com/yotarok/flacenc-rs/) and [claxon](https://github.com/ruuda/claxon).
## Installation
### Install music server
```shell ```shell
cargo install lonelyradio cargo install --git https://github.com/ivabus/lonelyradio --tag 0.5.0 lonelyradio
``` ```
## Build ### Install CLI client
```shell ```shell
cargo build -r cargo install --git https://github.com/ivabus/lonelyradio --tag 0.5.0 monoclient
```
### Install GUI (Slint) client
```shell
cargo install --git https://github.com/ivabus/lonelyradio --tag 0.5.0 monoclient-s
``` ```
## Run ## Run
``` ```
lonelyradio <MUSIC_FOLDER> [-a <ADDRESS:PORT>] [-p] [-w] [-m|--max-samplerate M] lonelyradio [-a <ADDRESS:PORT>] [-p|--public-log] [-w|--war] [-m|--max-samplerate M] [--xor-key-file FILE] [--no-resampling] [-f|--flac] <MUSIC_FOLDER>
``` ```
All files (recursively) will be shuffled and played back. Public log will be displayed to stdout, private to stderr. All files (recursively) will be shuffled and played back. Public log will be displayed to stdout, private to stderr.
`-m|--max-samplerate M` will resample tracks which samplerate exceeds M to M `-m|--max-samplerate M` will resample tracks which samplerate exceeds M to M
`--xor-key-file FILE` will XOR all outgoing bytes looping through FILE
`-f|--flac` will enable (experimental) FLAC compression
### Clients ### Clients
[monoclient](./monoclient) is a recommended CLI client for lonelyradio that uses [monolib](./monolib) [monoclient](./monoclient) is a recommended CLI player for lonelyradio that uses [monolib](./monolib)
```shell ```shell
monoclient <SERVER>:<PORT> monoclient <SERVER>:<PORT>
``` ```
[monoclient-s](./monoclient-s) is a experimental GUI player for lonelyradio built with [Slint](https://slint.dev)
```shell
monoclient-s
```
Desktop integration will be added later.
### Other clients ### Other clients
SwiftUI client is availible in [platform](./platform) directory. SwiftUI client is availible in [platform](./platform) directory.
[monolib](./monolib) provides lonelyradio-compatible C API for creating custom clients. [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.
#### monolib API stability
As lonelyradio has not yet reached its first major release, the API may (and will) break at any point.
### Microphone server
Experimental server (lonelyradio-compatible) for streaming audio from your microphone is available in the [microserve](./microserve) crate.
## License ## License
lonelyradio, monolib and monoclient are licensed under the terms of the [MIT license](./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,9 +1,11 @@
[package] [package]
name = "lonelyradio_types" name = "lonelyradio_types"
version = "0.4.0" description = "Shared types for lonelyradio"
license = "MIT"
version = "0.5.0"
edition = "2021" edition = "2021"
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html repository = "https://github.com/ivabus/lonelyradio"
[dependencies] [dependencies]
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.197", features = ["derive"] }

View file

@ -12,6 +12,7 @@ pub struct TrackMetadata {
pub track_length_frac: f32, pub track_length_frac: f32,
pub channels: u16, pub channels: u16,
pub sample_rate: u32, pub sample_rate: u32,
pub flac: bool,
pub title: String, pub title: String,
pub album: String, pub album: String,
pub artist: String, pub artist: String,
@ -19,11 +20,6 @@ pub struct TrackMetadata {
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] #[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct FragmentMetadata { pub struct FragmentMetadata {
// In samples // In samples or bytes (if FLAC)
pub length: u64, pub length: u64,
} }
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
pub struct SessionSettings {
pub gzip: bool,
}

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",
] }

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

@ -0,0 +1,95 @@
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 {
flac: false,
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));
}
}

19
monoclient-s/Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
name = "monoclient-s"
description = "Client for lonelyradio built with Slint"
version = "0.5.0"
edition = "2021"
[dependencies]
slint = { version = "1.6.0", features = ["backend-android-activity-06"] }
monolib = { path = "../monolib" }
# TODO: Set up cargo-bundle
#[package.metadata.bundle]
#name = "monoclient-s"
#identifier = "dev.ivabus.monoclient-s"
#icon = ["lonelyradio.png", "lonelyradio.icns"]
#version = "0.5.0"
#copyright = "Copyright (c) 2024 Ivan Bushchik."
#category = "Music"

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

@ -0,0 +1,7 @@
mod main;
#[no_mangle]
fn android_main(app: slint::android::AndroidApp) {
slint::android::init(app).unwrap();
main::main()
}

181
monoclient-s/src/main.rs Normal file
View file

@ -0,0 +1,181 @@
use std::time::Duration;
use monolib::State;
use slint::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;
title: "monoclient-s";
min-width: 192px;
max-width: 768px;
VerticalBox {
alignment: center;
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)
}
}
}
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, None));
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());
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();
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();
}

View file

@ -1,10 +1,12 @@
[package] [package]
name = "monoclient" name = "monoclient"
version = "0.4.0"
edition = "2021"
license = "MIT" license = "MIT"
version = "0.5.0"
edition = "2021"
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
repository = "https://github.com/ivabus/lonelyradio"
[dependencies] [dependencies]
monolib = { path = "../monolib" } monolib = { version = "0.5.0", path = "../monolib" }
clap = { version = "4.4.18", features = ["derive"] } clap = { version = "4.4.18", features = ["derive"] }
crossterm = "0.27.0" crossterm = "0.27.0"

View file

@ -1,27 +1,29 @@
use clap::Parser; use clap::Parser;
use crossterm::cursor::MoveToColumn; use crossterm::cursor::MoveToColumn;
use crossterm::event::{poll, read, Event};
use crossterm::style::Print; use crossterm::style::Print;
use crossterm::terminal::{Clear, ClearType}; use crossterm::terminal::{Clear, ClearType};
use std::io::{stdout, IsTerminal}; use std::io::stdout;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::{Duration, Instant}; use std::time::Instant;
#[derive(Parser)] #[derive(Parser)]
struct Args { struct Args {
/// Remote address /// Remote address
address: String, address: String,
/// Do not use backspace control char
#[arg(short)]
no_backspace: bool,
#[arg(long)] #[arg(long)]
xor_key_file: Option<PathBuf>, xor_key_file: Option<PathBuf>,
} }
const HELP: &str = r#"Keybinds:
Up - Volume up
Down - Volume down
Q - Quit monoclient
H - Show this help"#;
fn main() { fn main() {
let mut args = Args::parse(); let args = Args::parse();
args.no_backspace |= !std::io::stdout().is_terminal();
std::thread::spawn(move || { std::thread::spawn(move || {
monolib::run( monolib::run(
&args.address, &args.address,
@ -46,7 +48,58 @@ fn main() {
.unwrap(); .unwrap();
let mut track_length = md.track_length_secs as f64 + md.track_length_frac as f64; let mut track_length = md.track_length_secs as f64 + md.track_length_frac as f64;
let mut next_md = md.clone(); let mut next_md = md.clone();
crossterm::terminal::enable_raw_mode().unwrap();
loop { loop {
if let Ok(true) = poll(std::time::Duration::from_micros(1)) {
if let Event::Key(event) = read().unwrap() {
match (event.code, event.modifiers) {
(crossterm::event::KeyCode::Up, _) => {
monolib::set_volume(monolib::get_volume().saturating_add(4));
}
(crossterm::event::KeyCode::Down, _) => {
monolib::set_volume(monolib::get_volume().saturating_sub(4));
}
(crossterm::event::KeyCode::Char('q' | 'й'), _)
| (
crossterm::event::KeyCode::Char('c' | 'с'),
crossterm::event::KeyModifiers::CONTROL,
) => {
crossterm::terminal::disable_raw_mode().unwrap();
println!();
std::process::exit(0)
}
(crossterm::event::KeyCode::Char('h' | 'р'), _) => {
crossterm::terminal::disable_raw_mode().unwrap();
crossterm::execute!(
stdout(),
Clear(ClearType::CurrentLine),
MoveToColumn(0)
)
.unwrap();
println!("{}", HELP);
crossterm::terminal::enable_raw_mode().unwrap();
seconds_past = (Instant::now() - track_start).as_secs();
crossterm::execute!(
stdout(),
Print(format!(
"Playing: {} - {} - {} ({}:{:02} / {}:{:02}) [{:.2}]",
md.artist,
md.album,
md.title,
seconds_past / 60,
seconds_past % 60,
md.track_length_secs / 60,
md.track_length_secs % 60,
monolib::get_volume() as f32 / 255.0
))
)
.unwrap();
}
(crossterm::event::KeyCode::Char(' '), _) => monolib::toggle(),
_ => {}
}
}
}
if monolib::get_metadata().unwrap() != md if monolib::get_metadata().unwrap() != md
&& track_length <= (Instant::now() - track_start).as_secs_f64() && track_length <= (Instant::now() - track_start).as_secs_f64()
{ {
@ -58,7 +111,7 @@ fn main() {
md.album, md.album,
md.title, md.title,
md.track_length_secs / 60, md.track_length_secs / 60,
md.track_length_secs % 60 md.track_length_secs % 60,
); );
track_start = Instant::now(); track_start = Instant::now();
seconds_past = 0; seconds_past = 0;
@ -66,24 +119,25 @@ fn main() {
} else if next_md == md { } else if next_md == md {
next_md = monolib::get_metadata().unwrap(); next_md = monolib::get_metadata().unwrap();
} }
if (Instant::now() - track_start).as_secs() > seconds_past && !args.no_backspace { if (Instant::now() - track_start).as_secs() > seconds_past {
seconds_past = (Instant::now() - track_start).as_secs(); seconds_past = (Instant::now() - track_start).as_secs();
crossterm::execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap(); crossterm::execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap();
crossterm::execute!( crossterm::execute!(
stdout(), stdout(),
Print(format!( Print(format!(
"Playing: {} - {} - {} ({}:{:02} / {}:{:02})", "Playing: {} - {} - {} ({}:{:02} / {}:{:02}) [{:.2}]",
md.artist, md.artist,
md.album, md.album,
md.title, md.title,
seconds_past / 60, seconds_past / 60,
seconds_past % 60, seconds_past % 60,
md.track_length_secs / 60, md.track_length_secs / 60,
md.track_length_secs % 60 md.track_length_secs % 60,
monolib::get_volume() as f32 / 255.0
)) ))
) )
.unwrap(); .unwrap();
} }
std::thread::sleep(Duration::from_secs_f32(0.25)) std::thread::sleep(std::time::Duration::from_secs_f32(0.0125))
} }
} }

View file

@ -1,6 +1,6 @@
[package] [package]
name = "monolib" name = "monolib"
version = "0.4.0" version = "0.5.0"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
description = "A library implementing the lonely radio audio streaming protocol" description = "A library implementing the lonely radio audio streaming protocol"
@ -15,4 +15,5 @@ crate-type = ["staticlib", "cdylib", "rlib"]
rodio = { version = "0.17.3", default-features = false } rodio = { version = "0.17.3", default-features = false }
byteorder = "1.5.0" byteorder = "1.5.0"
rmp-serde = "1.1.2" rmp-serde = "1.1.2"
lonelyradio_types = { path = "../lonelyradio_types" } lonelyradio_types = { version = "0.5.0", path = "../lonelyradio_types" }
claxon = "0.4.3"

View file

@ -12,7 +12,7 @@
//! println!("Playing: {} - {} - {} ({}:{:02})", md.artist, md.album, md.title, seconds / 60, seconds % 60); //! println!("Playing: {} - {} - {} ({}:{:02})", md.artist, md.album, md.title, seconds / 60, seconds % 60);
//! sleep(Duration::from_secs(10)); //! sleep(Duration::from_secs(10));
//! monolib::stop(); //! monolib::stop();
//!``` //! ```
/// Functions, providing C-like API /// Functions, providing C-like API
pub mod c; pub mod c;
@ -22,14 +22,17 @@ use byteorder::{LittleEndian, ReadBytesExt};
use lonelyradio_types::{Message, TrackMetadata}; use lonelyradio_types::{Message, TrackMetadata};
use rodio::buffer::SamplesBuffer; use rodio::buffer::SamplesBuffer;
use rodio::{OutputStream, Sink}; use rodio::{OutputStream, Sink};
use std::io::BufReader; use std::error::Error;
use std::io::{BufReader, Read};
use std::net::TcpStream; use std::net::TcpStream;
use std::sync::atomic::AtomicU8;
use std::sync::RwLock; use std::sync::RwLock;
use std::time::Instant; use std::time::Instant;
const CACHE_SIZE: usize = 32; const CACHE_SIZE: usize = 128;
static SINK: RwLock<Option<Sink>> = RwLock::new(None); static SINK: RwLock<Option<Sink>> = RwLock::new(None);
static VOLUME: AtomicU8 = AtomicU8::new(255);
static MD: RwLock<Option<TrackMetadata>> = RwLock::new(None); static MD: RwLock<Option<TrackMetadata>> = RwLock::new(None);
static STATE: RwLock<State> = RwLock::new(State::NotStarted); static STATE: RwLock<State> = RwLock::new(State::NotStarted);
@ -77,9 +80,10 @@ pub fn stop() {
} }
drop(sink); drop(sink);
drop(state); drop(state);
// Blocking main thread // Blocking main thread
while *STATE.read().unwrap() == State::Resetting { while *STATE.read().unwrap() == State::Resetting {
std::thread::sleep(std::time::Duration::from_secs_f32(0.01)) std::thread::sleep(std::time::Duration::from_secs_f32(0.1))
} }
} }
@ -94,10 +98,8 @@ pub fn get_metadata() -> Option<TrackMetadata> {
fn _stop() { fn _stop() {
let sink = SINK.read().unwrap(); let sink = SINK.read().unwrap();
if let Some(sink) = sink.as_ref() { if let Some(sink) = sink.as_ref() {
sink.pause();
sink.clear(); sink.clear();
} }
let mut md = MD.write().unwrap(); let mut md = MD.write().unwrap();
if md.is_some() { if md.is_some() {
*md = None; *md = None;
@ -110,7 +112,7 @@ fn _stop() {
fn watching_sleep(dur: f32) -> bool { fn watching_sleep(dur: f32) -> bool {
let start = Instant::now(); let start = Instant::now();
while Instant::now() < start + std::time::Duration::from_secs_f32(dur) { while Instant::now() < start + std::time::Duration::from_secs_f32(dur) {
std::thread::sleep(std::time::Duration::from_secs_f32(0.0001)); std::thread::sleep(std::time::Duration::from_secs_f32(0.01));
if *STATE.read().unwrap() == State::Resetting { if *STATE.read().unwrap() == State::Resetting {
return true; return true;
} }
@ -118,6 +120,28 @@ fn watching_sleep(dur: f32) -> bool {
false false
} }
fn watching_sleep_until_end() -> bool {
while SINK.read().unwrap().as_ref().unwrap().len() != 0 {
std::thread::sleep(std::time::Duration::from_secs_f32(0.01));
if *STATE.read().unwrap() == State::Resetting {
return true;
}
}
false
}
pub fn get_volume() -> u8 {
VOLUME.load(std::sync::atomic::Ordering::Acquire)
}
pub fn set_volume(volume: u8) {
let sink = SINK.read().unwrap();
if let Some(sink) = sink.as_ref() {
sink.set_volume(get_volume() as f32 / 255.0)
}
VOLUME.store(volume, std::sync::atomic::Ordering::Relaxed)
}
/// Download track as samples /// Download track as samples
pub fn get_track(server: &str, xor_key: Option<Vec<u8>>) -> Option<(TrackMetadata, Vec<i16>)> { pub fn get_track(server: &str, xor_key: Option<Vec<u8>>) -> Option<(TrackMetadata, Vec<i16>)> {
let mut stream = BufReader::new(match xor_key { let mut stream = BufReader::new(match xor_key {
@ -137,16 +161,30 @@ pub fn get_track(server: &str, xor_key: Option<Vec<u8>>) -> Option<(TrackMetadat
md = Some(tmd); md = Some(tmd);
} }
Message::F(fmd) => { Message::F(fmd) => {
let mut buf = vec![0; fmd.length as usize]; if !md.clone().unwrap().flac {
stream.read_i16_into::<LittleEndian>(&mut buf).unwrap(); let mut buf = vec![0; fmd.length as usize];
samples.append(&mut buf); stream.read_i16_into::<LittleEndian>(&mut buf).unwrap();
samples.append(&mut buf);
} else {
let take = stream.by_ref().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>>(),
);
}
} }
} }
} }
md.map(|md| (md, samples)) md.map(|md| (md, samples))
} }
fn unwrap<T, E: Error>(thing: Result<T, E>) -> T {
if thing.is_err() {
*STATE.write().unwrap() = State::NotStarted;
}
thing.unwrap()
}
/// Starts playing at "server:port" /// Starts playing at "server:port"
pub fn run(server: &str, xor_key: Option<Vec<u8>>) { pub fn run(server: &str, xor_key: Option<Vec<u8>>) {
let mut state = STATE.write().unwrap(); let mut state = STATE.write().unwrap();
@ -157,12 +195,12 @@ pub fn run(server: &str, xor_key: Option<Vec<u8>>) {
drop(state); drop(state);
let mut stream = BufReader::new(match xor_key { let mut stream = BufReader::new(match xor_key {
Some(k) => reader::Reader::XorEncrypted(TcpStream::connect(server).unwrap(), k, 0), Some(k) => reader::Reader::XorEncrypted(unwrap(TcpStream::connect(server)), k, 0),
None => reader::Reader::Unencrypted(TcpStream::connect(server).unwrap()), None => reader::Reader::Unencrypted(unwrap(TcpStream::connect(server))),
}); });
let mut sink = SINK.write().unwrap(); let mut sink = SINK.write().unwrap();
let (_stream, stream_handle) = OutputStream::try_default().unwrap(); let (_stream, stream_handle) = unwrap(OutputStream::try_default());
// Can't reuse old sink for some reason // Can't reuse old sink for some reason
let audio_sink = Sink::try_new(&stream_handle).unwrap(); let audio_sink = Sink::try_new(&stream_handle).unwrap();
@ -174,8 +212,14 @@ pub fn run(server: &str, xor_key: Option<Vec<u8>>) {
let recv_md: Message = 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 { match recv_md {
Message::T(tmd) => { Message::T(tmd) => {
// No metadata shift
if watching_sleep_until_end() {
_stop();
return;
}
let mut md = MD.write().unwrap(); let mut md = MD.write().unwrap();
*md = Some(tmd.clone()); *md = Some(tmd.clone());
drop(md);
} }
Message::F(fmd) => { Message::F(fmd) => {
while *STATE.read().unwrap() == State::Paused { while *STATE.read().unwrap() == State::Paused {
@ -185,21 +229,34 @@ pub fn run(server: &str, xor_key: Option<Vec<u8>>) {
_stop(); _stop();
return; return;
} }
let mut samples_i16 = vec![0; fmd.length as usize]; if !MD.read().unwrap().clone().unwrap().flac {
if stream.read_i16_into::<LittleEndian>(&mut samples_i16).is_err() { let mut samples_i16 = vec![0; fmd.length as usize];
return; if stream.read_i16_into::<LittleEndian>(&mut samples_i16).is_err() {
}; return;
samples.append( };
&mut samples_i16.iter().map(|sample| *sample as f32 / 32767.0).collect(), samples.append(
); &mut samples_i16.iter().map(|sample| *sample as f32 / 32767.0).collect(),
);
} else {
let take = stream.by_ref().take(fmd.length);
let mut reader = claxon::FlacReader::new(take).unwrap();
samples.append(
&mut reader
.samples()
.map(|x| x.unwrap_or(0) as f32 / 32767.0)
.collect::<Vec<f32>>(),
);
}
// Sink's thread is detached from main thread, so we need to synchronize with it // Sink's thread is detached from main thread, so we need to synchronize with it
// Why we should synchronize with it? // Why we should synchronize with it?
// Let's say, that if we don't synchronize with it, we would have // Let's say, that if we don't synchronize with it, we would have
// a lot (no upper limit, actualy) of buffered sound, waiting for playing in sink // a lot (no upper limit, actualy) of buffered sound, waiting for playing in
// sink
let sink = SINK.read().unwrap(); let sink = SINK.read().unwrap();
let md = MD.read().unwrap(); let _md = MD.read().unwrap();
let md = md.as_ref().unwrap(); let md = _md.as_ref().unwrap().clone();
drop(_md);
if let Some(sink) = sink.as_ref() { if let Some(sink) = sink.as_ref() {
while sink.len() >= CACHE_SIZE { while sink.len() >= CACHE_SIZE {
// Sleeping exactly one buffer and watching for reset signal // Sleeping exactly one buffer and watching for reset signal

View file

@ -101,7 +101,7 @@ pub fn decode_file_stream(file_path: PathBuf) -> impl Stream<Item = Vec<i16>> {
.expect("no supported audio tracks"); .expect("no supported audio tracks");
let mut decoder = symphonia::default::get_codecs() 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"); .expect("unsupported codec");
let track_id = track.id; let track_id = track.id;
stream! { stream! {
@ -122,20 +122,27 @@ pub fn decode_file_stream(file_path: PathBuf) -> impl Stream<Item = Vec<i16>> {
let mut byte_buf = let mut byte_buf =
SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec()); SampleBuffer::<f32>::new(decoded.capacity() as u64, *decoded.spec());
byte_buf.copy_interleaved_ref(decoded); byte_buf.copy_interleaved_ref(decoded);
let output_rate = get_resampling_rate(&spec.rate, &args.max_samplerate);
// About Samplerate struct: // About Samplerate struct:
// We are downsampling, not upsampling, so we should be fine // We are downsampling, not upsampling, so we should be fine
yield (samplerate::convert( yield (
spec.rate, if output_rate == spec.rate {
args.max_samplerate, byte_buf.samples().iter().map(|x| (*x * 32768.0) as i16).collect()
spec.channels.count(), } else {
samplerate::ConverterType::Linear, samplerate::convert(
byte_buf.samples(), spec.rate,
) args.max_samplerate,
.unwrap() spec.channels.count(),
.iter() samplerate::ConverterType::Linear,
.map(|x| (*x * 32768.0) as i16) byte_buf.samples(),
.collect()); )
.unwrap()
.iter()
.map(|x| (*x * 32768.0) as i16)
.collect()
}
);
} else { } else {
let mut byte_buf = let mut byte_buf =
@ -153,3 +160,15 @@ pub fn decode_file_stream(file_path: PathBuf) -> impl Stream<Item = Vec<i16>> {
} }
} }
} }
fn get_resampling_rate(in_rate: &u32, max_samplerate: &u32) -> u32 {
if in_rate < max_samplerate {
*in_rate
} else if in_rate % 44100 == 0 {
max_samplerate - (max_samplerate % 44100)
} else if in_rate % 48000 == 0 {
max_samplerate - (max_samplerate % 48000)
} else {
*max_samplerate
}
}

View file

@ -7,16 +7,19 @@ use std::sync::Arc;
use chrono::Local; use chrono::Local;
use clap::Parser; use clap::Parser;
use flacenc::component::BitRepr;
use flacenc::error::Verify;
use flacenc::source::MemSource;
use futures_util::pin_mut; use futures_util::pin_mut;
use futures_util::StreamExt;
use lofty::Accessor; use lofty::Accessor;
use lofty::TaggedFileExt; use lofty::TaggedFileExt;
use lonelyradio_types::{FragmentMetadata, Message, TrackMetadata}; use lonelyradio_types::{FragmentMetadata, Message, TrackMetadata};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use rand::prelude::*; use rand::prelude::*;
use std::io::Write; use std::io::Write;
use tokio::net::{TcpListener, TcpStream}; use tokio::net::TcpListener;
use tokio_stream::Stream; use tokio_stream::Stream;
use tokio_stream::StreamExt;
use walkdir::DirEntry; use walkdir::DirEntry;
use writer::Writer; use writer::Writer;
@ -25,19 +28,34 @@ use crate::decode::get_meta;
#[derive(Parser)] #[derive(Parser)]
struct Args { struct Args {
/// Directory with audio files
dir: PathBuf, dir: PathBuf,
/// Address:port to bind
#[arg(short, default_value = "0.0.0.0:5894")] #[arg(short, default_value = "0.0.0.0:5894")]
address: String, address: String,
/// Enable "public" log (without sensitive information)
#[arg(short, long)] #[arg(short, long)]
public_log: bool, public_log: bool,
/// Process all samples to -1 or 1
#[arg(short, long)] #[arg(short, long)]
war: bool, war: bool,
/// Resample all tracks, which samplerate exceeds N
#[arg(short, long, default_value = "96000")] #[arg(short, long, default_value = "96000")]
max_samplerate: u32, max_samplerate: u32,
/// Disable all audio processing (disable resampling)
#[arg(long)]
no_resampling: bool,
/// Use FLAC compression
#[arg(short, long)]
flac: bool,
/// Enable XOR "encryption"
#[arg(long)] #[arg(long)]
xor_key_file: Option<PathBuf>, xor_key_file: Option<PathBuf>,
} }
@ -60,35 +78,80 @@ async fn stream_track(
) -> bool { ) -> bool {
pin_mut!(samples_stream); pin_mut!(samples_stream);
if s.write_all(rmp_serde::to_vec(&Message::T(md)).unwrap().as_slice()).is_err() { let _md = md.clone();
if s.write_all(rmp_serde::to_vec(&Message::T(_md)).unwrap().as_slice()).is_err() {
return true; return true;
}; };
while let Some(mut _samples) = samples_stream.next().await { // Why chunks?
let md = Message::F(FragmentMetadata { // flacenc is broken on low amount of samples (Symphonia's AIFF decoder returns ~2304
length: _samples.len() as u64, // samples per packet (on bo en's tracks), instead of usual ~8192 on any other lossless decoder)
}); while let Some(mut _samples) = samples_stream
if s.write_all(rmp_serde::to_vec(&md).unwrap().as_slice()).is_err() { .as_mut()
return true; .chunks(if md.flac && md.track_length_secs > 1 {
} 2
} else {
1
})
.next()
.await
{
let mut _samples = _samples.concat();
if war { if war {
_samples.iter_mut().for_each(|sample| { _samples.iter_mut().for_each(|sample| {
*sample = sample.signum() * 32767; *sample = sample.signum() * 32767;
}); });
} }
// Launching lonelyradio on the router moment if !md.flac {
if cfg!(target_endian = "big") { let _md = Message::F(FragmentMetadata {
_samples.iter_mut().for_each(|sample| { length: _samples.len() as u64,
*sample = sample.to_le();
}); });
} if s.write_all(rmp_serde::to_vec(&_md).unwrap().as_slice()).is_err() {
return true;
}
// Sowwy about that // Launching lonelyradio on the router moment
let (_, samples, _) = unsafe { _samples.align_to::<u8>() }; if cfg!(target_endian = "big") {
_samples.iter_mut().for_each(|sample| {
*sample = sample.to_le();
});
}
if s.write_all(samples).is_err() { // Sowwy about that
return true; let (_, samples, _) = unsafe { _samples.align_to::<u8>() };
if s.write_all(samples).is_err() {
return true;
}
} else {
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 i32).collect::<Vec<i32>>(),
md.channels as usize,
16,
md.sample_rate as usize,
),
256,
);
if encoded.is_err() {
return true;
}
let mut sink = flacenc::bitsink::ByteSink::new();
encoded.unwrap().write(&mut sink).unwrap();
let _md = Message::F(FragmentMetadata {
length: sink.as_slice().len() as u64,
});
if s.write_all(rmp_serde::to_vec(&_md).unwrap().as_slice()).is_err() {
return true;
}
if s.write_all(sink.as_slice()).is_err() {
return true;
}
} }
} }
false false
@ -96,6 +159,7 @@ async fn stream_track(
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let args = Args::parse();
let listener = TcpListener::bind(Args::parse().address).await.unwrap(); let listener = TcpListener::bind(Args::parse().address).await.unwrap();
let tracklist = Arc::new( let tracklist = Arc::new(
walkdir::WalkDir::new(Args::parse().dir) walkdir::WalkDir::new(Args::parse().dir)
@ -108,7 +172,23 @@ async fn main() {
); );
loop { loop {
let (socket, _) = listener.accept().await.unwrap(); let (socket, _) = listener.accept().await.unwrap();
tokio::spawn(stream(socket, tracklist.clone())); let s = socket.into_std().unwrap();
s.set_nonblocking(false).unwrap();
let s = if args.xor_key_file.is_some() {
Writer::XorEncrypted(
s,
match &*KEY {
Some(a) => a.clone(),
_ => {
unreachable!()
}
},
0,
)
} else {
Writer::Unencrypted(s)
};
tokio::spawn(stream(s, tracklist.clone()));
} }
} }
fn is_not_hidden(entry: &DirEntry) -> bool { fn is_not_hidden(entry: &DirEntry) -> bool {
@ -127,24 +207,9 @@ fn track_valid(track: &Path) -> bool {
true true
} }
async fn stream(s: TcpStream, tracklist: Arc<Vec<PathBuf>>) { async fn stream(mut s: Writer, tracklist: Arc<Vec<PathBuf>>) {
let args = Args::parse(); let args = Args::parse();
let s = s.into_std().unwrap();
s.set_nonblocking(false).unwrap();
let mut s = if args.xor_key_file.is_some() {
Writer::XorEncrypted(
s,
match &*KEY {
Some(a) => a.clone(),
_ => {
unreachable!()
}
},
0,
)
} else {
Writer::Unencrypted(s)
};
loop { loop {
let track = tracklist.choose(&mut thread_rng()).unwrap().clone(); let track = tracklist.choose(&mut thread_rng()).unwrap().clone();
@ -152,7 +217,10 @@ async fn stream(s: TcpStream, tracklist: Arc<Vec<PathBuf>>) {
let mut artist = String::new(); let mut artist = String::new();
let mut album = String::new(); let mut album = String::new();
let mut file = std::fs::File::open(&track).unwrap(); let mut file = std::fs::File::open(&track).unwrap();
let tagged = lofty::read_from(&mut file).unwrap(); let tagged = match lofty::read_from(&mut file) {
Ok(f) => f,
_ => continue,
};
if let Some(id3v2) = tagged.primary_tag() { if let Some(id3v2) = tagged.primary_tag() {
title = title =
id3v2.title().unwrap_or(track.file_stem().unwrap().to_string_lossy()).to_string(); id3v2.title().unwrap_or(track.file_stem().unwrap().to_string_lossy()).to_string();
@ -194,6 +262,7 @@ async fn stream(s: TcpStream, tracklist: Arc<Vec<PathBuf>>) {
TrackMetadata { TrackMetadata {
track_length_frac: time.frac as f32, track_length_frac: time.frac as f32,
track_length_secs: time.seconds, track_length_secs: time.seconds,
flac: args.flac,
album, album,
artist, artist,
title, title,