Skip to content

Commit

Permalink
feat/sounds_playback in ovos-audio (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
JarbasAl authored Sep 7, 2023
1 parent d9706f2 commit 756db47
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 34 deletions.
5 changes: 4 additions & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
include requirements.txt
include LICENSE.md
include LICENSE.md
include ovos_audio/res/snd/start_listening.wav
include ovos_audio/res/snd/acknowledge.mp3
include ovos_audio/res/snd/error.mp3
13 changes: 2 additions & 11 deletions ovos_audio/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from ovos_plugin_manager.templates.audio import RemoteAudioBackend
from ovos_utils.log import LOG
from ovos_utils.process_utils import MonotonicEvent
from ovos_audio.utils import validate_message_context

try:
from ovos_plugin_common_play import OCPAudioBackend
Expand Down Expand Up @@ -353,17 +354,7 @@ def play(self, tracks, prefered_service, repeat=False):
def _is_message_for_service(self, message):
if not message or not self.validate_source:
return True
destination = message.context.get("destination")
if destination:
native_sources = Configuration()["Audio"].get(
"native_sources", ["debug_cli", "audio"]) or []
if any(s in destination for s in native_sources):
# request from device
return True
# external request, do not handle
return False
# broadcast for everyone
return True
return validate_message_context(message)

def _queue(self, message):
if not self._is_message_for_service(message):
Expand Down
Binary file added ovos_audio/res/snd/acknowledge.mp3
Binary file not shown.
Binary file added ovos_audio/res/snd/error.mp3
Binary file not shown.
Binary file added ovos_audio/res/snd/start_listening.wav
Binary file not shown.
68 changes: 55 additions & 13 deletions ovos_audio/service.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import os
import os.path
import time
from os.path import exists, expanduser
from os.path import exists
from threading import Thread, Lock

from ovos_bus_client import Message, MessageBusClient
Expand All @@ -9,14 +11,15 @@
from ovos_plugin_manager.g2p import get_g2p_lang_configs, get_g2p_supported_langs, get_g2p_module_configs
from ovos_plugin_manager.tts import TTS
from ovos_plugin_manager.tts import get_tts_supported_langs, get_tts_lang_configs, get_tts_module_configs
from ovos_utils.file_utils import resolve_resource_file
from ovos_utils.log import LOG
from ovos_utils.metrics import Stopwatch
from ovos_utils.process_utils import ProcessStatus, StatusCallbackMap
from ovos_utils.signal import check_for_signal
from ovos_utils.sound import play_audio

from ovos_audio.audio import AudioService
from ovos_audio.tts import TTSFactory
from ovos_audio.utils import report_timing
from ovos_audio.utils import report_timing, validate_message_context


def on_ready():
Expand Down Expand Up @@ -253,8 +256,8 @@ def handle_speak(self, message):
# if the message is targeted and audio is not the target don't
# don't synthesise speech
message.context = message.context or {}
if self.validate_source and message.context.get('destination') and not \
any(s in message.context['destination'] for s in self.native_sources):
if self.validate_source and not validate_message_context(message, self.native_sources):
LOG.debug("ignoring speak from non-native source, playback handled directly by client")
return

# Get conversation ID
Expand Down Expand Up @@ -360,33 +363,70 @@ def execute_fallback_tts(self, utterance, ident, listen, message: Message = None
LOG.error(e)
LOG.exception(f"TTS FAILURE! utterance : {utterance}")

def handle_stop(self, message):
@property
def is_speaking(self):
return self.tts.playback is not None and \
self.tts.playback._now_playing is not None

def handle_speak_status(self, message: Message):
self.bus.emit(message.reply("mycroft.audio.is_speaking",
{"speaking": self.is_speaking}))

def handle_stop(self, message: Message):
"""Handle stop message.
Shutdown any speech.
"""
if check_for_signal("isSpeaking", -1):
# check PlaybackThread
if self.is_speaking:
self._last_stop_signal = time.time()
self.tts.playback.clear() # Clear here to get instant stop
self.bus.emit(Message("mycroft.stop.handled", {"by": "TTS"}))
self.bus.emit(message.forward("mycroft.stop.handled", {"by": "TTS"}))

@staticmethod
def _resolve_sound_uri(uri: str):
""" helper to resolve sound files full path"""
if uri is None:
return None
if uri.startswith("snd/"):
local_uri = f"{os.path.dirname(__file__)}/res/{uri}"
if os.path.isfile(local_uri):
return local_uri
audio_file = resolve_resource_file(uri)
if audio_file is None or not exists(audio_file):
raise FileNotFoundError(f"{audio_file} does not exist")
return audio_file

def handle_queue_audio(self, message):
""" Queue a sound file to play in speech thread
ensures it doesnt play over TTS """
if not validate_message_context(message):
LOG.debug("ignoring playback, message is not from a native source")
return
viseme = message.data.get("viseme")
audio_ext = message.data.get("audio_ext") # unused ?
audio_file = message.data.get("filename")
audio_file = message.data.get("uri") or \
message.data.get("filename") # backwards compat
if not audio_file:
raise ValueError(f"'filename' missing from message.data: {message.data}")
audio_file = expanduser(audio_file)
if not exists(audio_file):
raise FileNotFoundError(f"{audio_file} does not exist")
raise ValueError(f"'uri' missing from message.data: {message.data}")
audio_file = self._resolve_sound_uri(audio_file)
audio_ext = audio_ext or audio_file.split(".")[-1]
listen = message.data.get("listen", False)

sess_id = SessionManager.get(message).session_id
TTS.queue.put((audio_ext, str(audio_file), viseme, sess_id, listen, message))

def handle_instant_play(self, message):
""" play a sound file immediately (may play over TTS) """
if not validate_message_context(message):
LOG.debug("ignoring playback, message is not from a native source")
return
audio_file = message.data.get("uri")
if not audio_file:
raise ValueError(f"'uri' missing from message.data: {message.data}")
audio_file = self._resolve_sound_uri(audio_file)
play_audio(audio_file)

def handle_get_languages_tts(self, message):
"""
Handle a request for supported TTS languages
Expand Down Expand Up @@ -415,7 +455,9 @@ def init_messagebus(self):
Configuration.set_config_update_handlers(self.bus)
self.bus.on('mycroft.stop', self.handle_stop)
self.bus.on('mycroft.audio.speech.stop', self.handle_stop)
self.bus.on('mycroft.audio.speak.status', self.handle_speak_status)
self.bus.on('mycroft.audio.queue', self.handle_queue_audio)
self.bus.on('mycroft.audio.play_sound', self.handle_instant_play)
self.bus.on('speak', self.handle_speak)
self.bus.on('ovos.languages.tts', self.handle_get_languages_tts)
self.bus.on("opm.tts.query", self.handle_opm_tts_query)
Expand Down
30 changes: 25 additions & 5 deletions ovos_audio/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,29 @@
# limitations under the License.
#
import time

from ovos_bus_client.send_func import send
from ovos_utils.log import LOG
from ovos_config import Configuration
from ovos_utils.log import LOG, deprecated
from ovos_utils.signal import check_for_signal


def validate_message_context(message, native_sources=None):
destination = message.context.get("destination")
if destination:
native_sources = native_sources or Configuration()["Audio"].get(
"native_sources", ["debug_cli", "audio"]) or []
if any(s in destination for s in native_sources):
# request from device
return True
# external request, do not handle
return False
# broadcast for everyone
return True


# NOTE: nothing imports these from here, utils accidentally dragged while isolating ovos-audio
@deprecated("file signals have been deprecated", "0.1.0")
def is_speaking():
"""Determine if Text to Speech is occurring
Expand All @@ -27,6 +45,8 @@ def is_speaking():
return check_for_signal("isSpeaking", -1)


# NOTE: nothing imports these from here, utils accidentally dragged while isolating ovos-audio
@deprecated("file signals have been deprecated", "0.1.0")
def wait_while_speaking():
"""Pause as long as Text to Speech is still happening
Expand All @@ -39,16 +59,16 @@ def wait_while_speaking():
time.sleep(0.1)


# NOTE: nothing imports these from here, utils accidentally dragged while isolating ovos-audio
@deprecated("file signals have been deprecated", "0.1.0")
def stop_speaking():
"""Stop mycroft speech.
TODO: Skills should only be able to stop speech they've initiated
"""
print(666, is_speaking())
if is_speaking():
from ovos_config import Configuration
bus_cfg = Configuration().get("websocket", {})
send('mycroft.audio.speech.stop', config=bus_cfg)

send('mycroft.audio.speech.stop')

# Block until stopped
while check_for_signal("isSpeaking", -1):
Expand Down
11 changes: 7 additions & 4 deletions test/unittests/test_speech.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,20 +73,23 @@ def test_error_life_cycle(self, tts_factory_mock, config_mock):
speech.run()
self.assertTrue(speech.status.state == ProcessState.ERROR)

@mock.patch('ovos_audio.service.check_for_signal')
def test_stop(self, check_for_signal_mock, tts_factory_mock, config_mock):
def test_stop(self, tts_factory_mock, config_mock):
"""Ensure the stop handler signals stop correctly."""
setup_mocks(config_mock, tts_factory_mock)
bus = mock.Mock()
config_mock.get.return_value = {'tts': {'module': 'test'}}
speech = PlaybackService(bus=bus)
speech.tts.playback = mock.Mock()
speech.tts.playback.return_value = True # not None
speech.tts.playback._now_playing = None

speech._last_stop_signal = 0
check_for_signal_mock.return_value = False
self.assertFalse(speech.is_speaking)
speech.handle_stop(Message('mycroft.stop'))
self.assertEqual(speech._last_stop_signal, 0)

check_for_signal_mock.return_value = True
speech.tts.playback._now_playing = True # not None
self.assertTrue(speech.is_speaking)
speech.handle_stop(Message('mycroft.stop'))
self.assertNotEqual(speech._last_stop_signal, 0)
speech.shutdown()
Expand Down

0 comments on commit 756db47

Please sign in to comment.