From 9165c43daec67b4c4e6c5a32f081ba52e003beeb Mon Sep 17 00:00:00 2001 From: Ivan Bushchik Date: Fri, 15 Mar 2024 18:31:06 +0300 Subject: [PATCH] v0.2.1: Refactor monoclient into monoclient and monolib Signed-off-by: Ivan Bushchik --- Cargo.lock | 11 +- Cargo.toml | 4 +- README.md | 4 +- monoclient/Cargo.toml | 9 +- monoclient/src/main.rs | 179 +++++++-------- .../swiftui/monolib => monolib}/Cargo.lock | 0 .../swiftui/monolib => monolib}/Cargo.toml | 14 +- monolib/src/c.rs | 75 +++++++ monolib/src/lib.rs | 186 +++++++++++++++ monolib/src/monolib.h | 20 ++ platform/README.md | 6 +- platform/swiftui/monoclient/ContentView.swift | 52 +++-- platform/swiftui/monolib/src/lib.rs | 212 ------------------ platform/swiftui/monolib/src/monolib.h | 11 - src/main.rs | 12 +- 15 files changed, 428 insertions(+), 367 deletions(-) rename {platform/swiftui/monolib => monolib}/Cargo.lock (100%) rename {platform/swiftui/monolib => monolib}/Cargo.toml (50%) create mode 100644 monolib/src/c.rs create mode 100644 monolib/src/lib.rs create mode 100644 monolib/src/monolib.h delete mode 100644 platform/swiftui/monolib/src/lib.rs delete mode 100644 platform/swiftui/monolib/src/monolib.h diff --git a/Cargo.lock b/Cargo.lock index 1352cc5..0f606d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -597,7 +597,7 @@ checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "lonelyradio" -version = "0.2.0" +version = "0.2.1" dependencies = [ "chrono", "clap", @@ -653,18 +653,15 @@ dependencies = [ [[package]] name = "monoclient" -version = "0.2.0" +version = "0.2.1" dependencies = [ - "byteorder", "clap", - "rmp-serde", - "rodio", - "serde", + "monolib", ] [[package]] name = "monolib" -version = "0.2.0" +version = "0.2.1" dependencies = [ "byteorder", "rmp-serde", diff --git a/Cargo.toml b/Cargo.toml index 29835e4..295dee8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,10 @@ [workspace] -members = ["monoclient", "platform/swiftui/monolib"] +members = [ "monoclient", "monolib"] [package] name = "lonelyradio" description = "TCP radio for lonely ones" -version = "0.2.0" +version = "0.2.1" edition = "2021" license = "MIT" authors = ["Ivan Bushchik "] diff --git a/README.md b/README.md index 143a6b9..ed5019b 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ All files (recursively) will be shuffled and played back. Public log will be dis ### Clients -[monoclient](./monoclient) is a recommended client for lonelyradio +[monoclient](./monoclient) is a recommended CLI client for lonelyradio that uses [monolib](./monolib) ```shell monoclient : @@ -32,6 +32,8 @@ monoclient : SwiftUI client is availible in [platform](./platform) directory. +[monolib](./monolib) provides lonelyradio-compatible C API for creating custom clients. + ## License lonelyradio and monoclient are licensed under the terms of the [MIT license](./LICENSE). diff --git a/monoclient/Cargo.toml b/monoclient/Cargo.toml index 86523f9..7f888a7 100644 --- a/monoclient/Cargo.toml +++ b/monoclient/Cargo.toml @@ -1,12 +1,9 @@ [package] name = "monoclient" -version = "0.2.0" +version = "0.2.1" edition = "2021" license = "MIT" [dependencies] -clap = { version = "4.4.18", features = ["derive"] } -rodio = { version = "0.17.3", default-features = false } -byteorder = "1.5.0" -rmp-serde = "1.1.2" -serde = { version = "1.0.197", features = ["derive"] } +monolib = { path="../monolib" } +clap = { version = "4.4.18", features = ["derive"] } \ No newline at end of file diff --git a/monoclient/src/main.rs b/monoclient/src/main.rs index 5365df2..43183e1 100644 --- a/monoclient/src/main.rs +++ b/monoclient/src/main.rs @@ -1,25 +1,6 @@ -use byteorder::ByteOrder; use clap::Parser; -use rodio::buffer::SamplesBuffer; -use rodio::{OutputStream, Sink}; -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, -} +use std::io::{IsTerminal, Write}; +use std::time::{Duration, Instant}; #[derive(Parser)] struct Args { @@ -31,84 +12,98 @@ struct Args { no_backspace: bool, } -fn delete_chars(n: usize) { - print!("{}{}{}", "\u{8}".repeat(n), " ".repeat(n), "\u{8}".repeat(n)); - std::io::stdout().flush().expect("Failed to flush stdout") +fn delete_chars(n: usize, nb: bool) { + if !nb { + 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() { let mut args = Args::parse(); args.no_backspace |= !std::io::stdout().is_terminal(); - let mut stream = TcpStream::connect(&args.address) - .unwrap_or_else(|err| panic!("Failed to connect to {}: {}", args.address, err.to_string())); - println!("Connected to {} from {}", stream.peer_addr().unwrap(), stream.local_addr().unwrap()); - let (_stream, stream_handle) = OutputStream::try_default().unwrap(); - let sink = Sink::try_new(&stream_handle).unwrap(); - let mut buffer = [0u8; 2]; - let mut samples = [0f32; BUFFER_SIZE]; - let mut latest_msg_len = 0; - print!("Playing: "); + std::thread::spawn(move || monolib::run(&args.address)); + while monolib::get_metadata().is_none() {} + let mut md = monolib::get_metadata().unwrap(); + let seconds = md.length / md.sample_rate as u64 / 2; + let mut track_start = Instant::now(); + let mut seconds_past = 0; + let mut msg_len = format!( + "Playing: {} - {} - {} ({}:{:02})", + 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 { - let mut index = 0usize; - - let md: SentMetadata = - rmp_serde::from_read(&stream).expect("Failed to parse track metadata"); - let seconds = md.lenght / (2 * md.sample_rate as u64); - let total_lenght = format!("{}:{:02}", seconds / 60, seconds % 60); - let message = format!("{} - {} - {} ", md.artist, md.album, md.title); - if latest_msg_len != 0 { - if args.no_backspace { - print!("\nPlaying: "); - } else { - delete_chars(latest_msg_len) - } + if monolib::get_metadata().unwrap() != md { + md = monolib::get_metadata().unwrap(); + let seconds = md.length / md.sample_rate as u64 / 2; + delete_chars(msg_len, args.no_backspace); + msg_len = format!( + "Playing: {} - {} - {} ({}:{:02})", + md.artist, + md.album, + md.title, + seconds / 60, + seconds % 60 + ) + .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); - let mut prev_timestamp_len = 0; - if args.no_backspace { - print!("({})", &total_lenght) - } else { - print!("(0:00 / {})", &total_lenght); - // (0:00/ + :00 + minutes len - prev_timestamp_len = 12 + format!("{}", seconds / 60).len(); + if (Instant::now() - track_start).as_secs() > seconds_past && !args.no_backspace { + seconds_past = (Instant::now() - track_start).as_secs(); + msg_len = format!( + "Playing: {} - {} - {} ({}:{:02} / {}:{:02})", + md.artist, + md.album, + md.title, + 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"); - 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() + std::thread::sleep(Duration::from_secs_f32(0.05)) } } diff --git a/platform/swiftui/monolib/Cargo.lock b/monolib/Cargo.lock similarity index 100% rename from platform/swiftui/monolib/Cargo.lock rename to monolib/Cargo.lock diff --git a/platform/swiftui/monolib/Cargo.toml b/monolib/Cargo.toml similarity index 50% rename from platform/swiftui/monolib/Cargo.toml rename to monolib/Cargo.toml index 94de7ba..be3d8b9 100644 --- a/platform/swiftui/monolib/Cargo.toml +++ b/monolib/Cargo.toml @@ -1,16 +1,18 @@ [package] name = "monolib" -version = "0.2.0" +version = "0.2.1" edition = "2021" license = "MIT" +description = "A library implementing the lonely radio audio streaming protocol" +repository = "https://github.com/ivabus/lonelyradio" +authors = [ "Ivan Bushchik "] + +[lib] +name = "monolib" +crate-type = ["staticlib", "cdylib", "rlib"] [dependencies] rodio = { version = "0.17.3", default-features = false } byteorder = "1.5.0" rmp-serde = "1.1.2" serde = { version = "1.0.197", features = ["derive"] } - - -[lib] -name = "monolib" -crate-type = ["staticlib", "cdylib"] diff --git a/monolib/src/c.rs b/monolib/src/c.rs new file mode 100644 index 0000000..5aee7f1 --- /dev/null +++ b/monolib/src/c.rs @@ -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, + } +} diff --git a/monolib/src/lib.rs b/monolib/src/lib.rs new file mode 100644 index 0000000..2c16ede --- /dev/null +++ b/monolib/src/lib.rs @@ -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> = RwLock::new(None); +static MD: RwLock> = RwLock::new(None); +static STATE: RwLock = 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 { + 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; + } + } + } + } +} diff --git a/monolib/src/monolib.h b/monolib/src/monolib.h new file mode 100644 index 0000000..34bd411 --- /dev/null +++ b/monolib/src/monolib.h @@ -0,0 +1,20 @@ +#include +#include +#include +#include + +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); diff --git a/platform/README.md b/platform/README.md index a5d506c..3929e03 100644 --- a/platform/README.md +++ b/platform/README.md @@ -4,16 +4,14 @@ ### 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 ``` -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 diff --git a/platform/swiftui/monoclient/ContentView.swift b/platform/swiftui/monoclient/ContentView.swift index 30d4b7b..d3849ee 100644 --- a/platform/swiftui/monoclient/ContentView.swift +++ b/platform/swiftui/monoclient/ContentView.swift @@ -20,14 +20,14 @@ class MonoLib { } catch { print("Failed to set the audio session configuration") } - start(server) + c_start(server) } } struct ContentView: View { let timer = Timer.publish(every: 0.25, on: .main, in: .common).autoconnect() - @State private var server: String = "" - @State private var port: String = "" + @State private var server: String = "ivabus.dev" + @State private var port: String = "5894" @State private var playing: Bool = true @State private var running: Bool = false @@ -62,7 +62,7 @@ struct ContentView: View { Button(action: { if running { playing = !playing - toggle() + c_toggle() } running = true let a = MonoLib() @@ -76,23 +76,35 @@ struct ContentView: View { ).font(.largeTitle) }.buttonStyle( .borderedProminent) - Button(action: { - reset() - running = false - playing = true - }) { Image(systemName: "stop").font(.title3) }.buttonStyle( - .bordered - ).disabled(!running) + HStack{ + Button(action: { + c_stop() + running = false + playing = true + }) { Image(systemName: "stop").font(.title3) }.buttonStyle( + .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) - Text(now_playing_artist).font(.title2).onReceive(timer) { _ in - now_playing_artist = String(cString: get_metadata_artist()!) - } - Text(now_playing_album).onReceive(timer) { _ in - now_playing_album = String(cString: get_metadata_album()!) - } - Text(now_playing_title).font(.title).bold().onReceive(timer) { _ in - now_playing_title = String(cString: get_metadata_title()!) - } + VStack(spacing: 10) { + 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: c_get_metadata_album()!) + } + Text(now_playing_title).onReceive(timer) { _ in + now_playing_title = String(cString: c_get_metadata_title()!) + }.bold() + }.frame(minHeight: 100) }.padding() diff --git a/platform/swiftui/monolib/src/lib.rs b/platform/swiftui/monolib/src/lib.rs deleted file mode 100644 index 75552f3..0000000 --- a/platform/swiftui/monolib/src/lib.rs +++ /dev/null @@ -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> = None; -static mut MD: Option = 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; - } - } - } - } -} diff --git a/platform/swiftui/monolib/src/monolib.h b/platform/swiftui/monolib/src/monolib.h deleted file mode 100644 index 72737a6..0000000 --- a/platform/swiftui/monolib/src/monolib.h +++ /dev/null @@ -1,11 +0,0 @@ -#include - -void start(const char *server); - -void toggle(); - -void reset(); - -const char *get_metadata_artist(); -const char *get_metadata_album(); -const char *get_metadata_title(); diff --git a/src/main.rs b/src/main.rs index 5f8c6d1..7819c29 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,7 +32,7 @@ struct Args { #[derive(Serialize)] struct SentMetadata { // In bytes, we need to read next track metadata - lenght: u64, + length: u64, // Yep, no more interpolation sample_rate: u32, title: String, @@ -41,7 +41,7 @@ struct SentMetadata { } async fn stream_samples( - track_samples: Vec, + track_samples: Vec, war: bool, md: SentMetadata, s: &mut TcpStream, @@ -53,9 +53,9 @@ async fn stream_samples( for sample in track_samples { if s.write_all( &(if war { - sample.signum() as i16 * 32767 + sample.signum() * 32767 } else { - (sample * 32768_f32) as i16 + sample } .to_le_bytes()), ) @@ -191,7 +191,7 @@ async fn stream(mut s: TcpStream, tracklist: Arc>) { Ok(decoded) => { sample_rate = decoded.spec().rate; let mut byte_buf = - SampleBuffer::::new(decoded.capacity() as u64, *decoded.spec()); + SampleBuffer::::new(decoded.capacity() as u64, *decoded.spec()); byte_buf.copy_interleaved_ref(decoded); samples.append(&mut byte_buf.samples_mut().to_vec()); continue; @@ -203,7 +203,7 @@ async fn stream(mut s: TcpStream, tracklist: Arc>) { } } let md = SentMetadata { - lenght: samples.len() as u64, + length: samples.len() as u64, sample_rate, title, album,