mycroft-core/mycroft/audio/audioservice.py

552 lines
19 KiB
Python
Raw Normal View History

2018-08-16 14:17:41 +00:00
# Copyright 2017 Mycroft AI Inc.
#
# 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.
import importlib
2018-08-16 14:17:41 +00:00
import sys
import time
from os import listdir
from os.path import abspath, dirname, basename, isdir, join
from threading import Lock
2018-08-16 14:17:41 +00:00
from mycroft.configuration import Configuration
from mycroft.messagebus.message import Message
from mycroft.util.log import LOG
from mycroft.util.monotonic_event import MonotonicEvent
from mycroft.util.plugins import find_plugins
2018-08-16 14:17:41 +00:00
from .services import RemoteAudioBackend
MINUTES = 60 # Seconds in a minute
2018-08-16 14:17:41 +00:00
MAINMODULE = '__init__'
sys.path.append(abspath(dirname(__file__)))
def create_service_spec(service_folder):
2018-08-16 14:17:41 +00:00
"""Prepares a descriptor that can be used together with imp.
Args:
service_folder: folder that shall be imported.
Returns:
Dict with import information
"""
module_name = 'audioservice_' + basename(service_folder)
path = join(service_folder, MAINMODULE + '.py')
spec = importlib.util.spec_from_file_location(module_name, path)
mod = importlib.util.module_from_spec(spec)
info = {'spec': spec, 'mod': mod, 'module_name': module_name}
2018-08-16 14:17:41 +00:00
return {"name": basename(service_folder), "info": info}
def get_services(services_folder):
"""
Load and initialize services from all subfolders.
Args:
services_folder: base folder to look for services in.
Returns:
Sorted list of audio services.
"""
LOG.info("Loading services from " + services_folder)
services = []
possible_services = listdir(services_folder)
for i in possible_services:
location = join(services_folder, i)
if (isdir(location) and
not MAINMODULE + ".py" in listdir(location)):
for j in listdir(location):
name = join(location, j)
if (not isdir(name) or
not MAINMODULE + ".py" in listdir(name)):
continue
try:
services.append(create_service_spec(name))
2018-08-16 14:17:41 +00:00
except Exception:
LOG.error('Failed to create service from ' + name,
exc_info=True)
if (not isdir(location) or
not MAINMODULE + ".py" in listdir(location)):
continue
try:
services.append(create_service_spec(location))
2018-08-16 14:17:41 +00:00
except Exception:
LOG.error('Failed to create service from ' + location,
exc_info=True)
return sorted(services, key=lambda p: p.get('name'))
def setup_service(service_module, config, bus):
"""Run the appropriate setup function and return created service objects.
2021-05-05 04:41:56 +00:00
Args:
service_module: Python module to run
config (dict): Mycroft configuration dict
bus (MessageBusClient): Messagebus interface
2021-05-05 04:41:56 +00:00
Returns:
(list) List of created services.
2018-08-16 14:17:41 +00:00
"""
if (hasattr(service_module, 'autodetect') and
callable(service_module.autodetect)):
try:
return service_module.autodetect(config, bus)
except Exception as e:
LOG.error('Failed to autodetect. ' + repr(e))
elif hasattr(service_module, 'load_service'):
try:
return service_module.load_service(config, bus)
except Exception as e:
LOG.error('Failed to load service. ' + repr(e))
else:
2021-07-22 19:31:23 +00:00
LOG.error('Failed to load service. loading function not found')
return None
2018-08-16 14:17:41 +00:00
def load_internal_services(config, bus, path=None):
"""Load audio services included in Mycroft-core.
2021-05-05 04:41:56 +00:00
Args:
config: configuration dict for the audio backends.
bus: Mycroft messagebus
path: (default None) optional path for builtin audio service
implementations
2021-05-05 04:41:56 +00:00
Returns:
List of started services
2018-08-16 14:17:41 +00:00
"""
if path is None:
path = dirname(abspath(__file__)) + '/services/'
service_directories = get_services(path)
service = []
for descriptor in service_directories:
LOG.info('Loading ' + descriptor['name'])
try:
service_module = descriptor['info']['mod']
spec = descriptor['info']['spec']
module_name = descriptor['info']['module_name']
sys.modules[module_name] = service_module
spec.loader.exec_module(service_module)
2018-08-16 14:17:41 +00:00
except Exception as e:
LOG.error('Failed to import module ' + descriptor['name'] + '\n' +
repr(e))
else:
s = setup_service(service_module, config, bus)
if s:
2018-08-16 14:17:41 +00:00
service += s
return service
def load_plugins(config, bus):
"""Load installed audioservice plugins.
2021-05-05 04:41:56 +00:00
Args:
config: configuration dict for the audio backends.
bus: Mycroft messagebus
Returns:
List of started services
"""
plugin_services = []
2021-07-22 19:31:23 +00:00
found_plugins = find_plugins('mycroft.plugin.audioservice')
for plugin_name, plugin_module in found_plugins.items():
LOG.info(f'Loading audio service plugin: {plugin_name}')
service = setup_service(plugin_module, config, bus)
if service:
plugin_services += service
return plugin_services
def load_services(config, bus, path=None):
"""Load builtin services as well as service plugins
The builtin service folder is scanned (or a folder indicated by the path
parameter) for services and plugins registered with the
"mycroft.plugin.audioservice" entrypoint group.
2021-05-05 04:41:56 +00:00
Args:
config: configuration dict for the audio backends.
bus: Mycroft messagebus
path: (default None) optional path for builtin audio service
implementations
Returns:
List of started services.
"""
return (load_internal_services(config, bus, path) +
load_plugins(config, bus))
class AudioService:
2018-08-16 14:17:41 +00:00
""" Audio Service class.
Handles playback of audio and selecting proper backend for the uri
to be played.
"""
def __init__(self, bus):
2018-08-16 14:17:41 +00:00
"""
Args:
bus: Mycroft messagebus
2018-08-16 14:17:41 +00:00
"""
self.bus = bus
2018-08-16 14:17:41 +00:00
self.config = Configuration.get().get("Audio")
self.service_lock = Lock()
self.default = None
self.service = []
self.current = None
self.play_start_time = 0
2018-08-16 14:17:41 +00:00
self.volume_is_low = False
self._loaded = MonotonicEvent()
self.load_services()
2018-08-16 14:17:41 +00:00
def load_services(self):
"""Method for loading services.
Sets up the global service, default and registers the event handlers
for the subsystem.
2018-08-16 14:17:41 +00:00
"""
services = load_services(self.config, self.bus)
# Sort services so local services are checked first
local = [s for s in services if not isinstance(s, RemoteAudioBackend)]
remote = [s for s in services if isinstance(s, RemoteAudioBackend)]
self.service = local + remote
2018-08-16 14:17:41 +00:00
# Register end of track callback
for s in self.service:
s.set_track_start_callback(self.track_start)
# Find default backend
default_name = self.config.get('default-backend', '')
LOG.info('Finding default backend...')
for s in self.service:
if s.name == default_name:
self.default = s
LOG.info('Found ' + self.default.name)
break
else:
self.default = None
LOG.info('no default found')
# 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.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)
2018-08-16 14:17:41 +00:00
self._loaded.set() # Report services loaded
def wait_for_load(self, timeout=3 * MINUTES):
"""Wait for services to be loaded.
2021-05-05 04:41:56 +00:00
Args:
timeout (float): Seconds to wait (default 3 minutes)
Returns:
(bool) True if loading completed within timeout, else False.
"""
return self._loaded.wait(timeout)
2018-08-16 14:17:41 +00:00
def track_start(self, track):
"""Callback method called from the services to indicate start of
playback of a track or end of playlist.
2018-08-16 14:17:41 +00:00
"""
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'))
2018-08-16 14:17:41 +00:00
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 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 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 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 self.current:
self.current.previous()
def _perform_stop(self):
"""Stop audioservice if active."""
if self.current:
name = self.current.name
if self.current.stop():
self.bus.emit(Message("mycroft.stop.handled",
{"by": "audio:" + name}))
self.current = None
2018-08-16 14:17:41 +00:00
def _stop(self, message=None):
"""
Handler for mycroft.stop. Stops any playing service.
Args:
message: message bus message, not used but required
"""
if time.monotonic() - self.play_start_time > 1:
LOG.debug('stopping all playing services')
with self.service_lock:
self._perform_stop()
LOG.info('END Stop')
2018-08-16 14:17:41 +00:00
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 self.current:
LOG.debug('lowering volume')
self.current.lower_volume()
self.volume_is_low = True
2021-01-15 20:58:34 +00:00
def _restore_volume(self, _=None):
"""Triggered when mycroft is done speaking and restores the volume."""
current = self.current
if current:
2018-08-16 14:17:41 +00:00
LOG.debug('restoring volume')
self.volume_is_low = False
2021-01-15 20:58:34 +00:00
current.restore_volume()
2018-08-16 14:17:41 +00:00
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
"""
2020-06-29 05:00:39 +00:00
def restore_volume():
LOG.debug('restoring volume')
self.current.restore_volume()
2020-06-29 05:00:39 +00:00
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)
2020-06-29 05:00:39 +00:00
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):
2018-08-16 14:17:41 +00:00
"""
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
2018-08-16 14:17:41 +00:00
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]
2018-08-16 14:17:41 +00:00
# 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]
2018-08-16 14:17:41 +00:00
selected_service.clear_list()
selected_service.add_list(tracks)
selected_service.play(repeat)
2018-08-16 14:17:41 +00:00
self.current = selected_service
self.play_start_time = time.monotonic()
2018-08-16 14:17:41 +00:00
def _queue(self, message):
if self.current:
with self.service_lock:
tracks = message.data['tracks']
self.current.add_list(tracks)
2018-08-16 14:17:41 +00:00
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
"""
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:
if ('utterance' in message.data and
s.name in message.data['utterance']):
prefered_service = s
LOG.debug(s.name + ' would be prefered')
break
else:
prefered_service = None
self.play(tracks, prefered_service, repeat)
time.sleep(0.5)
2018-08-16 14:17:41 +00:00
def _track_info(self, message):
"""
Returns track info on the message bus.
Args:
message: message bus message, not used but required
"""
if self.current:
track_info = self.current.track_info()
else:
track_info = {}
self.bus.emit(Message('mycroft.audio.service.track_info_reply',
data=track_info))
2018-08-16 14:17:41 +00:00
def _list_backends(self, message):
""" Return a dict of available backends. """
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 _seek_forward(self, message):
"""
Handle message bus command to skip X seconds
Args:
message: message bus message
"""
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
"""
seconds = message.data.get("seconds", 1)
if self.current:
self.current.seek_backward(seconds)
2018-08-16 14:17:41 +00:00
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.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)