mirror of
https://github.com/ivabus/lonelyradio
synced 2024-11-22 08:05:10 +03:00
0.4.0: XOR encryption + memory optimization
- Optional XOR encryption is now available in lonelyradio and monolib - lonelyradio was optimized to use less TCP calls and less memory copies - New program: monoloader - music downloader for lonelyradio - New monolib function - get_track() for downloading track (as samples), not playing - lonelyradio and monolib are using extensible Writer and Reader enums to provide ability to use lonelyradio with different transport protocols or encryption - Add lonelyradio_types crate to share types Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
This commit is contained in:
parent
05b65e25ed
commit
39cc35e16d
16 changed files with 1074 additions and 343 deletions
818
Cargo.lock
generated
818
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
12
Cargo.toml
12
Cargo.toml
|
@ -1,10 +1,16 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["monoclient", "monolib"]
|
members = [
|
||||||
|
"lonelyradio_types",
|
||||||
|
"monoclient",
|
||||||
|
"monolib",
|
||||||
|
"monoloader",
|
||||||
|
"platform/gtk",
|
||||||
|
]
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lonelyradio"
|
name = "lonelyradio"
|
||||||
description = "TCP radio for lonely ones"
|
description = "TCP radio for lonely ones"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
||||||
|
@ -36,6 +42,8 @@ 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" }
|
||||||
|
once_cell = "1.19.0"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
|
@ -21,11 +21,13 @@ cargo build -r
|
||||||
## Run
|
## Run
|
||||||
|
|
||||||
```
|
```
|
||||||
lonelyradio [-a <ADDRESS:PORT>] <MUSIC_FOLDER> [-p] [-w]
|
lonelyradio <MUSIC_FOLDER> [-a <ADDRESS:PORT>] [-p] [-w] [-m|--max-samplerate M]
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
### Clients
|
### Clients
|
||||||
|
|
||||||
[monoclient](./monoclient) is a recommended CLI client for lonelyradio that uses [monolib](./monolib)
|
[monoclient](./monoclient) is a recommended CLI client for lonelyradio that uses [monolib](./monolib)
|
||||||
|
|
9
lonelyradio_types/Cargo.toml
Normal file
9
lonelyradio_types/Cargo.toml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
[package]
|
||||||
|
name = "lonelyradio_types"
|
||||||
|
version = "0.4.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0.197", features = ["derive"] }
|
29
lonelyradio_types/src/lib.rs
Normal file
29
lonelyradio_types/src/lib.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
|
pub enum Message {
|
||||||
|
T(TrackMetadata),
|
||||||
|
F(FragmentMetadata),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
|
pub struct TrackMetadata {
|
||||||
|
pub track_length_secs: u64,
|
||||||
|
pub track_length_frac: f32,
|
||||||
|
pub channels: u16,
|
||||||
|
pub sample_rate: u32,
|
||||||
|
pub title: String,
|
||||||
|
pub album: String,
|
||||||
|
pub artist: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
|
pub struct FragmentMetadata {
|
||||||
|
// In samples
|
||||||
|
pub length: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
||||||
|
pub struct SessionSettings {
|
||||||
|
pub gzip: bool,
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "monoclient"
|
name = "monoclient"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ use crossterm::cursor::MoveToColumn;
|
||||||
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, IsTerminal};
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
@ -13,12 +14,20 @@ struct Args {
|
||||||
/// Do not use backspace control char
|
/// Do not use backspace control char
|
||||||
#[arg(short)]
|
#[arg(short)]
|
||||||
no_backspace: bool,
|
no_backspace: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
xor_key_file: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut args = Args::parse();
|
let mut args = Args::parse();
|
||||||
args.no_backspace |= !std::io::stdout().is_terminal();
|
args.no_backspace |= !std::io::stdout().is_terminal();
|
||||||
std::thread::spawn(move || monolib::run(&args.address));
|
std::thread::spawn(move || {
|
||||||
|
monolib::run(
|
||||||
|
&args.address,
|
||||||
|
args.xor_key_file.map(|key| std::fs::read(key).expect("Failed to read preshared key")),
|
||||||
|
)
|
||||||
|
});
|
||||||
while monolib::get_metadata().is_none() {}
|
while monolib::get_metadata().is_none() {}
|
||||||
let mut md = monolib::get_metadata().unwrap();
|
let mut md = monolib::get_metadata().unwrap();
|
||||||
let mut track_start = Instant::now();
|
let mut track_start = Instant::now();
|
||||||
|
@ -35,9 +44,13 @@ fn main() {
|
||||||
))
|
))
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let mut track_length = md.track_length_secs as f64 + md.track_length_frac as f64;
|
||||||
|
let mut next_md = md.clone();
|
||||||
loop {
|
loop {
|
||||||
if monolib::get_metadata().unwrap() != md {
|
if monolib::get_metadata().unwrap() != md
|
||||||
md = monolib::get_metadata().unwrap();
|
&& track_length <= (Instant::now() - track_start).as_secs_f64()
|
||||||
|
{
|
||||||
|
md = next_md.clone();
|
||||||
crossterm::execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap();
|
crossterm::execute!(stdout(), Clear(ClearType::CurrentLine), MoveToColumn(0)).unwrap();
|
||||||
print!(
|
print!(
|
||||||
"Playing: {} - {} - {} (0:00 / {}:{:02})",
|
"Playing: {} - {} - {} (0:00 / {}:{:02})",
|
||||||
|
@ -49,6 +62,9 @@ fn main() {
|
||||||
);
|
);
|
||||||
track_start = Instant::now();
|
track_start = Instant::now();
|
||||||
seconds_past = 0;
|
seconds_past = 0;
|
||||||
|
track_length = md.track_length_secs as f64 + md.track_length_frac as f64
|
||||||
|
} else if next_md == md {
|
||||||
|
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 && !args.no_backspace {
|
||||||
seconds_past = (Instant::now() - track_start).as_secs();
|
seconds_past = (Instant::now() - track_start).as_secs();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "monolib"
|
name = "monolib"
|
||||||
version = "0.3.0"
|
version = "0.4.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,4 @@ 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"
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
lonelyradio_types = { path = "../lonelyradio_types" }
|
||||||
|
|
|
@ -7,10 +7,13 @@ use std::ffi::{CStr, CString};
|
||||||
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||||
pub extern "C" fn c_start(server: *const c_char) {
|
pub extern "C" fn c_start(server: *const c_char) {
|
||||||
let serv = unsafe { CStr::from_ptr(server) };
|
let serv = unsafe { CStr::from_ptr(server) };
|
||||||
run(match serv.to_str() {
|
run(
|
||||||
|
match serv.to_str() {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
_ => "",
|
_ => "",
|
||||||
})
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
@ -69,7 +72,7 @@ pub extern "C" fn c_get_metadata_title() -> *mut c_char {
|
||||||
pub extern "C" fn c_get_metadata_length() -> *mut c_float {
|
pub extern "C" fn c_get_metadata_length() -> *mut c_float {
|
||||||
let md = MD.read().unwrap();
|
let md = MD.read().unwrap();
|
||||||
match md.as_ref() {
|
match md.as_ref() {
|
||||||
Some(md) => &mut (md.length as c_float / md.sample_rate as c_float),
|
Some(md) => &mut (md.track_length_secs as c_float + md.track_length_frac as c_float),
|
||||||
None => &mut 0.0,
|
None => &mut 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,20 +16,21 @@
|
||||||
|
|
||||||
/// Functions, providing C-like API
|
/// Functions, providing C-like API
|
||||||
pub mod c;
|
pub mod c;
|
||||||
|
mod reader;
|
||||||
|
|
||||||
use byteorder::ByteOrder;
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
|
use lonelyradio_types::{Message, TrackMetadata};
|
||||||
use rodio::buffer::SamplesBuffer;
|
use rodio::buffer::SamplesBuffer;
|
||||||
use rodio::{OutputStream, Sink};
|
use rodio::{OutputStream, Sink};
|
||||||
use serde::Deserialize;
|
use std::io::BufReader;
|
||||||
use std::io::Read;
|
|
||||||
use std::net::TcpStream;
|
use std::net::TcpStream;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
const CACHE_SIZE: usize = 500;
|
const CACHE_SIZE: usize = 32;
|
||||||
|
|
||||||
static SINK: RwLock<Option<Sink>> = RwLock::new(None);
|
static SINK: RwLock<Option<Sink>> = RwLock::new(None);
|
||||||
static MD: RwLock<Option<Metadata>> = 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);
|
||||||
|
|
||||||
/// Player state
|
/// Player state
|
||||||
|
@ -42,22 +43,6 @@ pub enum State {
|
||||||
Paused = 3,
|
Paused = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Track metadata
|
|
||||||
#[derive(Deserialize, Clone, Debug, PartialEq)]
|
|
||||||
pub struct Metadata {
|
|
||||||
/// Fragment length
|
|
||||||
pub length: u64,
|
|
||||||
/// Total track length
|
|
||||||
pub track_length_secs: u64,
|
|
||||||
pub track_length_frac: f32,
|
|
||||||
pub channels: u16,
|
|
||||||
// Yep, no more interpolation
|
|
||||||
pub sample_rate: u32,
|
|
||||||
pub title: String,
|
|
||||||
pub album: String,
|
|
||||||
pub artist: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Play/pauses playback
|
/// Play/pauses playback
|
||||||
pub fn toggle() {
|
pub fn toggle() {
|
||||||
let mut state = crate::STATE.write().unwrap();
|
let mut state = crate::STATE.write().unwrap();
|
||||||
|
@ -81,6 +66,9 @@ pub fn toggle() {
|
||||||
/// Stops playback
|
/// Stops playback
|
||||||
pub fn stop() {
|
pub fn stop() {
|
||||||
let mut state = STATE.write().unwrap();
|
let mut state = STATE.write().unwrap();
|
||||||
|
if *state == State::NotStarted {
|
||||||
|
return;
|
||||||
|
}
|
||||||
*state = State::Resetting;
|
*state = State::Resetting;
|
||||||
|
|
||||||
let sink = SINK.read().unwrap();
|
let sink = SINK.read().unwrap();
|
||||||
|
@ -99,7 +87,7 @@ pub fn get_state() -> State {
|
||||||
*STATE.read().unwrap()
|
*STATE.read().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_metadata() -> Option<Metadata> {
|
pub fn get_metadata() -> Option<TrackMetadata> {
|
||||||
MD.read().unwrap().clone()
|
MD.read().unwrap().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,8 +118,37 @@ fn watching_sleep(dur: f32) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Download track as samples
|
||||||
|
pub fn get_track(server: &str, xor_key: Option<Vec<u8>>) -> Option<(TrackMetadata, Vec<i16>)> {
|
||||||
|
let mut stream = BufReader::new(match xor_key {
|
||||||
|
Some(k) => reader::Reader::XorEncrypted(TcpStream::connect(server).unwrap(), k, 0),
|
||||||
|
None => reader::Reader::Unencrypted(TcpStream::connect(server).unwrap()),
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut samples = vec![];
|
||||||
|
let mut md: Option<TrackMetadata> = None;
|
||||||
|
loop {
|
||||||
|
let recv_md: Message = rmp_serde::from_read(&mut stream).expect("Failed to parse message");
|
||||||
|
match recv_md {
|
||||||
|
Message::T(tmd) => {
|
||||||
|
if md.is_some() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
md = Some(tmd);
|
||||||
|
}
|
||||||
|
Message::F(fmd) => {
|
||||||
|
let mut buf = vec![0; fmd.length as usize];
|
||||||
|
stream.read_i16_into::<LittleEndian>(&mut buf).unwrap();
|
||||||
|
samples.append(&mut buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
md.map(|md| (md, samples))
|
||||||
|
}
|
||||||
|
|
||||||
/// Starts playing at "server:port"
|
/// Starts playing at "server:port"
|
||||||
pub fn run(server: &str) {
|
pub fn run(server: &str, xor_key: Option<Vec<u8>>) {
|
||||||
let mut state = STATE.write().unwrap();
|
let mut state = STATE.write().unwrap();
|
||||||
if *state == State::Playing || *state == State::Paused {
|
if *state == State::Playing || *state == State::Paused {
|
||||||
return;
|
return;
|
||||||
|
@ -139,7 +156,11 @@ pub fn run(server: &str) {
|
||||||
*state = State::Playing;
|
*state = State::Playing;
|
||||||
drop(state);
|
drop(state);
|
||||||
|
|
||||||
let mut stream = TcpStream::connect(server).unwrap();
|
let mut stream = BufReader::new(match xor_key {
|
||||||
|
Some(k) => reader::Reader::XorEncrypted(TcpStream::connect(server).unwrap(), k, 0),
|
||||||
|
None => reader::Reader::Unencrypted(TcpStream::connect(server).unwrap()),
|
||||||
|
});
|
||||||
|
|
||||||
let mut sink = SINK.write().unwrap();
|
let mut sink = SINK.write().unwrap();
|
||||||
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
|
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
|
||||||
|
|
||||||
|
@ -148,16 +169,15 @@ pub fn run(server: &str) {
|
||||||
*sink = Some(audio_sink);
|
*sink = Some(audio_sink);
|
||||||
drop(sink);
|
drop(sink);
|
||||||
|
|
||||||
let mut buffer = [0u8; 2];
|
|
||||||
let mut samples = Vec::with_capacity(8192);
|
let mut samples = Vec::with_capacity(8192);
|
||||||
loop {
|
loop {
|
||||||
let recv_md: Metadata =
|
let recv_md: Message = rmp_serde::from_read(&mut stream).expect("Failed to parse message");
|
||||||
rmp_serde::from_read(&stream).expect("Failed to parse track metadata");
|
match recv_md {
|
||||||
|
Message::T(tmd) => {
|
||||||
let mut md = MD.write().unwrap();
|
let mut md = MD.write().unwrap();
|
||||||
*md = Some(recv_md.clone());
|
*md = Some(tmd.clone());
|
||||||
drop(md);
|
}
|
||||||
for _ in 0..recv_md.length {
|
Message::F(fmd) => {
|
||||||
while *STATE.read().unwrap() == State::Paused {
|
while *STATE.read().unwrap() == State::Paused {
|
||||||
std::thread::sleep(std::time::Duration::from_secs_f32(0.25))
|
std::thread::sleep(std::time::Duration::from_secs_f32(0.25))
|
||||||
}
|
}
|
||||||
|
@ -165,17 +185,21 @@ pub fn run(server: &str) {
|
||||||
_stop();
|
_stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let mut samples_i16 = vec![0; fmd.length as usize];
|
||||||
if stream.read_exact(&mut buffer).is_err() {
|
if stream.read_i16_into::<LittleEndian>(&mut samples_i16).is_err() {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
samples.push(byteorder::LittleEndian::read_i16(&buffer[..2]) as f32 / 32768.0);
|
samples.append(
|
||||||
}
|
&mut samples_i16.iter().map(|sample| *sample as f32 / 32767.0).collect(),
|
||||||
|
);
|
||||||
|
|
||||||
// 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.as_ref().unwrap();
|
||||||
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
|
||||||
|
@ -184,20 +208,21 @@ pub fn run(server: &str) {
|
||||||
sink.len() as f32 - 2.0
|
sink.len() as f32 - 2.0
|
||||||
} else {
|
} else {
|
||||||
0.25
|
0.25
|
||||||
} * recv_md.length as f32
|
} * fmd.length as f32 / md.sample_rate as f32
|
||||||
/ recv_md.sample_rate as f32
|
/ 4.0,
|
||||||
/ 2.0,
|
|
||||||
) {
|
) {
|
||||||
_stop();
|
_stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sink.append(SamplesBuffer::new(
|
sink.append(SamplesBuffer::new(
|
||||||
recv_md.channels,
|
md.channels,
|
||||||
recv_md.sample_rate,
|
md.sample_rate,
|
||||||
samples.as_slice(),
|
samples.as_slice(),
|
||||||
));
|
));
|
||||||
samples.clear();
|
samples.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
25
monolib/src/reader.rs
Normal file
25
monolib/src/reader.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
use std::{io, net::TcpStream};
|
||||||
|
|
||||||
|
pub(crate) enum Reader {
|
||||||
|
Unencrypted(TcpStream),
|
||||||
|
XorEncrypted(TcpStream, Vec<u8>, u64),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl io::Read for Reader {
|
||||||
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
match self {
|
||||||
|
Self::Unencrypted(s) => s.read(buf),
|
||||||
|
Self::XorEncrypted(s, key, n) => {
|
||||||
|
let out = s.read(buf);
|
||||||
|
if let Ok(i) = &out {
|
||||||
|
for k in buf.iter_mut().take(*i) {
|
||||||
|
*k ^= key[*n as usize];
|
||||||
|
*n += 1;
|
||||||
|
*n %= key.len() as u64;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
monoloader/Cargo.toml
Normal file
11
monoloader/Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "monoloader"
|
||||||
|
version = "0.4.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
monolib = { path = "../monolib" }
|
||||||
|
clap = { version = "4.4.18", features = ["derive"] }
|
||||||
|
hound = "3.5.1"
|
38
monoloader/src/main.rs
Normal file
38
monoloader/src/main.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
use clap::Parser;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
struct Args {
|
||||||
|
/// Remote address
|
||||||
|
address: String,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
xor_key_file: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
let (md, samples) = monolib::get_track(
|
||||||
|
&args.address,
|
||||||
|
args.xor_key_file.map(|key| std::fs::read(key).expect("Failed to read preshared key")),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
println!(
|
||||||
|
"Downloaded: {} - {} - {} ({} MB)",
|
||||||
|
md.artist,
|
||||||
|
md.album,
|
||||||
|
md.title,
|
||||||
|
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: 16,
|
||||||
|
sample_format: hound::SampleFormat::Int,
|
||||||
|
};
|
||||||
|
let mut writer =
|
||||||
|
hound::WavWriter::create(format!("{} - {}.wav", md.artist, md.title), spec).unwrap();
|
||||||
|
let mut writer_i16 = writer.get_i16_writer(samples.len() as u32);
|
||||||
|
samples.iter().for_each(|s| writer_i16.write_sample(*s));
|
||||||
|
writer_i16.flush().unwrap();
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use async_stream::stream;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use futures_util::Stream;
|
||||||
use symphonia::core::audio::SampleBuffer;
|
use symphonia::core::audio::SampleBuffer;
|
||||||
use symphonia::core::codecs::CODEC_TYPE_NULL;
|
use symphonia::core::codecs::CODEC_TYPE_NULL;
|
||||||
use symphonia::core::io::MediaSourceStream;
|
use symphonia::core::io::MediaSourceStream;
|
||||||
|
@ -75,7 +77,7 @@ pub async fn get_meta(file_path: &Path) -> (u16, u32, Time) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Getting samples
|
/// Getting samples
|
||||||
pub async fn decode_file(file_path: PathBuf, tx: tokio::sync::mpsc::Sender<Vec<i16>>) {
|
pub fn decode_file_stream(file_path: PathBuf) -> impl Stream<Item = Vec<i16>> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
let file = Box::new(std::fs::File::open(&file_path).unwrap());
|
let file = Box::new(std::fs::File::open(&file_path).unwrap());
|
||||||
let mut hint = Hint::new();
|
let mut hint = Hint::new();
|
||||||
|
@ -102,6 +104,7 @@ pub async fn decode_file(file_path: PathBuf, tx: tokio::sync::mpsc::Sender<Vec<i
|
||||||
.make(&track.codec_params, &Default::default())
|
.make(&track.codec_params, &Default::default())
|
||||||
.expect("unsupported codec");
|
.expect("unsupported codec");
|
||||||
let track_id = track.id;
|
let track_id = track.id;
|
||||||
|
stream! {
|
||||||
loop {
|
loop {
|
||||||
let packet = match format.next_packet() {
|
let packet = match format.next_packet() {
|
||||||
Ok(packet) => packet,
|
Ok(packet) => packet,
|
||||||
|
@ -120,8 +123,9 @@ pub async fn decode_file(file_path: PathBuf, tx: tokio::sync::mpsc::Sender<Vec<i
|
||||||
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);
|
||||||
|
|
||||||
tx.send(
|
// About Samplerate struct:
|
||||||
samplerate::convert(
|
// We are downsampling, not upsampling, so we should be fine
|
||||||
|
yield (samplerate::convert(
|
||||||
spec.rate,
|
spec.rate,
|
||||||
args.max_samplerate,
|
args.max_samplerate,
|
||||||
spec.channels.count(),
|
spec.channels.count(),
|
||||||
|
@ -130,16 +134,14 @@ pub async fn decode_file(file_path: PathBuf, tx: tokio::sync::mpsc::Sender<Vec<i
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| (*x * 32767.0) as i16)
|
.map(|x| (*x * 32768.0) as i16)
|
||||||
.collect(),
|
.collect());
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
} else {
|
} else {
|
||||||
let mut byte_buf =
|
let mut byte_buf =
|
||||||
SampleBuffer::<i16>::new(decoded.capacity() as u64, *decoded.spec());
|
SampleBuffer::<i16>::new(decoded.capacity() as u64, *decoded.spec());
|
||||||
byte_buf.copy_interleaved_ref(decoded);
|
byte_buf.copy_interleaved_ref(decoded);
|
||||||
tx.send(byte_buf.samples().to_vec()).await.unwrap();
|
yield (byte_buf.samples().to_vec());
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -150,3 +152,4 @@ pub async fn decode_file(file_path: PathBuf, tx: tokio::sync::mpsc::Sender<Vec<i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
121
src/main.rs
121
src/main.rs
|
@ -1,4 +1,5 @@
|
||||||
mod decode;
|
mod decode;
|
||||||
|
mod writer;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
@ -9,16 +10,17 @@ use clap::Parser;
|
||||||
use futures_util::pin_mut;
|
use futures_util::pin_mut;
|
||||||
use lofty::Accessor;
|
use lofty::Accessor;
|
||||||
use lofty::TaggedFileExt;
|
use lofty::TaggedFileExt;
|
||||||
|
use lonelyradio_types::{FragmentMetadata, Message, TrackMetadata};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use serde::Serialize;
|
use std::io::Write;
|
||||||
use tokio::io::AsyncWriteExt;
|
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
use tokio::sync::mpsc;
|
|
||||||
use tokio_stream::Stream;
|
use tokio_stream::Stream;
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
use walkdir::DirEntry;
|
use walkdir::DirEntry;
|
||||||
|
use writer::Writer;
|
||||||
|
|
||||||
use crate::decode::decode_file;
|
use crate::decode::decode_file_stream;
|
||||||
use crate::decode::get_meta;
|
use crate::decode::get_meta;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
@ -35,50 +37,58 @@ struct Args {
|
||||||
|
|
||||||
#[arg(short, long, default_value = "96000")]
|
#[arg(short, long, default_value = "96000")]
|
||||||
max_samplerate: u32,
|
max_samplerate: u32,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
xor_key_file: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
static KEY: Lazy<Option<Arc<Vec<u8>>>> = Lazy::new(|| {
|
||||||
struct SentMetadata {
|
let args = Args::parse();
|
||||||
/// Fragment length
|
if let Some(path) = args.xor_key_file {
|
||||||
length: u64,
|
let key = std::fs::read(path).expect("Failed to read preshared key");
|
||||||
/// Total track length
|
Some(Arc::new(key))
|
||||||
track_length_secs: u64,
|
} else {
|
||||||
track_length_frac: f32,
|
None
|
||||||
channels: u16,
|
|
||||||
sample_rate: u32,
|
|
||||||
title: String,
|
|
||||||
album: String,
|
|
||||||
artist: String,
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
async fn stream_samples(
|
async fn stream_track(
|
||||||
samples_stream: impl Stream<Item = Vec<i16>>,
|
samples_stream: impl Stream<Item = Vec<i16>>,
|
||||||
war: bool,
|
war: bool,
|
||||||
md: SentMetadata,
|
md: TrackMetadata,
|
||||||
s: &mut TcpStream,
|
s: &mut Writer,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
pin_mut!(samples_stream);
|
pin_mut!(samples_stream);
|
||||||
|
|
||||||
while let Some(samples) = samples_stream.next().await {
|
if s.write_all(rmp_serde::to_vec(&Message::T(md)).unwrap().as_slice()).is_err() {
|
||||||
let mut md = md.clone();
|
|
||||||
md.length = samples.len() as u64;
|
|
||||||
if s.write_all(rmp_serde::to_vec(&md).unwrap().as_slice()).await.is_err() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
for sample in samples {
|
|
||||||
if s.write_all(
|
|
||||||
&(if war {
|
|
||||||
sample.signum() * 32767
|
|
||||||
} else {
|
|
||||||
sample
|
|
||||||
}
|
|
||||||
.to_le_bytes()),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.is_err()
|
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
while let Some(mut _samples) = samples_stream.next().await {
|
||||||
|
let md = Message::F(FragmentMetadata {
|
||||||
|
length: _samples.len() as u64,
|
||||||
|
});
|
||||||
|
if s.write_all(rmp_serde::to_vec(&md).unwrap().as_slice()).is_err() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if war {
|
||||||
|
_samples.iter_mut().for_each(|sample| {
|
||||||
|
*sample = sample.signum() * 32767;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Launching lonelyradio on the router moment
|
||||||
|
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>() };
|
||||||
|
|
||||||
|
if s.write_all(samples).is_err() {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
false
|
false
|
||||||
|
@ -117,10 +127,24 @@ fn track_valid(track: &Path) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn stream(mut s: TcpStream, tracklist: Arc<Vec<PathBuf>>) {
|
async fn stream(s: TcpStream, tracklist: Arc<Vec<PathBuf>>) {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
s.set_nodelay(true).unwrap();
|
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();
|
||||||
|
|
||||||
|
@ -163,23 +187,16 @@ async fn stream(mut s: TcpStream, tracklist: Arc<Vec<PathBuf>>) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let (channels, sample_rate, time) = get_meta(track.as_path()).await;
|
let (channels, sample_rate, time) = get_meta(track.as_path()).await;
|
||||||
let (tx, mut rx) = mpsc::channel::<Vec<i16>>(8192);
|
let stream = decode_file_stream(track);
|
||||||
tokio::spawn(decode_file(track, tx));
|
if stream_track(
|
||||||
let stream = async_stream::stream! {
|
|
||||||
while let Some(item) = rx.recv().await {
|
|
||||||
yield item;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if stream_samples(
|
|
||||||
stream,
|
stream,
|
||||||
args.war,
|
args.war,
|
||||||
SentMetadata {
|
TrackMetadata {
|
||||||
|
track_length_frac: time.frac as f32,
|
||||||
|
track_length_secs: time.seconds,
|
||||||
album,
|
album,
|
||||||
artist,
|
artist,
|
||||||
title,
|
title,
|
||||||
length: 0,
|
|
||||||
track_length_frac: time.frac as f32,
|
|
||||||
track_length_secs: time.seconds,
|
|
||||||
sample_rate,
|
sample_rate,
|
||||||
channels,
|
channels,
|
||||||
},
|
},
|
||||||
|
|
59
src/writer.rs
Normal file
59
src/writer.rs
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
use std::{
|
||||||
|
borrow::BorrowMut,
|
||||||
|
io,
|
||||||
|
net::{SocketAddr, TcpStream},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) enum Writer {
|
||||||
|
Unencrypted(TcpStream),
|
||||||
|
XorEncrypted(TcpStream, Arc<Vec<u8>>, u64),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl io::Write for Writer {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
match self {
|
||||||
|
Self::Unencrypted(s) => s.write(buf),
|
||||||
|
Self::XorEncrypted(s, key, n) => {
|
||||||
|
for mut k in buf.iter().copied() {
|
||||||
|
k ^= key[*n as usize];
|
||||||
|
*n += 1;
|
||||||
|
*n %= key.len() as u64;
|
||||||
|
s.write_all(&[k])?;
|
||||||
|
}
|
||||||
|
Ok(buf.len())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Unencrypted(s) => s.write_all(buf),
|
||||||
|
Self::XorEncrypted(s, key, n) => s.write_all(
|
||||||
|
&buf.iter()
|
||||||
|
.borrow_mut()
|
||||||
|
.copied()
|
||||||
|
.map(|mut k| {
|
||||||
|
k ^= key[*n as usize];
|
||||||
|
*n += 1;
|
||||||
|
*n %= key.len() as u64;
|
||||||
|
k
|
||||||
|
})
|
||||||
|
// I don't like it
|
||||||
|
.collect::<Vec<u8>>(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::XorEncrypted(s, _, _) | Self::Unencrypted(s) => s.flush(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Writer {
|
||||||
|
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||||
|
match self {
|
||||||
|
Self::XorEncrypted(s, _, _) | Self::Unencrypted(s) => s.peer_addr(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue