Skip to content

Commit c02ce27

Browse files
committed
simplify media device creation, hide apm details
1 parent ef56542 commit c02ce27

File tree

2 files changed

+36
-18
lines changed

2 files changed

+36
-18
lines changed

examples/local_audio/full_duplex.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ async def main() -> None:
2222

2323
devices = rtc.MediaDevices()
2424

25-
# Open microphone with AEC and prepare a player for remote audio feeding AEC reverse stream
25+
# Open microphone with AEC; output will auto-wire reverse stream for AEC
2626
mic = devices.open_input(enable_aec=True)
27-
player = devices.open_output(apm_for_reverse=mic.apm)
27+
player = devices.open_output()
2828

2929
# Mixer for all remote audio streams
3030
mixer = rtc.AudioMixer(sample_rate=48000, num_channels=1)

livekit-rtc/livekit/rtc/media_devices.py

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,6 @@ async def aclose(self) -> None:
120120
class OutputPlayer:
121121
"""Simple audio output helper using `sounddevice.OutputStream`.
122122
123-
When `apm_for_reverse` is provided, this player will feed the same PCM it
124-
renders (in 10 ms frames) into the APM reverse path so that echo
125-
cancellation can correlate mic input with speaker output.
126123
"""
127124

128125
def __init__(
@@ -262,6 +259,10 @@ def __init__(
262259
self._channels = num_channels
263260
self._blocksize = blocksize
264261
self._delay_estimator: Optional[_APMDelayEstimator] = None
262+
# Internal: last opened input's APM instance (if any). automatically associated with output player for AEC.
263+
self._apm: Optional[AudioProcessingModule] = None
264+
# Track a single output player
265+
self._output: Optional[OutputPlayer] = None
265266

266267
# Device enumeration
267268
def list_input_devices(self) -> list[dict[str, Any]]:
@@ -314,9 +315,9 @@ def open_input(
314315
an `AudioProcessingModule` is created and applied to each frame before it
315316
is queued for `AudioSource.capture_frame`.
316317
317-
To enable AEC end-to-end, pass the returned `apm` to
318-
`open_output(apm_for_reverse=...)` and route remote audio through
319-
that player so reverse frames are provided to APM.
318+
To enable AEC end-to-end, open the output on the same `MediaDevices`
319+
instance after opening input with processing enabled. The APM will be
320+
automatically associated so reverse frames are provided for AEC.
320321
321322
Args:
322323
enable_aec: Enable acoustic echo cancellation.
@@ -341,11 +342,18 @@ def open_input(
341342
high_pass_filter=high_pass_filter,
342343
auto_gain_control=auto_gain_control,
343344
)
344-
delay_estimator: Optional[_APMDelayEstimator] = (
345-
_APMDelayEstimator() if apm is not None else None
346-
)
347-
# Store the shared estimator on the device helper so the output player can reuse it
348-
self._delay_estimator = delay_estimator
345+
# Ensure we have a shared delay estimator when processing is enabled
346+
if self._delay_estimator is None:
347+
self._delay_estimator = _APMDelayEstimator()
348+
# Store APM internally for automatic association with output
349+
self._apm = apm
350+
# Update existing output player so order of creation doesn't matter
351+
if self._output is not None:
352+
try:
353+
self._output._apm = self._apm
354+
self._output._delay_estimator = self._delay_estimator
355+
except Exception:
356+
pass
349357

350358
# Queue from callback to async task
351359
q: asyncio.Queue[AudioFrame] = asyncio.Queue(maxsize=queue_capacity)
@@ -439,20 +447,30 @@ async def _pump() -> None:
439447
def open_output(
440448
self,
441449
*,
442-
apm_for_reverse: Optional[AudioProcessingModule] = None,
443450
output_device: Optional[int] = None,
444451
) -> OutputPlayer:
445-
"""Create an `OutputPlayer` for rendering and (optionally) AEC reverse.
452+
"""Create an `OutputPlayer` for rendering.
446453
447454
Args:
448-
apm_for_reverse: Pass the APM used by the audio input device to enable AEC.
449455
output_device: Optional output device index (default system device if None).
450456
"""
451-
return OutputPlayer(
457+
# If an output player already exists, warn and return it
458+
if self._output is not None:
459+
logging.warning("OutputPlayer already created on this MediaDevices; returning existing instance")
460+
return self._output
461+
462+
# Ensure we have a shared delay estimator so output can report render delay
463+
if self._delay_estimator is None:
464+
self._delay_estimator = _APMDelayEstimator()
465+
466+
player = OutputPlayer(
452467
sample_rate=self._out_sr,
453468
num_channels=self._channels,
454469
blocksize=self._blocksize,
455-
apm_for_reverse=apm_for_reverse,
470+
apm_for_reverse=self._apm,
456471
output_device=output_device,
457472
delay_estimator=self._delay_estimator,
458473
)
474+
# Track player for future APM/delay updates when input is opened later
475+
self._output = player
476+
return player

0 commit comments

Comments
 (0)