mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-23 09:46:00 +00:00
[skip ci] feat(audio recording): adding new asyn start_recording, stop_recording and read functions to avoid for loop delays
This commit is contained in:
@@ -80,6 +80,7 @@ from lerobot.datasets.video_utils import (
|
|||||||
get_video_info,
|
get_video_info,
|
||||||
)
|
)
|
||||||
from lerobot.microphones import Microphone
|
from lerobot.microphones import Microphone
|
||||||
|
from lerobot.microphones.utils import async_microphones_start_recording
|
||||||
from lerobot.utils.constants import HF_LEROBOT_HOME
|
from lerobot.utils.constants import HF_LEROBOT_HOME
|
||||||
|
|
||||||
CODEBASE_VERSION = "v3.0"
|
CODEBASE_VERSION = "v3.0"
|
||||||
@@ -1312,7 +1313,7 @@ class LeRobotDataset(torch.utils.data.Dataset):
|
|||||||
|
|
||||||
self.episode_buffer["size"] += 1
|
self.episode_buffer["size"] += 1
|
||||||
|
|
||||||
def add_microphone_recording(self, microphone: Microphone, microphone_key: str) -> None:
|
def add_microphone_recording(self, microphone_key: str, microphone: Microphone) -> None:
|
||||||
"""
|
"""
|
||||||
Starts recording audio data provided by the microphone and directly writes it in a .wav file.
|
Starts recording audio data provided by the microphone and directly writes it in a .wav file.
|
||||||
"""
|
"""
|
||||||
@@ -1320,6 +1321,19 @@ class LeRobotDataset(torch.utils.data.Dataset):
|
|||||||
audio_file = self._get_raw_audio_file_path(self.num_episodes, "observation.audio." + microphone_key)
|
audio_file = self._get_raw_audio_file_path(self.num_episodes, "observation.audio." + microphone_key)
|
||||||
microphone.start_recording(output_file=audio_file)
|
microphone.start_recording(output_file=audio_file)
|
||||||
|
|
||||||
|
def add_microphones_recordings(self, microphones: dict[str, Microphone]) -> None:
|
||||||
|
"""
|
||||||
|
Starts recording audio data provided by multiple microphones and directly writes it in appropriate .wav files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
output_files = []
|
||||||
|
for microphone_key in microphones:
|
||||||
|
output_files.append(
|
||||||
|
self._get_raw_audio_file_path(self.num_episodes, "observation.audio." + microphone_key)
|
||||||
|
)
|
||||||
|
|
||||||
|
async_microphones_start_recording(microphones, output_files)
|
||||||
|
|
||||||
def save_episode(
|
def save_episode(
|
||||||
self,
|
self,
|
||||||
episode_data: dict | None = None,
|
episode_data: dict | None = None,
|
||||||
|
|||||||
@@ -185,9 +185,10 @@ class PortAudioMicrophone(Microphone):
|
|||||||
logging.warning(status)
|
logging.warning(status)
|
||||||
# Slicing makes copy unnecessary
|
# Slicing makes copy unnecessary
|
||||||
# Two separate queues are necessary because .get() also pops the data from the queue
|
# 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 self.is_writing:
|
if self.is_writing:
|
||||||
self.record_queue.put(indata[:, self.channels_index])
|
self.record_queue.put_nowait(indata[:, self.channels_index])
|
||||||
self.read_queue.put(indata[:, self.channels_index])
|
self.read_queue.put_nowait(indata[:, self.channels_index])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _record_loop(queue, event: Event, sample_rate: int, channels: list[int], output_file: Path) -> None:
|
def _record_loop(queue, event: Event, sample_rate: int, channels: list[int], output_file: Path) -> None:
|
||||||
@@ -206,8 +207,8 @@ class PortAudioMicrophone(Microphone):
|
|||||||
while not event.is_set():
|
while not event.is_set():
|
||||||
try:
|
try:
|
||||||
file.write(
|
file.write(
|
||||||
queue.get(timeout=0.02)
|
queue.get(timeout=0.01)
|
||||||
) # Timeout set as twice the usual sounddevice buffer size
|
) # Timeout set as the usual sounddevice buffer size. get_nowait is not possible here as it saturates the thread.
|
||||||
queue.task_done()
|
queue.task_done()
|
||||||
except Empty:
|
except Empty:
|
||||||
continue
|
continue
|
||||||
@@ -257,6 +258,7 @@ class PortAudioMicrophone(Microphone):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Starts the recording of the microphone. If output_file is provided, the audio will be written to this file.
|
Starts the recording of the microphone. If output_file is provided, the audio will be written to this file.
|
||||||
|
Remark: multiprocessing is implemented, but does not work well with sounddevice (launching delays, tricky memory sharing, sounddevice streams are not picklable (even with dill #pathos), etc.).
|
||||||
"""
|
"""
|
||||||
if not self.is_connected:
|
if not self.is_connected:
|
||||||
raise DeviceNotConnectedError(f"Microphone {self.microphone_index} is not connected.")
|
raise DeviceNotConnectedError(f"Microphone {self.microphone_index} is not connected.")
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from queue import Queue
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
from .configs import MicrophoneConfig
|
from .configs import MicrophoneConfig
|
||||||
from .microphone import Microphone
|
from .microphone import Microphone
|
||||||
|
|
||||||
@@ -28,3 +31,70 @@ def make_microphones_from_configs(microphone_configs: dict[str, MicrophoneConfig
|
|||||||
raise ValueError(f"The microphone type '{cfg.type}' is not valid.")
|
raise ValueError(f"The microphone type '{cfg.type}' is not valid.")
|
||||||
|
|
||||||
return microphones
|
return microphones
|
||||||
|
|
||||||
|
|
||||||
|
def async_microphones_start_recording(
|
||||||
|
microphones: dict[str, Microphone],
|
||||||
|
output_files: list[str | None] | None,
|
||||||
|
multiprocessing: bool = False,
|
||||||
|
overwrite: bool = True,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Starts recording on multiple microphones asynchronously to avoid delays
|
||||||
|
"""
|
||||||
|
|
||||||
|
start_recording_threads = []
|
||||||
|
if output_files is None:
|
||||||
|
output_files = [None] * len(microphones)
|
||||||
|
|
||||||
|
for microphone, output_file in zip(microphones.values(), output_files, strict=False):
|
||||||
|
start_recording_threads.append(
|
||||||
|
Thread(target=microphone.start_recording, args=(output_file, multiprocessing, overwrite))
|
||||||
|
)
|
||||||
|
|
||||||
|
for thread in start_recording_threads:
|
||||||
|
thread.start()
|
||||||
|
for thread in start_recording_threads:
|
||||||
|
thread.join()
|
||||||
|
|
||||||
|
|
||||||
|
def async_microphones_stop_recording(microphones: dict[str, Microphone]):
|
||||||
|
"""
|
||||||
|
Stops recording on multiple microphones asynchronously to avoid delays
|
||||||
|
"""
|
||||||
|
|
||||||
|
stop_recording_threads = []
|
||||||
|
|
||||||
|
for microphone in microphones.values():
|
||||||
|
stop_recording_threads.append(Thread(target=microphone.stop_recording))
|
||||||
|
|
||||||
|
for thread in stop_recording_threads:
|
||||||
|
thread.start()
|
||||||
|
for thread in stop_recording_threads:
|
||||||
|
thread.join()
|
||||||
|
|
||||||
|
|
||||||
|
def async_microphones_read(microphones: dict[str, Microphone]):
|
||||||
|
"""
|
||||||
|
Reads from multiple microphones asynchronously to avoid delays
|
||||||
|
"""
|
||||||
|
|
||||||
|
read_threads = []
|
||||||
|
read_queue = Queue()
|
||||||
|
|
||||||
|
for microphone_key, microphone in microphones.items():
|
||||||
|
read_threads.append(
|
||||||
|
Thread(
|
||||||
|
target=lambda microphone, output, microphone_key: output.put_nowait(
|
||||||
|
{microphone_key: microphone.read()}
|
||||||
|
),
|
||||||
|
args=(microphone, read_queue, microphone_key),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for thread in read_threads:
|
||||||
|
thread.start()
|
||||||
|
for thread in read_threads:
|
||||||
|
thread.join()
|
||||||
|
|
||||||
|
return dict(kv for d in read_queue.queue for kv in d.items())
|
||||||
|
|||||||
Reference in New Issue
Block a user