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
@@ -18,7 +18,11 @@ Provides the PortAudioMicrophone class for capturing audio from microphones usin
import logging import logging
import time 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 pathlib import Path
from queue import Empty from queue import Empty
from threading import Barrier, Event, Event as thread_Event, Thread 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 ..microphone import Microphone
from .configuration_portaudio import PortAudioMicrophoneConfig from .configuration_portaudio import PortAudioMicrophoneConfig
logger = logging.getLogger(__name__)
class PortAudioMicrophone(Microphone): class PortAudioMicrophone(Microphone):
""" """
@@ -152,7 +158,7 @@ class PortAudioMicrophone(Microphone):
return found_microphones_info[0] return found_microphones_info[0]
if len(found_microphones_info) == 0: if len(found_microphones_info) == 0:
logging.warning("No microphone found !") logger.warning("No microphone found !")
return found_microphones_info return found_microphones_info
@@ -206,7 +212,7 @@ class PortAudioMicrophone(Microphone):
) )
else: else:
if self.sample_rate < actual_sample_rate: 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." "Provided sample rate is lower than the sample rate of the microphone. Performance may be impacted."
) )
else: else:
@@ -220,9 +226,7 @@ class PortAudioMicrophone(Microphone):
] ]
if self.channels is not None and len(self.channels) > 0: if self.channels is not None and len(self.channels) > 0:
if any( if not all(channel in actual_channels for channel in self.channels):
all(c > actual_channels) or c <= 0 or not isinstance(c, np.integer) for c in self.channels
):
raise RuntimeError( raise RuntimeError(
f"Some of the provided channels {self.channels} are outside the possible channel range of the microphone {actual_channels}." 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.daemon = True
self.record_process.start() 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: if not self.is_connected:
raise RuntimeError(f"Error connecting microphone {self.microphone_index}.") raise RuntimeError(f"Error connecting microphone {self.microphone_index}.")
logger.info(f"{self} connected.")
def disconnect(self) -> None: def disconnect(self) -> None:
""" """
Disconnects the microphone and stops the recording. Disconnects the microphone and stops the recording.
@@ -295,6 +303,8 @@ class PortAudioMicrophone(Microphone):
if self.is_connected: if self.is_connected:
raise RuntimeError(f"Error disconnecting microphone {self.microphone_index}.") raise RuntimeError(f"Error disconnecting microphone {self.microphone_index}.")
logger.info(f"{self} disconnected.")
def _read(self) -> np.ndarray: def _read(self) -> np.ndarray:
""" """
Thread/Process-safe callback to read available audio data Thread/Process-safe callback to read available audio data
@@ -356,15 +366,13 @@ class PortAudioMicrophone(Microphone):
Low-level sounddevice callback. Low-level sounddevice callback.
""" """
if status: if status:
logging.warning(status) logger.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.
if audio_callback_start_event.is_set(): if audio_callback_start_event.is_set():
write_queue.put_nowait(indata[:, channels_index]) write_queue.put_nowait(indata[:, channels_index])
read_queue.put_nowait(indata[:, channels_index]) read_queue.put_nowait(indata[:, channels_index])
# Create the audio stream # Create the audio stream
# InputStream must be instantiated in the process as it is not pickable.
stream = sounddevice_sdk.InputStream( stream = sounddevice_sdk.InputStream(
device=microphone_index, device=microphone_index,
samplerate=sample_rate, samplerate=sample_rate,
@@ -385,9 +393,8 @@ class PortAudioMicrophone(Microphone):
stream.start() stream.start()
record_is_started_event.set() record_is_started_event.set()
record_stop_event.wait() 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() record_is_started_event.clear()
# Remark : stream.abort() flushes the buffers !
stream.close() stream.close()
def start_recording( def start_recording(
@@ -488,6 +495,11 @@ class PortAudioMicrophone(Microphone):
self.write_stop_event.set() self.write_stop_event.set()
self.write_thread.join() 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: if self.is_recording:
raise RuntimeError(f"Error stopping recording for microphone {self.microphone_index}.") raise RuntimeError(f"Error stopping recording for microphone {self.microphone_index}.")
if self.is_writing: if self.is_writing:
@@ -516,6 +528,10 @@ class PortAudioMicrophone(Microphone):
except Empty: except Empty:
continue continue
def __del__(self) -> None:
if self.is_connected:
self.disconnect()
@staticmethod @staticmethod
def _clear_queue(queue, join_queue: bool = False): def _clear_queue(queue, join_queue: bool = False):
""" """