diff --git a/Cargo.lock b/Cargo.lock index b6f0b34..0590845 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -387,6 +387,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "extended" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" + [[package]] name = "getrandom" version = "0.2.12" @@ -568,7 +574,7 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "lonelyradio" -version = "0.1.6" +version = "0.1.7" dependencies = [ "chrono", "clap", @@ -679,6 +685,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.3.3" @@ -690,6 +705,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.17" @@ -815,6 +839,15 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "primal-check" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df7f93fd637f083201473dab4fee2db4c429d32e55e3299980ab3957ab916a0" +dependencies = [ + "num-integer", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -938,6 +971,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustfft" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43806561bc506d0c5d160643ad742e3161049ac01027b5e6d7524091fd401d86" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", + "version_check", +] + [[package]] name = "same-file" version = "1.0.6" @@ -984,6 +1032,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + [[package]] name = "strsim" version = "0.10.0" @@ -992,9 +1046,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "symphonia" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e48dba70095f265fdb269b99619b95d04c89e619538138383e63310b14d941" +checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" dependencies = [ "lazy_static", "symphonia-bundle-flac", @@ -1005,18 +1059,19 @@ dependencies = [ "symphonia-codec-pcm", "symphonia-codec-vorbis", "symphonia-core", + "symphonia-format-caf", "symphonia-format-isomp4", "symphonia-format-mkv", "symphonia-format-ogg", - "symphonia-format-wav", + "symphonia-format-riff", "symphonia-metadata", ] [[package]] name = "symphonia-bundle-flac" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f23b0482a7cb18fcdf9981ab0b78df800ef0080187d294650023c462439058d" +checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97" dependencies = [ "log", "symphonia-core", @@ -1026,11 +1081,10 @@ dependencies = [ [[package]] name = "symphonia-bundle-mp3" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f31d7fece546f1e6973011a9eceae948133bbd18fd3d52f6073b1e38ae6368a" +checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" dependencies = [ - "bitflags 1.3.2", "lazy_static", "log", "symphonia-core", @@ -1039,9 +1093,9 @@ dependencies = [ [[package]] name = "symphonia-codec-aac" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68bdd75b25ce4b84b12a4bd20bfea2460c2dbd7fc1d227ef5533504d3168109d" +checksum = "cdbf25b545ad0d3ee3e891ea643ad115aff4ca92f6aec472086b957a58522f70" dependencies = [ "lazy_static", "log", @@ -1050,9 +1104,9 @@ dependencies = [ [[package]] name = "symphonia-codec-adpcm" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870e7dc1865d818c7b6318879d060553a73a3b2a3b8443dff90910f10ac41150" +checksum = "c94e1feac3327cd616e973d5be69ad36b3945f16b06f19c6773fc3ac0b426a0f" dependencies = [ "log", "symphonia-core", @@ -1060,9 +1114,9 @@ dependencies = [ [[package]] name = "symphonia-codec-alac" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a27e8763d1c9eff666faf903e73a99d4de2f7a93fca4e3c214c1d68432903b9" +checksum = "2d8a6666649a08412906476a8b0efd9b9733e241180189e9f92b09c08d0e38f3" dependencies = [ "log", "symphonia-core", @@ -1070,9 +1124,9 @@ dependencies = [ [[package]] name = "symphonia-codec-pcm" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47f1fbd220a06a641c8ce2ddad10f5ef6ee5cc0c54d9044d25d43b0d3119deaa" +checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b" dependencies = [ "log", "symphonia-core", @@ -1080,9 +1134,9 @@ dependencies = [ [[package]] name = "symphonia-codec-vorbis" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3953397e3506aa01350c4205817e4f95b58d476877a42f0458d07b665749e203" +checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30" dependencies = [ "log", "symphonia-core", @@ -1091,22 +1145,34 @@ dependencies = [ [[package]] name = "symphonia-core" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c73eb88fee79705268cc7b742c7bc93a7b76e092ab751d0833866970754142" +checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" dependencies = [ "arrayvec", "bitflags 1.3.2", "bytemuck", "lazy_static", "log", + "rustfft", +] + +[[package]] +name = "symphonia-format-caf" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e43c99c696a388295a29fe71b133079f5d8b18041cf734c5459c35ad9097af50" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", ] [[package]] name = "symphonia-format-isomp4" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf14bae5cf352032416bc64151e5d6242d29d33cbf3238513b44d4427a1efb" +checksum = "abfdf178d697e50ce1e5d9b982ba1b94c47218e03ec35022d9f0e071a16dc844" dependencies = [ "encoding_rs", "log", @@ -1117,9 +1183,9 @@ dependencies = [ [[package]] name = "symphonia-format-mkv" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5c61dfc851ad25d4043d8c231d8617e8f7cd02a6cc0edad21ade21848d58895" +checksum = "1bb43471a100f7882dc9937395bd5ebee8329298e766250b15b3875652fe3d6f" dependencies = [ "lazy_static", "log", @@ -1130,9 +1196,9 @@ dependencies = [ [[package]] name = "symphonia-format-ogg" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bf1a00ccd11452d44048a0368828040f778ae650418dbd9d8765b7ee2574c8d" +checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931" dependencies = [ "log", "symphonia-core", @@ -1141,11 +1207,12 @@ dependencies = [ ] [[package]] -name = "symphonia-format-wav" -version = "0.5.3" +name = "symphonia-format-riff" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da76614728fa27c003bdcdfbac51396bd8fcbf94c95fe8e62f1d2bac58ef03a4" +checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50" dependencies = [ + "extended", "log", "symphonia-core", "symphonia-metadata", @@ -1153,9 +1220,9 @@ dependencies = [ [[package]] name = "symphonia-metadata" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89c3e1937e31d0e068bbe829f66b2f2bfaa28d056365279e0ef897172c3320c0" +checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" dependencies = [ "encoding_rs", "lazy_static", @@ -1165,9 +1232,9 @@ dependencies = [ [[package]] name = "symphonia-utils-xiph" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a450ca645b80d69aff8b35576cbfdc7f20940b29998202aab910045714c951f8" +checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe" dependencies = [ "symphonia-core", "symphonia-metadata", @@ -1260,6 +1327,16 @@ dependencies = [ "winnow", ] +[[package]] +name = "transpose" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6522d49d03727ffb138ae4cbc1283d3774f0d10aa7f9bf52e6784c45daf9b23" +dependencies = [ + "num-integer", + "strength_reduce", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -1272,6 +1349,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "walkdir" version = "2.4.0" diff --git a/Cargo.toml b/Cargo.toml index 2d7aea3..8be46ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = ["monoclient"] [package] name = "lonelyradio" description = "TCP radio for singles" -version = "0.1.6" +version = "0.1.7" edition = "2021" license = "MIT" authors = ["Ivan Bushchik "] @@ -22,11 +22,10 @@ tokio = { version = "1.35.1", features = [ "macros", ] } walkdir = "2.4.0" -symphonia = { version = "0.5.3", features = [ +symphonia = { version = "0.5.4", features = [ "all-codecs", "all-formats", - "pcm", - "symphonia-codec-pcm", + "opt-simd", ] } samplerate = "0.2.4" chrono = "0.4" diff --git a/monoclient/src/main.rs b/monoclient/src/main.rs index d5ee267..852fc40 100644 --- a/monoclient/src/main.rs +++ b/monoclient/src/main.rs @@ -4,9 +4,12 @@ use rodio::buffer::SamplesBuffer; use rodio::{OutputStream, Sink}; use std::io::Read; use std::net::TcpStream; +use std::time::Instant; -// How many samples to cache before playing (half a second) -const BUFFER_SIZE: usize = 44100; +// 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 = 100; enum Channel { Right, @@ -18,16 +21,29 @@ enum Channel { 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, + + /// More verbose + #[arg(short)] + verbose: bool, } fn main() { + let start = Instant::now(); let args = Args::parse(); let mut stream = TcpStream::connect(args.address).unwrap(); + if args.verbose { + eprintln!( + "Connected to {} from {}", + stream.peer_addr().unwrap(), + stream.local_addr().unwrap() + ) + } let channel = match args.channel.to_ascii_lowercase().as_str() { "l" => Channel::Left, @@ -38,12 +54,14 @@ fn main() { let (_stream, stream_handle) = OutputStream::try_default().unwrap(); let sink = Sink::try_new(&stream_handle).unwrap(); let mut buffer = [0u8; 4]; - let mut samples = vec![]; + let mut samples = [0f32; BUFFER_SIZE]; + let mut index = 0usize; + let mut first = true; while stream.read_exact(&mut buffer).is_ok() { 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.push(match channel { + samples[index] = match channel { Channel::Left | Channel::Stereo => sample_l, Channel::Right => { if args.single { @@ -52,9 +70,10 @@ fn main() { sample_r } } - }); + }; + index += 1; // Right channel - samples.push(match channel { + samples[index] = match channel { Channel::Right | Channel::Stereo => sample_r, Channel::Left => { if args.single { @@ -63,20 +82,35 @@ fn main() { sample_l } } - }); - if samples.len() >= BUFFER_SIZE { - // What is next three lines? - // Sink's thread is detached from main thread, so we need to somehow synchronize with it + }; + index += 1; + if index == BUFFER_SIZE { + let mut first_wait_iteration = true; + // 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() >= 3 - // buffered >= 1.5 sec - { - std::thread::sleep(std::time::Duration::from_secs_f32(0.25)) + while sink.len() >= CACHE_SIZE { + if args.verbose && first_wait_iteration { + eprint!("."); + first_wait_iteration = false; + } + // 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, + )) } - sink.append(SamplesBuffer::new(2, 44100, samples)); - samples = vec![]; + if first && args.verbose { + eprintln!("Started playing in {} ms", (Instant::now() - start).as_millis()); + first = false; + } + sink.append(SamplesBuffer::new(2, 44100, samples.as_slice())); + index = 0; } } } diff --git a/src/main.rs b/src/main.rs index 8f6cfc1..038eff3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use std::path::PathBuf; +use std::sync::Arc; use chrono::Local; use clap::Parser; @@ -6,9 +7,7 @@ use rand::prelude::*; use samplerate::ConverterType; use symphonia::core::audio::SampleBuffer; use symphonia::core::codecs::CODEC_TYPE_NULL; -use symphonia::core::formats::FormatOptions; use symphonia::core::io::MediaSourceStream; -use symphonia::core::meta::MetadataOptions; use symphonia::core::probe::Hint; use tokio::io::AsyncWriteExt; use tokio::net::{TcpListener, TcpStream}; @@ -30,42 +29,43 @@ struct Args { #[tokio::main] async fn main() { let listener = TcpListener::bind(Args::parse().address).await.unwrap(); - + let tracklist = Arc::new( + walkdir::WalkDir::new(Args::parse().dir) + .into_iter() + .filter_entry(is_not_hidden) + .filter_map(|v| v.ok()) + .map(|x| x.into_path()) + .filter(track_valid) + .into_iter() + .collect::>(), + ); loop { let (socket, _) = listener.accept().await.unwrap(); - tokio::spawn(stream(socket)); + tokio::spawn(stream(socket, tracklist.clone())); } } fn is_not_hidden(entry: &DirEntry) -> bool { entry.file_name().to_str().map(|s| entry.depth() == 0 || !s.starts_with('.')).unwrap_or(false) } -// Recursively finding music file -fn pick_track(tracklist: &Vec) -> &PathBuf { - let mut track = tracklist.choose(&mut thread_rng()).unwrap(); - while !track.metadata().unwrap().is_file() { - track = pick_track(tracklist) +fn track_valid(track: &PathBuf) -> bool { + if !track.metadata().unwrap().is_file() { + return false; } // Skipping "images" (covers) - while "jpgjpegpngwebp" - .contains(&track.extension().unwrap().to_str().unwrap().to_ascii_lowercase()) + if "jpgjpegpngwebp".contains(&track.extension().unwrap().to_str().unwrap().to_ascii_lowercase()) { - track = pick_track(tracklist) + return false; } - track + true } -async fn stream(mut s: TcpStream) { +async fn stream(mut s: TcpStream, tracklist: Arc>) { let args = Args::parse(); - let tracklist = walkdir::WalkDir::new(Args::parse().dir) - .into_iter() - .filter_entry(is_not_hidden) - .filter_map(|v| v.ok()) - .map(|x| x.into_path()) - .collect::>(); + 'track: loop { - let track = pick_track(&tracklist); - println!( + let track = tracklist.choose(&mut thread_rng()).unwrap(); + eprintln!( "[{}] {} to {}:{}{}", Local::now().to_rfc3339(), track.to_str().unwrap(), @@ -79,7 +79,7 @@ async fn stream(mut s: TcpStream) { ); if args.public_log { - eprintln!( + println!( "[{}] {} to {}{}", Local::now().to_rfc3339(), track.to_str().unwrap(), @@ -95,13 +95,14 @@ async fn stream(mut s: TcpStream) { let file = Box::new(std::fs::File::open(track).unwrap()); let mut hint = Hint::new(); hint.with_extension(track.extension().unwrap().to_str().unwrap()); - let mss = MediaSourceStream::new(file, Default::default()); - - let meta_opts: MetadataOptions = Default::default(); - let fmt_opts: FormatOptions = Default::default(); let probed = symphonia::default::get_probe() - .format(&hint, mss, &fmt_opts, &meta_opts) + .format( + &hint, + MediaSourceStream::new(file, Default::default()), + &Default::default(), + &Default::default(), + ) .expect("unsupported format"); let mut format = probed.format; @@ -138,14 +139,18 @@ async fn stream(mut s: TcpStream) { let mut byte_buf = SampleBuffer::::new(decoded.capacity() as u64, *decoded.spec()); byte_buf.copy_interleaved_ref(decoded); - let samples = samplerate::convert( - rate, - 44100, - 2, - ConverterType::Linear, - byte_buf.samples(), - ) - .unwrap(); + let samples = if rate != 44100 { + samplerate::convert( + rate, + 44100, + 2, + ConverterType::Linear, + byte_buf.samples(), + ) + .unwrap() + } else { + byte_buf.samples().to_vec() + }; for sample in samples { let result = s .write(