Merge pull request #12 from imonlinux/Pulse_Audio_ALSA_OWW_MWW
Pulse audio alsa oww mww
This commit is contained in:
@@ -7,7 +7,11 @@ Supports announcments, start/continue conversation, and timers.
|
||||
|
||||
See [the tutorial](docs/linux-voice-assistant-2mic-install.md) to build a satellite using a [Raspberry Pi Zero 2 W](https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/) and a [ReSpeaker 2Mic HAT](https://wiki.keyestudio.com/Ks0314_keyestudio_ReSpeaker_2-Mic_Pi_HAT_V1.0).
|
||||
|
||||
This fork introduces the ability to use Wyoming OpenWakeWord instead of MicroWakeWord.
|
||||
### What's working:
|
||||
- This fork introduces the ability to use Wyoming OpenWakeWord (OWW) instead of MicroWakeWord (MWW).
|
||||
- Now supports **both ALSA and PulseAudio** playback backends using the updated `linux_voice_assistant/mpv_player.py`.
|
||||
- You can choose between ALSA and PulseAudio with either MWW or OWW simply by enabling the matching systemd service file.
|
||||
- You can change the OWW wake word within HA after the VLA is registered. (defaults to the wake word defind in the systemd config file)
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -42,3 +46,5 @@ See `--help` for more options.
|
||||
<!-- Links -->
|
||||
[homeassistant]: https://www.home-assistant.io/
|
||||
[esphome]: https://esphome.io/
|
||||
[wyoming]: https://github.com/rhasspy/wyoming-openwakeword/
|
||||
[future proof home]: https://github.com/FutureProofHomes/wyoming-enhancements/
|
||||
|
||||
@@ -9,12 +9,14 @@
|
||||
```
|
||||
> Modeled after the Wyoming Satellite two‑mic tutorial, adapted from actual shell history.
|
||||
|
||||
This guide reproduces a working setup of the **linux-voice-assistant** project with **Wyoming OpenWakeWord** on a Raspberry PI Zero 2W and a Respeaker 2‑mic HAT (e.g., seeed-2mic-voicecard). It assumes a fresh system with sudo access.
|
||||
This guide reproduces a working setup of the **linux-voice-assistant** project with **Wyoming OpenWakeWord** or **MicroWakeWord** on a Raspberry PI Zero 2W and a Respeaker 2‑mic HAT (e.g., seeed-2mic-voicecard). It assumes a fresh system with sudo access and the default "pi" user. Included is the option to use Pulse Audio instead of ALSA.
|
||||
|
||||
## Prerequisites
|
||||
- A Linux system (Debian/Ubuntu/Raspberry Pi OS or compatible)
|
||||
- Python 3.11+ recommended
|
||||
- A 2‑mic sound card
|
||||
- Raspberry Pi OS Lite (64-bit)
|
||||
- Linux 6.12.34+rpt-rpi-v8 #1 SMP PREEMPT Debian 1:6.12.34-1+rpt1~bookworm
|
||||
- (2025-06-26) aarch64 GNU/Linux
|
||||
- Default Python 3.11+ recommended
|
||||
- A ReSpeaker 2‑mic sound card or compatable
|
||||
- Network access to your Home Assistant instance
|
||||
|
||||
|
||||
@@ -64,13 +66,13 @@ script/setup
|
||||
|
||||
## 6. Configure audio devices, choose either Pulse Audio or ALSA (default)
|
||||
|
||||
### WIP Don't Use Yet! (Optional) Pulse Audio
|
||||
### (Optional) Pulse Audio
|
||||
|
||||
See [the tutorial](docs/install_pulseaudio.md) to install and configure Pulse Audio.
|
||||
|
||||
### For Standard ALSA
|
||||
|
||||
You shouldn't have to change anything if you are using the driver provided in this repo. If you are using something else, find your sound device names and update the linux-voice-assistant/service/linux-voice-assistant.service file to match sound card details:
|
||||
You shouldn't have to change anything if you are using the driver provided in this repo. If you are using something else, find your sound device names and update the linux-voice-assistant/service/alsa-oww-linux-voice-assistant.service used with OpenWakeWord or linux-voice-assistant/service/alsa-mww-linux-voice-assistant.service file used with MicroWakeWord to match sound card details:
|
||||
|
||||
```bash
|
||||
arecord -l
|
||||
@@ -80,20 +82,53 @@ aplay -l
|
||||
|
||||
## 7. Systemd services
|
||||
|
||||
### (Optional) for Pulse Audio copy this service file into /etc/systemd/system/:
|
||||
### (Optional) for Pulse Audio with OWW copy this service file into /etc/systemd/system/:
|
||||
|
||||
```bash
|
||||
sudo cp ~/linux-voice-assistant/service/pa-linux-voice-assistant.service /etc/systemd/system/linux-voice-assistant.service
|
||||
sudo cp ~/linux-voice-assistant/service/pa-oww-linux-voice-assistant.service /etc/systemd/system/linux-voice-assistant.service
|
||||
```
|
||||
Create the systemd drop-in directory for linux-voice-assistant.service:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/systemd/system/linux-voice-assistant.service.d
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo cp ~/linux-voice-assistant/service/10-tuning.conf \
|
||||
/etc/systemd/system/linux-voice-assistant.service.d/10-tuning.conf
|
||||
```
|
||||
|
||||
### OR for Pulse Audio with MWW copy this service file into /etc/systemd/system/:
|
||||
|
||||
```bash
|
||||
sudo cp ~/linux-voice-assistant/service/pa-mww-linux-voice-assistant.service /etc/systemd/system/linux-voice-assistant.service
|
||||
```
|
||||
Create the systemd drop-in directory for linux-voice-assistant.service:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/systemd/system/linux-voice-assistant.service.d
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo cp ~/linux-voice-assistant/service/10-tuning.conf \
|
||||
/etc/systemd/system/linux-voice-assistant.service.d/10-tuning.conf
|
||||
```
|
||||
|
||||
|
||||
### For ALSA copy this service file into /etc/systemd/system/:
|
||||
### For ALSA with OWW copy this service file into /etc/systemd/system/:
|
||||
|
||||
```bash
|
||||
sudo cp ~/linux-voice-assistant/service/linux-voice-assistant.service /etc/systemd/system/linux-voice-assistant.service
|
||||
sudo cp ~/linux-voice-assistant/service/alsa-oww-linux-voice-assistant.service /etc/systemd/system/linux-voice-assistant.service
|
||||
```
|
||||
|
||||
### For either ALSA or Pusle Audio systems, copy the Wyoming OpenWakeWord service file into /etc/systemd/system/:
|
||||
|
||||
### OR for ALSA with MWW copy this service file into /etc/systemd/system/:
|
||||
|
||||
```bash
|
||||
sudo cp ~/linux-voice-assistant/service/alsa-mww-linux-voice-assistant.service /etc/systemd/system/linux-voice-assistant.service
|
||||
```
|
||||
|
||||
### For either ALSA or Pusle Audio systems with the OWW copy this service file into /etc/systemd/system/:
|
||||
|
||||
```bash
|
||||
sudo cp ~/linux-voice-assistant/service/wyoming-openwakeword.service /etc/systemd/system/wyoming-openwakeword.service
|
||||
@@ -119,156 +154,44 @@ sudo systemctl status linux-voice-assistant wyoming-openwakeword --no-pager -l
|
||||
|
||||
|
||||
## 9. Verification
|
||||
|
||||
- Expect logs like `Connected to Home Assistant`
|
||||
- Look for `[OWW] Detection: name=...` followed by re-arming/cycling
|
||||
- Ask: *“What time is it?”* and confirm TTS reply
|
||||
- Use journalctl -fu linux-voice-assistant.service to check for errors. Debugging is enabled.
|
||||
- Expect logs like `Connected to Home Assistant`
|
||||
- Look for `[OWW] Detection: name=...` followed by re-arming/cycling
|
||||
- Ask: *“What time is it?”* and confirm TTS reply
|
||||
- If you do not get a voice response, check the Voice Assistant that you choose during registration has a voice assigned to it.
|
||||
|
||||
Settings -> Voice assistants -> Assist (the assistant you configured) -> Text-to-speech -> Voice
|
||||
### Settings -> Voice assistants -> Assist (the assistant you configured) -> Text-to-speech -> Voice
|
||||
|
||||
|
||||
## 10. Change OWW detection model (Depricated-Wake word can be selected in HA now)
|
||||
## 10. Change OWW detection model
|
||||
|
||||
After the LVA is registered with HA, you can change the Wake Word model used in the ESPHome Voice Assistant entity.
|
||||
|
||||
Edit the linux-voice-assistant.service file and change the OWW configuration argument for --wake-word-name.
|
||||
Project OWW models include:
|
||||
|
||||
```text
|
||||
alexa_v0.1.tflite
|
||||
hey_jarvis_v0.1.tflite
|
||||
hey_mycroft_v0.1.tflite
|
||||
hey_rhasspy_v0.1.tflite
|
||||
ok_nabu_v0.1.tflite
|
||||
alexa_v0.1.tflite -> Alexa
|
||||
hey_jarvis_v0.1.tflite -> Hey Jarvis
|
||||
hey_mycroft_v0.1.tflite -> Hey Mycroft
|
||||
hey_rhasspy_v0.1.tflite -> Hey Rhasspy
|
||||
ok_nabu_v0.1.tflite -> OK Nabo **(not working for me right now)**
|
||||
```
|
||||
|
||||
Additional community provided OWW models available from this repository:
|
||||
https://github.com/fwartner/home-assistant-wakewords-collection
|
||||
|
||||
You just copy the ones you want into the ~/wyoming-openwakeword/wyoming-openwakeword/models directory. If a model is currupted, the wyoming-openwakeword.service will fail to start.
|
||||
|
||||
**Word of warning. I have had problems with some of the community provided wake words. YMMV**
|
||||
|
||||
Edit linux-voice-assistant.service file:
|
||||
```bash
|
||||
sudo systemctl edit --force --full linux-voice-assistant.service
|
||||
```
|
||||
|
||||
Service file as provided using OWW 'ok_nabu':
|
||||
```text
|
||||
[Unit]
|
||||
Description=Linux Voice Assistant
|
||||
Requires=wyoming-openwakeword.service
|
||||
After=sound.target network-online.target
|
||||
Wants=network-online.target
|
||||
## 11. Switching between OWW and MWW. Or ALSA and PA see section 7.
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/home/pi/linux-voice-assistant
|
||||
ExecStart=/home/pi/linux-voice-assistant/script/run \
|
||||
--name 'Linux Satellite' \
|
||||
--audio-input-device seeed-2mic-voicecard \
|
||||
--audio-output-device alsa/hw:1,0 \
|
||||
--wake-uri 'tcp://127.0.0.1:10400' \
|
||||
--wake-word-name 'ok_nabu'
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Service file using OWW 'alexa':
|
||||
|
||||
```text
|
||||
[Unit]
|
||||
Description=Linux Voice Assistant
|
||||
Requires=wyoming-openwakeword.service
|
||||
After=sound.target network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/home/pi/linux-voice-assistant
|
||||
ExecStart=/home/pi/linux-voice-assistant/script/run \
|
||||
--name 'Linux Satellite' \
|
||||
--audio-input-device seeed-2mic-voicecard \
|
||||
--audio-output-device alsa/hw:1,0 \
|
||||
--wake-uri 'tcp://127.0.0.1:10400' \
|
||||
--wake-word-name 'alexa'
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Reload systemd with updated linux-voice-assistant.service file, restart linux-voice-assistant.service, and confirm successful run:
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart linux-voice-assistant.service
|
||||
sudo systemctl status linux-voice-assistant wyoming-openwakeword --no-pager -l
|
||||
```
|
||||
|
||||
## 11. Revert to MicroWakeWord
|
||||
|
||||
Edit the linux-voice-assistant.service file and remove the OWW configuration arguments:
|
||||
If you intend to switch from PA to ALSA, you must first stop the pusleaudio.service.
|
||||
|
||||
```bash
|
||||
sudo systemctl edit --force --full linux-voice-assistant.service
|
||||
sudo systemctl stop pulseaudio.service
|
||||
```
|
||||
|
||||
Service file with OWW:
|
||||
```text
|
||||
[Unit]
|
||||
Description=Linux Voice Assistant
|
||||
Requires=wyoming-openwakeword.service
|
||||
After=wyoming-openwakeword.service
|
||||
After=sound.target network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/home/pi/linux-voice-assistant
|
||||
ExecStart=/home/pi/linux-voice-assistant/script/run \
|
||||
--name 'Linux Satellite' \
|
||||
--audio-input-device seeed-2mic-voicecard \
|
||||
--audio-output-device alsa/hw:1,0 \
|
||||
--wake-uri 'tcp://127.0.0.1:10400' \
|
||||
--wake-word-name 'ok_nabu'
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Service file without OWW:
|
||||
```text
|
||||
[Unit]
|
||||
Description=Linux Voice Assistant
|
||||
Requires=wyoming-openwakeword.service
|
||||
After=wyoming-openwakeword.service
|
||||
After=sound.target network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/home/pi/linux-voice-assistant
|
||||
ExecStart=/home/pi/linux-voice-assistant/script/run \
|
||||
--name 'Linux Satellite' \
|
||||
--audio-input-device seeed-2mic-voicecard \
|
||||
--audio-output-device alsa/hw:1,0
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
Reload systemd with updated linux-voice-assistant.service file and restart linux-voice-assistant.service and confirm successful run:
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart linux-voice-assistant.service
|
||||
sudo systemctl status linux-voice-assistant wyoming-openwakeword --no-pager -l
|
||||
sudo systemctl disable pulseaudio.service
|
||||
```
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
"""Media player using mpv in a subprocess."""
|
||||
"""Media player using mpv in a subprocess.
|
||||
|
||||
- Supports ALSA and PulseAudio
|
||||
- Duck/unduck
|
||||
- Robust URL handling (str | list/tuple | bytes)
|
||||
- Backwards-compatible API: play(url, done_callback=None, stop_first=False), pause, resume, stop, set_volume
|
||||
- Pulse-friendly defaults (44100 Hz stereo) and short network timeout
|
||||
- Default watchdog = 8s (configurable), to avoid hangs if mpv fails to finish
|
||||
- End-of-playback detection uses BOTH `eof-reached` and `idle-active` to finish immediately after short clips
|
||||
- mpv log level can be controlled with env var LVA_MPV_MSG_LEVEL (e.g. "all=warn", "all=info")
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from threading import Lock
|
||||
from typing import List, Optional, Union
|
||||
import os
|
||||
from collections.abc import Callable, Sequence
|
||||
from threading import Lock, Timer
|
||||
from typing import Optional, Union
|
||||
|
||||
from mpv import MPV
|
||||
|
||||
@@ -11,84 +24,255 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MpvMediaPlayer:
|
||||
def __init__(self, device: Optional[str] = None) -> None:
|
||||
self.player = MPV()
|
||||
def __init__(self, device: Optional[str] = None, watchdog_sec: float = 8.0) -> None:
|
||||
# Bridge mpv logs into Python logging
|
||||
self.player = MPV(video=False, terminal=False, log_handler=self._mpv_log)
|
||||
|
||||
# Pulse-friendly defaults
|
||||
try:
|
||||
self.player['audio-samplerate'] = 44100
|
||||
self.player['audio-channels'] = 'stereo'
|
||||
self.player['keep-open'] = 'no'
|
||||
self.player['network-timeout'] = 7
|
||||
# Log level configurable via env; default to warn
|
||||
msg_level = os.environ.get("LVA_MPV_MSG_LEVEL", "all=warn")
|
||||
self.player['msg-level'] = msg_level
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Normalize/select backend and device
|
||||
ao = None
|
||||
norm = None
|
||||
|
||||
if device:
|
||||
self.player["audio-device"] = device
|
||||
d = device.strip()
|
||||
if d.startswith("alsa/"):
|
||||
ao = "alsa"; norm = d
|
||||
elif d.startswith("pulse/"):
|
||||
ao = "pulse"; norm = d
|
||||
elif d == "default":
|
||||
try:
|
||||
self.player["ao"] = "pulse"; norm = "pulse/default"
|
||||
except Exception:
|
||||
try:
|
||||
self.player["ao"] = "alsa"; norm = "alsa/default"
|
||||
except Exception:
|
||||
_LOGGER.warning("Neither Pulse nor ALSA available for 'default'")
|
||||
else:
|
||||
# Assume Pulse sink name if no prefix
|
||||
ao = "pulse"; norm = f"pulse/{d}"
|
||||
|
||||
self.is_playing = False
|
||||
if ao:
|
||||
try:
|
||||
self.player["ao"] = ao
|
||||
except Exception:
|
||||
_LOGGER.warning("Requested ao=%s not available", ao)
|
||||
|
||||
self._playlist: List[str] = []
|
||||
if norm:
|
||||
try:
|
||||
self.player["audio-device"] = norm
|
||||
except Exception:
|
||||
_LOGGER.warning("Failed to set audio-device=%s", norm)
|
||||
|
||||
# State
|
||||
self.is_playing: bool = False
|
||||
self._done_callback: Optional[Callable[[], None]] = None
|
||||
self._done_callback_lock = Lock()
|
||||
self._pre_duck_volume: Optional[int] = None # stores 0..100 when ducked
|
||||
self._watchdog: Optional[Timer] = None
|
||||
self._watchdog_sec: float = float(watchdog_sec)
|
||||
|
||||
self._duck_volume: int = 50
|
||||
self._unduck_volume: int = 100
|
||||
# Playback end detection (both EOF and idle)
|
||||
self.player.observe_property("eof-reached", self._on_eof)
|
||||
self.player.observe_property("idle-active", self._on_idle_active)
|
||||
|
||||
self.player.event_callback("end-file")(self._on_end_file)
|
||||
# -------------------- public API --------------------
|
||||
|
||||
def play(
|
||||
self,
|
||||
url: Union[str, List[str]],
|
||||
url: Union[str, Sequence[str]],
|
||||
done_callback: Optional[Callable[[], None]] = None,
|
||||
stop_first: bool = True,
|
||||
stop_first: bool = False,
|
||||
) -> None:
|
||||
self.stop()
|
||||
"""Begin playback of a URL or local file. Replaces any existing playback.
|
||||
Accepts a single URL (str) or a sequence of URLs; if a sequence is given, the first item is played.
|
||||
"""
|
||||
if stop_first:
|
||||
try:
|
||||
self.stop()
|
||||
except Exception:
|
||||
_LOGGER.debug("stop_first=True: stop() raised, continuing", exc_info=True)
|
||||
|
||||
if isinstance(url, str):
|
||||
self._playlist = [url]
|
||||
else:
|
||||
self._playlist = url
|
||||
with self._done_callback_lock:
|
||||
self._done_callback = done_callback
|
||||
|
||||
next_url = self._playlist.pop(0)
|
||||
_LOGGER.debug("Playing %s", next_url)
|
||||
|
||||
self._done_callback = done_callback
|
||||
self.is_playing = True
|
||||
self.player.play(next_url)
|
||||
|
||||
def pause(self) -> None:
|
||||
self.player.pause = True
|
||||
self.is_playing = False
|
||||
|
||||
def resume(self) -> None:
|
||||
self.player.pause = False
|
||||
if self._playlist:
|
||||
self.is_playing = True
|
||||
|
||||
def stop(self) -> None:
|
||||
self.player.stop()
|
||||
self._playlist.clear()
|
||||
|
||||
def duck(self) -> None:
|
||||
self.player.volume = self._duck_volume
|
||||
|
||||
def unduck(self) -> None:
|
||||
self.player.volume = self._unduck_volume
|
||||
|
||||
def set_volume(self, volume: int) -> None:
|
||||
volume = max(0, min(100, volume))
|
||||
self.player.volume = volume
|
||||
|
||||
self._unduck_volume = volume
|
||||
self._duck_volume = volume // 2
|
||||
|
||||
def _on_end_file(self, event) -> None:
|
||||
if self._playlist:
|
||||
self.player.play(self._playlist.pop(0))
|
||||
# Normalize url input
|
||||
if isinstance(url, (list, tuple)):
|
||||
if not url:
|
||||
_LOGGER.error("mpv play() received empty URL list")
|
||||
self._run_done_callback()
|
||||
return
|
||||
url = url[0]
|
||||
if isinstance(url, bytes):
|
||||
try:
|
||||
url = url.decode("utf-8")
|
||||
except Exception:
|
||||
url = url.decode(errors="ignore")
|
||||
if not isinstance(url, str):
|
||||
_LOGGER.error("mpv play() expected str URL, got %r", type(url))
|
||||
self._run_done_callback()
|
||||
return
|
||||
|
||||
self.is_playing = False
|
||||
try:
|
||||
self.player["mute"] = "no"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
todo_callback: Optional[Callable[[], None]] = None
|
||||
with self._done_callback_lock:
|
||||
if self._done_callback:
|
||||
todo_callback = self._done_callback
|
||||
self._done_callback = None
|
||||
self.is_playing = True
|
||||
try:
|
||||
self.player.play(url)
|
||||
self._arm_watchdog()
|
||||
except Exception:
|
||||
self.is_playing = False
|
||||
_LOGGER.exception("mpv failed to play %r", url)
|
||||
self._run_done_callback()
|
||||
|
||||
if todo_callback:
|
||||
def pause(self) -> None:
|
||||
try:
|
||||
self.player.pause = True
|
||||
except Exception:
|
||||
_LOGGER.exception("mpv pause() failed")
|
||||
|
||||
def resume(self) -> None:
|
||||
try:
|
||||
self.player.pause = False
|
||||
except Exception:
|
||||
_LOGGER.exception("mpv resume() failed")
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop playback immediately and clear playing state; run done callback."""
|
||||
try:
|
||||
try:
|
||||
todo_callback()
|
||||
self.player.command('stop')
|
||||
except Exception:
|
||||
self.player.command('loadfile', 'null://', 'replace')
|
||||
finally:
|
||||
self._cancel_watchdog()
|
||||
was_playing = self.is_playing
|
||||
self.is_playing = False
|
||||
if was_playing:
|
||||
self._run_done_callback()
|
||||
|
||||
def set_volume(self, *args, **kwargs) -> None:
|
||||
"""Set volume 0..100. Accepts positional or keyword 'volume' (backwards-compatible)."""
|
||||
vol_arg = args[0] if args else kwargs.get("volume")
|
||||
if vol_arg is None:
|
||||
_LOGGER.error("set_volume() requires an integer (0..100)")
|
||||
return
|
||||
|
||||
try:
|
||||
vol = max(0, min(100, int(vol_arg)))
|
||||
except Exception:
|
||||
_LOGGER.exception("set_volume(): invalid value %r", vol_arg)
|
||||
return
|
||||
|
||||
try:
|
||||
self.player.volume = vol
|
||||
except Exception:
|
||||
_LOGGER.exception("mpv set_volume(%s) failed", vol)
|
||||
|
||||
def duck(self, target_percent: int = 20) -> None:
|
||||
if self._pre_duck_volume is not None:
|
||||
return # already ducked
|
||||
|
||||
try:
|
||||
current = int(round(float(self.player.volume)))
|
||||
except Exception:
|
||||
current = 100
|
||||
|
||||
self._pre_duck_volume = max(0, min(100, current))
|
||||
|
||||
try:
|
||||
self.player.volume = max(0, min(100, int(target_percent)))
|
||||
except Exception:
|
||||
_LOGGER.exception("duck(): failed to set duck volume")
|
||||
|
||||
def unduck(self) -> None:
|
||||
if self._pre_duck_volume is None:
|
||||
return
|
||||
|
||||
try:
|
||||
self.player.volume = self._pre_duck_volume
|
||||
except Exception:
|
||||
_LOGGER.exception("unduck(): failed to restore volume")
|
||||
finally:
|
||||
self._pre_duck_volume = None
|
||||
|
||||
# -------------------- callbacks & watchdog --------------------
|
||||
|
||||
def _on_eof(self, _name: str, reached: bool) -> None:
|
||||
if not reached:
|
||||
return
|
||||
self._cancel_watchdog()
|
||||
self.is_playing = False
|
||||
self._run_done_callback()
|
||||
|
||||
def _on_idle_active(self, _name: str, active: bool) -> None:
|
||||
# When playback ends, mpv enters idle; treat that as completion.
|
||||
if not self.is_playing:
|
||||
return
|
||||
if bool(active):
|
||||
_LOGGER.debug("mpv became idle; treating as end-of-playback")
|
||||
self._cancel_watchdog()
|
||||
self.is_playing = False
|
||||
self._run_done_callback()
|
||||
|
||||
def _arm_watchdog(self) -> None:
|
||||
self._cancel_watchdog()
|
||||
if self._watchdog_sec <= 0:
|
||||
return
|
||||
self._watchdog = Timer(self._watchdog_sec, self._watchdog_trip)
|
||||
self._watchdog.daemon = True
|
||||
self._watchdog.start()
|
||||
|
||||
def _cancel_watchdog(self) -> None:
|
||||
if self._watchdog is not None:
|
||||
try:
|
||||
self._watchdog.cancel()
|
||||
except Exception:
|
||||
pass
|
||||
self._watchdog = None
|
||||
|
||||
def _watchdog_trip(self) -> None:
|
||||
if self.is_playing:
|
||||
_LOGGER.warning("mpv watchdog fired after %.1fs; stopping playback", self._watchdog_sec)
|
||||
try:
|
||||
self.player.command('stop')
|
||||
except Exception:
|
||||
pass
|
||||
self.is_playing = False
|
||||
self._run_done_callback()
|
||||
|
||||
def _run_done_callback(self) -> None:
|
||||
cb: Optional[Callable[[], None]] = None
|
||||
with self._done_callback_lock:
|
||||
cb = self._done_callback
|
||||
self._done_callback = None
|
||||
|
||||
if cb is not None:
|
||||
try:
|
||||
cb()
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected error running done callback")
|
||||
|
||||
# -------------------- mpv log bridge --------------------
|
||||
def _mpv_log(self, level: str, prefix: str, text: str) -> None:
|
||||
msg = f"mpv[{level}] {prefix}: {text}".rstrip()
|
||||
if level in ("fatal", "error"):
|
||||
_LOGGER.error(msg)
|
||||
elif level in ("warn", "warning"):
|
||||
_LOGGER.warning(msg)
|
||||
elif level in ("info",):
|
||||
_LOGGER.info(msg)
|
||||
else:
|
||||
_LOGGER.debug(msg)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
|
||||
def list_devices(ao="pulse"):
|
||||
result = subprocess.run(
|
||||
["mpv", f"--ao={ao}", "--audio-device=help"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
print(result.stdout)
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("PulseAudio devices:")
|
||||
list_devices("pulse")
|
||||
print("\nALSA devices:")
|
||||
list_devices("alsa")
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# /etc/systemd/system/linux-voice-assistant.service.d/10-tuning.conf
|
||||
[Service]
|
||||
# Keep mpv logs at warning by default; override with environment if desired
|
||||
Environment=LVA_MPV_MSG_LEVEL=all=warn
|
||||
|
||||
# Faster, cleaner shutdowns
|
||||
TimeoutStopSec=5s
|
||||
KillMode=mixed
|
||||
@@ -0,0 +1,31 @@
|
||||
[Unit]
|
||||
Description=Linux Voice Assistant (ALSA - ReSpeaker 2mic HAT)
|
||||
After=network-online.target wyoming-openwakeword.service
|
||||
Wants=network-online.target
|
||||
Requires=wyoming-openwakeword.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pi
|
||||
Group=pi
|
||||
WorkingDirectory=/home/pi/linux-voice-assistant
|
||||
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
# Use ALSA ReSpeaker 2mic HAT hardware device
|
||||
Environment="MPV_DEFAULT_DEVICE=alsa/plughw:CARD=seeed2micvoicec,DEV=0"
|
||||
|
||||
ExecStart=/home/pi/linux-voice-assistant/script/run \
|
||||
--name 'Linux Satellite ALSA MWW' \
|
||||
--audio-input-device default \
|
||||
--audio-output-device ${MPV_DEFAULT_DEVICE} \
|
||||
# --wake-uri 'tcp://127.0.0.1:10400' \
|
||||
# --wake-word-name 'alexa' \
|
||||
# --wake-models-dir '/home/pi/wyoming-openwakeword/wyoming_openwakeword/models'
|
||||
--debug
|
||||
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,31 @@
|
||||
[Unit]
|
||||
Description=Linux Voice Assistant (ALSA - ReSpeaker 2mic HAT)
|
||||
After=network-online.target wyoming-openwakeword.service
|
||||
Wants=network-online.target
|
||||
Requires=wyoming-openwakeword.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pi
|
||||
Group=pi
|
||||
WorkingDirectory=/home/pi/linux-voice-assistant
|
||||
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
# Use ALSA ReSpeaker 2mic HAT hardware device
|
||||
Environment="MPV_DEFAULT_DEVICE=alsa/plughw:CARD=seeed2micvoicec,DEV=0"
|
||||
|
||||
ExecStart=/home/pi/linux-voice-assistant/script/run \
|
||||
--name 'Linux Satellite ALSA OWW' \
|
||||
--audio-input-device default \
|
||||
--audio-output-device ${MPV_DEFAULT_DEVICE} \
|
||||
--wake-uri 'tcp://127.0.0.1:10400' \
|
||||
--wake-word-name 'alexa' \
|
||||
--wake-models-dir '/home/pi/wyoming-openwakeword/wyoming_openwakeword/models'
|
||||
--debug
|
||||
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,23 +0,0 @@
|
||||
[Unit]
|
||||
Description=Linux Voice Assistant
|
||||
Requires=wyoming-openwakeword.service
|
||||
After=wyoming-openwakeword.service
|
||||
After=sound.target network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/home/pi/linux-voice-assistant
|
||||
ExecStart=/home/pi/linux-voice-assistant/script/run \
|
||||
--name 'Linux Satellite' \
|
||||
--audio-input-device seeed-2mic-voicecard \
|
||||
--audio-output-device alsa/hw:1,0 \
|
||||
--wake-uri 'tcp://127.0.0.1:10400' \
|
||||
--wake-word-name 'alexa' \
|
||||
--wake-models-dir '/home/pi/wyoming-openwakeword/wyoming_openwakeword/models'
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -1,34 +0,0 @@
|
||||
[Unit]
|
||||
Description=Linux Voice Assistant
|
||||
After=network-online.target pulseaudio.service wyoming-openwakeword.service
|
||||
Wants=network-online.target
|
||||
Requires=pulseaudio.service wyoming-openwakeword.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pi
|
||||
Group=pi
|
||||
WorkingDirectory=/home/pi/linux-voice-assistant
|
||||
|
||||
# Ensure the app talks to system-wide PulseAudio
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
Environment="PULSE_SERVER=unix:/var/run/pulse/native"
|
||||
|
||||
# Use PulseAudio defaults (set via pactl) for input/output
|
||||
ExecStart=/home/pi/linux-voice-assistant/script/run \
|
||||
--name 'Linux Satellite' \
|
||||
--audio-input-device default \
|
||||
--audio-output-device default \
|
||||
--wake-uri 'tcp://127.0.0.1:10400' \
|
||||
--wake-word-name 'alexa' \
|
||||
--wake-models-dir '/home/pi/wyoming-openwakeword/wyoming_openwakeword/models' \
|
||||
--debug
|
||||
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
|
||||
# If you need network and filesystems ready:
|
||||
#TimeoutStartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,36 @@
|
||||
[Unit]
|
||||
Description=Linux Voice Assistant (PulseAudio - Headphone Jack)
|
||||
After=network-online.target pulseaudio.service wyoming-openwakeword.service
|
||||
Wants=network-online.target
|
||||
Requires=pulseaudio.service wyoming-openwakeword.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pi
|
||||
Group=pi
|
||||
WorkingDirectory=/home/pi/linux-voice-assistant
|
||||
|
||||
# Environment
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
Environment="PULSE_SERVER=unix:/var/run/pulse/native"
|
||||
Environment="PULSE_LATENCY_MSEC=60"
|
||||
Environment="MPV_DEFAULT_DEVICE=pulse/alsa_output.platform-soc_sound.stereo-fallback"
|
||||
|
||||
# Wait briefly for the Pulse native socket to be ready
|
||||
ExecStartPre=/bin/sh -c 'for i in $(seq 1 40); do [ -S /var/run/pulse/native ] && exit 0; sleep 0.5; done; echo "PulseAudio socket not found" >&2; exit 1'
|
||||
|
||||
# Launch the assistant
|
||||
ExecStart=/home/pi/linux-voice-assistant/script/run \
|
||||
--name 'Linux Satellite PA MWW' \
|
||||
--audio-input-device default \
|
||||
--audio-output-device ${MPV_DEFAULT_DEVICE} \
|
||||
# --wake-uri 'tcp://127.0.0.1:10400' \
|
||||
# --wake-word-name 'alexa' \
|
||||
# --wake-models-dir '/home/pi/wyoming-openwakeword/wyoming_openwakeword/models' \
|
||||
--debug
|
||||
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,36 @@
|
||||
[Unit]
|
||||
Description=Linux Voice Assistant (PulseAudio - Headphone Jack)
|
||||
After=network-online.target pulseaudio.service wyoming-openwakeword.service
|
||||
Wants=network-online.target
|
||||
Requires=pulseaudio.service wyoming-openwakeword.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=pi
|
||||
Group=pi
|
||||
WorkingDirectory=/home/pi/linux-voice-assistant
|
||||
|
||||
# Environment
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
Environment="PULSE_SERVER=unix:/var/run/pulse/native"
|
||||
Environment="PULSE_LATENCY_MSEC=60"
|
||||
Environment="MPV_DEFAULT_DEVICE=pulse/alsa_output.platform-soc_sound.stereo-fallback"
|
||||
|
||||
# Wait briefly for the Pulse native socket to be ready
|
||||
ExecStartPre=/bin/sh -c 'for i in $(seq 1 40); do [ -S /var/run/pulse/native ] && exit 0; sleep 0.5; done; echo "PulseAudio socket not found" >&2; exit 1'
|
||||
|
||||
# Launch the assistant
|
||||
ExecStart=/home/pi/linux-voice-assistant/script/run \
|
||||
--name 'Linux Satellite PA OWW' \
|
||||
--audio-input-device default \
|
||||
--audio-output-device ${MPV_DEFAULT_DEVICE} \
|
||||
--wake-uri 'tcp://127.0.0.1:10400' \
|
||||
--wake-word-name 'alexa' \
|
||||
--wake-models-dir '/home/pi/wyoming-openwakeword/wyoming_openwakeword/models' \
|
||||
--debug
|
||||
|
||||
Restart=always
|
||||
RestartSec=2
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
Reference in New Issue
Block a user