Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
2,176 changes: 1,575 additions & 601 deletions examples/Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
members = [
"basic_room",
"local_audio",
"mobile",
"save_to_disk",
"wgpu_room",
Expand Down
15 changes: 15 additions & 0 deletions examples/local_audio/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "local_audio"
version = "0.1.0"
edition = "2021"

[dependencies]
tokio = { version = "1", features = ["full"] }
env_logger = "0.10"
livekit = { path = "../../livekit", features = ["rustls-tls-native-roots"]}
livekit-api = { path = "../../livekit-api", features = ["rustls-tls-native-roots"]}
log = "0.4"
cpal = "0.15"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can upgrade to cpal 0.16. I tested the example with it and it works just as well.

anyhow = "1.0"
clap = { version = "4.0", features = ["derive"] }
futures-util = "0.3"
164 changes: 164 additions & 0 deletions examples/local_audio/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Local Audio Capture Example

This example demonstrates how to capture audio from a local microphone and stream it to a LiveKit room while simultaneously playing back audio from other participants. It provides a complete bidirectional audio experience with real-time level monitoring.

## Features

- **Bidirectional Audio**: Capture from local microphone and play back remote participants
- **Device Selection**: Choose specific input/output devices or use system defaults
- **Real-time Level Meter**: Visual dB meter showing local microphone levels
- **Audio Processing**: Echo cancellation, noise suppression, and auto gain control (enabled by default)
- **Volume Control**: Adjustable playback volume for remote participants
- **Audio Mixing**: Combines audio from multiple remote participants
- **Format Support**: Handles F32, I16, and U16 sample formats
- **Cross-platform**: Works on Windows, macOS, and Linux

## Prerequisites

1. **Rust**: Install Rust 1.70+ from [rustup.rs](https://rustup.rs/)
2. **LiveKit Server**: Access to a LiveKit server instance
3. **Audio Devices**: Working microphone and speakers/headphones
4. **System Permissions**: Audio device access permissions

### Platform-specific Requirements

- **macOS**: Grant microphone permissions in System Preferences → Privacy & Security → Microphone
- **Windows**: Ensure audio drivers are installed and microphone is not in use by other applications
- **Linux**: May need ALSA or PulseAudio libraries (`sudo apt install libasound2-dev` on Ubuntu/Debian)

## Setup

1. **LiveKit Connection Details** (choose one method):

**Option A: Environment Variables**
```bash
export LIVEKIT_URL="wss://your-livekit-server.com"
export LIVEKIT_API_KEY="your-api-key"
export LIVEKIT_API_SECRET="your-api-secret"
```

**Option B: CLI Arguments**
Pass connection details directly to the command (see examples below)

**Note**: CLI arguments take precedence over environment variables. You can mix both methods - for example, set API credentials via environment variables but override the URL via CLI.

2. **Build the Example**:

```bash
cd examples/local_audio
cargo build --release
```

## Usage

### List Available Audio Devices

```bash
cargo run -- --list-devices
```

Example output:
```
Available Input Devices:
───────────────────────────────────────────────────────────────
1. MacBook Pro Microphone
├─ Sample Rate: 8000-48000 Hz
├─ Channels: 1-2
└─ Formats: F32, I16

2. USB Microphone
├─ Sample Rate: 44100-48000 Hz
├─ Channels: 1-2
└─ Formats: F32, I16

Default Input Device: MacBook Pro Microphone

Available Output Devices:
───────────────────────────────────────────────────────────────
1. MacBook Pro Speakers
├─ Sample Rate: 8000-48000 Hz
├─ Channels: 2
└─ Formats: F32, I16

2. USB Headphones
├─ Sample Rate: 44100-48000 Hz
├─ Channels: 2
└─ Formats: F32, I16

Default Output Device: MacBook Pro Speakers
```

### Basic Usage

Stream audio with default settings (using environment variables):

```bash
cargo run
```

Using CLI arguments for connection details:

```bash
cargo run -- \
--url "wss://your-project.livekit.cloud" \
--api-key "your-api-key" \
--api-secret "your-api-secret"
```

Join a specific room with custom identity:

```bash
cargo run -- \
--url "wss://your-project.livekit.cloud" \
--api-key "your-api-key" \
--api-secret "your-api-secret" \
--room-name "my-meeting" \
--identity "john-doe"
```

### Advanced Configuration

```bash
cargo run -- \
--url "wss://your-project.livekit.cloud" \
--api-key "your-api-key" \
--api-secret "your-api-secret" \
--input-device "USB Microphone" \
--output-device "USB Headphones" \
--sample-rate 44100 \
--channels 2 \
--volume 0.8 \
--room-name "conference-room"
```

### Capture-Only Mode

Disable audio playback and only capture:

```bash
cargo run -- \
--url "wss://your-project.livekit.cloud" \
--api-key "your-api-key" \
--api-secret "your-api-secret" \
--no-playback
```

## Command Line Options

| Option | Description | Default |
|--------|-------------|---------|
| `--list-devices` | List available audio devices and exit | - |
| `--input-device <NAME>` | Input device name | System default |
| `--output-device <NAME>` | Output device name | System default |
| `--sample-rate <HZ>` | Sample rate in Hz | 48000 |
| `--channels <COUNT>` | Number of channels | 1 |
| `--echo-cancellation` | Enable echo cancellation | true |
| `--noise-suppression` | Enable noise suppression | true |
| `--auto-gain-control` | Enable auto gain control | true |
| `--no-playback` | Disable audio playback (capture only) | false |
| `--volume <LEVEL>` | Playback volume (0.0 to 1.0) | 1.0 |
| `--identity <NAME>` | LiveKit participant identity | "rust-audio-streamer" |
| `--room-name <NAME>` | LiveKit room name | "audio-room" |
| `--url <URL>` | LiveKit server URL | From LIVEKIT_URL env var |
| `--api-key <KEY>` | LiveKit API key | From LIVEKIT_API_KEY env var |
| `--api-secret <SECRET>` | LiveKit API secret | From LIVEKIT_API_SECRET env var |
110 changes: 110 additions & 0 deletions examples/local_audio/src/audio_capture.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use anyhow::{anyhow, Result};
use cpal::traits::{DeviceTrait, StreamTrait};
use cpal::{Device, SampleFormat, Stream, StreamConfig, SizedSample};
use log::{error, info, warn};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use tokio::sync::mpsc;

pub struct AudioCapture {
_stream: Stream,
is_running: Arc<AtomicBool>,
}

impl AudioCapture {
pub async fn new(
device: Device,
config: StreamConfig,
sample_format: SampleFormat,
audio_tx: mpsc::UnboundedSender<Vec<i16>>,
db_tx: Option<mpsc::UnboundedSender<f32>>,
) -> Result<Self> {
let is_running = Arc::new(AtomicBool::new(true));
let is_running_clone = is_running.clone();

let stream = match sample_format {
SampleFormat::F32 => Self::create_input_stream::<f32>(device, config, audio_tx, db_tx, is_running_clone)?,
SampleFormat::I16 => Self::create_input_stream::<i16>(device, config, audio_tx, db_tx, is_running_clone)?,
SampleFormat::U16 => Self::create_input_stream::<u16>(device, config, audio_tx, db_tx, is_running_clone)?,
sample_format => {
return Err(anyhow!("Unsupported sample format: {:?}", sample_format));
}
};

stream.play()?;
info!("Audio capture stream started");

Ok(AudioCapture {
_stream: stream,
is_running,
})
}

fn create_input_stream<T>(
device: Device,
config: StreamConfig,
audio_tx: mpsc::UnboundedSender<Vec<i16>>,
db_tx: Option<mpsc::UnboundedSender<f32>>,
is_running: Arc<AtomicBool>,
) -> Result<Stream>
where
T: SizedSample + Send + 'static,
{
let stream = device.build_input_stream(
&config,
move |data: &[T], _: &cpal::InputCallbackInfo| {
if !is_running.load(Ordering::Relaxed) {
return;
}

let converted: Vec<i16> = data.iter().map(|&sample| {
Self::convert_sample_to_i16(sample)
}).collect();

// Calculate and send dB level if channel is available
if let Some(ref db_sender) = db_tx {
let db_level = crate::db_meter::calculate_db_level(&converted);
if let Err(e) = db_sender.send(db_level) {
warn!("Failed to send dB level: {}", e);
}
}

if let Err(e) = audio_tx.send(converted) {
warn!("Failed to send audio data: {}", e);
}
},
move |err| {
error!("Audio input stream error: {}", err);
},
None,
)?;

Ok(stream)
}

fn convert_sample_to_i16<T: SizedSample>(sample: T) -> i16 {
if std::mem::size_of::<T>() == std::mem::size_of::<f32>() {
let sample_f32 = unsafe { std::mem::transmute_copy::<T, f32>(&sample) };
(sample_f32.clamp(-1.0, 1.0) * i16::MAX as f32) as i16
} else if std::mem::size_of::<T>() == std::mem::size_of::<i16>() {
unsafe { std::mem::transmute_copy::<T, i16>(&sample) }
} else if std::mem::size_of::<T>() == std::mem::size_of::<u16>() {
let sample_u16 = unsafe { std::mem::transmute_copy::<T, u16>(&sample) };
((sample_u16 as i32) - (u16::MAX as i32 / 2)) as i16
} else {
0
}
}

pub fn stop(&self) {
self.is_running.store(false, Ordering::Relaxed);
}
}

impl Drop for AudioCapture {
fn drop(&mut self) {
self.stop();
}
}
60 changes: 60 additions & 0 deletions examples/local_audio/src/audio_mixer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use std::sync::{Arc, Mutex};

#[derive(Clone)]
pub struct AudioMixer {
buffer: Arc<Mutex<std::collections::VecDeque<i16>>>,
sample_rate: u32,
channels: u32,
volume: f32,
max_buffer_size: usize,
}

impl AudioMixer {
pub fn new(sample_rate: u32, channels: u32, volume: f32) -> Self {
// Buffer for 1 second of audio
let max_buffer_size = sample_rate as usize * channels as usize;

Self {
buffer: Arc::new(Mutex::new(std::collections::VecDeque::with_capacity(max_buffer_size))),
sample_rate,
channels,
volume: volume.clamp(0.0, 1.0),
max_buffer_size,
}
}

pub fn add_audio_data(&self, data: &[i16]) {
let mut buffer = self.buffer.lock().unwrap();

// Apply volume scaling and add to buffer
for &sample in data.iter() {
let scaled_sample = (sample as f32 * self.volume) as i16;
buffer.push_back(scaled_sample);

// Prevent buffer from growing too large
if buffer.len() > self.max_buffer_size {
buffer.pop_front();
}
}
}

pub fn get_samples(&self, requested_samples: usize) -> Vec<i16> {
let mut buffer = self.buffer.lock().unwrap();
let mut result = Vec::with_capacity(requested_samples);

// Fill the requested samples
for _ in 0..requested_samples {
if let Some(sample) = buffer.pop_front() {
result.push(sample);
} else {
result.push(0); // Silence when no data available
}
}

result
}

pub fn buffer_size(&self) -> usize {
self.buffer.lock().unwrap().len()
}
}
Loading
Loading