Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1915d3fce3 | ||
|
|
a2bb7c8f57 | ||
|
|
cc9b33e8b8 | ||
|
|
862d102405 | ||
|
|
65c123ffb9 | ||
|
|
6d11883dcb | ||
|
|
2f4276e180 | ||
|
|
397d4c8b61 | ||
|
|
f6c0d29405 | ||
|
|
a80c7724a6 | ||
|
|
2e046c6b8b | ||
|
|
5d3c243a88 | ||
|
|
2b394504a0 | ||
|
|
c6d1dec918 | ||
|
|
2f6ec1d3b0 | ||
|
|
41f107548f | ||
|
|
f0297415ed | ||
|
|
69ceeef566 | ||
|
|
886019e04f | ||
|
|
61ae4ce68c | ||
|
|
00188fa5da | ||
|
|
8a5f3fb409 |
+11
-1
@@ -6,7 +6,7 @@ This tutorial should work for almost any Raspberry Pi and USB microphone. Audio
|
||||
|
||||
## Install OS
|
||||
|
||||
Follow instructions to [install Raspberry Pi OS](https://www.raspberrypi.com/software/). Under "Choose OS", pick "Raspberry Pi OS (other)" and "Raspberry Pi OS (Legacy, **64-bit**) Lite".
|
||||
Follow instructions to [install Raspberry Pi OS](https://www.raspberrypi.com/software/). Under "Choose OS", pick "Raspberry Pi OS (other)" and "Raspberry Pi OS (**64-bit**) Lite".
|
||||
|
||||
When asking if you'd like to apply customization settings, choose "Edit Settings" and:
|
||||
|
||||
@@ -305,6 +305,13 @@ python3 -m venv --system-site-packages .venv
|
||||
.venv/bin/pip3 install 'wyoming==1.5.2'
|
||||
```
|
||||
|
||||
In case you use a ReSpeaker USB 4mic array v2.0, additionally install pixel-ring:
|
||||
|
||||
```sh
|
||||
.venv/bin/pip3 install 'pixel-ring'
|
||||
```
|
||||
|
||||
|
||||
The `--system-site-packages` argument is used to access the pre-installed `gpiozero` and `spidev` Python packages. If these are **not already installed** in your system, run:
|
||||
|
||||
```sh
|
||||
@@ -385,4 +392,7 @@ Try a voice command and see if the LEDs change. Use `journalctl` to check the lo
|
||||
journalctl -u 2mic_leds.service -f
|
||||
```
|
||||
|
||||
If you encounter any issues, you can add the `--debug` argument to the command line to increase the log level.
|
||||
To control the brightness of the LEDS, use the `--led-brightness ` argument, which accepts integer numbers from 1 to 31.
|
||||
|
||||
Make sure to run `sudo systemctl daemon-reload` every time you make changes to the service.
|
||||
|
||||
@@ -4,7 +4,7 @@ Create a voice satellite using a Raspberry Pi 3+ and USB microphone and speakers
|
||||
|
||||
## Install OS
|
||||
|
||||
Follow instructions to [install Raspberry Pi OS](https://www.raspberrypi.com/software/). Under "Choose OS", pick "Raspberry Pi OS (other)" and "Raspberry Pi OS (Legacy, **64-bit**) Lite".
|
||||
Follow instructions to [install Raspberry Pi OS](https://www.raspberrypi.com/software/). Under "Choose OS", pick "Raspberry Pi OS (other)" and "Raspberry Pi OS (**64-bit**) Lite".
|
||||
|
||||
When asking if you'd like to apply customization settings, choose "Edit Settings" and:
|
||||
|
||||
|
||||
@@ -43,12 +43,18 @@ mod='seeed-voicecard'
|
||||
src='./'
|
||||
kernel="$(uname -r)"
|
||||
marker='0.0.0'
|
||||
threads="$(getconf _NPROCESSORS_ONLN)"
|
||||
memory="$(LANG=C free -m|awk '/^Mem:/{print $2}')"
|
||||
|
||||
if [ "${memory}" -le 512 ] && [ "${threads}" -gt 2 ]; then
|
||||
threads=2
|
||||
fi
|
||||
|
||||
mkdir -p "/usr/src/${mod}-${ver}"
|
||||
cp -a "${src}"/* "/usr/src/${mod}-${ver}/"
|
||||
|
||||
dkms add -m "${mod}" -v "${ver}"
|
||||
dkms build -k "${kernel}" -m "${mod}" -v "${ver}" && {
|
||||
dkms build -k "${kernel}" -m "${mod}" -v "${ver}" -j "${threads}" && {
|
||||
dkms install --force -k "${kernel}" -m "${mod}" -v "${ver}"
|
||||
}
|
||||
|
||||
|
||||
@@ -41,8 +41,14 @@ async def main() -> None:
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--uri", required=True, help="unix:// or tcp://")
|
||||
#
|
||||
parser.add_argument("--debug", action="store_true", help="Log DEBUG messages")
|
||||
parser.add_argument(
|
||||
"--led-brightness",
|
||||
type=int,
|
||||
choices=range(1, 32),
|
||||
default=31,
|
||||
help="LED brightness (integer from 1 to 31)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
|
||||
@@ -54,7 +60,7 @@ async def main() -> None:
|
||||
led_power = gpiozero.LED(LEDS_GPIO, active_high=False)
|
||||
led_power.on()
|
||||
|
||||
leds = APA102(num_led=NUM_LEDS)
|
||||
leds = APA102(num_led=NUM_LEDS, global_brightness=args.led_brightness)
|
||||
|
||||
# Start server
|
||||
server = AsyncServer.from_uri(args.uri)
|
||||
@@ -148,7 +154,7 @@ class APA102:
|
||||
def __init__(
|
||||
self,
|
||||
num_led,
|
||||
global_brightness=MAX_BRIGHTNESS,
|
||||
global_brightness,
|
||||
order="rgb",
|
||||
bus=0,
|
||||
device=1,
|
||||
@@ -162,6 +168,7 @@ class APA102:
|
||||
self.global_brightness = self.MAX_BRIGHTNESS
|
||||
else:
|
||||
self.global_brightness = global_brightness
|
||||
_LOGGER.debug("LED brightness: %d", self.global_brightness)
|
||||
|
||||
self.leds = [self.LED_START, 0, 0, 0] * self.num_led # Pixel buffer
|
||||
self.spi = spidev.SpiDev() # Init the SPI device
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Controls the LEDs on the ReSpeaker Mic Array v2.0 (USB) ."""
|
||||
import argparse
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from functools import partial
|
||||
|
||||
from wyoming.event import Event
|
||||
from wyoming.satellite import (
|
||||
SatelliteConnected,
|
||||
SatelliteDisconnected,
|
||||
StreamingStarted,
|
||||
StreamingStopped,
|
||||
)
|
||||
from wyoming.snd import Played
|
||||
from wyoming.server import AsyncEventHandler, AsyncServer
|
||||
from wyoming.vad import VoiceStarted, VoiceStopped
|
||||
from wyoming.wake import Detection
|
||||
|
||||
from pixel_ring import pixel_ring
|
||||
|
||||
_LOGGER = logging.getLogger()
|
||||
|
||||
async def main() -> None:
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--uri", required=True, help="unix:// or tcp://")
|
||||
parser.add_argument("--debug", action="store_true", help="Log DEBUG messages")
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
|
||||
_LOGGER.debug(args)
|
||||
|
||||
_LOGGER.info("Ready")
|
||||
|
||||
# Turn on power to LEDs
|
||||
pixel_ring.set_color_palette(0x0080ff, 0x007a37)
|
||||
pixel_ring.think()
|
||||
await asyncio.sleep(3)
|
||||
pixel_ring.off()
|
||||
|
||||
# Start server
|
||||
server = AsyncServer.from_uri(args.uri)
|
||||
|
||||
try:
|
||||
await server.run(partial(LEDsEventHandler, args))
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
pixel_ring.off()
|
||||
|
||||
|
||||
class LEDsEventHandler(AsyncEventHandler):
|
||||
"""Event handler for clients."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cli_args: argparse.Namespace,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.cli_args = cli_args
|
||||
self.client_id = str(time.monotonic_ns())
|
||||
|
||||
_LOGGER.debug("Client connected: %s", self.client_id)
|
||||
|
||||
async def handle_event(self, event: Event) -> bool:
|
||||
_LOGGER.debug(event)
|
||||
|
||||
if Detection.is_type(event.type):
|
||||
_LOGGER.debug("Detection")
|
||||
pixel_ring.wakeup()
|
||||
elif VoiceStarted.is_type(event.type):
|
||||
_LOGGER.debug("VoiceStarted")
|
||||
pixel_ring.speak()
|
||||
elif VoiceStopped.is_type(event.type):
|
||||
_LOGGER.debug("VoiceStopped")
|
||||
pixel_ring.spin()
|
||||
elif StreamingStopped.is_type(event.type):
|
||||
_LOGGER.debug("StreamingStopped")
|
||||
pixel_ring.off()
|
||||
elif SatelliteConnected.is_type(event.type):
|
||||
_LOGGER.debug("SatelliteConnected")
|
||||
pixel_ring.think()
|
||||
await asyncio.sleep(2)
|
||||
pixel_ring.off()
|
||||
elif Played.is_type(event.type):
|
||||
_LOGGER.debug("Played")
|
||||
pixel_ring.off()
|
||||
elif SatelliteDisconnected.is_type(event.type):
|
||||
_LOGGER.debug("SatelliteDisconnected")
|
||||
pixel_ring.mono(0xff0000)
|
||||
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import subprocess
|
||||
import venv
|
||||
from pathlib import Path
|
||||
|
||||
_DIR = Path(__file__).parent
|
||||
_PROGRAM_DIR = _DIR.parent
|
||||
_VENV_DIR = _PROGRAM_DIR / ".venv"
|
||||
|
||||
context = venv.EnvBuilder().ensure_directories(_VENV_DIR)
|
||||
subprocess.check_call(
|
||||
[context.env_exe, str(_PROGRAM_DIR / "examples" / "usbmic_service.py")]
|
||||
+ sys.argv[1:]
|
||||
)
|
||||
@@ -308,14 +308,14 @@ async def main() -> None:
|
||||
try:
|
||||
import webrtc_noise_gain # noqa: F401
|
||||
except ImportError:
|
||||
_LOGGER.fatal("Install extras for webrtc")
|
||||
_LOGGER.exception("Extras for webrtc are not installed")
|
||||
sys.exit(1)
|
||||
|
||||
if needs_silero(args):
|
||||
try:
|
||||
import pysilero_vad # noqa: F401
|
||||
except ImportError:
|
||||
_LOGGER.fatal("Install extras for silerovad")
|
||||
_LOGGER.exception("Extras for silerovad are not installed")
|
||||
sys.exit(1)
|
||||
|
||||
if args.awake_wav and (not Path(args.awake_wav).is_file()):
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"""Satellite code."""
|
||||
|
||||
import array
|
||||
import asyncio
|
||||
import logging
|
||||
import math
|
||||
import time
|
||||
import wave
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, auto
|
||||
from pathlib import Path
|
||||
from typing import Callable, Dict, Final, List, Optional, Set, Union
|
||||
@@ -60,6 +62,12 @@ class State(Enum):
|
||||
STOPPED = auto()
|
||||
|
||||
|
||||
@dataclass
|
||||
class SoundEvent:
|
||||
event: Event
|
||||
is_tts: bool
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -76,7 +84,7 @@ class SatelliteBase:
|
||||
self._mic_task: Optional[asyncio.Task] = None
|
||||
self._mic_webrtc: Optional[Callable[[bytes], bytes]] = None
|
||||
self._snd_task: Optional[asyncio.Task] = None
|
||||
self._snd_queue: "Optional[asyncio.Queue[Event]]" = None
|
||||
self._snd_queue: "Optional[asyncio.Queue[SoundEvent]]" = None
|
||||
self._wake_task: Optional[asyncio.Task] = None
|
||||
self._wake_queue: "Optional[asyncio.Queue[Event]]" = None
|
||||
self._event_task: Optional[asyncio.Task] = None
|
||||
@@ -507,9 +515,11 @@ class SatelliteBase:
|
||||
event = AudioChunk(
|
||||
rate=chunk.rate,
|
||||
width=chunk.width,
|
||||
channels=chunk.channels
|
||||
channels=(
|
||||
chunk.channels
|
||||
if (self.settings.mic.channel_index is None)
|
||||
else 1,
|
||||
else 1
|
||||
),
|
||||
audio=audio_bytes,
|
||||
).event()
|
||||
else:
|
||||
@@ -543,10 +553,10 @@ class SatelliteBase:
|
||||
# Sound
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
async def event_to_snd(self, event: Event) -> None:
|
||||
async def event_to_snd(self, event: Event, is_tts: bool = True) -> None:
|
||||
"""Send an event to the sound service."""
|
||||
if self._snd_queue is not None:
|
||||
self._snd_queue.put_nowait(event)
|
||||
self._snd_queue.put_nowait(SoundEvent(event, is_tts))
|
||||
|
||||
def _make_snd_client(self) -> Optional[AsyncClient]:
|
||||
"""Create client for snd service."""
|
||||
@@ -581,7 +591,8 @@ class SatelliteBase:
|
||||
if self._snd_queue is None:
|
||||
self._snd_queue = asyncio.Queue()
|
||||
|
||||
event = await self._snd_queue.get()
|
||||
snd_event = await self._snd_queue.get()
|
||||
event = snd_event.event
|
||||
|
||||
if snd_client is None:
|
||||
snd_client = self._make_snd_client()
|
||||
@@ -608,6 +619,7 @@ class SatelliteBase:
|
||||
event.type
|
||||
):
|
||||
await _disconnect()
|
||||
if snd_event.is_tts:
|
||||
await self.trigger_played()
|
||||
snd_client = None # reconnect on next event
|
||||
except asyncio.CancelledError:
|
||||
@@ -652,9 +664,8 @@ class SatelliteBase:
|
||||
for event in wav_to_events(
|
||||
wav_path,
|
||||
samples_per_chunk=self.settings.snd.samples_per_chunk,
|
||||
volume_multiplier=self.settings.snd.volume_multiplier,
|
||||
):
|
||||
await self.event_to_snd(event)
|
||||
await self.event_to_snd(event, is_tts=False)
|
||||
except Exception:
|
||||
# Unmute in case of an error
|
||||
self.microphone_muted = False
|
||||
@@ -797,6 +808,13 @@ class SatelliteBase:
|
||||
wake_names = [w.name for w in self.settings.wake.names]
|
||||
|
||||
await self.event_to_wake(Detect(names=wake_names).event())
|
||||
await self.event_to_wake(
|
||||
AudioStart(
|
||||
rate=self.settings.mic.rate,
|
||||
width=self.settings.mic.width,
|
||||
channels=self.settings.mic.channels,
|
||||
).event()
|
||||
)
|
||||
await self.trigger_detect()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user