Initial commit of audio service

Three backends have been added, mopidy, vlc and mpg123. Depending on uri type an apporpriate service is selected when media playback is requested. (for example mopidy service can handle spotify://... and local://...)

A playback Control skill can pause, resume, stop change track, etc any started media. So for example if the NPR news skill used this after starting playing the news the user can say "Hey Mycroft, pause" and the playback will pause. The playback control also handles stuff like lowering the volume of the playback if mycroft is asked another question.

Currently the different backend runs under the playbeck control, this was made most for convenience and the services should be moved in the future.

Usage:
The user needs to import the audioservice interface
`from mycroft.skills.audioservice import AudioService`

and initialize an instance in the skill `initialize` method

`self.audio_service = AudioService(self.emitter)`

Then playing an uri is as simple as

`self.audio_service.play(uri)`

TODO:
* Configuration (Alias for the different backends, service specific config, active services, etc.)
* Manual selection of backend (This is prepared in the audioservice interface biut not implemented)
* More feature complete audio service interface (playback control, get trackname etc)
* Separate audio services from the playback control
* Probably lots more
pull/433/head
Åke Forslund 2016-12-02 07:12:21 +01:00
parent edbd8edeb2
commit 89adf6e8ac
9 changed files with 547 additions and 0 deletions

View File

@ -0,0 +1,12 @@
from mycroft.messagebus.message import Message
class AudioService():
def __init__(self, emitter):
self.emitter = emitter
def play(self, tracks=[], utterance=''):
self.emitter.emit(Message('MycroftAudioServicePlay',
metadata={'tracks': tracks,
'utterance': utterance}))

View File

@ -0,0 +1,183 @@
import sys
from os.path import dirname, abspath, basename
from mycroft.skills.media import MediaSkill
from adapt.intent import IntentBuilder
from mycroft.messagebus.message import Message
from mycroft.configuration import ConfigurationManager
import subprocess
import time
import requests
from os.path import dirname
from mycroft.util.log import getLogger
logger = getLogger(abspath(__file__).split('/')[-2])
__author__ = 'forslund'
sys.path.append(abspath(dirname(__file__)))
MopidyService = __import__('mopidy_service').MopidyService
VlcService = __import__('vlc_service').VlcService
class Mpg123Service():
def __init__(self, emitter):
self.process = None
self.emitter = emitter
self.emitter.on('Mpg123ServicePlay', self._play)
def supported_uris(self):
return ['file', 'http']
def clear_list(self):
self.tracks = []
def add_list(self, tracks):
self.tracks = tracks
logger.info("Track list is " + str(tracks))
def _play(self, message):
logger.info('Mpg123Service._play')
track = self.tracks[self.index]
self.process = subprocess.Popen(['mpg123', track])
self.process.communicate()
self.process = None
self.index += 1
if self.index >= len(self.tracks):
self.emitter.emit(Message('Mpg123ServicePlay'))
def play(self):
logger.info('Call Mpg123ServicePlay')
self.index = 0
self.emitter.emit(Message('Mpg123ServicePlay'))
def stop(self):
logger.info('Mpg123ServiceStop')
self.clear_list()
if self.process:
self.process.terminate()
self.process = None
def pause(self):
pass
def resume(self):
pass
def next(self):
self.process.terminate()
def previous(self):
pass
def lower_volume(self):
pass
def restore_volume(self):
pass
def track_info(self):
pass
class PlaybackControlSkill(MediaSkill):
def __init__(self):
super(PlaybackControlSkill, self).__init__('Playback Control Skill')
self.volume_is_low = False
self.current = None
logger.info('Playback Control Inited')
self.service = []
def initialize(self):
logger.info('initializing Playback Control Skill')
super(PlaybackControlSkill, self).initialize()
self.load_data_files(dirname(__file__))
logger.info('starting VLC service')
self.service.append(VlcService(self.emitter))
logger.info('starting Mopidy service')
self.service.append(MopidyService(self.emitter))
logger.info('starting Mpg123 service')
self.service.append(Mpg123Service(self.emitter))
self.emitter.on('MycroftAudioServicePlay', self._play)
def play(self, tracks):
logger.info('play')
self.stop()
uri_type = tracks[0].split(':')[0]
logger.info('uri_type: ' + uri_type)
for s in self.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()
self.current = service
def _play(self, message):
logger.info('MycroftAudioServicePlay')
logger.info(message.metadata['tracks'])
tracks = message.metadata['tracks']
self.play(tracks)
def stop(self, message=None):
logger.info('stopping all playing services')
if self.current:
self.current.stop()
self.current = None
def handle_next(self, message):
if self.current:
self.current.next()
def handle_prev(self, message):
if self.current:
self.current.previous()
def handle_pause(self, message):
if self.current:
self.current.pause()
def handle_play(self, message):
"""Resume playback if paused"""
if self.current:
self.current.resume()
def lower_volume(self, message):
logger.info('lowering volume')
if self.current:
self.current.lower_volume()
self.volume_is_low = True
def restore_volume(self, message):
logger.info('maybe restoring volume')
if self.current:
self.volume_is_low = False
time.sleep(2)
if not self.volume_is_low:
logger.info('restoring volume')
self.current.restore_volume()
def handle_currently_playing(self, message):
if self.current:
track_info = self.current.track_info()
if track_info is not None:
data = {'current_track': track_info['name'],
'artist': track_info['album']}
self.speak_dialog('currently_playing', data)
time.sleep(6)
def create_skill():
return PlaybackControlSkill()

View File

@ -0,0 +1 @@
We're listening to {{current_track}} by {{artist}}.

View File

@ -0,0 +1 @@
Listening to {{tracks}}.

View File

@ -0,0 +1,80 @@
from mycroft.messagebus.message import Message
from mycroft.util.log import getLogger
from os.path import dirname, abspath, basename
import sys
import time
logger = getLogger(abspath(__file__).split('/')[-2])
__author__ = 'forslund'
sys.path.append(abspath(dirname(__file__)))
Mopidy = __import__('mopidypost').Mopidy
class MopidyService():
def _connect(self, message):
logger.debug('Could not connect to server, will retry quietly')
url = 'http://localhost:6680'
try:
self.mopidy = Mopidy(url)
except:
if self.connection_attempts < 1:
logger.debug('Could not connect to server, will retry quietly')
self.connection_attempts += 1
time.sleep(10)
self.emitter.emit(Message('MopidyServiceConnect'))
return
logger.info('Connected to mopidy server')
def __init__(self, emitter):
self.connection_attempts = 0
self.emitter = emitter
self.emitter.on('MopidyServiceConnect', self._connect)
self._connect(None)
def supported_uris(self):
return ['file', 'http', 'https', 'local', 'spotify', 'gmusic']
def clear_list(self):
self.mopidy.clear_list()
def add_list(self, tracks):
self.mopidy.add_list(tracks)
def play(self):
self.mopidy.play()
def stop(self):
self.mopidy.clear_list()
self.mopidy.stop()
def pause(self):
self.mopidy.pause()
def resume(self):
self.mopidy.resume()
def next(self):
self.mopidy.next()
def previous(self):
self.mopidy.previous()
def lower_volume(self):
self.mopidy.lower_volume()
def restore_volume(self):
self.mopidy.restore_volume()
def track_info(self):
info = self.mopidy.currently_playing()
ret = {}
ret['name'] = info.get('name', '')
if 'album' in info:
ret['artist'] = info['album']['artists'][0]['name']
ret['album'] = info['album'].get('name', '')
else:
ret['artist'] = ''
ret['album'] = ''
return ret

View File

@ -0,0 +1,201 @@
import requests
from copy import copy
import json
MOPIDY_API = '/mopidy/rpc'
_base_dict = {'jsonrpc': '2.0', 'id': 1, 'params': {}}
class Mopidy(object):
def __init__(self, url):
print "MOPIDY URL: " + url
self.is_playing = False
self.url = url + MOPIDY_API
self.volume = None
self.clear_list(force=True)
self.volume_low = 3
self.volume_high = 100
def find_artist(self, artist):
d = copy(_base_dict)
d['method'] = 'core.library.search'
d['params'] = {'artist': [artist]}
r = requests.post(self.url, data=json.dumps(d))
return r.json()['result'][1]['artists']
def get_playlists(self, filter=None):
print "GETTING PLAYLISTS"
d = copy(_base_dict)
d['method'] = 'core.playlists.as_list'
r = requests.post(self.url, data=json.dumps(d))
if filter is None:
return r.json()['result']
else:
return [l for l in r.json()['result'] if filter + ':' in l['uri']]
def find_album(self, album, filter=None):
d = copy(_base_dict)
d['method'] = 'core.library.search'
d['params'] = {'album': [album]}
r = requests.post(self.url, data=json.dumps(d))
l = [res['albums'] for res in r.json()['result'] if 'albums' in res]
if filter is None:
return l
else:
return [i for sl in l for i in sl if filter + ':' in i['uri']]
def find_exact(self, uris='null'):
d = copy(_base_dict)
d['method'] = 'core.library.find_exact'
d['params'] = {'uris': uris}
r = requests.post(self.url, data=json.dumps(d))
return r.json()
def browse(self, uri):
d = copy(_base_dict)
d['method'] = 'core.library.browse'
d['params'] = {'uri': uri}
print "BROWSE"
r = requests.post(self.url, data=json.dumps(d))
if 'result' in r.json():
return r.json()['result']
else:
return None
def clear_list(self, force=False):
if self.is_playing or force:
d = copy(_base_dict)
d['method'] = 'core.tracklist.clear'
r = requests.post(self.url, data=json.dumps(d))
return r
def add_list(self, uri):
d = copy(_base_dict)
d['method'] = 'core.tracklist.add'
if type(uri) == str or type(uri) == unicode:
d['params'] = {'uri': uri}
elif type(uri) == list:
d['params'] = {'uris': uri}
else:
return None
r = requests.post(self.url, data=json.dumps(d))
return r
def play(self):
self.is_playing = True
self.restore_volume()
d = copy(_base_dict)
d['method'] = 'core.playback.play'
r = requests.post(self.url, data=json.dumps(d))
def next(self):
if self.is_playing:
d = copy(_base_dict)
d['method'] = 'core.playback.next'
r = requests.post(self.url, data=json.dumps(d))
def previous(self):
if self.is_playing:
d = copy(_base_dict)
d['method'] = 'core.playback.previous'
r = requests.post(self.url, data=json.dumps(d))
def stop(self):
print self.is_playing
if self.is_playing:
d = copy(_base_dict)
d['method'] = 'core.playback.stop'
r = requests.post(self.url, data=json.dumps(d))
self.is_playing = False
def currently_playing(self):
if self.is_playing:
d = copy(_base_dict)
d['method'] = 'core.playback.get_current_track'
r = requests.post(self.url, data=json.dumps(d))
return r.json()['result']
else:
return None
def set_volume(self, percent):
if self.is_playing:
d = copy(_base_dict)
d['method'] = 'core.mixer.set_volume'
d['params'] = {'volume': percent}
r = requests.post(self.url, data=json.dumps(d))
def lower_volume(self):
self.set_volume(self.volume_low)
def restore_volume(self):
self.set_volume(self.volume_high)
def pause(self):
if self.is_playing:
d = copy(_base_dict)
d['method'] = 'core.playback.pause'
r = requests.post(self.url, data=json.dumps(d))
def resume(self):
if self.is_playing:
d = copy(_base_dict)
d['method'] = 'core.playback.resume'
r = requests.post(self.url, data=json.dumps(d))
def get_items(self, uri):
d = copy(_base_dict)
d['method'] = 'core.playlists.get_items'
d['params'] = {'uri': uri}
r = requests.post(self.url, data=json.dumps(d))
if 'result' in r.json():
print r.json()
return [e['uri'] for e in r.json()['result']]
else:
return None
def get_tracks(self, uri):
tracks = self.browse(uri)
ret = [t['uri'] for t in tracks if t['type'] == 'track']
sub_tracks = [t['uri'] for t in tracks if t['type'] != 'track']
for t in sub_tracks:
ret = ret + self.get_tracks(t)
return ret
def get_local_albums(self):
p = self.browse('local:directory?type=album')
return {e['name']: e for e in p if e['type'] == 'album'}
def get_local_artists(self):
p = self.browse('local:directory?type=artist')
return {e['name']: e for e in p if e['type'] == 'artist'}
def get_local_genres(self):
p = self.browse('local:directory?type=genre')
return {e['name']: e for e in p if e['type'] == 'directory'}
def get_local_playlists(self):
print "GETTING PLAYLISTS"
p = self.get_playlists('m3u')
print "RETURNING PLAYLISTS"
return {e['name']: e for e in p}
def get_spotify_playlists(self):
p = self.get_playlists('spotify')
return {e['name'].split('(by')[0].strip().lower(): e for e in p}
def get_gmusic_albums(self):
p = self.browse('gmusic:album')
print p
p = {e['name']: e for e in p if e['type'] == 'directory'}
print p
return {e.split(' - ')[1]: p[e] for e in p}
def get_gmusic_artists(self):
p = self.browse('gmusic:artist')
return {e['name']: e for e in p if e['type'] == 'directory'}
def get_gmusic_radio(self):
p = self.browse('gmusic:radio')
return {e['name']: e for e in p if e['type'] == 'directory'}

View File

@ -0,0 +1,67 @@
from os.path import dirname, abspath, basename
from mycroft.util.log import getLogger
logger = getLogger(abspath(__file__).split('/')[-2])
import vlc
class VlcService():
def __init__(self, emitter):
self.instance = vlc.Instance()
self.list_player = self.instance.media_list_player_new()
self.player = self.instance.media_player_new()
self.list_player.set_media_player(self.player)
self.emitter = emitter
def supported_uris(self):
return ['file', 'http']
def clear_list(self):
empty = self.instance.media_list_new()
self.list_player.set_media_list(empty)
def add_list(self, tracks):
logger.info("Track list is " + str(tracks))
vlc_tracks = self.instance.media_list_new()
for t in tracks:
vlc_tracks.add_media(self.instance.media_new(t))
self.list_player.set_media_list(vlc_tracks)
def play(self):
logger.info('VLCService Play')
self.list_player.play()
def stop(self):
logger.info('VLCService Stop')
self.clear_list()
self.list_player.stop()
def pause(self):
self.player.set_pause(1)
def resume(self):
self.player.set_pause(0)
def next(self):
self.list_player.next()
def previous(self):
self.list_player.previous()
def lower_volume(self):
self.player.audio_set_volume(30)
def restore_volume(self):
self.player.audio_set_volume(100)
def track_info(self):
ret = {}
meta = vlc.Meta
t = self.player.get_media()
ret['album'] = t.get_meta(meta.Album)
ret['artists'] = [t.get_meta(meta.Artist)]
ret['name'] = t.get_meta(meta.Title)
return ret

View File

@ -0,0 +1 @@
Search

View File

@ -0,0 +1 @@
Spotify