fix(Microphone): adding proper logger definition, timeout for end of recording monitoring and failsafe __del__ method

This commit is contained in:
CarolinePascal
2025-08-09 01:22:22 +02:00
parent af2f044f5a
commit 9271a0c900

View File

@@ -18,7 +18,11 @@ Provides the PortAudioMicrophone class for capturing audio from microphones usin
import logging
import time
from multiprocessing import Event as process_Event, JoinableQueue as process_Queue, Process
from multiprocessing import (
Event as process_Event,
JoinableQueue as process_Queue,
Process,
)
from pathlib import Path
from queue import Empty
from threading import Barrier, Event, Event as thread_Event, Thread
@@ -38,6 +42,8 @@ from lerobot.microphones.portaudio.interface_sounddevice_sdk import ISounddevice
from ..microphone import Microphone
from .configuration_portaudio import PortAudioMicrophoneConfig
logger = logging.getLogger(__name__)
class PortAudioMicrophone(Microphone):
"""
@@ -152,7 +158,7 @@ class PortAudioMicrophone(Microphone):
return found_microphones_info[0]
if len(found_microphones_info) == 0:
logging.warning("No microphone found !")
logger.warning("No microphone found !")
return found_microphones_info
@@ -206,7 +212,7 @@ class PortAudioMicrophone(Microphone):
)
else:
if self.sample_rate < actual_sample_rate:
logging.warning(
logger.warning(
"Provided sample rate is lower than the sample rate of the microphone. Performance may be impacted."
)
else:
@@ -220,9 +226,7 @@ class PortAudioMicrophone(Microphone):
]
if self.channels is not None and len(self.channels) > 0:
if any(
all(c > actual_channels) or c <= 0 or not isinstance(c, np.integer) for c in self.channels
):
if not all(channel in actual_channels for channel in self.channels):
raise RuntimeError(
f"Some of the provided channels {self.channels} are outside the possible channel range of the microphone {actual_channels}."
)
@@ -273,10 +277,14 @@ class PortAudioMicrophone(Microphone):
self.record_process.daemon = True
self.record_process.start()
time.sleep(0.1) # Wait for the recording process to be started...
time.sleep(
0.1
) # Wait for the recording process to be started, and to potentially raise an error on failure.
if not self.is_connected:
raise RuntimeError(f"Error connecting microphone {self.microphone_index}.")
logger.info(f"{self} connected.")
def disconnect(self) -> None:
"""
Disconnects the microphone and stops the recording.
@@ -295,6 +303,8 @@ class PortAudioMicrophone(Microphone):
if self.is_connected:
raise RuntimeError(f"Error disconnecting microphone {self.microphone_index}.")
logger.info(f"{self} disconnected.")
def _read(self) -> np.ndarray:
"""
Thread/Process-safe callback to read available audio data
@@ -356,15 +366,13 @@ class PortAudioMicrophone(Microphone):
Low-level sounddevice callback.
"""
if status:
logging.warning(status)
# Slicing makes copy unnecessary
# Two separate queues are necessary because .get() also pops the data from the queue
# Remark: this also ensures that file-recorded data and chunk-audio data are the same.
logger.warning(status)
if audio_callback_start_event.is_set():
write_queue.put_nowait(indata[:, channels_index])
read_queue.put_nowait(indata[:, channels_index])
# Create the audio stream
# InputStream must be instantiated in the process as it is not pickable.
stream = sounddevice_sdk.InputStream(
device=microphone_index,
samplerate=sample_rate,
@@ -385,9 +393,8 @@ class PortAudioMicrophone(Microphone):
stream.start()
record_is_started_event.set()
record_stop_event.wait()
stream.stop() # stream.stop() waits for all buffers to be processed
stream.stop() # stream.stop() waits for all buffers to be processed, stream.abort() flushes the buffers !
record_is_started_event.clear()
# Remark : stream.abort() flushes the buffers !
stream.close()
def start_recording(
@@ -488,6 +495,11 @@ class PortAudioMicrophone(Microphone):
self.write_stop_event.set()
self.write_thread.join()
timeout = 1.0
while self.is_recording and timeout > 0:
time.sleep(0.01)
timeout -= 0.01
if self.is_recording:
raise RuntimeError(f"Error stopping recording for microphone {self.microphone_index}.")
if self.is_writing:
@@ -516,6 +528,10 @@ class PortAudioMicrophone(Microphone):
except Empty:
continue
def __del__(self) -> None:
if self.is_connected:
self.disconnect()
@staticmethod
def _clear_queue(queue, join_queue: bool = False):
"""