Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e00dcc8
init media devices
chenosaurus Sep 8, 2025
8f13bbd
add MediaDevices to rtc/__init__.py
chenosaurus Sep 9, 2025
cd9d873
clean up examples
chenosaurus Sep 9, 2025
b58dd7d
fix syntax to create inputstream
chenosaurus Sep 10, 2025
825e9d5
fix audio output thru mixer
chenosaurus Sep 11, 2025
74582ec
remove unused import
chenosaurus Sep 11, 2025
9b2f466
fix linter error
chenosaurus Sep 11, 2025
efb5473
ruff format
chenosaurus Sep 11, 2025
7f1d59e
allow AudioMixer to unwrap AudioFrameEvent
chenosaurus Sep 11, 2025
c8f8c0c
rename dir to match convention
chenosaurus Sep 11, 2025
30ee183
rename methods to be more clear
chenosaurus Sep 11, 2025
89fb1ba
update example
chenosaurus Sep 24, 2025
c48e1eb
update comments
chenosaurus Sep 24, 2025
72f546f
ruff format
chenosaurus Sep 25, 2025
ef56542
clean up input stream creation
chenosaurus Oct 2, 2025
236fad1
add missing dep
chenosaurus Oct 3, 2025
7cc6efb
remove mapping
chenosaurus Oct 3, 2025
1ba7f9f
make apm internal
chenosaurus Oct 3, 2025
7e0df4f
add db meter
chenosaurus Oct 5, 2025
8458783
fix lint issues
chenosaurus Oct 5, 2025
ca27e5f
display room name
chenosaurus Oct 7, 2025
846538f
move audio mixer inside of MediaDevices for ease of playback
chenosaurus Oct 7, 2025
58483ac
remove unused import
chenosaurus Oct 7, 2025
c8ca2eb
adding to readme for MediaDevices usage
chenosaurus Oct 7, 2025
15d104e
format
chenosaurus Oct 10, 2025
4a85e01
Merge branch 'main' into dc/media_devices
chenosaurus Oct 10, 2025
7a3e04b
revert changes to audio mixer as we no longer need it to handle Audio…
chenosaurus Oct 16, 2025
723627c
format
chenosaurus Oct 16, 2025
78468ea
fix comment
chenosaurus Oct 16, 2025
2b32567
fix media devices lint
chenosaurus Oct 16, 2025
1abe8c5
clean up media devices
chenosaurus Oct 16, 2025
8e9dc24
format
chenosaurus Oct 16, 2025
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
92 changes: 92 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,98 @@ except Exception as e:

You may find it useful to adjust the `response_timeout` parameter, which indicates the amount of time you will wait for a response. We recommend keeping this value as low as possible while still satisfying the constraints of your application.

## Using local media devices

The `MediaDevices` class provides a high-level interface for working with local audio input (microphone) and output (speakers) devices. It's built on top of the `sounddevice` library and integrates seamlessly with LiveKit's audio processing features. In order to use `MediaDevices`, you must have the `sounddevice` library installed in your local Python environment, if it's not available, `MediaDevices` will not work.

### Capturing microphone input

```python
from livekit import rtc

# Create a MediaDevices instance
devices = rtc.MediaDevices()

# Open the default microphone with audio processing enabled
mic = devices.open_input(
enable_aec=True, # Acoustic Echo Cancellation
noise_suppression=True, # Noise suppression
high_pass_filter=True, # High-pass filter
auto_gain_control=True # Automatic gain control
)

# Use the audio source to create a track and publish it
track = rtc.LocalAudioTrack.create_audio_track("microphone", mic.source)
await room.local_participant.publish_track(track)

# Clean up when done
await mic.aclose()
```

### Playing audio to speakers

```python
# Open the default output device
player = devices.open_output()

# Add remote audio tracks to the player (typically in a track_subscribed handler)
@room.on("track_subscribed")
def on_track_subscribed(track: rtc.Track, publication, participant):
if track.kind == rtc.TrackKind.KIND_AUDIO:
player.add_track(track)

# Start playback (mixes all added tracks)
await player.start()

# Clean up when done
await player.aclose()
```

### Full duplex audio (microphone + speakers)

For full duplex audio with echo cancellation, open the input device first (with AEC enabled), then open the output device. The output player will automatically feed the APM's reverse stream for effective echo cancellation:

```python
devices = rtc.MediaDevices()

# Open microphone with AEC
mic = devices.open_input(enable_aec=True)

# Open speakers - automatically uses the mic's APM for echo cancellation
player = devices.open_output()

# Publish microphone
track = rtc.LocalAudioTrack.create_audio_track("mic", mic.source)
await room.local_participant.publish_track(track)

# Add remote tracks and start playback
player.add_track(remote_audio_track)
await player.start()
```

### Listing available devices

```python
devices = rtc.MediaDevices()

# List input devices
input_devices = devices.list_input_devices()
for device in input_devices:
print(f"{device['index']}: {device['name']}")

# List output devices
output_devices = devices.list_output_devices()
for device in output_devices:
print(f"{device['index']}: {device['name']}")

# Get default device indices
default_input = devices.default_input_device()
default_output = devices.default_output_device()
```

See [publish_mic.py](examples/local_audio/publish_mic.py) and [full_duplex.py](examples/local_audio/full_duplex.py) for complete examples.


#### Errors

LiveKit is a dynamic realtime environment and calls can fail for various reasons.
Expand Down
262 changes: 262 additions & 0 deletions examples/local_audio/db_meter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
"""
Audio dB meter utilities for LiveKit Python SDK examples.

This module provides functions to calculate and display audio levels in decibels (dB)
from raw audio samples, useful for monitoring microphone input and room audio levels.
"""

import math
import queue
import time
from typing import List

# dB meter configuration constants
DB_METER_UPDATE_INTERVAL_MS = 50 # Update every 50ms
MIC_METER_WIDTH = 25 # Width of the mic dB meter bar
ROOM_METER_WIDTH = 25 # Width of the room dB meter bar


def calculate_db_level(samples: List[int]) -> float:
"""
Calculate decibel level from audio samples.

Args:
samples: List of 16-bit audio samples

Returns:
dB level as float. Returns -60.0 for silence/empty samples.
"""
if not samples:
return -60.0 # Very quiet

# Calculate RMS (Root Mean Square)
sum_squares = sum(
(sample / 32767.0) ** 2 # Normalize to -1.0 to 1.0 range
for sample in samples
)

rms = math.sqrt(sum_squares / len(samples))

# Convert to dB (20 * log10(rms))
if rms > 0.0:
return 20.0 * math.log10(rms)
else:
return -60.0 # Very quiet


def get_meter_color(db_level: float, position_ratio: float) -> str:
"""
Get ANSI color code based on dB level and position in meter.

Args:
db_level: Current dB level
position_ratio: Position in meter (0.0 to 1.0)

Returns:
ANSI color code string
"""
# Determine color based on both dB level and position in the meter
if db_level > -6.0 and position_ratio > 0.85:
return "\x1b[91m" # Bright red - clipping/very loud
elif db_level > -12.0 and position_ratio > 0.7:
return "\x1b[31m" # Red - loud
elif db_level > -18.0 and position_ratio > 0.5:
return "\x1b[93m" # Bright yellow - medium-loud
elif db_level > -30.0 and position_ratio > 0.3:
return "\x1b[33m" # Yellow - medium
elif position_ratio > 0.1:
return "\x1b[92m" # Bright green - low-medium
else:
return "\x1b[32m" # Green - low


def format_single_meter(db_level: float, meter_width: int, meter_label: str) -> str:
"""
Format a single dB meter with colors.

Args:
db_level: dB level to display
meter_width: Width of the meter bar in characters
meter_label: Label text for the meter

Returns:
Formatted meter string with ANSI colors
"""
# ANSI color codes
COLOR_RESET = "\x1b[0m"
COLOR_DIM = "\x1b[2m"

db_clamped = max(-60.0, min(0.0, db_level))
normalized = (db_clamped + 60.0) / 60.0 # Normalize to 0.0-1.0
filled_width = int(normalized * meter_width)

meter = meter_label

# Add the dB value with appropriate color
if db_level > -6.0:
db_color = "\x1b[91m" # Bright red
elif db_level > -12.0:
db_color = "\x1b[31m" # Red
elif db_level > -24.0:
db_color = "\x1b[33m" # Yellow
else:
db_color = "\x1b[32m" # Green

meter += f"{db_color}{db_level:>7.1f}{COLOR_RESET} "

# Add the visual meter with colors
meter += "["
for i in range(meter_width):
position_ratio = i / meter_width

if i < filled_width:
color = get_meter_color(db_level, position_ratio)
meter += f"{color}█{COLOR_RESET}" # Full block for active levels
else:
meter += f"{COLOR_DIM}░{COLOR_RESET}" # Light shade for empty

meter += "]"
return meter


def format_dual_meters(mic_db: float, room_db: float) -> str:
"""
Format both dB meters on the same line.

Args:
mic_db: Microphone dB level
room_db: Room audio dB level

Returns:
Formatted dual meter string
"""
mic_meter = format_single_meter(mic_db, MIC_METER_WIDTH, "Mic: ")
room_meter = format_single_meter(room_db, ROOM_METER_WIDTH, " Room: ")

return f"{mic_meter}{room_meter}"


def display_dual_db_meters(
mic_db_receiver, room_db_receiver, room_name: str = "Audio Levels Monitor"
) -> None:
"""
Display dual dB meters continuously until interrupted.

Args:
mic_db_receiver: Queue or receiver for microphone dB levels
room_db_receiver: Queue or receiver for room dB levels
room_name: Name of the room to display as the title
"""
try:
last_update = time.time()
current_mic_db = -60.0
current_room_db = -60.0

print() # Start on a new line
print(f"\x1b[92mRoom [{room_name}]\x1b[0m")
print(
"\x1b[2m────────────────────────────────────────────────────────────────────────────────\x1b[0m"
)

while True:
# Check for new data (non-blocking)
try:
while True: # Drain all available data
mic_db = mic_db_receiver.get_nowait()
current_mic_db = mic_db
except queue.Empty:
pass # No more data available

try:
while True: # Drain all available data
room_db = room_db_receiver.get_nowait()
current_room_db = room_db
except queue.Empty:
pass # No more data available

# Update display at regular intervals
current_time = time.time()
if current_time - last_update >= DB_METER_UPDATE_INTERVAL_MS / 1000.0:
# Clear current line and display meters in place
print(
f"\r\x1b[K{format_dual_meters(current_mic_db, current_room_db)}",
end="",
flush=True,
)
last_update = current_time

# Small sleep to prevent busy waiting
time.sleep(0.01)

except KeyboardInterrupt:
print() # Move to next line after Ctrl+C


def display_single_db_meter(db_receiver, label: str = "Mic Level: ") -> None:
"""
Display a single dB meter continuously until interrupted.

Args:
db_receiver: Queue or receiver for dB levels
label: Label for the meter display
"""
try:
last_update = time.time()
current_db = -60.0
first_display = True

if first_display:
print() # Start on a new line
print(f"\x1b[92m{label}\x1b[0m")
print("\x1b[2m────────────────────────────────────────\x1b[0m")
first_display = False

while True:
# Check for new data (non-blocking)
try:
while True: # Drain all available data
db_level = db_receiver.get_nowait()
current_db = db_level
except queue.Empty:
pass # No more data available

# Update display at regular intervals
current_time = time.time()
if current_time - last_update >= DB_METER_UPDATE_INTERVAL_MS / 1000.0:
# Clear current line and display meter in place
meter = format_single_meter(current_db, 40, label)
print(f"\r\x1b[K{meter}", end="", flush=True)
last_update = current_time

# Small sleep to prevent busy waiting
time.sleep(0.01)

except KeyboardInterrupt:
print() # Move to next line after Ctrl+C


# Example usage and testing functions
def demo_db_meter() -> None:
"""Demo function to test dB meter functionality."""
import random

# Simulate some test data
class MockReceiver:
def __init__(self):
self.data = []

def get_nowait(self):
if not self.data:
# Generate random dB value between -60 and 0
self.data.append(random.uniform(-60, 0))
return self.data.pop(0)

mic_receiver = MockReceiver()
room_receiver = MockReceiver()

print("Starting dB meter demo (Ctrl+C to stop)...")
display_dual_db_meters(mic_receiver, room_receiver)


if __name__ == "__main__":
demo_db_meter()
Loading
Loading