Skip to content

Commit 2ef05f0

Browse files
addressed the comment and fixed publish_mic.py and local_audio.py
1 parent 40e1e65 commit 2ef05f0

3 files changed

Lines changed: 231 additions & 44 deletions

File tree

examples/basic_room.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,10 @@ def load_wav_file(path: str) -> tuple[bytes, int, int, int]:
155155
# Convert 24-bit to 16-bit (drop lower 8 bits)
156156
samples = []
157157
for i in range(0, len(frames), 3):
158-
# 24-bit little-endian, take upper 16 bits
159-
sample = struct.unpack("<i", frames[i : i + 3] + b"\x00")[0] >> 8
158+
# 24-bit little-endian: sign-extend to 32-bit, then take upper 16 bits
159+
# If high bit (bit 23) is set, the sample is negative - extend with 0xFF
160+
sign_byte = b"\xff" if frames[i + 2] & 0x80 else b"\x00"
161+
sample = struct.unpack("<i", frames[i : i + 3] + sign_byte)[0] >> 8
160162
samples.append(sample)
161163
frames = struct.pack(f"{len(samples)}h", *samples)
162164
bits_per_sample = 16

examples/local_audio/full_duplex.py

Lines changed: 130 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,88 @@
1-
import os
1+
"""
2+
Full-duplex audio using PlatformAudio.
3+
4+
This example demonstrates:
5+
- Using PlatformAudio for microphone capture with built-in voice processing (AEC, NS, AGC)
6+
- Automatic speaker playout for received audio (handled by PlatformAudio)
7+
- Monitoring microphone dB levels via AudioStream
8+
- Device enumeration and selection
9+
10+
With PlatformAudio:
11+
- Microphone audio is captured with AEC, NS, and AGC applied automatically
12+
- Received audio from remote participants is played through speakers automatically
13+
- No manual audio routing needed
14+
15+
Usage:
16+
python full_duplex.py
17+
python full_duplex.py --list-devices
18+
python full_duplex.py --mic-id "mic-guid" --speaker-id "speaker-guid"
19+
"""
20+
21+
import argparse
222
import asyncio
323
import logging
4-
import threading
24+
import os
525
import queue
6-
from dotenv import load_dotenv, find_dotenv
26+
import threading
27+
28+
try:
29+
from dotenv import find_dotenv, load_dotenv
30+
HAS_DOTENV = True
31+
except ImportError:
32+
HAS_DOTENV = False
733

834
from livekit import api, rtc
35+
936
from db_meter import calculate_db_level, display_single_db_meter
1037

1138

12-
async def main() -> None:
39+
def parse_args() -> argparse.Namespace:
40+
parser = argparse.ArgumentParser(description="Full-duplex audio using PlatformAudio")
41+
parser.add_argument(
42+
"--list-devices",
43+
action="store_true",
44+
help="List available audio devices and exit",
45+
)
46+
parser.add_argument(
47+
"--mic-id",
48+
type=str,
49+
help="Select microphone by device ID (from --list-devices)",
50+
)
51+
parser.add_argument(
52+
"--speaker-id",
53+
type=str,
54+
help="Select speaker by device ID (from --list-devices)",
55+
)
56+
return parser.parse_args()
57+
58+
59+
def list_audio_devices() -> None:
60+
"""List available audio devices using PlatformAudio."""
61+
try:
62+
platform_audio = rtc.PlatformAudio()
63+
except rtc.PlatformAudioError as e:
64+
print(f"Failed to initialize PlatformAudio: {e}")
65+
return
66+
67+
print("\nRecording devices (microphones):")
68+
for device in platform_audio.recording_devices():
69+
print(f" [{device.index}] {device.name}")
70+
print(f" ID: {device.id}")
71+
72+
print("\nPlayout devices (speakers):")
73+
for device in platform_audio.playout_devices():
74+
print(f" [{device.index}] {device.name}")
75+
print(f" ID: {device.id}")
76+
77+
print()
78+
79+
80+
async def main(args: argparse.Namespace) -> None:
1381
logging.basicConfig(level=logging.INFO)
1482

1583
# Load environment variables from a .env file if present
16-
load_dotenv(find_dotenv())
84+
if HAS_DOTENV:
85+
load_dotenv(find_dotenv())
1786

1887
url = os.getenv("LIVEKIT_URL")
1988
api_key = os.getenv("LIVEKIT_API_KEY")
@@ -25,25 +94,50 @@ async def main() -> None:
2594
"LIVEKIT_TOKEN or LIVEKIT_API_KEY and LIVEKIT_API_SECRET must be set in env"
2695
)
2796

28-
room = rtc.Room()
97+
# Initialize PlatformAudio for microphone capture and speaker playout
98+
# PlatformAudio provides:
99+
# - Built-in AEC (echo cancellation), NS (noise suppression), AGC (auto gain control)
100+
# - Automatic speaker playout for received audio
101+
try:
102+
platform_audio = rtc.PlatformAudio()
103+
logging.info("PlatformAudio initialized")
104+
except rtc.PlatformAudioError as e:
105+
logging.error(f"Failed to initialize PlatformAudio: {e}")
106+
return
107+
108+
# Select microphone if specified
109+
if args.mic_id:
110+
try:
111+
platform_audio.set_recording_device(args.mic_id)
112+
logging.info(f"Selected microphone: {args.mic_id}")
113+
except rtc.PlatformAudioError as e:
114+
logging.warning(f"Failed to select microphone: {e}")
29115

30-
devices = rtc.MediaDevices()
116+
# Select speaker if specified
117+
if args.speaker_id:
118+
try:
119+
platform_audio.set_playout_device(args.speaker_id)
120+
logging.info(f"Selected speaker: {args.speaker_id}")
121+
except rtc.PlatformAudioError as e:
122+
logging.warning(f"Failed to select speaker: {e}")
31123

32-
# Open microphone & speaker
33-
mic = devices.open_input()
34-
player = devices.open_output()
124+
room = rtc.Room()
35125

36126
# dB level monitoring (mic only)
37-
mic_db_queue = queue.Queue()
127+
mic_db_queue: queue.Queue[float] = queue.Queue()
38128

129+
# With PlatformAudio, received audio is automatically played through speakers
130+
# We just log when tracks are subscribed/unsubscribed
39131
def on_track_subscribed(
40132
track: rtc.Track,
41133
publication: rtc.RemoteTrackPublication,
42134
participant: rtc.RemoteParticipant,
43135
):
44136
if track.kind == rtc.TrackKind.KIND_AUDIO:
45-
asyncio.create_task(player.add_track(track))
46-
logging.info("subscribed to audio from %s", participant.identity)
137+
logging.info(
138+
"Subscribed to audio from %s (auto-playing through speaker)",
139+
participant.identity,
140+
)
47141

48142
room.on("track_subscribed", on_track_subscribed)
49143

@@ -52,8 +146,8 @@ def on_track_unsubscribed(
52146
publication: rtc.RemoteTrackPublication,
53147
participant: rtc.RemoteParticipant,
54148
):
55-
asyncio.create_task(player.remove_track(track))
56-
logging.info("unsubscribed from audio of %s", participant.identity)
149+
if track.kind == rtc.TrackKind.KIND_AUDIO:
150+
logging.info("Unsubscribed from audio of %s", participant.identity)
57151

58152
room.on("track_unsubscribed", on_track_unsubscribed)
59153

@@ -76,12 +170,20 @@ def on_track_unsubscribed(
76170
await room.connect(url, token)
77171
logging.info("connected to room %s", room.name)
78172

79-
# Publish microphone
80-
track = rtc.LocalAudioTrack.create_audio_track("mic", mic.source)
173+
# Create audio source with voice processing enabled
174+
source = platform_audio.create_audio_source(
175+
rtc.PlatformAudioOptions(
176+
echo_cancellation=True,
177+
noise_suppression=True,
178+
auto_gain_control=True,
179+
)
180+
)
181+
track = rtc.LocalAudioTrack.create_audio_track("mic", source)
182+
81183
pub_opts = rtc.TrackPublishOptions()
82184
pub_opts.source = rtc.TrackSource.SOURCE_MICROPHONE
83185
await room.local_participant.publish_track(track, pub_opts)
84-
logging.info("published local microphone")
186+
logging.info("published local microphone with PlatformAudio")
85187

86188
# Start dB meter display in a separate thread
87189
meter_thread = threading.Thread(
@@ -92,10 +194,7 @@ def on_track_unsubscribed(
92194
)
93195
meter_thread.start()
94196

95-
# Start playing mixed remote audio (tracks added via event handlers)
96-
await player.start()
97-
98-
# Monitor microphone dB levels
197+
# Monitor microphone dB levels via AudioStream
99198
async def monitor_mic_db():
100199
mic_stream = rtc.AudioStream(track, sample_rate=48000, num_channels=1)
101200
frame_count = 0
@@ -117,6 +216,8 @@ async def monitor_mic_db():
117216
mic_db_queue.put_nowait(db_level)
118217
except queue.Full:
119218
pass # Drop if queue is full
219+
except asyncio.CancelledError:
220+
pass
120221
except Exception:
121222
pass
122223
finally:
@@ -127,16 +228,19 @@ async def monitor_mic_db():
127228
# Run until Ctrl+C
128229
while True:
129230
await asyncio.sleep(1)
130-
except KeyboardInterrupt:
231+
except (KeyboardInterrupt, asyncio.CancelledError):
131232
pass
132233
finally:
133-
await mic.aclose()
134-
await player.aclose()
135234
try:
136235
await room.disconnect()
137236
except Exception:
138237
pass
139238

140239

141240
if __name__ == "__main__":
142-
asyncio.run(main())
241+
args = parse_args()
242+
243+
if args.list_devices:
244+
list_audio_devices()
245+
else:
246+
asyncio.run(main(args))

0 commit comments

Comments
 (0)