mirror of
https://github.com/ivabus/lonelyradio
synced 2024-11-21 23:55:09 +03:00
v0.2.1: Refactor monoclient into monoclient and monolib
Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
This commit is contained in:
parent
6e3327aa1f
commit
9165c43dae
15 changed files with 428 additions and 367 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
@ -597,7 +597,7 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lonelyradio"
|
name = "lonelyradio"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
|
@ -653,18 +653,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "monoclient"
|
name = "monoclient"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
|
||||||
"clap",
|
"clap",
|
||||||
"rmp-serde",
|
"monolib",
|
||||||
"rodio",
|
|
||||||
"serde",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "monolib"
|
name = "monolib"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["monoclient", "platform/swiftui/monolib"]
|
members = [ "monoclient", "monolib"]
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lonelyradio"
|
name = "lonelyradio"
|
||||||
description = "TCP radio for lonely ones"
|
description = "TCP radio for lonely ones"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
authors = ["Ivan Bushchik <ivabus@ivabus.dev>"]
|
||||||
|
|
|
@ -22,7 +22,7 @@ All files (recursively) will be shuffled and played back. Public log will be dis
|
||||||
|
|
||||||
### Clients
|
### Clients
|
||||||
|
|
||||||
[monoclient](./monoclient) is a recommended client for lonelyradio
|
[monoclient](./monoclient) is a recommended CLI client for lonelyradio that uses [monolib](./monolib)
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
monoclient <SERVER>:<PORT>
|
monoclient <SERVER>:<PORT>
|
||||||
|
@ -32,6 +32,8 @@ monoclient <SERVER>:<PORT>
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
lonelyradio and monoclient are licensed under the terms of the [MIT license](./LICENSE).
|
lonelyradio and monoclient are licensed under the terms of the [MIT license](./LICENSE).
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
[package]
|
[package]
|
||||||
name = "monoclient"
|
name = "monoclient"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "4.4.18", features = ["derive"] }
|
monolib = { path="../monolib" }
|
||||||
rodio = { version = "0.17.3", default-features = false }
|
clap = { version = "4.4.18", features = ["derive"] }
|
||||||
byteorder = "1.5.0"
|
|
||||||
rmp-serde = "1.1.2"
|
|
||||||
serde = { version = "1.0.197", features = ["derive"] }
|
|
|
@ -1,25 +1,6 @@
|
||||||
use byteorder::ByteOrder;
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use rodio::buffer::SamplesBuffer;
|
use std::io::{IsTerminal, Write};
|
||||||
use rodio::{OutputStream, Sink};
|
use std::time::{Duration, Instant};
|
||||||
use serde::Deserialize;
|
|
||||||
use std::io::{IsTerminal, Read, Write};
|
|
||||||
use std::net::TcpStream;
|
|
||||||
|
|
||||||
// How many samples to cache before playing in samples (both channels) SHOULD BE EVEN
|
|
||||||
const BUFFER_SIZE: usize = 4800;
|
|
||||||
// How many buffers to cache
|
|
||||||
const CACHE_SIZE: usize = 10;
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct SentMetadata {
|
|
||||||
// In bytes, we need to read next track metadata
|
|
||||||
lenght: u64,
|
|
||||||
sample_rate: u32,
|
|
||||||
title: String,
|
|
||||||
album: String,
|
|
||||||
artist: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
struct Args {
|
struct Args {
|
||||||
|
@ -31,84 +12,98 @@ struct Args {
|
||||||
no_backspace: bool,
|
no_backspace: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_chars(n: usize) {
|
fn delete_chars(n: usize, nb: bool) {
|
||||||
print!("{}{}{}", "\u{8}".repeat(n), " ".repeat(n), "\u{8}".repeat(n));
|
if !nb {
|
||||||
std::io::stdout().flush().expect("Failed to flush stdout")
|
print!("{}{}{}", "\u{8}".repeat(n), " ".repeat(n), "\u{8}".repeat(n));
|
||||||
|
std::io::stdout().flush().expect("Failed to flush stdout")
|
||||||
|
} else {
|
||||||
|
println!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush() {
|
||||||
|
std::io::stdout().flush().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
let mut stream = TcpStream::connect(&args.address)
|
std::thread::spawn(move || monolib::run(&args.address));
|
||||||
.unwrap_or_else(|err| panic!("Failed to connect to {}: {}", args.address, err.to_string()));
|
while monolib::get_metadata().is_none() {}
|
||||||
println!("Connected to {} from {}", stream.peer_addr().unwrap(), stream.local_addr().unwrap());
|
let mut md = monolib::get_metadata().unwrap();
|
||||||
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
|
let seconds = md.length / md.sample_rate as u64 / 2;
|
||||||
let sink = Sink::try_new(&stream_handle).unwrap();
|
let mut track_start = Instant::now();
|
||||||
let mut buffer = [0u8; 2];
|
let mut seconds_past = 0;
|
||||||
let mut samples = [0f32; BUFFER_SIZE];
|
let mut msg_len = format!(
|
||||||
let mut latest_msg_len = 0;
|
"Playing: {} - {} - {} ({}:{:02})",
|
||||||
print!("Playing: ");
|
md.artist,
|
||||||
|
md.album,
|
||||||
|
md.title,
|
||||||
|
seconds / 60,
|
||||||
|
seconds % 60
|
||||||
|
)
|
||||||
|
.len();
|
||||||
|
print!(
|
||||||
|
"Playing: {} - {} - {} ({}:{:02})",
|
||||||
|
md.artist,
|
||||||
|
md.album,
|
||||||
|
md.title,
|
||||||
|
seconds / 60,
|
||||||
|
seconds % 60
|
||||||
|
);
|
||||||
|
flush();
|
||||||
loop {
|
loop {
|
||||||
let mut index = 0usize;
|
if monolib::get_metadata().unwrap() != md {
|
||||||
|
md = monolib::get_metadata().unwrap();
|
||||||
let md: SentMetadata =
|
let seconds = md.length / md.sample_rate as u64 / 2;
|
||||||
rmp_serde::from_read(&stream).expect("Failed to parse track metadata");
|
delete_chars(msg_len, args.no_backspace);
|
||||||
let seconds = md.lenght / (2 * md.sample_rate as u64);
|
msg_len = format!(
|
||||||
let total_lenght = format!("{}:{:02}", seconds / 60, seconds % 60);
|
"Playing: {} - {} - {} ({}:{:02})",
|
||||||
let message = format!("{} - {} - {} ", md.artist, md.album, md.title);
|
md.artist,
|
||||||
if latest_msg_len != 0 {
|
md.album,
|
||||||
if args.no_backspace {
|
md.title,
|
||||||
print!("\nPlaying: ");
|
seconds / 60,
|
||||||
} else {
|
seconds % 60
|
||||||
delete_chars(latest_msg_len)
|
)
|
||||||
}
|
.len();
|
||||||
|
print!(
|
||||||
|
"Playing: {} - {} - {} (0:00 / {}:{:02})",
|
||||||
|
md.artist,
|
||||||
|
md.album,
|
||||||
|
md.title,
|
||||||
|
seconds / 60,
|
||||||
|
seconds % 60
|
||||||
|
);
|
||||||
|
flush();
|
||||||
|
track_start = Instant::now();
|
||||||
|
seconds_past = 0;
|
||||||
}
|
}
|
||||||
print!("{}", message);
|
if (Instant::now() - track_start).as_secs() > seconds_past && !args.no_backspace {
|
||||||
let mut prev_timestamp_len = 0;
|
seconds_past = (Instant::now() - track_start).as_secs();
|
||||||
if args.no_backspace {
|
msg_len = format!(
|
||||||
print!("({})", &total_lenght)
|
"Playing: {} - {} - {} ({}:{:02} / {}:{:02})",
|
||||||
} else {
|
md.artist,
|
||||||
print!("(0:00 / {})", &total_lenght);
|
md.album,
|
||||||
// (0:00/ + :00 + minutes len
|
md.title,
|
||||||
prev_timestamp_len = 12 + format!("{}", seconds / 60).len();
|
seconds_past / 60,
|
||||||
|
seconds_past % 60,
|
||||||
|
seconds / 60,
|
||||||
|
seconds % 60
|
||||||
|
)
|
||||||
|
.len();
|
||||||
|
delete_chars(msg_len, args.no_backspace);
|
||||||
|
print!(
|
||||||
|
"Playing: {} - {} - {} ({}:{:02} / {}:{:02})",
|
||||||
|
md.artist,
|
||||||
|
md.album,
|
||||||
|
md.title,
|
||||||
|
seconds_past / 60,
|
||||||
|
seconds_past % 60,
|
||||||
|
seconds / 60,
|
||||||
|
seconds % 60
|
||||||
|
);
|
||||||
|
flush();
|
||||||
}
|
}
|
||||||
std::io::stdout().flush().expect("Failed to flush stdout");
|
std::thread::sleep(Duration::from_secs_f32(0.05))
|
||||||
latest_msg_len = message.chars().count();
|
|
||||||
let mut second = 0;
|
|
||||||
for sample_index in 0..md.lenght {
|
|
||||||
if (sample_index / (md.sample_rate as u64 * 2)) > second {
|
|
||||||
second += 1;
|
|
||||||
if !args.no_backspace {
|
|
||||||
delete_chars(prev_timestamp_len);
|
|
||||||
let current_timestamp =
|
|
||||||
format!("({}:{:02} / {})", second / 60, second % 60, &total_lenght);
|
|
||||||
print!("{}", ¤t_timestamp);
|
|
||||||
std::io::stdout().flush().expect("Failed to flush stdout");
|
|
||||||
prev_timestamp_len = current_timestamp.len()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if stream.read_exact(&mut buffer).is_err() {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
samples[index] = byteorder::LittleEndian::read_i16(&buffer[..2]) as f32 / 32768.0;
|
|
||||||
index += 1;
|
|
||||||
|
|
||||||
if index == BUFFER_SIZE {
|
|
||||||
// Sink's thread is detached from main thread, so we need to synchronize with it
|
|
||||||
// Why we should synchronize with it?
|
|
||||||
// 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
|
|
||||||
while sink.len() >= CACHE_SIZE {
|
|
||||||
// Sleeping exactly one buffer
|
|
||||||
std::thread::sleep(std::time::Duration::from_secs_f32(
|
|
||||||
BUFFER_SIZE as f32 / md.sample_rate as f32 / 2.0,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
sink.append(SamplesBuffer::new(2, md.sample_rate, samples.as_slice()));
|
|
||||||
index = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sink.sleep_until_end()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
[package]
|
[package]
|
||||||
name = "monolib"
|
name = "monolib"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
description = "A library implementing the lonely radio audio streaming protocol"
|
||||||
|
repository = "https://github.com/ivabus/lonelyradio"
|
||||||
|
authors = [ "Ivan Bushchik <ivabus@ivabus.dev>"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "monolib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
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"] }
|
serde = { version = "1.0.197", features = ["derive"] }
|
||||||
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "monolib"
|
|
||||||
crate-type = ["staticlib", "cdylib"]
|
|
75
monolib/src/c.rs
Normal file
75
monolib/src/c.rs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
use std::ffi::{c_char, c_float, c_ushort};
|
||||||
|
use std::ffi::{CStr, CString};
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||||
|
pub extern "C" fn c_start(server: *const c_char) {
|
||||||
|
let serv = unsafe { CStr::from_ptr(server) };
|
||||||
|
run(match serv.to_str() {
|
||||||
|
Ok(s) => s,
|
||||||
|
_ => "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn c_toggle() {
|
||||||
|
toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn c_stop() {
|
||||||
|
stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn c_get_state() -> c_ushort {
|
||||||
|
let state = STATE.read().unwrap();
|
||||||
|
*state as c_ushort
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn c_get_metadata_artist() -> *mut c_char {
|
||||||
|
let md = MD.read().unwrap();
|
||||||
|
let md = md.clone();
|
||||||
|
CString::new(match md {
|
||||||
|
Some(md) => md.artist,
|
||||||
|
None => "".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.into_raw()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn c_get_metadata_album() -> *mut c_char {
|
||||||
|
let md = MD.read().unwrap();
|
||||||
|
let md = md.clone();
|
||||||
|
CString::new(match md {
|
||||||
|
Some(md) => md.album,
|
||||||
|
None => "".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.into_raw()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn c_get_metadata_title() -> *mut c_char {
|
||||||
|
let md = MD.read().unwrap();
|
||||||
|
let md = md.clone();
|
||||||
|
CString::new(match md {
|
||||||
|
Some(md) => md.title,
|
||||||
|
None => "".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.into_raw()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn c_get_metadata_length() -> *mut c_float {
|
||||||
|
let md = MD.read().unwrap();
|
||||||
|
match md.as_ref() {
|
||||||
|
Some(md) => &mut (md.length as c_float / md.sample_rate as c_float),
|
||||||
|
None => &mut 0.0,
|
||||||
|
}
|
||||||
|
}
|
186
monolib/src/lib.rs
Normal file
186
monolib/src/lib.rs
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
/// Functions, providing C-like API
|
||||||
|
pub mod c;
|
||||||
|
|
||||||
|
use byteorder::ByteOrder;
|
||||||
|
use rodio::buffer::SamplesBuffer;
|
||||||
|
use rodio::{OutputStream, Sink};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::net::TcpStream;
|
||||||
|
use std::sync::RwLock;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
// How many samples to cache before playing in samples (both channels) SHOULD BE EVEN
|
||||||
|
const BUFFER_SIZE: usize = 4800;
|
||||||
|
// How many buffers to cache
|
||||||
|
const CACHE_SIZE: usize = 160;
|
||||||
|
|
||||||
|
static SINK: RwLock<Option<Sink>> = RwLock::new(None);
|
||||||
|
static MD: RwLock<Option<Metadata>> = RwLock::new(None);
|
||||||
|
static STATE: RwLock<State> = RwLock::new(State::NotStarted);
|
||||||
|
|
||||||
|
/// Player state
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum State {
|
||||||
|
NotStarted = 0,
|
||||||
|
Resetting = 1,
|
||||||
|
Playing = 2,
|
||||||
|
Paused = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Track metadata
|
||||||
|
#[derive(Deserialize, Clone, Debug, PartialEq)]
|
||||||
|
pub struct Metadata {
|
||||||
|
/// In samples, length / (sample_rate * 2 (channels)) = length in seconds
|
||||||
|
pub length: u64,
|
||||||
|
pub sample_rate: u32,
|
||||||
|
pub title: String,
|
||||||
|
pub album: String,
|
||||||
|
pub artist: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Play/pauses playback
|
||||||
|
pub fn toggle() {
|
||||||
|
let mut state = crate::STATE.write().unwrap();
|
||||||
|
if *state == State::Playing {
|
||||||
|
*state = State::Paused;
|
||||||
|
|
||||||
|
let sink = SINK.read().unwrap();
|
||||||
|
if let Some(sink) = sink.as_ref() {
|
||||||
|
sink.pause()
|
||||||
|
}
|
||||||
|
} else if *state == State::Paused {
|
||||||
|
*state = State::Playing;
|
||||||
|
|
||||||
|
let sink = SINK.read().unwrap();
|
||||||
|
if let Some(sink) = sink.as_ref() {
|
||||||
|
sink.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stops playback
|
||||||
|
pub fn stop() {
|
||||||
|
let mut state = STATE.write().unwrap();
|
||||||
|
*state = State::Resetting;
|
||||||
|
|
||||||
|
let sink = SINK.read().unwrap();
|
||||||
|
if let Some(sink) = sink.as_ref() {
|
||||||
|
sink.pause()
|
||||||
|
}
|
||||||
|
drop(sink);
|
||||||
|
drop(state);
|
||||||
|
// Blocking main thread
|
||||||
|
while *STATE.read().unwrap() == State::Resetting {
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs_f32(0.01))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_state() -> State {
|
||||||
|
*STATE.read().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_metadata() -> Option<Metadata> {
|
||||||
|
MD.read().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _stop() {
|
||||||
|
let sink = SINK.read().unwrap();
|
||||||
|
if let Some(sink) = sink.as_ref() {
|
||||||
|
sink.pause();
|
||||||
|
sink.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut md = MD.write().unwrap();
|
||||||
|
if md.is_some() {
|
||||||
|
*md = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
*STATE.write().unwrap() = State::NotStarted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset - true, not - false
|
||||||
|
fn watching_sleep(dur: f32) -> bool {
|
||||||
|
let start = Instant::now();
|
||||||
|
while Instant::now() < start + std::time::Duration::from_secs_f32(dur) {
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs_f32(0.0001));
|
||||||
|
if *STATE.read().unwrap() == State::Resetting {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts playing at "server:port"
|
||||||
|
pub fn run(server: &str) {
|
||||||
|
let mut state = STATE.write().unwrap();
|
||||||
|
if *state == State::Playing || *state == State::Paused {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*state = State::Playing;
|
||||||
|
drop(state);
|
||||||
|
|
||||||
|
let mut stream = TcpStream::connect(server).unwrap();
|
||||||
|
let mut sink = SINK.write().unwrap();
|
||||||
|
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
|
||||||
|
|
||||||
|
// Can't reuse old sink for some reason
|
||||||
|
let audio_sink = Sink::try_new(&stream_handle).unwrap();
|
||||||
|
*sink = Some(audio_sink);
|
||||||
|
drop(sink);
|
||||||
|
|
||||||
|
let mut buffer = [0u8; 2];
|
||||||
|
let mut samples = [0f32; BUFFER_SIZE];
|
||||||
|
loop {
|
||||||
|
let mut index = 0usize;
|
||||||
|
let recv_md: Metadata =
|
||||||
|
rmp_serde::from_read(&stream).expect("Failed to parse track metadata");
|
||||||
|
|
||||||
|
let mut md = MD.write().unwrap();
|
||||||
|
*md = Some(recv_md.clone());
|
||||||
|
drop(md);
|
||||||
|
for _ in 0..recv_md.length {
|
||||||
|
while *STATE.read().unwrap() == State::Paused {
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs_f32(0.25))
|
||||||
|
}
|
||||||
|
if *STATE.read().unwrap() == State::Resetting {
|
||||||
|
_stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if stream.read_exact(&mut buffer).is_err() {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
samples[index] = byteorder::LittleEndian::read_i16(&buffer[..2]) as f32 / 32768.0;
|
||||||
|
index += 1;
|
||||||
|
|
||||||
|
if index == BUFFER_SIZE {
|
||||||
|
// Sink's thread is detached from main thread, so we need to synchronize with it
|
||||||
|
// Why we should synchronize with it?
|
||||||
|
// 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
|
||||||
|
let sink = SINK.read().unwrap();
|
||||||
|
if let Some(sink) = sink.as_ref() {
|
||||||
|
while sink.len() >= CACHE_SIZE {
|
||||||
|
// Sleeping exactly one buffer and watching for reset signal
|
||||||
|
if watching_sleep(
|
||||||
|
if sink.len() > 2 {
|
||||||
|
sink.len() as f32 - 2.0
|
||||||
|
} else {
|
||||||
|
0.5
|
||||||
|
} * BUFFER_SIZE as f32 / recv_md.sample_rate as f32
|
||||||
|
/ 2.0,
|
||||||
|
) {
|
||||||
|
_stop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sink.append(SamplesBuffer::new(2, recv_md.sample_rate, samples.as_slice()));
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
monolib/src/monolib.h
Normal file
20
monolib/src/monolib.h
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
void c_start(const char *server);
|
||||||
|
|
||||||
|
void c_toggle(void);
|
||||||
|
|
||||||
|
void c_stop(void);
|
||||||
|
|
||||||
|
unsigned short c_get_state(void);
|
||||||
|
|
||||||
|
char *c_get_metadata_artist(void);
|
||||||
|
|
||||||
|
char *c_get_metadata_album(void);
|
||||||
|
|
||||||
|
char *c_get_metadata_title(void);
|
||||||
|
|
||||||
|
float *c_get_metadata_length(void);
|
|
@ -4,16 +4,14 @@
|
||||||
|
|
||||||
### Build `monolib`
|
### Build `monolib`
|
||||||
|
|
||||||
Run in `monolib` directory
|
|
||||||
|
|
||||||
```
|
```
|
||||||
cargo lipo --release --targets aarch64-apple-ios
|
cargo lipo --release --targets aarch64-apple-ios -p monolib
|
||||||
```
|
```
|
||||||
|
|
||||||
For running in simulator
|
For running in simulator
|
||||||
|
|
||||||
```
|
```
|
||||||
cargo lipo --release --targets aarch64-apple-ios-sim,x86_64-apple-ios
|
cargo lipo --release --targets aarch64-apple-ios-sim,x86_64-apple-ios -p monolib
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build and run app
|
### Build and run app
|
||||||
|
|
|
@ -20,14 +20,14 @@ class MonoLib {
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to set the audio session configuration")
|
print("Failed to set the audio session configuration")
|
||||||
}
|
}
|
||||||
start(server)
|
c_start(server)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
let timer = Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()
|
let timer = Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()
|
||||||
@State private var server: String = ""
|
@State private var server: String = "ivabus.dev"
|
||||||
@State private var port: String = ""
|
@State private var port: String = "5894"
|
||||||
@State private var playing: Bool = true
|
@State private var playing: Bool = true
|
||||||
@State private var running: Bool = false
|
@State private var running: Bool = false
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ struct ContentView: View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
if running {
|
if running {
|
||||||
playing = !playing
|
playing = !playing
|
||||||
toggle()
|
c_toggle()
|
||||||
}
|
}
|
||||||
running = true
|
running = true
|
||||||
let a = MonoLib()
|
let a = MonoLib()
|
||||||
|
@ -76,23 +76,35 @@ struct ContentView: View {
|
||||||
).font(.largeTitle)
|
).font(.largeTitle)
|
||||||
}.buttonStyle(
|
}.buttonStyle(
|
||||||
.borderedProminent)
|
.borderedProminent)
|
||||||
Button(action: {
|
HStack{
|
||||||
reset()
|
Button(action: {
|
||||||
running = false
|
c_stop()
|
||||||
playing = true
|
running = false
|
||||||
}) { Image(systemName: "stop").font(.title3) }.buttonStyle(
|
playing = true
|
||||||
.bordered
|
}) { Image(systemName: "stop").font(.title3) }.buttonStyle(
|
||||||
).disabled(!running)
|
.bordered
|
||||||
|
).disabled(!running)
|
||||||
|
Button(action: {
|
||||||
|
c_stop()
|
||||||
|
playing = true
|
||||||
|
let a = MonoLib()
|
||||||
|
Task.init {
|
||||||
|
await a.run(server: server + ":" + port)
|
||||||
|
}
|
||||||
|
}) {Image(systemName: "forward").font(.title3)}.buttonStyle(.bordered).disabled(!running)
|
||||||
|
}
|
||||||
}.frame(width: 300)
|
}.frame(width: 300)
|
||||||
Text(now_playing_artist).font(.title2).onReceive(timer) { _ in
|
VStack(spacing: 10) {
|
||||||
now_playing_artist = String(cString: get_metadata_artist()!)
|
Text(now_playing_artist).onReceive(timer) { _ in
|
||||||
}
|
now_playing_artist = String(cString: c_get_metadata_artist()!)
|
||||||
Text(now_playing_album).onReceive(timer) { _ in
|
}
|
||||||
now_playing_album = String(cString: get_metadata_album()!)
|
Text(now_playing_album).onReceive(timer) { _ in
|
||||||
}
|
now_playing_album = String(cString: c_get_metadata_album()!)
|
||||||
Text(now_playing_title).font(.title).bold().onReceive(timer) { _ in
|
}
|
||||||
now_playing_title = String(cString: get_metadata_title()!)
|
Text(now_playing_title).onReceive(timer) { _ in
|
||||||
}
|
now_playing_title = String(cString: c_get_metadata_title()!)
|
||||||
|
}.bold()
|
||||||
|
}.frame(minHeight: 100)
|
||||||
|
|
||||||
}.padding()
|
}.padding()
|
||||||
|
|
||||||
|
|
|
@ -1,212 +0,0 @@
|
||||||
use byteorder::ByteOrder;
|
|
||||||
use rodio::buffer::SamplesBuffer;
|
|
||||||
use rodio::{OutputStream, Sink};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use std::ffi::{CStr, CString};
|
|
||||||
use std::io::Read;
|
|
||||||
use std::net::TcpStream;
|
|
||||||
use std::os::raw::c_char;
|
|
||||||
use std::time::Instant;
|
|
||||||
|
|
||||||
// How many samples to cache before playing in samples (both channels) SHOULD BE EVEN
|
|
||||||
const BUFFER_SIZE: usize = 2400;
|
|
||||||
// How many buffers to cache
|
|
||||||
const CACHE_SIZE: usize = 40;
|
|
||||||
|
|
||||||
static mut SINK: Option<Box<Sink>> = None;
|
|
||||||
static mut MD: Option<SentMetadata> = None;
|
|
||||||
static mut STATE: State = State::NotStarted;
|
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
enum State {
|
|
||||||
NotStarted,
|
|
||||||
Resetting,
|
|
||||||
Playing,
|
|
||||||
Paused,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn start(server: *const c_char) {
|
|
||||||
let serv = unsafe { CStr::from_ptr(server) };
|
|
||||||
unsafe {
|
|
||||||
run(match serv.to_str() {
|
|
||||||
Ok(s) => s,
|
|
||||||
_ => "",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn toggle() {
|
|
||||||
unsafe {
|
|
||||||
if STATE == State::Playing {
|
|
||||||
STATE = State::Paused;
|
|
||||||
if let Some(sink) = &SINK {
|
|
||||||
sink.pause();
|
|
||||||
}
|
|
||||||
} else if STATE == State::Paused {
|
|
||||||
STATE = State::Playing;
|
|
||||||
if let Some(sink) = &SINK {
|
|
||||||
sink.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn reset() {
|
|
||||||
unsafe {
|
|
||||||
STATE = State::Resetting;
|
|
||||||
if let Some(sink) = &SINK {
|
|
||||||
sink.pause();
|
|
||||||
}
|
|
||||||
// Blocking main thread
|
|
||||||
while STATE == State::Resetting {
|
|
||||||
std::thread::sleep(std::time::Duration::from_secs_f32(0.01))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn get_metadata_artist() -> *mut c_char {
|
|
||||||
unsafe {
|
|
||||||
match &MD {
|
|
||||||
Some(md) => CString::new(md.artist.clone()).unwrap().into_raw(),
|
|
||||||
_ => CString::new("").unwrap().into_raw(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn get_metadata_album() -> *mut c_char {
|
|
||||||
unsafe {
|
|
||||||
match &MD {
|
|
||||||
Some(md) => CString::new(md.album.clone()).unwrap().into_raw(),
|
|
||||||
_ => CString::new("").unwrap().into_raw(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn get_metadata_title() -> *mut c_char {
|
|
||||||
unsafe {
|
|
||||||
match &MD {
|
|
||||||
Some(md) => CString::new(md.title.clone()).unwrap().into_raw(),
|
|
||||||
_ => CString::new("").unwrap().into_raw(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe fn _reset() {
|
|
||||||
if let Some(sink) = &SINK {
|
|
||||||
sink.pause();
|
|
||||||
sink.clear();
|
|
||||||
}
|
|
||||||
SINK = None;
|
|
||||||
MD = None;
|
|
||||||
STATE = State::NotStarted;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset - true, not - false
|
|
||||||
unsafe fn watching_sleep(dur: f32) -> bool {
|
|
||||||
let start = Instant::now();
|
|
||||||
while Instant::now() < start + std::time::Duration::from_secs_f32(dur) {
|
|
||||||
std::thread::sleep(std::time::Duration::from_secs_f32(0.0001));
|
|
||||||
if STATE == State::Resetting {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Clone, Debug)]
|
|
||||||
struct SentMetadata {
|
|
||||||
// In bytes, we need to read next track metadata
|
|
||||||
lenght: u64,
|
|
||||||
sample_rate: u32,
|
|
||||||
title: String,
|
|
||||||
album: String,
|
|
||||||
artist: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe fn run(server: &str) {
|
|
||||||
if STATE == State::Playing || STATE == State::Paused {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
STATE = State::Playing;
|
|
||||||
let mut stream = TcpStream::connect(server).unwrap();
|
|
||||||
println!("Connected to {} from {}", stream.peer_addr().unwrap(), stream.local_addr().unwrap());
|
|
||||||
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
|
|
||||||
match &SINK {
|
|
||||||
None => {
|
|
||||||
let sink = Sink::try_new(&stream_handle).unwrap();
|
|
||||||
SINK = Some(Box::new(sink));
|
|
||||||
}
|
|
||||||
Some(s) => {
|
|
||||||
if s.is_paused() {
|
|
||||||
s.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match &SINK {
|
|
||||||
None => {
|
|
||||||
let sink = Sink::try_new(&stream_handle).unwrap();
|
|
||||||
SINK = Some(Box::new(sink));
|
|
||||||
}
|
|
||||||
Some(s) => {
|
|
||||||
if s.is_paused() {
|
|
||||||
s.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut buffer = [0u8; 2];
|
|
||||||
let mut samples = [0f32; BUFFER_SIZE];
|
|
||||||
loop {
|
|
||||||
let mut index = 0usize;
|
|
||||||
|
|
||||||
let md: SentMetadata =
|
|
||||||
rmp_serde::from_read(&stream).expect("Failed to parse track metadata");
|
|
||||||
MD = Some(md.clone());
|
|
||||||
for _ in 0..md.lenght {
|
|
||||||
while STATE == State::Paused {
|
|
||||||
std::thread::sleep(std::time::Duration::from_secs_f32(0.25))
|
|
||||||
}
|
|
||||||
if STATE == State::Resetting {
|
|
||||||
_reset();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if stream.read_exact(&mut buffer).is_err() {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
samples[index] = byteorder::LittleEndian::read_i16(&buffer[..2]) as f32 / 32768.0;
|
|
||||||
index += 1;
|
|
||||||
|
|
||||||
if index == BUFFER_SIZE {
|
|
||||||
// Sink's thread is detached from main thread, so we need to synchronize with it
|
|
||||||
// Why we should synchronize with it?
|
|
||||||
// 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
|
|
||||||
if let Some(sink) = &SINK {
|
|
||||||
while sink.len() >= CACHE_SIZE {
|
|
||||||
// Sleeping exactly one buffer and watching for reset signal
|
|
||||||
if watching_sleep(
|
|
||||||
if sink.len() > 2 {
|
|
||||||
sink.len() as f32 - 2.0
|
|
||||||
} else {
|
|
||||||
0.5
|
|
||||||
} * BUFFER_SIZE as f32 / md.sample_rate as f32
|
|
||||||
/ 2.0,
|
|
||||||
) {
|
|
||||||
_reset();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sink.append(SamplesBuffer::new(2, md.sample_rate, samples.as_slice()));
|
|
||||||
index = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
void start(const char *server);
|
|
||||||
|
|
||||||
void toggle();
|
|
||||||
|
|
||||||
void reset();
|
|
||||||
|
|
||||||
const char *get_metadata_artist();
|
|
||||||
const char *get_metadata_album();
|
|
||||||
const char *get_metadata_title();
|
|
12
src/main.rs
12
src/main.rs
|
@ -32,7 +32,7 @@ struct Args {
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct SentMetadata {
|
struct SentMetadata {
|
||||||
// In bytes, we need to read next track metadata
|
// In bytes, we need to read next track metadata
|
||||||
lenght: u64,
|
length: u64,
|
||||||
// Yep, no more interpolation
|
// Yep, no more interpolation
|
||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
title: String,
|
title: String,
|
||||||
|
@ -41,7 +41,7 @@ struct SentMetadata {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn stream_samples(
|
async fn stream_samples(
|
||||||
track_samples: Vec<f32>,
|
track_samples: Vec<i16>,
|
||||||
war: bool,
|
war: bool,
|
||||||
md: SentMetadata,
|
md: SentMetadata,
|
||||||
s: &mut TcpStream,
|
s: &mut TcpStream,
|
||||||
|
@ -53,9 +53,9 @@ async fn stream_samples(
|
||||||
for sample in track_samples {
|
for sample in track_samples {
|
||||||
if s.write_all(
|
if s.write_all(
|
||||||
&(if war {
|
&(if war {
|
||||||
sample.signum() as i16 * 32767
|
sample.signum() * 32767
|
||||||
} else {
|
} else {
|
||||||
(sample * 32768_f32) as i16
|
sample
|
||||||
}
|
}
|
||||||
.to_le_bytes()),
|
.to_le_bytes()),
|
||||||
)
|
)
|
||||||
|
@ -191,7 +191,7 @@ async fn stream(mut s: TcpStream, tracklist: Arc<Vec<PathBuf>>) {
|
||||||
Ok(decoded) => {
|
Ok(decoded) => {
|
||||||
sample_rate = decoded.spec().rate;
|
sample_rate = decoded.spec().rate;
|
||||||
let mut byte_buf =
|
let mut byte_buf =
|
||||||
SampleBuffer::<f32>::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);
|
||||||
samples.append(&mut byte_buf.samples_mut().to_vec());
|
samples.append(&mut byte_buf.samples_mut().to_vec());
|
||||||
continue;
|
continue;
|
||||||
|
@ -203,7 +203,7 @@ async fn stream(mut s: TcpStream, tracklist: Arc<Vec<PathBuf>>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let md = SentMetadata {
|
let md = SentMetadata {
|
||||||
lenght: samples.len() as u64,
|
length: samples.len() as u64,
|
||||||
sample_rate,
|
sample_rate,
|
||||||
title,
|
title,
|
||||||
album,
|
album,
|
||||||
|
|
Loading…
Reference in a new issue