From 6e3327aa1fa1ce6cfffa0b6eb589718c3b9b6647 Mon Sep 17 00:00:00 2001 From: Ivan Bushchik Date: Thu, 14 Mar 2024 20:50:01 +0300 Subject: [PATCH] v0.2: Metadata support, samplerate removal Signed-off-by: Ivan Bushchik --- Cargo.lock | 32 +-- Cargo.toml | 2 +- README.md | 6 +- monoclient/src/main.rs | 128 +++++------- platform/README.md | 2 +- platform/swiftui/MonoLib-Bridging-Header.h | 1 - platform/swiftui/monoclient/ContentView.swift | 16 +- platform/swiftui/monolib/Cargo.toml | 5 +- platform/swiftui/monolib/src/lib.rs | 196 +++++++++++++----- platform/swiftui/monolib/src/monolib.h | 4 + src/main.rs | 57 ++--- 11 files changed, 238 insertions(+), 211 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 22a6552..1352cc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -289,15 +289,6 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" -[[package]] -name = "cmake" -version = "0.1.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" -dependencies = [ - "cc", -] - [[package]] name = "colorchoice" version = "1.0.0" @@ -572,15 +563,6 @@ dependencies = [ "windows-targets 0.52.4", ] -[[package]] -name = "libsamplerate-sys" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28853b399f78f8281cd88d333b54a63170c4275f6faea66726a2bea5cca72e0d" -dependencies = [ - "cmake", -] - [[package]] name = "lofty" version = "0.18.2" @@ -622,7 +604,6 @@ dependencies = [ "lofty", "rand", "rmp-serde", - "samplerate", "serde", "symphonia", "tokio", @@ -683,10 +664,12 @@ dependencies = [ [[package]] name = "monolib" -version = "0.1.0" +version = "0.2.0" dependencies = [ "byteorder", + "rmp-serde", "rodio", + "serde", ] [[package]] @@ -1030,15 +1013,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "samplerate" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e032b2b24715c4f982f483ea3abdb3c9ba444d9f63e87b2843d6f998f5ba2698" -dependencies = [ - "libsamplerate-sys", -] - [[package]] name = "serde" version = "1.0.197" diff --git a/Cargo.toml b/Cargo.toml index f742a89..29835e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,7 +27,7 @@ symphonia = { version = "0.5.4", features = [ "all-formats", "opt-simd", ] } -samplerate = "0.2.4" +#samplerate = "0.2.4" chrono = "0.4" rmp-serde = "1.1.2" serde = { version = "1.0.197", features = ["derive"] } diff --git a/README.md b/README.md index 6ab092f..143a6b9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > TCP radio for singles -Radio that uses unencrypted TCP socket for broadcasting raw PCM (16/44.1/LE) stream +Radio that uses unencrypted TCP socket for broadcasting tagged audio data. Decodes audio streams using [symphonia](https://github.com/pdeljanov/Symphonia). @@ -22,7 +22,7 @@ All files (recursively) will be shuffled and played back. Public log will be dis ### Clients -[monoclient](./monoclient) with optional channel separation, hardcoded input (16/44.1/LE). +[monoclient](./monoclient) is a recommended client for lonelyradio ```shell monoclient : @@ -30,7 +30,7 @@ monoclient : ### Other clients -SwiftUI client is availible in [platform](./platform) directory (not yet adapted for lonelyradio 0.2). +SwiftUI client is availible in [platform](./platform) directory. ## License diff --git a/monoclient/src/main.rs b/monoclient/src/main.rs index cdd8278..5365df2 100644 --- a/monoclient/src/main.rs +++ b/monoclient/src/main.rs @@ -3,24 +3,19 @@ use clap::Parser; use rodio::buffer::SamplesBuffer; use rodio::{OutputStream, Sink}; use serde::Deserialize; -use std::io::{Read, Write}; +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 = 2400; +const BUFFER_SIZE: usize = 4800; // How many buffers to cache -const CACHE_SIZE: usize = 100; - -enum Channel { - Right, - Left, - Stereo, -} +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, @@ -30,91 +25,75 @@ struct SentMetadata { struct Args { /// Remote address address: String, - #[arg(short, long, default_value = "s")] - /// L, R or S for Left, Right or Stereo - channel: String, - #[arg(short)] - /// Play only on specified channel, with it if channel = Right => L=0 and R=R, without L=R and R=R. No effect on Stereo - single: bool, - /// Do not erase previously played track from stdout + /// Do not use backspace control char #[arg(short)] no_backspace: bool, } -fn main() { - let args = Args::parse(); - let mut stream = TcpStream::connect(args.address).unwrap(); - println!("Connected to {} from {}", stream.peer_addr().unwrap(), stream.local_addr().unwrap()); +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") +} - let channel = match args.channel.to_ascii_lowercase().as_str() { - "l" => Channel::Left, - "r" => Channel::Right, - "s" => Channel::Stereo, - _ => panic!("Wrong channel specified"), - }; +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; 4]; + let mut buffer = [0u8; 2]; let mut samples = [0f32; BUFFER_SIZE]; let mut latest_msg_len = 0; print!("Playing: "); loop { let mut index = 0usize; - let md: SentMetadata = rmp_serde::from_read(&stream).unwrap(); - let seconds = md.lenght / (2 * 44100); - let message = format!( - "{} - {} - {} ({}:{:02})", - md.artist, - md.album, - md.title, - seconds / 60, - seconds % 60 - ); + 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 { - print!("{}", "\u{8}".repeat(latest_msg_len)); - print!("{}", " ".repeat(latest_msg_len)); - print!("{}", "\u{8}".repeat(latest_msg_len)); + delete_chars(latest_msg_len) } } print!("{}", message); - std::io::stdout().flush().unwrap(); + 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(); + } + std::io::stdout().flush().expect("Failed to flush stdout"); latest_msg_len = message.chars().count(); - - for _ in 0..md.lenght / 2 { + 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; }; - let sample_l = byteorder::LittleEndian::read_i16(&buffer[..2]) as f32 / 32768.0; - let sample_r = byteorder::LittleEndian::read_i16(&buffer[2..]) as f32 / 32768.0; - // Left channel - samples[index] = match channel { - Channel::Left | Channel::Stereo => sample_l, - Channel::Right => { - if args.single { - 0f32 - } else { - sample_r - } - } - }; - index += 1; - // Right channel - samples[index] = match channel { - Channel::Right | Channel::Stereo => sample_r, - Channel::Left => { - if args.single { - 0f32 - } else { - sample_l - } - } - }; + + 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? @@ -123,20 +102,13 @@ fn main() { while sink.len() >= CACHE_SIZE { // Sleeping exactly one buffer std::thread::sleep(std::time::Duration::from_secs_f32( - (if sink.len() >= 2 { - sink.len() - 2 - } else { - 1 - } as f32) * BUFFER_SIZE as f32 - / 44100.0 / 2.0, + BUFFER_SIZE as f32 / md.sample_rate as f32 / 2.0, )) } - sink.append(SamplesBuffer::new(2, 44100, samples.as_slice())); + sink.append(SamplesBuffer::new(2, md.sample_rate, samples.as_slice())); index = 0; } } - while sink.len() != 0 { - std::thread::sleep(std::time::Duration::from_secs_f32(0.01)) - } + sink.sleep_until_end() } } diff --git a/platform/README.md b/platform/README.md index 313c80e..a5d506c 100644 --- a/platform/README.md +++ b/platform/README.md @@ -20,4 +20,4 @@ cargo lipo --release --targets aarch64-apple-ios-sim,x86_64-apple-ios Open Xcode and run. -[Screenshots](./screenshots/swiftui) +[Screenshots (pre v0.2)](./screenshots/swiftui) diff --git a/platform/swiftui/MonoLib-Bridging-Header.h b/platform/swiftui/MonoLib-Bridging-Header.h index 9ae8bb1..cad221a 100644 --- a/platform/swiftui/MonoLib-Bridging-Header.h +++ b/platform/swiftui/MonoLib-Bridging-Header.h @@ -8,7 +8,6 @@ #ifndef MonoLib_Bridging_Header_h #define MonoLib_Bridging_Header_h - #import "monolib.h" #endif /* MonoLib_Bridging_Header_h */ diff --git a/platform/swiftui/monoclient/ContentView.swift b/platform/swiftui/monoclient/ContentView.swift index 4efde8d..30d4b7b 100644 --- a/platform/swiftui/monoclient/ContentView.swift +++ b/platform/swiftui/monoclient/ContentView.swift @@ -25,14 +25,19 @@ class MonoLib { } 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 playing: Bool = true @State private var running: Bool = false + @State var now_playing_artist: String = "" + @State var now_playing_album: String = "" + @State var now_playing_title: String = "" + var body: some View { VStack { - Text("Monoclient").font(.largeTitle).fontWidth(.expanded).bold() //.padding(.top, 25) + Text("Monoclient").font(.largeTitle).fontWidth(.expanded).bold() VStack(alignment: .center) { HStack { Text("Server").frame(minWidth: 50, idealWidth: 60) @@ -79,6 +84,15 @@ struct ContentView: View { .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()!) + } }.padding() diff --git a/platform/swiftui/monolib/Cargo.toml b/platform/swiftui/monolib/Cargo.toml index 4b4d132..94de7ba 100644 --- a/platform/swiftui/monolib/Cargo.toml +++ b/platform/swiftui/monolib/Cargo.toml @@ -1,12 +1,15 @@ [package] name = "monolib" -version = "0.1.0" +version = "0.2.0" edition = "2021" license = "MIT" [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" diff --git a/platform/swiftui/monolib/src/lib.rs b/platform/swiftui/monolib/src/lib.rs index 7130d71..75552f3 100644 --- a/platform/swiftui/monolib/src/lib.rs +++ b/platform/swiftui/monolib/src/lib.rs @@ -1,10 +1,12 @@ use byteorder::ByteOrder; use rodio::buffer::SamplesBuffer; use rodio::{OutputStream, Sink}; -use std::ffi::CStr; +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; @@ -12,9 +14,16 @@ const BUFFER_SIZE: usize = 2400; const CACHE_SIZE: usize = 40; static mut SINK: Option> = None; -static mut RUNNING: bool = false; -static mut STOPPED: bool = false; -static mut RESET: bool = false; +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) { @@ -30,13 +39,13 @@ pub extern "C" fn start(server: *const c_char) { #[no_mangle] pub extern "C" fn toggle() { unsafe { - if !STOPPED { - STOPPED = true; + if STATE == State::Playing { + STATE = State::Paused; if let Some(sink) = &SINK { sink.pause(); } - } else { - STOPPED = false; + } else if STATE == State::Paused { + STATE = State::Playing; if let Some(sink) = &SINK { sink.play(); } @@ -47,19 +56,84 @@ pub extern "C" fn toggle() { #[no_mangle] pub extern "C" fn reset() { unsafe { - RESET = true; + STATE = State::Resetting; + if let Some(sink) = &SINK { + sink.pause(); + } // Blocking main thread - while RESET { - std::thread::sleep(std::time::Duration::from_secs_f32(0.02)) + 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 RUNNING { + if STATE == State::Playing || STATE == State::Paused { return; } - RUNNING = true; + 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(); @@ -74,52 +148,64 @@ unsafe fn run(server: &str) { } } } - let mut buffer = [0u8; 4]; - let mut samples = [0f32; BUFFER_SIZE]; - let mut index = 0usize; - while stream.read_exact(&mut buffer).is_ok() { - while STOPPED { - std::thread::sleep(std::time::Duration::from_secs_f32(0.5)) + match &SINK { + None => { + let sink = Sink::try_new(&stream_handle).unwrap(); + SINK = Some(Box::new(sink)); } - if RESET { - RUNNING = false; - STOPPED = false; - - if let Some(sink) = &SINK { - sink.pause(); - sink.clear(); + Some(s) => { + if s.is_paused() { + s.play() } - SINK = None; - RESET = false; - return; } - let sample_l = byteorder::LittleEndian::read_i16(&buffer[..2]) as f32 / 32768.0; - let sample_r = byteorder::LittleEndian::read_i16(&buffer[2..]) as f32 / 32768.0; - // Left channel - samples[index] = sample_l; - index += 1; - // Right channel - samples[index] = sample_r; - 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 - std::thread::sleep(std::time::Duration::from_secs_f32( - (if sink.len() >= 2 { - sink.len() - 2 - } else { - 1 - } as f32) * BUFFER_SIZE as f32 - / 44100.0 / 2.0, - )) + } + 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; } - sink.append(SamplesBuffer::new(2, 44100, samples.as_slice())); - index = 0; } } } diff --git a/platform/swiftui/monolib/src/monolib.h b/platform/swiftui/monolib/src/monolib.h index 7e49ae3..72737a6 100644 --- a/platform/swiftui/monolib/src/monolib.h +++ b/platform/swiftui/monolib/src/monolib.h @@ -5,3 +5,7 @@ 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 7ee97ee..5f8c6d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,12 +7,10 @@ use clap::Parser; use lofty::Accessor; use lofty::TaggedFileExt; use rand::prelude::*; -use samplerate::ConverterType; use serde::Serialize; use symphonia::core::audio::SampleBuffer; use symphonia::core::codecs::CODEC_TYPE_NULL; use symphonia::core::io::MediaSourceStream; -use symphonia::core::meta::StandardTagKey; use symphonia::core::probe::Hint; use tokio::io::AsyncWriteExt; use tokio::net::{TcpListener, TcpStream}; @@ -35,6 +33,8 @@ struct Args { struct SentMetadata { // In bytes, we need to read next track metadata lenght: u64, + // Yep, no more interpolation + sample_rate: u32, title: String, album: String, artist: String, @@ -42,23 +42,15 @@ struct SentMetadata { async fn stream_samples( track_samples: Vec, - rate: u32, war: bool, md: SentMetadata, s: &mut TcpStream, ) -> bool { - let resampled = if rate != 44100 { - samplerate::convert(rate, 44100, 2, ConverterType::Linear, track_samples.as_slice()) - .unwrap() - } else { - track_samples - }; - let mut md = md; - md.lenght = resampled.len() as u64; if s.write_all(rmp_serde::to_vec(&md).unwrap().as_slice()).await.is_err() { return true; } - for sample in resampled { + + for sample in track_samples { if s.write_all( &(if war { sample.signum() as i16 * 32767 @@ -121,7 +113,8 @@ async fn stream(mut s: TcpStream, tracklist: Arc>) { let mut file = std::fs::File::open(track).unwrap(); let tagged = lofty::read_from(&mut file).unwrap(); if let Some(id3v2) = tagged.primary_tag() { - title = id3v2.title().unwrap_or("[No tag]".into()).to_string(); + title = + id3v2.title().unwrap_or(track.file_stem().unwrap().to_string_lossy()).to_string(); album = id3v2.album().unwrap_or("[No tag]".into()).to_string(); artist = id3v2.artist().unwrap_or("[No tag]".into()).to_string() }; @@ -179,7 +172,7 @@ async fn stream(mut s: TcpStream, tracklist: Arc>) { .expect("unsupported codec"); let track_id = track.id; - let mut track_rate = 0; + let mut sample_rate = 0; let mut samples = vec![]; loop { let packet = match format.next_packet() { @@ -188,18 +181,6 @@ async fn stream(mut s: TcpStream, tracklist: Arc>) { }; while !format.metadata().is_latest() { format.metadata().pop(); - if let Some(rev) = format.metadata().current() { - for tag in rev.tags() { - println!("Looped"); - match tag.std_key { - Some(StandardTagKey::Album) => album = tag.value.to_string(), - Some(StandardTagKey::Artist) => artist = tag.value.to_string(), - Some(StandardTagKey::TrackTitle) => title = tag.value.to_string(), - _ => {} - } - eprintln!("DBG: {} {} {}", &album, &artist, &title) - } - } } if packet.track_id() != track_id { @@ -208,7 +189,7 @@ async fn stream(mut s: TcpStream, tracklist: Arc>) { match decoder.decode(&packet) { Ok(decoded) => { - track_rate = decoded.spec().rate; + sample_rate = decoded.spec().rate; let mut byte_buf = SampleBuffer::::new(decoded.capacity() as u64, *decoded.spec()); byte_buf.copy_interleaved_ref(decoded); @@ -221,21 +202,15 @@ async fn stream(mut s: TcpStream, tracklist: Arc>) { } } } + let md = SentMetadata { + lenght: samples.len() as u64, + sample_rate, + title, + album, + artist, + }; if !samples.is_empty() { - if stream_samples( - samples, - track_rate, - args.war, - SentMetadata { - lenght: 0, - title, - album, - artist, - }, - &mut s, - ) - .await - { + if stream_samples(samples, args.war, md, &mut s).await { break; } }