Desmond-Dong commited on
Commit
d63dd5f
·
1 Parent(s): 7c7622b

fix: Use push_audio_sample instead of play_sound to avoid GStreamer conflicts

Browse files
Files changed (1) hide show
  1. reachy_mini_ha_voice/audio_player.py +61 -21
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 media system if available
 
 
81
  if self.reachy_mini is not None:
82
  try:
83
- # Use Reachy Mini's play_sound method
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("Reachy Mini audio failed, falling back to sounddevice: %s", e)
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
- """Fallback to sounddevice for audio playback."""
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
- sd.play(data, samplerate)
127
- sd.wait()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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."""