diff --git a/README.md b/README.md index f44aee0e..53af4964 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Follow the instructions above for your operating system to install a virtual aud - **TCP/UDP (WiFi)**: - Connect your phone and PC to the same network - - No additional setup required + - **Windows**: When the firewall prompt appears on first launch, click **Allow access**. Otherwise the PC can't receive connections from your phone. If you dismissed the prompt, allow AndroidMic in *Windows Security → Firewall & network protection → Allow an app through firewall* - **USB ADB**: - Install [Android Debug Bridge (ADB)](https://developer.android.com/tools/adb) @@ -119,6 +119,7 @@ Follow the instructions above for your operating system to install a virtual aud - Grant required permissions: - **Microphone**: To access your phone's mic - **Notification**: To show recording status in background + - **Nearby devices**: Required on Android 17 and above to connect to your PC. Approve it when prompted, or enable it in *Settings → Apps → AndroidMic → Permissions* - Start recording - Connect to PC: - **TCP/UDP**: Enter PC IP address and port (shown in PC app log) @@ -154,6 +155,15 @@ After setup, the virtual microphone will appear in your system's audio input dev - **USB Serial**: May need [Zadig](https://zadig.akeo.ie/) to change USB driver to WinUSB - **USB Serial (Linux)**: Configure udev rules for USB permissions +### Phone can't connect to PC +- **Android 37+**: Make sure the **Nearby devices** permission is granted to the app +- **Windows**: Make sure AndroidMic is allowed through the firewall (see [Step 2](#step-2-pc-application-setup)) +- Confirm the phone and PC are on the same network + +### Audio is choppy, stuttering or glitchy +- Increase the **jitter buffer** latency in the PC app settings. A larger buffer absorbs network delays at the cost of slightly higher latency. Raise it step by step until the audio is smooth +- The PC app logs a warning (`playout: ... underruns ...`) when the buffer runs dry, which is a sign you should increase it + ### Audio quality issues - Match audio settings between PC and Android app exactly - Try different sample rates (44.1kHz or 48kHz) diff --git a/RustApp/benches/audio.rs b/RustApp/benches/audio.rs index f2874400..f86365f9 100644 --- a/RustApp/benches/audio.rs +++ b/RustApp/benches/audio.rs @@ -14,7 +14,6 @@ use android_mic::{ }; use criterion::{Criterion, criterion_group, criterion_main}; - fn make_random(size: usize) -> Vec { use rand::rngs::StdRng; use rand::{Rng, SeedableRng}; diff --git a/RustApp/src/audio/player.rs b/RustApp/src/audio/player.rs index 493e9b87..a5013ff3 100644 --- a/RustApp/src/audio/player.rs +++ b/RustApp/src/audio/player.rs @@ -57,21 +57,11 @@ pub fn create_audio_stream( // create stream config let stream: cpal::Stream = match audio_format { - AudioFormat::I16 => { - build_output_stream::(device, config, consumer, target_latency) - } - AudioFormat::I24 => { - build_output_stream::(device, config, consumer, target_latency) - } - AudioFormat::I32 => { - build_output_stream::(device, config, consumer, target_latency) - } - AudioFormat::U8 => { - build_output_stream::(device, config, consumer, target_latency) - } - AudioFormat::F32 => { - build_output_stream::(device, config, consumer, target_latency) - } + AudioFormat::I16 => build_output_stream::(device, config, consumer, target_latency), + AudioFormat::I24 => build_output_stream::(device, config, consumer, target_latency), + AudioFormat::I32 => build_output_stream::(device, config, consumer, target_latency), + AudioFormat::U8 => build_output_stream::(device, config, consumer, target_latency), + AudioFormat::F32 => build_output_stream::(device, config, consumer, target_latency), }?; // convert stream config to AudioPacketFormat @@ -142,7 +132,11 @@ mod tests { // the filter delay passes first, then the signal comes through let mut output = [0_i16; 256]; playout.fill(&mut output); - assert!(output[..filter_delay_frames() - 8].iter().all(|s| s.abs() < 1000)); + assert!( + output[..filter_delay_frames() - 8] + .iter() + .all(|s| s.abs() < 1000) + ); assert!(output[200..].iter().all(|s| (*s - 16383).abs() < 20)); } } diff --git a/RustApp/src/audio/playout.rs b/RustApp/src/audio/playout.rs index 99a5c3b1..aada42cc 100644 --- a/RustApp/src/audio/playout.rs +++ b/RustApp/src/audio/playout.rs @@ -300,7 +300,10 @@ impl Playout { let capacity = consumer.buffer().capacity() / channels; let mut target = to_frames(target_latency).max(1); if target > capacity / 2 { - warn!("playout: target latency is too big for the buffer, using {} frames", capacity / 2); + warn!( + "playout: target latency is too big for the buffer, using {} frames", + capacity / 2 + ); target = capacity / 2; } @@ -841,7 +844,10 @@ mod tests { for drift in [-200.0, -30.0, 0.0, 50.0, 300.0] { // pseudo random jitter up to 15 ms, with a 30 ms spike every second let jitter = |n: usize| { - let r = ((n as u64).wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407) >> 40) as f64 + let r = ((n as u64) + .wrapping_mul(6364136223846793005) + .wrapping_add(1442695040888963407) + >> 40) as f64 / (1u64 << 24) as f64; r * 0.015 + if n % 200 == 0 { 0.03 } else { 0.0 } }; @@ -859,7 +865,10 @@ mod tests { ); assert!(s < 0.2, "latency moved by {s} ms with {drift} ppm drift"); // clicks 204.53 ms apart at the mic stay 204.53 ms apart, within a sample - assert!(interval < 0.02, "interval off by {interval} ms with {drift} ppm drift"); + assert!( + interval < 0.02, + "interval off by {interval} ms with {drift} ppm drift" + ); } } @@ -879,7 +888,12 @@ mod tests { let latencies = click_latencies(&output, &clicks, 0.0); // the clicks inside the stall are lost, all the others are played on time - assert!(latencies.len() + 4 >= clicks.len(), "{} {}", latencies.len(), clicks.len()); + assert!( + latencies.len() + 4 >= clicks.len(), + "{} {}", + latencies.len(), + clicks.len() + ); let before = &latencies[250..390]; let after = &latencies[latencies.len() - 250..latencies.len() - 50]; diff --git a/RustApp/src/audio/process.rs b/RustApp/src/audio/process.rs index 148073d1..a5c3e49f 100644 --- a/RustApp/src/audio/process.rs +++ b/RustApp/src/audio/process.rs @@ -197,7 +197,11 @@ impl AudioStream { let num_frames = std::cmp::min(self.buff.producer.slots() / num_channels, total_frames); if num_frames > 0 { - match self.buff.producer.write_chunk_uninit(num_frames * num_channels) { + match self + .buff + .producer + .write_chunk_uninit(num_frames * num_channels) + { Ok(chunk) => { let buffer_ref = &buffer; diff --git a/RustApp/src/audio/resampler.rs b/RustApp/src/audio/resampler.rs index ee7422bf..21675a44 100644 --- a/RustApp/src/audio/resampler.rs +++ b/RustApp/src/audio/resampler.rs @@ -9,7 +9,7 @@ pub struct ResamplerCache { result: Vec>, } -const CHUNK_SIZE: usize = 1024; +const CHUNK_SIZE: usize = 256; pub fn resample_f32_stream_owned( data: &[Vec], @@ -124,6 +124,15 @@ pub fn resample_f32_stream<'a>( channel.clear(); } input_offset = missing; + } else { + // not enough data to fill a chunk yet, keep accumulating + for (data, unprocessed_buffer) in data.iter().zip(unprocessed_buffer.iter_mut()) { + unprocessed_buffer.extend_from_slice(data); + } + for channel in &mut cache.result { + channel.clear(); + } + return Ok(&mut cache.result); } }; diff --git a/RustApp/src/ui/tray.rs b/RustApp/src/ui/tray.rs index 244ac98f..e9a7d549 100644 --- a/RustApp/src/ui/tray.rs +++ b/RustApp/src/ui/tray.rs @@ -18,7 +18,6 @@ pub struct SystemTrayStream { #[cfg(not(target_os = "linux"))] mod tray_icon_backend { - use futures::SinkExt; use std::sync::Arc; use tokio::sync::{Mutex, mpsc}; use tray_icon::{ @@ -114,10 +113,10 @@ pub use tray_icon_backend::SystemTray; #[cfg(target_os = "linux")] mod ksni_backend { - use std::sync::Arc; - use tokio::sync::{Mutex, mpsc}; use ksni::TrayMethods; use ksni::menu::{MenuItem as MenuEntry, StandardItem}; + use std::sync::Arc; + use tokio::sync::{Mutex, mpsc}; use crate::{fl, utils::APP_ID}; @@ -140,7 +139,10 @@ mod ksni_backend { fn apply(&mut self, cmd: TrayCommand) { match cmd { - TrayCommand::MenuState { disconnected, status } => { + TrayCommand::MenuState { + disconnected, + status, + } => { self.disconnected = disconnected; self.description = format!("AndroidMic - {status}"); }