Skip to content

Commit 2503dfd

Browse files
committed
bug #397: new --fix-audio argument for fixing wrongly initialized respeaker
1 parent a04ab09 commit 2503dfd

File tree

4 files changed

+54
-104
lines changed

4 files changed

+54
-104
lines changed

src/reachy_mini/daemon/app/main.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ class Args:
5555

5656
localhost_only: bool | None = None
5757

58+
fix_audio: bool = False
59+
5860

5961
def create_app(args: Args, health_check_event: asyncio.Event | None = None) -> FastAPI:
6062
"""Create and configure the FastAPI application."""
@@ -152,6 +154,17 @@ def run_app(args: Args) -> None:
152154
"""Run the FastAPI app with Uvicorn."""
153155
logging.basicConfig(level=logging.INFO)
154156

157+
if args.fix_audio:
158+
"""Temporary fix for audio issues with beta version."""
159+
logging.info("Applying audio fix for beta version.")
160+
161+
from reachy_mini.media.audio_control_utils import init_respeaker_usb
162+
163+
respeaker = init_respeaker_usb()
164+
respeaker.write("REBOOT", [1])
165+
respeaker.close()
166+
logging.debug("Respeaker rebooted.")
167+
155168
health_check_event = asyncio.Event()
156169
app = create_app(args, health_check_event)
157170

@@ -314,6 +327,12 @@ def main() -> None:
314327
help="Set the logging level (default: INFO).",
315328
)
316329

330+
parser.add_argument(
331+
"--fix-audio",
332+
action="store_true",
333+
help="Fix audio issues with beta version (default: False).",
334+
)
335+
317336
args = parser.parse_args()
318337
run_app(Args(**vars(args)))
319338

src/reachy_mini/media/audio_base.py

Lines changed: 7 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,30 @@
55
"""
66

77
import logging
8-
import struct
98
from abc import ABC, abstractmethod
10-
from typing import List, Optional
9+
from typing import Optional
1110

1211
import numpy as np
1312
import numpy.typing as npt
14-
import usb
15-
from libusb_package import get_libusb1_backend
13+
14+
from reachy_mini.media.audio_control_utils import ReSpeaker, init_respeaker_usb
1615

1716

1817
class AudioBase(ABC):
1918
"""Abstract class for opening and managing audio devices."""
2019

2120
SAMPLE_RATE = 16000 # respeaker samplerate
22-
TIMEOUT = 100000
23-
PARAMETERS = {
24-
"VERSION": (48, 0, 4, "ro", "uint8"),
25-
"AEC_AZIMUTH_VALUES": (33, 75, 16 + 1, "ro", "radians"),
26-
"DOA_VALUE": (20, 18, 4 + 1, "ro", "uint16"),
27-
"DOA_VALUE_RADIANS": (20, 19, 8 + 1, "ro", "radians"),
28-
}
2921

3022
def __init__(self, log_level: str = "INFO") -> None:
3123
"""Initialize the audio device."""
3224
self.logger = logging.getLogger(__name__)
3325
self.logger.setLevel(log_level)
34-
self._respeaker = self._init_respeaker_usb()
35-
# name, resid, cmdid, length, type
26+
self._respeaker: ReSpeaker = init_respeaker_usb()
3627

3728
def __del__(self) -> None:
3829
"""Destructor to ensure resources are released."""
3930
if self._respeaker:
40-
usb.util.dispose_resources(self._respeaker)
31+
self._respeaker.close()
4132

4233
@abstractmethod
4334
def start_recording(self) -> None:
@@ -79,63 +70,6 @@ def play_sound(self, sound_file: str) -> None:
7970
"""
8071
pass
8172

82-
def _init_respeaker_usb(self) -> Optional[usb.core.Device]:
83-
try:
84-
dev = usb.core.find(
85-
idVendor=0x2886, idProduct=0x001A, backend=get_libusb1_backend()
86-
)
87-
return dev
88-
except usb.core.NoBackendError:
89-
self.logger.error(
90-
"No USB backend was found ! Make sure libusb_package is correctly installed with `pip install libusb_package`."
91-
)
92-
return None
93-
94-
def _read_usb(self, name: str) -> Optional[List[int] | List[float]]:
95-
try:
96-
data = self.PARAMETERS[name]
97-
except KeyError:
98-
self.logger.error(f"Unknown parameter: {name}")
99-
return None
100-
101-
if not self._respeaker:
102-
self.logger.warning("ReSpeaker device not found.")
103-
return None
104-
105-
resid = data[0]
106-
cmdid = 0x80 | data[1]
107-
length = data[2]
108-
109-
response = self._respeaker.ctrl_transfer(
110-
usb.util.CTRL_IN
111-
| usb.util.CTRL_TYPE_VENDOR
112-
| usb.util.CTRL_RECIPIENT_DEVICE,
113-
0,
114-
cmdid,
115-
resid,
116-
length,
117-
self.TIMEOUT,
118-
)
119-
120-
self.logger.debug(f"Response for {name}: {response}")
121-
122-
result: Optional[List[float] | List[int]] = None
123-
if data[4] == "uint8":
124-
result = response.tolist()
125-
elif data[4] == "radians":
126-
byte_data = response.tobytes()
127-
num_values = (data[2] - 1) / 4
128-
match_str = "<"
129-
for i in range(int(num_values)):
130-
match_str += "f"
131-
result = [
132-
float(x) for x in struct.unpack(match_str, byte_data[1 : data[2]])
133-
]
134-
elif data[4] == "uint16":
135-
result = response.tolist()
136-
137-
return result
138-
13973
def get_DoA(self) -> tuple[float, bool] | None:
14074
"""Get the Direction of Arrival (DoA) value from the ReSpeaker device.
14175
@@ -153,7 +87,8 @@ def get_DoA(self) -> tuple[float, bool] | None:
15387
if not self._respeaker:
15488
self.logger.warning("ReSpeaker device not found.")
15589
return None
156-
result = self._read_usb("DOA_VALUE_RADIANS")
90+
91+
result = self._respeaker.read("DOA_VALUE_RADIANS")
15792
if result is None:
15893
return None
15994
return float(result[0]), bool(result[1])

src/reachy_mini/media/audio_sounddevice.py

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@ def __init__(
3030
self.stream = None
3131
self._output_stream = None
3232
self._buffer: List[npt.NDArray[np.float32]] = []
33-
self._output_device_id = self.get_output_device_id("respeaker")
34-
self._input_device_id = self.get_input_device_id("respeaker")
33+
self._output_device_id = self.get_device_id(
34+
["Reachy Mini Audio", "respeaker"], device_io_type="output"
35+
)
36+
self._input_device_id = self.get_device_id(
37+
["Reachy Mini Audio", "respeaker"], device_io_type="input"
38+
)
3539

3640
def start_recording(self) -> None:
3741
"""Open the audio input stream, using ReSpeaker card if available."""
@@ -181,43 +185,27 @@ def _clean_up_thread() -> None:
181185
daemon=True,
182186
).start()
183187

184-
def get_output_device_id(self, name_contains: str) -> int:
188+
def get_device_id(
189+
self, names_contains: List[str], device_io_type: str = "output"
190+
) -> int:
185191
"""Return the output device id whose name contains the given string (case-insensitive).
186192
187193
If not found, return the default output device id.
188194
"""
189195
devices = sd.query_devices()
190196

191197
for idx, dev in enumerate(devices):
192-
if (
193-
name_contains.lower() in dev["name"].lower()
194-
and dev["max_output_channels"] > 0
195-
):
196-
return idx
198+
for name_contains in names_contains:
199+
if (
200+
name_contains.lower() in dev["name"].lower()
201+
and dev[f"max_{device_io_type}_channels"] > 0
202+
):
203+
return idx
197204
# Return default output device if not found
198205
self.logger.warning(
199-
f"No output device found containing '{name_contains}', using default."
200-
)
201-
return self._safe_query_device("output")
202-
203-
def get_input_device_id(self, name_contains: str) -> int:
204-
"""Return the input device id whose name contains the given string (case-insensitive).
205-
206-
If not found, return the default input device id.
207-
"""
208-
devices = sd.query_devices()
209-
210-
for idx, dev in enumerate(devices):
211-
if (
212-
name_contains.lower() in dev["name"].lower()
213-
and dev["max_input_channels"] > 0
214-
):
215-
return idx
216-
# Return default input device if not found
217-
self.logger.warning(
218-
f"No input device found containing '{name_contains}', using default."
206+
f"No {device_io_type} device found containing '{name_contains}', using default."
219207
)
220-
return self._safe_query_device("input")
208+
return self._safe_query_device(device_io_type)
221209

222210
def _safe_query_device(self, kind: str) -> int:
223211
try:

src/reachy_mini/media/audio_utils.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,20 @@ def get_respeaker_card_number() -> int:
1414

1515
lines = output.split("\n")
1616
for line in lines:
17-
if "respeaker" in line.lower() and "card" in line:
17+
if "reachy mini audio" in line.lower() and "card" in line:
1818
card_number = line.split(":")[0].split("card ")[1].strip()
19-
logging.debug(f"Found ReSpeaker sound card: {card_number}")
19+
logging.debug(f"Found Reachy Mini Audio sound card: {card_number}")
20+
return int(card_number)
21+
elif "respeaker" in line.lower() and "card" in line:
22+
card_number = line.split(":")[0].split("card ")[1].strip()
23+
logging.warning(
24+
f"Found ReSpeaker sound card: {card_number}. Please update firmware!"
25+
)
2026
return int(card_number)
2127

22-
logging.warning("ReSpeaker sound card not found. Returning default card")
28+
logging.warning(
29+
"Reachy Mini Audio sound card not found. Returning default card"
30+
)
2331
return 0 # default sound card
2432

2533
except subprocess.CalledProcessError as e:

0 commit comments

Comments
 (0)