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
222import asyncio
323import logging
4- import threading
24+ import os
525import 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
834from livekit import api , rtc
35+
936from 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 ("\n Recording 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 ("\n Playout 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
141240if __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