Commit ·
d63dd5f
1
Parent(s): 7c7622b
fix: Use push_audio_sample instead of play_sound to avoid GStreamer conflicts
Browse files
reachy_mini_ha_voice/audio_player.py
CHANGED
|
@@ -77,27 +77,14 @@ class AudioPlayer:
|
|
| 77 |
if self._stop_flag.is_set():
|
| 78 |
return
|
| 79 |
|
| 80 |
-
# Use Reachy Mini's
|
|
|
|
|
|
|
| 81 |
if self.reachy_mini is not None:
|
| 82 |
try:
|
| 83 |
-
|
| 84 |
-
self.reachy_mini.media.play_sound(file_path)
|
| 85 |
-
|
| 86 |
-
# Estimate playback duration and wait
|
| 87 |
-
import soundfile as sf
|
| 88 |
-
data, samplerate = sf.read(file_path)
|
| 89 |
-
duration = len(data) / samplerate
|
| 90 |
-
|
| 91 |
-
# Wait for playback to complete (with stop check)
|
| 92 |
-
start_time = time.time()
|
| 93 |
-
while time.time() - start_time < duration:
|
| 94 |
-
if self._stop_flag.is_set():
|
| 95 |
-
self.reachy_mini.media.clear_output_buffer()
|
| 96 |
-
break
|
| 97 |
-
time.sleep(0.1)
|
| 98 |
-
|
| 99 |
except Exception as e:
|
| 100 |
-
_LOGGER.warning("
|
| 101 |
self._play_file_fallback(file_path)
|
| 102 |
else:
|
| 103 |
self._play_file_fallback(file_path)
|
|
@@ -113,7 +100,7 @@ class AudioPlayer:
|
|
| 113 |
self._on_playback_finished()
|
| 114 |
|
| 115 |
def _play_file_fallback(self, file_path: str) -> None:
|
| 116 |
-
"""
|
| 117 |
import sounddevice as sd
|
| 118 |
import soundfile as sf
|
| 119 |
|
|
@@ -123,8 +110,61 @@ class AudioPlayer:
|
|
| 123 |
data = data * self._current_volume
|
| 124 |
|
| 125 |
if not self._stop_flag.is_set():
|
| 126 |
-
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
def _on_playback_finished(self) -> None:
|
| 130 |
"""Called when playback is finished."""
|
|
|
|
| 77 |
if self._stop_flag.is_set():
|
| 78 |
return
|
| 79 |
|
| 80 |
+
# Use Reachy Mini's push_audio_sample for safe playback
|
| 81 |
+
# This writes directly to the existing GStreamer pipeline
|
| 82 |
+
# instead of creating a new one (which can cause conflicts)
|
| 83 |
if self.reachy_mini is not None:
|
| 84 |
try:
|
| 85 |
+
self._play_via_push_audio(file_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
except Exception as e:
|
| 87 |
+
_LOGGER.warning("push_audio_sample failed, trying sounddevice: %s", e)
|
| 88 |
self._play_file_fallback(file_path)
|
| 89 |
else:
|
| 90 |
self._play_file_fallback(file_path)
|
|
|
|
| 100 |
self._on_playback_finished()
|
| 101 |
|
| 102 |
def _play_file_fallback(self, file_path: str) -> None:
|
| 103 |
+
"""Play audio using sounddevice (avoids GStreamer conflicts)."""
|
| 104 |
import sounddevice as sd
|
| 105 |
import soundfile as sf
|
| 106 |
|
|
|
|
| 110 |
data = data * self._current_volume
|
| 111 |
|
| 112 |
if not self._stop_flag.is_set():
|
| 113 |
+
try:
|
| 114 |
+
# Try to find the Reachy Mini audio output device
|
| 115 |
+
devices = sd.query_devices()
|
| 116 |
+
output_device = None
|
| 117 |
+
for i, dev in enumerate(devices):
|
| 118 |
+
if dev['max_output_channels'] > 0:
|
| 119 |
+
name = dev['name'].lower()
|
| 120 |
+
# Look for Reachy Mini's audio device
|
| 121 |
+
if 'reachymini' in name or 'reachy' in name or 'usb' in name:
|
| 122 |
+
output_device = i
|
| 123 |
+
break
|
| 124 |
+
|
| 125 |
+
if output_device is not None:
|
| 126 |
+
sd.play(data, samplerate, device=output_device)
|
| 127 |
+
else:
|
| 128 |
+
sd.play(data, samplerate)
|
| 129 |
+
sd.wait()
|
| 130 |
+
except Exception as e:
|
| 131 |
+
_LOGGER.warning("sounddevice playback failed: %s", e)
|
| 132 |
+
|
| 133 |
+
def _play_via_push_audio(self, file_path: str) -> None:
|
| 134 |
+
"""Play audio by pushing samples to Reachy Mini's GStreamer pipeline.
|
| 135 |
+
|
| 136 |
+
This method writes audio directly to the existing GStreamer pipeline
|
| 137 |
+
instead of creating a new one, avoiding conflicts that can crash the daemon.
|
| 138 |
+
"""
|
| 139 |
+
import soundfile as sf
|
| 140 |
+
|
| 141 |
+
# Read audio file
|
| 142 |
+
data, input_samplerate = sf.read(file_path, dtype='float32')
|
| 143 |
+
|
| 144 |
+
# Get output sample rate from Reachy Mini
|
| 145 |
+
output_samplerate = self.reachy_mini.media.get_output_audio_samplerate()
|
| 146 |
+
|
| 147 |
+
# Convert to mono if stereo
|
| 148 |
+
if data.ndim == 2:
|
| 149 |
+
data = data.mean(axis=1)
|
| 150 |
+
|
| 151 |
+
# Apply volume
|
| 152 |
+
data = data * self._current_volume
|
| 153 |
+
|
| 154 |
+
# Resample if needed
|
| 155 |
+
if input_samplerate != output_samplerate:
|
| 156 |
+
num_samples = int(len(data) * output_samplerate / input_samplerate)
|
| 157 |
+
data = scipy.signal.resample(data, num_samples)
|
| 158 |
+
|
| 159 |
+
# Push audio in chunks (similar to conversation_app)
|
| 160 |
+
chunk_size = int(output_samplerate * 0.1) # 100ms chunks
|
| 161 |
+
for i in range(0, len(data), chunk_size):
|
| 162 |
+
if self._stop_flag.is_set():
|
| 163 |
+
break
|
| 164 |
+
chunk = data[i:i + chunk_size].astype(np.float32)
|
| 165 |
+
self.reachy_mini.media.push_audio_sample(chunk)
|
| 166 |
+
# Small sleep to prevent buffer overflow
|
| 167 |
+
time.sleep(0.05)
|
| 168 |
|
| 169 |
def _on_playback_finished(self) -> None:
|
| 170 |
"""Called when playback is finished."""
|