@@ -120,9 +120,6 @@ async def aclose(self) -> None:
120
120
class OutputPlayer :
121
121
"""Simple audio output helper using `sounddevice.OutputStream`.
122
122
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.
126
123
"""
127
124
128
125
def __init__ (
@@ -262,6 +259,10 @@ def __init__(
262
259
self ._channels = num_channels
263
260
self ._blocksize = blocksize
264
261
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
265
266
266
267
# Device enumeration
267
268
def list_input_devices (self ) -> list [dict [str , Any ]]:
@@ -314,9 +315,9 @@ def open_input(
314
315
an `AudioProcessingModule` is created and applied to each frame before it
315
316
is queued for `AudioSource.capture_frame`.
316
317
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 .
320
321
321
322
Args:
322
323
enable_aec: Enable acoustic echo cancellation.
@@ -341,11 +342,18 @@ def open_input(
341
342
high_pass_filter = high_pass_filter ,
342
343
auto_gain_control = auto_gain_control ,
343
344
)
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
349
357
350
358
# Queue from callback to async task
351
359
q : asyncio .Queue [AudioFrame ] = asyncio .Queue (maxsize = queue_capacity )
@@ -439,20 +447,30 @@ async def _pump() -> None:
439
447
def open_output (
440
448
self ,
441
449
* ,
442
- apm_for_reverse : Optional [AudioProcessingModule ] = None ,
443
450
output_device : Optional [int ] = None ,
444
451
) -> OutputPlayer :
445
- """Create an `OutputPlayer` for rendering and (optionally) AEC reverse .
452
+ """Create an `OutputPlayer` for rendering.
446
453
447
454
Args:
448
- apm_for_reverse: Pass the APM used by the audio input device to enable AEC.
449
455
output_device: Optional output device index (default system device if None).
450
456
"""
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 (
452
467
sample_rate = self ._out_sr ,
453
468
num_channels = self ._channels ,
454
469
blocksize = self ._blocksize ,
455
- apm_for_reverse = apm_for_reverse ,
470
+ apm_for_reverse = self . _apm ,
456
471
output_device = output_device ,
457
472
delay_estimator = self ._delay_estimator ,
458
473
)
474
+ # Track player for future APM/delay updates when input is opened later
475
+ self ._output = player
476
+ return player
0 commit comments