diff --git a/README.md b/README.md index 09e2120..9e5dfc5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ The "mouth" of the OVOS assistant! -Handles TTS generation and audio playback +Handles TTS generation and sounds playback ## Install @@ -51,27 +51,6 @@ under mycroft.conf // Mechanism used to play OGG audio files // Override: SYSTEM - "play_ogg_cmdline": "ogg123 -q %1", - - "Audio": { - // message.context may contains a source and destination - // native audio (playback / TTS) will only be played if a - // message destination is a native_source or if missing (considered a broadcast) - "native_sources": ["debug_cli", "audio"], - - "backends": { - "OCP": { - "type": "ovos_common_play", - "active": true - }, - "simple": { - "type": "ovos_audio_simple", - "active": true - }, - "vlc": { - "type": "ovos_vlc", - "active": true - } - } + "play_ogg_cmdline": "ogg123 -q %1" } ``` \ No newline at end of file diff --git a/ovos_audio/audio.py b/ovos_audio/audio.py deleted file mode 100644 index d945272..0000000 --- a/ovos_audio/audio.py +++ /dev/null @@ -1,529 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from threading import Lock - -import time -from ovos_bus_client.message import Message -from ovos_config.config import Configuration -from ovos_plugin_manager.audio import load_audio_service_plugins as load_plugins, find_audio_service_plugins, setup_audio_service -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 -except ImportError: - OCPAudioBackend = None - -MINUTES = 60 # Seconds in a minute - - -class AudioService: - """ Audio Service class. - Handles playback of audio and selecting proper backend for the uri - to be played. - """ - - def __init__(self, bus, autoload=True, disable_ocp=False, validate_source=True): - """ - Args: - bus: Mycroft messagebus - """ - self.bus = bus - self.config = Configuration().get("Audio") or {} - self.service_lock = Lock() - - self.default = None - self.service = [] - self.current = None - self.play_start_time = 0 - self.volume_is_low = False - self.disable_ocp = disable_ocp - self.validate_source = validate_source - - self._loaded = MonotonicEvent() - if autoload: - self.load_services() - - def find_ocp(self): - for s in self.service: - if OCPAudioBackend is not None and isinstance(s, OCPAudioBackend): - LOG.info('OCP - OVOS Common Play set as default backend') - self.default = s - return True - - def find_default(self): - # Find default backend - default_name = self.config.get('default-backend', '') - if self.disable_ocp and default_name == "OCP": - LOG.warning("default backend set to OCP, but OCP is disabled") - default_name = "" - LOG.info('Finding default audio backend...') - for s in self.service: - if s.name == default_name: - self.default = s - LOG.info('Found ' + self.default.name) - return True - else: - self.default = None - LOG.info('no default found') - - def load_services(self): - """Method for loading services. - - Sets up the global service, default and registers the event handlers - for the subsystem. - """ - found_plugins = find_audio_service_plugins() - if 'ovos_common_play' in found_plugins and self.disable_ocp: - found_plugins.pop('ovos_common_play') - - local = [] - remote = [] - for plugin_name, plugin_module in found_plugins.items(): - LOG.info(f'Loading audio service plugin: {plugin_name}') - s = setup_audio_service(plugin_module, config=self.config, bus=self.bus) - if not s: - continue - if isinstance(s, RemoteAudioBackend): - remote += s - else: - local += s - - - # Sort services so local services are checked first - self.service = local + remote - - # Register end of track callback - for s in self.service: - s.set_track_start_callback(self.track_start) - - if self.disable_ocp: - # default to classic audio only service - self.find_default() - else: - # default to OCP, fallback to classic audio only service - self.find_ocp() or self.find_default() - - # Setup event handlers - self.bus.on('mycroft.audio.service.play', self._play) - self.bus.on('mycroft.audio.service.queue', self._queue) - self.bus.on('mycroft.audio.service.pause', self._pause) - self.bus.on('mycroft.audio.service.resume', self._resume) - self.bus.on('mycroft.audio.service.stop', self._stop) - self.bus.on('mycroft.audio.service.next', self._next) - self.bus.on('mycroft.audio.service.prev', self._prev) - self.bus.on('mycroft.audio.service.track_info', self._track_info) - self.bus.on('mycroft.audio.service.list_backends', self._list_backends) - self.bus.on('mycroft.audio.service.set_track_position', - self._set_track_position) - self.bus.on('mycroft.audio.service.get_track_position', - self._get_track_position) - self.bus.on('mycroft.audio.service.get_track_length', - self._get_track_length) - self.bus.on('mycroft.audio.service.seek_forward', self._seek_forward) - self.bus.on('mycroft.audio.service.seek_backward', self._seek_backward) - self.bus.on('recognizer_loop:audio_output_start', self._lower_volume) - self.bus.on('recognizer_loop:record_begin', self._lower_volume) - self.bus.on('recognizer_loop:audio_output_end', self._restore_volume) - self.bus.on('recognizer_loop:record_end', - self._restore_volume_after_record) - - self._loaded.set() # Report services loaded - - return self.service - - def wait_for_load(self, timeout=3 * MINUTES): - """Wait for services to be loaded. - - Args: - timeout (float): Seconds to wait (default 3 minutes) - Returns: - (bool) True if loading completed within timeout, else False. - """ - return self._loaded.wait(timeout) - - def track_start(self, track): - """Callback method called from the services to indicate start of - playback of a track or end of playlist. - """ - if track: - # Inform about the track about to start. - LOG.debug('New track coming up!') - self.bus.emit(Message('mycroft.audio.playing_track', - data={'track': track})) - else: - # If no track is about to start last track of the queue has been - # played. - LOG.debug('End of playlist!') - self.bus.emit(Message('mycroft.audio.queue_end')) - - def _pause(self, message=None): - """ - Handler for mycroft.audio.service.pause. Pauses the current audio - service. - - Args: - message: message bus message, not used but required - """ - if not self._is_message_for_service(message): - return - if self.current: - self.current.pause() - - def _resume(self, message=None): - """ - Handler for mycroft.audio.service.resume. - - Args: - message: message bus message, not used but required - """ - if not self._is_message_for_service(message): - return - if self.current: - self.current.resume() - - def _next(self, message=None): - """ - Handler for mycroft.audio.service.next. Skips current track and - starts playing the next. - - Args: - message: message bus message, not used but required - """ - if not self._is_message_for_service(message): - return - if self.current: - self.current.next() - - def _prev(self, message=None): - """ - Handler for mycroft.audio.service.prev. Starts playing the previous - track. - - Args: - message: message bus message, not used but required - """ - if not self._is_message_for_service(message): - return - if self.current: - self.current.previous() - - def _perform_stop(self, message=None): - """Stop audioservice if active.""" - if not self._is_message_for_service(message): - return - if self.current: - name = self.current.name - if self.current.stop(): - if message: - msg = message.reply("mycroft.stop.handled", - {"by": "audio:" + name}) - else: - msg = Message("mycroft.stop.handled", - {"by": "audio:" + name}) - self.bus.emit(msg) - - self.current = None - - def _stop(self, message=None): - """ - Handler for mycroft.stop. Stops any playing service. - - Args: - message: message bus message, not used but required - """ - if not self._is_message_for_service(message): - return - if time.monotonic() - self.play_start_time > 1: - LOG.debug('stopping all playing services') - with self.service_lock: - try: - self._perform_stop(message) - except Exception as e: - LOG.exception(e) - LOG.error("failed to stop!") - LOG.info('END Stop') - - def _lower_volume(self, message=None): - """ - Is triggered when mycroft starts to speak and reduces the volume. - - Args: - message: message bus message, not used but required - """ - if not self._is_message_for_service(message): - return - if self.current: - LOG.debug('lowering volume') - self.current.lower_volume() - self.volume_is_low = True - - def _restore_volume(self, message=None): - """Triggered when mycroft is done speaking and restores the volume.""" - if not self._is_message_for_service(message): - return - current = self.current - if current: - LOG.debug('restoring volume') - self.volume_is_low = False - current.restore_volume() - - def _restore_volume_after_record(self, message=None): - """ - Restores the volume when Mycroft is done recording. - If no utterance detected, restore immediately. - If no response is made in reasonable time, then also restore. - - Args: - message: message bus message, not used but required - """ - if not self._is_message_for_service(message): - return - - def restore_volume(): - LOG.debug('restoring volume') - self.current.restore_volume() - - if self.current: - self.bus.on('recognizer_loop:speech.recognition.unknown', - restore_volume) - speak_msg_detected = self.bus.wait_for_message('speak', - timeout=8.0) - if not speak_msg_detected: - restore_volume() - self.bus.remove('recognizer_loop:speech.recognition.unknown', - restore_volume) - else: - LOG.debug("No audio service to restore volume of") - - def play(self, tracks, prefered_service, repeat=False): - """ - play starts playing the audio on the prefered service if it - supports the uri. If not the next best backend is found. - - Args: - tracks: list of tracks to play. - repeat: should the playlist repeat - prefered_service: indecates the service the user prefer to play - the tracks. - """ - self._perform_stop() - - if isinstance(tracks[0], str): - uri_type = tracks[0].split(':')[0] - else: - uri_type = tracks[0][0].split(':')[0] - - # check if user requested a particular service - if prefered_service and uri_type in prefered_service.supported_uris(): - selected_service = prefered_service - # check if default supports the uri - elif self.default and uri_type in self.default.supported_uris(): - LOG.debug("Using default backend ({})".format(self.default.name)) - selected_service = self.default - else: # Check if any other service can play the media - LOG.debug("Searching the services") - for s in self.service: - if uri_type in s.supported_uris(): - LOG.debug("Service {} supports URI {}".format(s, uri_type)) - selected_service = s - break - else: - LOG.info('No service found for uri_type: ' + uri_type) - return - if not selected_service.supports_mime_hints: - tracks = [t[0] if isinstance(t, list) else t for t in tracks] - selected_service.clear_list() - selected_service.add_list(tracks) - selected_service.play(repeat) - self.current = selected_service - self.play_start_time = time.monotonic() - - def _is_message_for_service(self, message): - if not message or not self.validate_source: - return True - return validate_message_context(message) - - def _queue(self, message): - if not self._is_message_for_service(message): - return - if self.current: - with self.service_lock: - try: - tracks = message.data['tracks'] - self.current.add_list(tracks) - except Exception as e: - LOG.exception(e) - LOG.error("failed to queue tracks!") - else: - self._play(message) - - def _play(self, message): - """ - Handler for mycroft.audio.service.play. Starts playback of a - tracklist. Also determines if the user requested a special - service. - - Args: - message: message bus message, not used but required - """ - if not self._is_message_for_service(message): - return - with self.service_lock: - tracks = message.data['tracks'] - repeat = message.data.get('repeat', False) - # Find if the user wants to use a specific backend - for s in self.service: - try: - if ('utterance' in message.data and - s.name in message.data['utterance']): - prefered_service = s - LOG.debug(s.name + ' would be prefered') - break - except Exception as e: - LOG.error(f"failed to parse audio service name: {s}") - else: - prefered_service = None - try: - self.play(tracks, prefered_service, repeat) - # time.sleep(0.5) # TODO: Was this hard-coded delay necessary? - except Exception as e: - LOG.exception(e) - - def _track_info(self, message): - """ - Returns track info on the message bus. - - Args: - message: message bus message, not used but required - """ - if not self._is_message_for_service(message): - return - if self.current: - track_info = self.current.track_info() - else: - track_info = {} - self.bus.emit(message.reply('mycroft.audio.service.track_info_reply', - data=track_info)) - - def _list_backends(self, message): - """ Return a dict of available backends. """ - if not self._is_message_for_service(message): - return - data = {} - for s in self.service: - info = { - 'supported_uris': s.supported_uris(), - 'default': s == self.default, - 'remote': isinstance(s, RemoteAudioBackend) - } - data[s.name] = info - self.bus.emit(message.response(data)) - - def _get_track_length(self, message): - """ - getting the duration of the audio in milliseconds - """ - if not self._is_message_for_service(message): - return - dur = None - if self.current: - dur = self.current.get_track_length() - self.bus.emit(message.response({"length": dur})) - - def _get_track_position(self, message): - """ - get current position in milliseconds - """ - if not self._is_message_for_service(message): - return - pos = None - if self.current: - pos = self.current.get_track_position() - self.bus.emit(message.response({"position": pos})) - - def _set_track_position(self, message): - """ - Handle message bus command to go to position (in milliseconds) - - Args: - message: message bus message - """ - if not self._is_message_for_service(message): - return - milliseconds = message.data.get("position") - if milliseconds and self.current: - self.current.set_track_position(milliseconds) - - def _seek_forward(self, message): - """ - Handle message bus command to skip X seconds - - Args: - message: message bus message - """ - if not self._is_message_for_service(message): - return - seconds = message.data.get("seconds", 1) - if self.current: - self.current.seek_forward(seconds) - - def _seek_backward(self, message): - """ - Handle message bus command to rewind X seconds - - Args: - message: message bus message - """ - if not self._is_message_for_service(message): - return - seconds = message.data.get("seconds", 1) - if self.current: - self.current.seek_backward(seconds) - - def shutdown(self): - for s in self.service: - try: - LOG.info('shutting down ' + s.name) - s.shutdown() - except Exception as e: - LOG.error('shutdown of ' + s.name + ' failed: ' + repr(e)) - - # remove listeners - self.bus.remove('mycroft.audio.service.play', self._play) - self.bus.remove('mycroft.audio.service.queue', self._queue) - self.bus.remove('mycroft.audio.service.pause', self._pause) - self.bus.remove('mycroft.audio.service.resume', self._resume) - self.bus.remove('mycroft.audio.service.stop', self._stop) - self.bus.remove('mycroft.audio.service.next', self._next) - self.bus.remove('mycroft.audio.service.prev', self._prev) - self.bus.remove('mycroft.audio.service.track_info', self._track_info) - self.bus.remove('mycroft.audio.service.get_track_position', - self._get_track_position) - self.bus.remove('mycroft.audio.service.set_track_position', - self._set_track_position) - self.bus.remove('mycroft.audio.service.get_track_length', - self._get_track_length) - self.bus.remove('mycroft.audio.service.seek_forward', - self._seek_forward) - self.bus.remove('mycroft.audio.service.seek_backward', - self._seek_backward) - self.bus.remove('recognizer_loop:audio_output_start', - self._lower_volume) - self.bus.remove('recognizer_loop:record_begin', self._lower_volume) - self.bus.remove('recognizer_loop:audio_output_end', - self._restore_volume) - self.bus.remove('recognizer_loop:record_end', - self._restore_volume_after_record) diff --git a/ovos_audio/service.py b/ovos_audio/service.py index f1f92fd..35672a6 100644 --- a/ovos_audio/service.py +++ b/ovos_audio/service.py @@ -11,7 +11,6 @@ from ovos_bus_client import Message, MessageBusClient from ovos_bus_client.session import SessionManager from ovos_config.config import Configuration -from ovos_plugin_manager.audio import get_audio_service_configs 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 @@ -21,7 +20,6 @@ from ovos_utils.process_utils import ProcessStatus, StatusCallbackMap from ovos_utils.sound import play_audio -from ovos_audio.audio import AudioService from ovos_audio.playback import PlaybackThread from ovos_audio.transformers import DialogTransformersService from ovos_audio.tts import TTSFactory @@ -29,23 +27,23 @@ def on_ready(): - LOG.info('Audio service is ready.') + LOG.info('TTS service is ready.') def on_alive(): - LOG.info('Audio service is alive.') + LOG.info('TTS service is alive.') def on_started(): - LOG.info('Audio service started.') + LOG.info('TTS service started.') def on_error(e='Unknown'): - LOG.error(f'Audio service failed to launch ({e}).') + LOG.error(f'TTS service failed to launch ({e}).') def on_stopping(): - LOG.info('Audio service is shutting down...') + LOG.info('TTS service is shutting down...') class PlaybackService(Thread): @@ -93,12 +91,6 @@ def __init__(self, ready_hook=on_ready, error_hook=on_error, LOG.exception(e) self.status.set_error(e) - try: - self.audio = AudioService(self.bus, disable_ocp=disable_ocp, validate_source=validate_source) - except Exception as e: - LOG.exception(e) - self.status.set_error(e) - @staticmethod def get_tts_lang_options(lang, blacklist=None): """ returns a list of options to be consumed by an external UI @@ -156,29 +148,6 @@ def get_g2p_lang_options(lang, blacklist=None): opts.append(voice) return opts - @staticmethod - def get_audio_options(blacklist=None): - """ returns a list of options to be consumed by an external UI - each dict contains metadata about the plugins - - eg: - [{"type": "ovos_common_play", - "active": True, - "plugin_name": 'Ovos Common Play'}] - """ - blacklist = blacklist or [] - opts = [] - cfgs = get_audio_service_configs() - for name, config in cfgs.items(): - engine = config["type"] - if engine in blacklist: - continue - # For Display purposes, we want to show the engine name without the underscore or dash and capitalized all - plugin_display_name = engine.replace("_", " ").replace("-", " ").title() - config["plugin_name"] = plugin_display_name - opts.append(config) - return opts - def handle_opm_tts_query(self, message): """ Responds to opm.tts.query with data about installed plugins @@ -229,29 +198,8 @@ def handle_opm_g2p_query(self, message): } self.bus.emit(message.response(data)) - def handle_opm_audio_query(self, message): - """ Responds to opm.audio.query with data about installed plugins - - Response message.data will contain: - "plugins" - [list_of_plugins] - "configs" - {backend_name: backend_cfg}} - "options" - {lang: [list_of_valid_ui_metadata]} - """ - cfgs = get_audio_service_configs() - data = { - "plugins": list(cfgs.keys()), - "configs": cfgs, - "options": self.get_audio_options() - } - self.bus.emit(message.response(data)) - def run(self): self.status.set_alive() - if self.audio.wait_for_load(): - if len(self.audio.service) == 0: - LOG.warning('No audio backends loaded! ' - 'Audio playback is not available') - LOG.info("Running audio service in TTS only mode") # If at least TTS exists, report ready if self.tts: self.status.set_ready() @@ -492,14 +440,10 @@ def handle_instant_play(self, message): volume_changed = True elif muted: self.bus.emit(Message("mycroft.volume.unmute")) - if self.audio.current and not duck_pulse_handled: - self.audio.current.lower_volume() play_audio(audio_file).wait() # return to previous state - if self.audio.current and not duck_pulse_handled: - self.audio.current.restore_volume() if ensure_volume: if volume_changed: self.bus.emit(Message("mycroft.volume.set", {"percent": volume, @@ -528,7 +472,6 @@ def shutdown(self): if self.tts.playback: self.tts.playback.shutdown() self.tts.playback.join() - self.audio.shutdown() def init_messagebus(self): """ @@ -543,5 +486,4 @@ def init_messagebus(self): 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) - self.bus.on("opm.audio.query", self.handle_opm_audio_query) self.bus.on("opm.g2p.query", self.handle_opm_g2p_query) diff --git a/ovos_audio/version.py b/ovos_audio/version.py index f821e7f..be3eeb2 100644 --- a/ovos_audio/version.py +++ b/ovos_audio/version.py @@ -1,6 +1,6 @@ # START_VERSION_BLOCK VERSION_MAJOR = 0 -VERSION_MINOR = 0 -VERSION_BUILD = 2 -VERSION_ALPHA = 33 +VERSION_MINOR = 1 +VERSION_BUILD = 0 +VERSION_ALPHA = 1 # END_VERSION_BLOCK diff --git a/requirements/extras.txt b/requirements/extras.txt index 110e6f2..a822e9c 100644 --- a/requirements/extras.txt +++ b/requirements/extras.txt @@ -1,9 +1 @@ -ovos_plugin_common_play~=0.0, >=0.0.6a11 - -ovos-tts-plugin-server - -# ovos-ocp-youtube-plugin -ovos-ocp-m3u-plugin -ovos-ocp-rss-plugin -ovos-ocp-files-plugin -ovos-ocp-news-plugin \ No newline at end of file +ovos-tts-plugin-server \ No newline at end of file