mycroft-core/mycroft/audio/main.py

485 lines
13 KiB
Python
Raw Normal View History

# Copyright 2016 Mycroft AI, Inc.
#
# This file is part of Mycroft Core.
#
# Mycroft Core is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Mycroft Core is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Mycroft Core. If not, see <http://www.gnu.org/licenses/>.
import json
from os.path import expanduser, exists, abspath, dirname, basename, isdir, join
from os import listdir
import sys
import time
import imp
2017-06-15 12:47:44 +00:00
import subprocess
from mycroft.configuration import ConfigurationManager
from mycroft.messagebus.client.ws import WebsocketClient
from mycroft.messagebus.message import Message
from mycroft.util.log import getLogger
2017-06-15 12:47:44 +00:00
import mycroft.audio.speech as speech
try:
import pulsectl
except:
pulsectl = None
__author__ = 'forslund'
MainModule = '__init__'
sys.path.append(abspath(dirname(__file__)))
logger = getLogger("Audio")
ws = None
default = None
service = []
current = None
2017-06-15 12:47:44 +00:00
config = None
pulse = None
pulse_quiet = None
pulse_restore = None
def create_service_descriptor(service_folder):
2017-04-11 18:20:45 +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
"""
info = imp.find_module(MainModule, [service_folder])
return {"name": basename(service_folder), "info": info}
def get_services(services_folder):
2017-04-11 18:20:45 +00:00
"""
Load and initialize services from all subfolders.
Args:
services_folder: base folder to look for services in.
Returns:
Sorted list of audio services.
"""
logger.info("Loading skills 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_descriptor(name))
except:
logger.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_descriptor(location))
except:
logger.error('Failed to create service from ' + name,
exc_info=True)
return sorted(services, key=lambda p: p.get('name'))
2017-06-16 08:59:57 +00:00
def load_services(config, ws, path=None):
2017-04-11 18:20:45 +00:00
"""
Search though the service directory and load any services.
Args:
config: configuration dicrt for the audio backends.
ws: websocket object for communication.
Returns:
List of started services.
"""
logger.info("Loading services")
2017-06-16 08:59:57 +00:00
if path is None:
path = dirname(abspath(__file__)) + '/services/'
service_directories = get_services(path)
service = []
for descriptor in service_directories:
logger.info('Loading ' + descriptor['name'])
2017-03-27 19:10:27 +00:00
try:
service_module = imp.load_module(descriptor["name"] + MainModule,
*descriptor["info"])
except:
logger.error('Failed to import module ' + descriptor['name'],
exc_info=True)
if (hasattr(service_module, 'autodetect') and
callable(service_module.autodetect)):
2017-03-27 19:10:27 +00:00
try:
s = service_module.autodetect(config, ws)
service += s
except:
logger.error('Failed to autodetect...',
exc_info=True)
2017-03-27 05:37:25 +00:00
if (hasattr(service_module, 'load_service')):
2017-03-27 19:10:27 +00:00
try:
s = service_module.load_service(config, ws)
service += s
except:
logger.error('Failed to load service...',
exc_info=True)
return service
def load_services_callback():
2017-04-11 18:20:45 +00:00
"""
Main callback function for loading services. Sets up the globals
service and default and registers the event handlers for the subsystem.
"""
global ws
global default
global service
config = ConfigurationManager.get().get("Audio")
service = load_services(config, ws)
logger.info(service)
default_name = config.get('default-backend', '')
logger.info('Finding default backend...')
for s in service:
logger.info('checking ' + s.name)
if s.name == default_name:
default = s
logger.info('Found ' + default.name)
break
else:
default = None
logger.info('no default found')
logger.info('Default:' + str(default))
ws.on('mycroft.audio.service.play', _play)
ws.on('mycroft.audio.service.pause', _pause)
2017-06-14 17:21:23 +00:00
ws.on('mycroft.audio.service.resume', _resume)
ws.on('mycroft.audio.service.stop', _stop)
ws.on('mycroft.audio.service.next', _next)
ws.on('mycroft.audio.service.prev', _prev)
ws.on('mycroft.audio.service.track_info', _track_info)
ws.on('recognizer_loop:audio_output_start', _lower_volume)
ws.on('recognizer_loop:record_begin', _lower_volume)
ws.on('recognizer_loop:audio_output_end', _restore_volume)
ws.on('recognizer_loop:record_end', _restore_volume)
2017-03-27 05:31:51 +00:00
ws.on('mycroft.stop', _stop)
2017-03-27 05:50:51 +00:00
def _pause(message=None):
2017-03-27 05:50:51 +00:00
"""
Handler for mycroft.audio.service.pause. Pauses the current audio
service.
2017-04-11 18:20:45 +00:00
Args:
message: message bus message, not used but required
2017-03-27 05:50:51 +00:00
"""
global current
if current:
current.pause()
2017-03-22 06:48:11 +00:00
def _resume(message=None):
2017-03-27 05:50:51 +00:00
"""
Handler for mycroft.audio.service.resume.
2017-04-11 18:20:45 +00:00
Args:
message: message bus message, not used but required
2017-03-27 05:50:51 +00:00
"""
2017-03-22 06:48:11 +00:00
global current
if current:
current.resume()
def _next(message=None):
2017-03-27 05:50:51 +00:00
"""
Handler for mycroft.audio.service.next. Skips current track and
starts playing the next.
2017-04-11 18:20:45 +00:00
Args:
message: message bus message, not used but required
2017-03-27 05:50:51 +00:00
"""
global current
if current:
current.next()
def _prev(message=None):
2017-03-27 05:50:51 +00:00
"""
Handler for mycroft.audio.service.prev. Starts playing the previous
track.
2017-04-11 18:20:45 +00:00
Args:
message: message bus message, not used but required
2017-03-27 05:50:51 +00:00
"""
global current
if current:
current.prev()
def _stop(message=None):
2017-03-27 05:50:51 +00:00
"""
Handler for mycroft.stop. Stops any playing service.
2017-04-11 18:20:45 +00:00
Args:
message: message bus message, not used but required
2017-03-27 05:50:51 +00:00
"""
global current
logger.info('stopping all playing services')
if current:
current.stop()
current = None
logger.info('Stopped')
def _lower_volume(message):
2017-03-27 05:50:51 +00:00
"""
Is triggered when mycroft starts to speak and reduces the volume.
2017-04-11 18:20:45 +00:00
Args:
message: message bus message, not used but required
2017-03-27 05:50:51 +00:00
"""
global current
global volume_is_low
logger.info('lowering volume')
if current:
current.lower_volume()
volume_is_low = True
try:
if pulse_quiet:
pulse_quiet()
except Exception as e:
2017-08-01 09:45:42 +00:00
logger.error(e)
muted_sinks = []
def pulse_mute():
"""
Mute all pulse audio input sinks except for the one named
'mycroft-voice'.
"""
global muted_sinks
for sink in pulse.sink_input_list():
if sink.name != 'mycroft-voice':
pulse.sink_input_mute(sink.index, 1)
muted_sinks.append(sink.index)
def pulse_unmute():
"""
Unmute all pulse audio input sinks.
"""
global muted_sinks
for sink in pulse.sink_input_list():
if sink.index in muted_sinks:
pulse.sink_input_mute(sink.index, 0)
muted_sinks = []
def pulse_lower_volume():
"""
Lower volume of all pulse audio input sinks except the one named
'mycroft-voice'.
"""
for sink in pulse.sink_input_list():
if sink.name != 'mycroft-voice':
v = sink.volume
v.value_flat *= 0.3
pulse.volume_set(sink, v)
def pulse_restore_volume():
"""
Restore volume of all pulse audio input sinks except the one named
'mycroft-voice'.
"""
for sink in pulse.sink_input_list():
if sink.name != 'mycroft-voice':
v = sink.volume
v.value_flat /= 0.3
pulse.volume_set(sink, v)
def _restore_volume(message):
2017-03-27 05:50:51 +00:00
"""
Is triggered when mycroft is done speaking and restores the volume
2017-04-11 18:20:45 +00:00
Args:
message: message bus message, not used but required
2017-03-27 05:50:51 +00:00
"""
global current
global volume_is_low
logger.info('maybe restoring volume')
if current:
volume_is_low = False
time.sleep(2)
if not volume_is_low:
logger.info('restoring volume')
current.restore_volume()
if pulse_restore:
pulse_restore()
def play(tracks, prefered_service):
2017-03-27 05:50:51 +00:00
"""
play starts playing the audio on the prefered service if it supports
the uri. If not the next best backend is found.
2017-04-11 18:20:45 +00:00
Args:
tracks: list of tracks to play.
prefered_service: indecates the service the user prefer to play
the tracks.
2017-03-27 05:50:51 +00:00
"""
global current
logger.info('play')
_stop()
uri_type = tracks[0].split(':')[0]
logger.info('uri_type: ' + uri_type)
# check if user requested a particular service
if prefered_service and uri_type in prefered_service.supported_uris():
service = prefered_service
# check if default supports the uri
elif default and uri_type in default.supported_uris():
logger.info("Using default backend")
logger.info(default.name)
service = default
else: # Check if any other service can play the media
for s in service:
logger.info(str(s))
if uri_type in s.supported_uris():
service = s
break
else:
return
logger.info('Clear list')
service.clear_list()
logger.info('Add tracks' + str(tracks))
service.add_list(tracks)
logger.info('Playing')
service.play()
current = service
def _play(message):
2017-03-27 05:50:51 +00:00
"""
Handler for mycroft.audio.service.play. Starts playback of a
tracklist. Also determines if the user requested a special service.
2017-04-11 18:20:45 +00:00
Args:
message: message bus message, not used but required
2017-03-27 05:50:51 +00:00
"""
global service
logger.info('mycroft.audio.service.play')
logger.info(message.data['tracks'])
tracks = message.data['tracks']
# Find if the user wants to use a specific backend
for s in service:
logger.info(s.name)
if s.name in message.data['utterance']:
prefered_service = s
logger.info(s.name + ' would be prefered')
break
else:
prefered_service = None
play(tracks, prefered_service)
def _track_info(message):
2017-03-27 05:50:51 +00:00
"""
Returns track info on the message bus.
2017-04-11 18:20:45 +00:00
Args:
message: message bus message, not used but required
2017-03-27 05:50:51 +00:00
"""
global current
if current:
track_info = current.track_info()
else:
track_info = {}
ws.emit(Message('mycroft.audio.service.track_info_reply',
data=track_info))
def setup_pulseaudio_handlers(pulse_choice=None):
"""
Select functions for handling lower volume/restore of
pulse audio input sinks.
Args:
pulse_choice: method selection, can be eithe 'mute' or 'lower'
"""
global pulse, pulse_quiet, pulse_restore
if pulsectl and pulse_choice is not None:
pulse = pulsectl.Pulse('Mycroft-audio-service')
if pulse_choice == 'mute':
pulse_quiet = pulse_mute
pulse_restore = pulse_unmute
elif pulse_choice == 'lower':
pulse_quiet = pulse_lower_volume
pulse_restore = pulse_restore_volume
def connect():
global ws
ws.run_forever()
def main():
global ws
2017-06-15 12:47:44 +00:00
global config
ws = WebsocketClient()
ConfigurationManager.init(ws)
2017-06-15 12:47:44 +00:00
config = ConfigurationManager.get()
speech.init(ws)
# Setup control of pulse audio
setup_pulseaudio_handlers(config.get('Audio').get('pulseaudio'))
def echo(message):
try:
_message = json.loads(message)
if 'mycroft.audio.service' not in _message.get('type'):
return
message = json.dumps(_message)
except:
pass
logger.debug(message)
logger.info("Staring Audio Services")
ws.on('message', echo)
ws.once('open', load_services_callback)
try:
ws.run_forever()
except KeyboardInterrupt, e:
logger.exception(e)
speech.shutdown()
sys.exit()
if __name__ == "__main__":
main()