Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
1 change: 0 additions & 1 deletion RustApp/benches/audio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ use android_mic::{
};
use criterion::{Criterion, criterion_group, criterion_main};


fn make_random(size: usize) -> Vec<u8> {
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
Expand Down
26 changes: 10 additions & 16 deletions RustApp/src/audio/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,11 @@ pub fn create_audio_stream(

// create stream config
let stream: cpal::Stream = match audio_format {
AudioFormat::I16 => {
build_output_stream::<i16>(device, config, consumer, target_latency)
}
AudioFormat::I24 => {
build_output_stream::<f32>(device, config, consumer, target_latency)
}
AudioFormat::I32 => {
build_output_stream::<i32>(device, config, consumer, target_latency)
}
AudioFormat::U8 => {
build_output_stream::<u8>(device, config, consumer, target_latency)
}
AudioFormat::F32 => {
build_output_stream::<f32>(device, config, consumer, target_latency)
}
AudioFormat::I16 => build_output_stream::<i16>(device, config, consumer, target_latency),
AudioFormat::I24 => build_output_stream::<f32>(device, config, consumer, target_latency),
AudioFormat::I32 => build_output_stream::<i32>(device, config, consumer, target_latency),
AudioFormat::U8 => build_output_stream::<u8>(device, config, consumer, target_latency),
AudioFormat::F32 => build_output_stream::<f32>(device, config, consumer, target_latency),
}?;

// convert stream config to AudioPacketFormat
Expand Down Expand Up @@ -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));
}
}
22 changes: 18 additions & 4 deletions RustApp/src/audio/playout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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 }
};
Expand All @@ -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"
);
}
}

Expand All @@ -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];
Expand Down
6 changes: 5 additions & 1 deletion RustApp/src/audio/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
11 changes: 10 additions & 1 deletion RustApp/src/audio/resampler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub struct ResamplerCache {
result: Vec<Vec<f32>>,
}

const CHUNK_SIZE: usize = 1024;
const CHUNK_SIZE: usize = 256;

pub fn resample_f32_stream_owned(
data: &[Vec<f32>],
Expand Down Expand Up @@ -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);
}
};

Expand Down
10 changes: 6 additions & 4 deletions RustApp/src/ui/tray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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};

Expand All @@ -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}");
}
Expand Down
Loading