From c57fcfe3286f32c5ce74b5bd5e2c1ef5eb56a983 Mon Sep 17 00:00:00 2001 From: Josh Cox Date: Mon, 9 Jul 2018 11:15:44 -0500 Subject: [PATCH 1/4] Add Simple audio backend Uses simple command line tools to play audio. This replaces the mpg123 and the pogg123 backends. --- mycroft/audio/services/simple/__init__.py | 126 ++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 mycroft/audio/services/simple/__init__.py diff --git a/mycroft/audio/services/simple/__init__.py b/mycroft/audio/services/simple/__init__.py new file mode 100644 index 0000000000..4e94f33106 --- /dev/null +++ b/mycroft/audio/services/simple/__init__.py @@ -0,0 +1,126 @@ +# 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 subprocess +from time import sleep + +from mycroft.audio.services import AudioBackend +from mycroft.messagebus.message import Message +from mycroft.util.log import LOG +import mimetypes + + +class SimpleAudioService(AudioBackend): + """ + Simple Audio backend for both mpg123 and the ogg123 player. + This one is rather limited and + only implements basic usage. + """ + + def __init__(self, config, emitter, name='simple'): + super(SimpleAudioService, self).__init__(config, emitter) + self.config = config + self.process = None + self.emitter = emitter + self.name = name + self._stop_signal = False + self._is_playing = False + self.tracks = [] + self.index = 0 + mimetypes.init() + + self.emitter.on('SimpleAudioServicePlay', self._play) + + def supported_uris(self): + return ['file', 'http'] + + def clear_list(self): + self.tracks = [] + + def add_list(self, tracks): + self.tracks += tracks + LOG.info("Track list is " + str(tracks)) + + def _play(self, message=None): + """ Implementation specific async method to handle playback. + This allows mpg123 service to use the "next method as well + as basic play/stop. + """ + LOG.info('SimpleAudioService._play') + self._is_playing = True + track = self.tracks[self.index] + # Indicate to audio service which track is being played + if self._track_start_callback: + self._track_start_callback(track) + + # Replace file:// uri's with normal paths + track = track.replace('file://', '') + + self.process = subprocess.Popen(['mpg123', track]) + # Wait for completion or stop request + while self.process.poll() is None and not self._stop_signal: + sleep(0.25) + + if self._stop_signal: + self.process.terminate() + self.process = None + self._is_playing = False + return + + self.index += 1 + # if there are more tracks available play next + if self.index < len(self.tracks): + self.emitter.emit(Message('SimpleAudioServicePlay')) + else: + self._is_playing = False + + def play(self): + LOG.info('Call SimpleAudioServicePlay') + self.index = 0 + self.emitter.emit(Message('SimpleAudioServicePlay')) + + def stop(self): + LOG.info('SimpleAudioServiceStop') + self._stop_signal = True + while self._is_playing: + sleep(0.1) + self._stop_signal = False + + def pause(self): + pass + + def resume(self): + pass + + def next(self): + # Terminate process to continue to next + self.process.terminate() + + def previous(self): + pass + + def lower_volume(self): + pass + + def restore_volume(self): + pass + + +def load_service(base_config, emitter): + backends = base_config.get('backends', []) + services = [(b, backends[b]) for b in backends + if backends[b]['type'] == 'mpg123' and + backends[b].get('active', True)] + instances = [SimpleAudioService(s[1], emitter, s[0]) for s in services] + return instances From 7b4c6ed5837c40850814612789e03f0b1666deaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Mon, 16 Jul 2018 11:47:20 +0200 Subject: [PATCH 2/4] Audio service mimetypes Add possibility for mimetypes to be passed with each track. Make simple audio service try to detect mime type if missing --- mycroft/audio/audioservice.py | 9 +- mycroft/audio/services/__init__.py | 1 + mycroft/audio/services/mpg123/__init__.py | 125 --------------------- mycroft/audio/services/ogg123/__init__.py | 127 ---------------------- mycroft/audio/services/simple/__init__.py | 51 +++++++-- mycroft/configuration/mycroft.conf | 6 +- mycroft/skills/audioservice.py | 18 ++- 7 files changed, 66 insertions(+), 271 deletions(-) delete mode 100644 mycroft/audio/services/mpg123/__init__.py delete mode 100644 mycroft/audio/services/ogg123/__init__.py diff --git a/mycroft/audio/audioservice.py b/mycroft/audio/audioservice.py index 7a8b662f05..7db4fe2736 100644 --- a/mycroft/audio/audioservice.py +++ b/mycroft/audio/audioservice.py @@ -344,7 +344,12 @@ class AudioService(object): the tracks. """ self._stop() - uri_type = tracks[0].split(':')[0] + + 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 @@ -362,6 +367,8 @@ class AudioService(object): 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() diff --git a/mycroft/audio/services/__init__.py b/mycroft/audio/services/__init__.py index 10789fdcd5..c8d1508599 100644 --- a/mycroft/audio/services/__init__.py +++ b/mycroft/audio/services/__init__.py @@ -27,6 +27,7 @@ class AudioBackend(): def __init__(self, config, bus): self._track_start_callback = None + self.supports_mime_hints = False @abstractmethod def supported_uris(self): diff --git a/mycroft/audio/services/mpg123/__init__.py b/mycroft/audio/services/mpg123/__init__.py deleted file mode 100644 index 974840f7e8..0000000000 --- a/mycroft/audio/services/mpg123/__init__.py +++ /dev/null @@ -1,125 +0,0 @@ -# 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 subprocess -from time import sleep - -from mycroft.audio.services import AudioBackend -from mycroft.messagebus.message import Message -from mycroft.util.log import LOG - - -class Mpg123Service(AudioBackend): - """ - Audio backend for mpg123 player. This one is rather limited and - only implements basic usage. - """ - - def __init__(self, config, bus, name='mpg123'): - super(Mpg123Service, self).__init__(config, bus) - self.config = config - self.process = None - self.bus = bus - self.name = name - self._stop_signal = False - self._is_playing = False - - bus.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 - LOG.info("Track list is " + str(tracks)) - - def _play(self, message=None): - """ Implementation specific async method to handle playback. - This allows mpg123 service to use the "next method as well - as basic play/stop. - """ - LOG.info('Mpg123Service._play') - self._is_playing = True - track = self.tracks[self.index] - # Indicate to audio service which track is being played - if self._track_start_callback: - self._track_start_callback(track) - - # Replace file:// uri's with normal paths - track = track.replace('file://', '') - - self.process = subprocess.Popen(['mpg123', track]) - # Wait for completion or stop request - while self.process.poll() is None and not self._stop_signal: - sleep(0.25) - - if self._stop_signal: - self.process.terminate() - self.process = None - self._is_playing = False - return - - self.index += 1 - # if there are more tracks available play next - if self.index < len(self.tracks): - bus.emit(Message('Mpg123ServicePlay')) - else: - self._is_playing = False - - def play(self): - LOG.info('Call Mpg123ServicePlay') - self.index = 0 - bus.emit(Message('Mpg123ServicePlay')) - - def stop(self): - LOG.info('Mpg123ServiceStop') - if self._is_playing: - self._stop_signal = True - while self._is_playing: - sleep(0.01) - self._stop_signal = False - return True - else: - return False - - def pause(self): - pass - - def resume(self): - pass - - def next(self): - # Terminate process to continue to next - self.process.terminate() - - def previous(self): - pass - - def lower_volume(self): - pass - - def restore_volume(self): - pass - - -def load_service(base_config, bus): - backends = base_config.get('backends', []) - services = [(b, backends[b]) for b in backends - if backends[b]['type'] == 'mpg123' and - backends[b].get('active', True)] - instances = [Mpg123Service(s[1], bus, s[0]) for s in services] - return instances diff --git a/mycroft/audio/services/ogg123/__init__.py b/mycroft/audio/services/ogg123/__init__.py deleted file mode 100644 index 4ddf964ee3..0000000000 --- a/mycroft/audio/services/ogg123/__init__.py +++ /dev/null @@ -1,127 +0,0 @@ -# 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 subprocess -from time import sleep - -from mycroft.audio.services import AudioBackend -from mycroft.messagebus.message import Message -from mycroft.util.log import LOG - - -class Ogg123Service(AudioBackend): - """ - Audio backend for ogg123 player. This one is rather limited and - only implements basic usage. - """ - - def __init__(self, config, bus, name='ogg123'): - super(Ogg123Service, self).__init__(config, bus) - self.config = config - self.process = None - self.bus = bus - self.name = name - self._stop_signal = False - self._is_playing = False - self.index = 0 - self.tracks = [] - - self.bus.on('Ogg123ServicePlay', self._play) - - def supported_uris(self): - return ['file', 'http'] - - def clear_list(self): - self.tracks = [] - - def add_list(self, tracks): - self.tracks += tracks - LOG.info("Track list is " + str(tracks)) - - def _play(self, message=None): - """ Implementation specific async method to handle playback. - This allows ogg123 service to use the "next method as well - as basic play/stop. - """ - LOG.info('Ogg123Service._play') - self._is_playing = True - track = self.tracks[self.index] - # Indicate to audio service which track is being played - if self._track_start_callback: - self._track_start_callback(track) - - # Replace file:// uri's with normal paths - track = track.replace('file://', '') - - self.process = subprocess.Popen(['ogg123', track]) - # Wait for completion or stop request - while self.process.poll() is None and not self._stop_signal: - sleep(0.25) - - if self._stop_signal: - self.process.terminate() - self.process = None - self._is_playing = False - return - - self.index += 1 - # if there are more tracks available play next - if self.index < len(self.tracks): - self.bus.emit(Message('Ogg123ServicePlay')) - else: - self._is_playing = False - - def play(self): - LOG.info('Call Ogg123ServicePlay') - self.index = 0 - self.bus.emit(Message('Ogg123ServicePlay')) - - def stop(self): - LOG.info('Ogg123ServiceStop') - if self._is_playing: - self._stop_signal = True - while self._is_playing: - sleep(0.1) - self._stop_signal = False - return True - else: - return False - - def pause(self): - pass - - def resume(self): - pass - - def next(self): - # Terminate process to continue to next - self.process.terminate() - - def previous(self): - pass - - def lower_volume(self): - pass - - def restore_volume(self): - pass - - -def load_service(base_config, bus): - backends = base_config.get('backends', []) - services = [(b, backends[b]) for b in backends - if backends[b]['type'] == 'ogg123' and - backends[b].get('active', True)] - instances = [Ogg123Service(s[1], bus, s[0]) for s in services] - return instances diff --git a/mycroft/audio/services/simple/__init__.py b/mycroft/audio/services/simple/__init__.py index 4e94f33106..65d44887da 100644 --- a/mycroft/audio/services/simple/__init__.py +++ b/mycroft/audio/services/simple/__init__.py @@ -19,16 +19,32 @@ from mycroft.audio.services import AudioBackend from mycroft.messagebus.message import Message from mycroft.util.log import LOG import mimetypes +from requests import Session + + +def find_mime(path): + mime = None + if path.startswith('http'): + response = Session().head(path, allow_redirects=True) + if 200 <= response.status_code < 300: + mime = response.headers['content-type'] + if not mime: + mime = mimetypes.guess_mime(path)[0] + + if mime: + return mime.split('/') + else: + return (None, None) class SimpleAudioService(AudioBackend): """ Simple Audio backend for both mpg123 and the ogg123 player. - This one is rather limited and - only implements basic usage. + This one is rather limited and only implements basic usage. """ def __init__(self, config, emitter, name='simple'): + super(SimpleAudioService, self).__init__(config, emitter) self.config = config self.process = None @@ -38,6 +54,7 @@ class SimpleAudioService(AudioBackend): self._is_playing = False self.tracks = [] self.index = 0 + self.supports_mime_hints = True mimetypes.init() self.emitter.on('SimpleAudioServicePlay', self._play) @@ -59,18 +76,34 @@ class SimpleAudioService(AudioBackend): """ LOG.info('SimpleAudioService._play') self._is_playing = True - track = self.tracks[self.index] + if isinstance(self.tracks[self.index], list): + track = self.tracks[self.index][0] + mime = self.tracks[self.index][1] + mime = mime.split('/') + else: # Assume string + track = self.tracks[self.index] + mime = find_mime(track) + print('MIME: ' + str(mime)) # Indicate to audio service which track is being played if self._track_start_callback: self._track_start_callback(track) # Replace file:// uri's with normal paths track = track.replace('file://', '') - - self.process = subprocess.Popen(['mpg123', track]) - # Wait for completion or stop request - while self.process.poll() is None and not self._stop_signal: - sleep(0.25) + proc = None + if 'mpeg' in mime[1]: + proc = 'mpg123' + elif 'ogg' in mime[1]: + proc = 'ogg123' + elif 'wav' in mime[1]: + proc = 'aplay' + else: + proc = 'mpg123' # If no mime info could be determined gues mp3 + if proc: + self.process = subprocess.Popen([proc, track]) + # Wait for completion or stop request + while self.process.poll() is None and not self._stop_signal: + sleep(0.25) if self._stop_signal: self.process.terminate() @@ -120,7 +153,7 @@ class SimpleAudioService(AudioBackend): def load_service(base_config, emitter): backends = base_config.get('backends', []) services = [(b, backends[b]) for b in backends - if backends[b]['type'] == 'mpg123' and + if backends[b]['type'] == 'simple' and backends[b].get('active', True)] instances = [SimpleAudioService(s[1], emitter, s[0]) for s in services] return instances diff --git a/mycroft/configuration/mycroft.conf b/mycroft/configuration/mycroft.conf index 74ead7e749..1c47860655 100644 --- a/mycroft/configuration/mycroft.conf +++ b/mycroft/configuration/mycroft.conf @@ -291,11 +291,7 @@ "Audio": { "backends": { "local": { - "type": "mpg123", - "active": true - }, - "ogg": { - "type": "ogg123", + "type": "simple", "active": true }, "vlc": { diff --git a/mycroft/skills/audioservice.py b/mycroft/skills/audioservice.py index 36897542f5..de96abbbf3 100644 --- a/mycroft/skills/audioservice.py +++ b/mycroft/skills/audioservice.py @@ -28,10 +28,18 @@ def ensure_uri(s): Returns: if s is uri, s is returned otherwise file:// is prepended """ - if '://' not in s: - return 'file://' + abspath(s) + if isinstance(s, str): + if '://' not in s: + return 'file://' + abspath(s) + else: + return s + elif isinstance(s, (tuple, list)): + if '://' not in s[0]: + return 'file://' + abspath(s[0]), s[1] + else: + return s else: - return s + raise ValueError('Invalid track') class AudioService(object): @@ -74,11 +82,13 @@ class AudioService(object): Args: tracks: track uri or list of track uri's + Each track can be added as a tuple with (uri, mime) + to give a hint of the mime type to the system utterance: forward utterance for further processing by the audio service. """ tracks = tracks or [] - if isinstance(tracks, str): + if isinstance(tracks, (str, tuple)): tracks = [tracks] elif not isinstance(tracks, list): raise ValueError From c4afe7429aa8d8bc3259bdeb75d7ea4a7b2e0900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 23 Aug 2018 09:03:53 +0200 Subject: [PATCH 3/4] Rename emitter "bus" for consistency --- mycroft/audio/services/simple/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/mycroft/audio/services/simple/__init__.py b/mycroft/audio/services/simple/__init__.py index 65d44887da..8195f33edd 100644 --- a/mycroft/audio/services/simple/__init__.py +++ b/mycroft/audio/services/simple/__init__.py @@ -43,12 +43,12 @@ class SimpleAudioService(AudioBackend): This one is rather limited and only implements basic usage. """ - def __init__(self, config, emitter, name='simple'): + def __init__(self, config, bus, name='simple'): - super(SimpleAudioService, self).__init__(config, emitter) + super(SimpleAudioService, self).__init__(config, bus) self.config = config self.process = None - self.emitter = emitter + self.bus = bus self.name = name self._stop_signal = False self._is_playing = False @@ -57,7 +57,7 @@ class SimpleAudioService(AudioBackend): self.supports_mime_hints = True mimetypes.init() - self.emitter.on('SimpleAudioServicePlay', self._play) + self.bus.on('SimpleAudioServicePlay', self._play) def supported_uris(self): return ['file', 'http'] @@ -114,14 +114,14 @@ class SimpleAudioService(AudioBackend): self.index += 1 # if there are more tracks available play next if self.index < len(self.tracks): - self.emitter.emit(Message('SimpleAudioServicePlay')) + self.bus.emit(Message('SimpleAudioServicePlay')) else: self._is_playing = False def play(self): LOG.info('Call SimpleAudioServicePlay') self.index = 0 - self.emitter.emit(Message('SimpleAudioServicePlay')) + self.bus.emit(Message('SimpleAudioServicePlay')) def stop(self): LOG.info('SimpleAudioServiceStop') @@ -150,10 +150,10 @@ class SimpleAudioService(AudioBackend): pass -def load_service(base_config, emitter): +def load_service(base_config, bus): backends = base_config.get('backends', []) services = [(b, backends[b]) for b in backends if backends[b]['type'] == 'simple' and backends[b].get('active', True)] - instances = [SimpleAudioService(s[1], emitter, s[0]) for s in services] + instances = [SimpleAudioService(s[1], bus, s[0]) for s in services] return instances From a180672db523c80fd246c180a32a95fd14f37572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Forslund?= Date: Thu, 23 Aug 2018 09:38:14 +0200 Subject: [PATCH 4/4] Minor update to service loading testcase --- test/unittests/audio/services/working/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/unittests/audio/services/working/__init__.py b/test/unittests/audio/services/working/__init__.py index c9b2986e8a..47f950849d 100644 --- a/test/unittests/audio/services/working/__init__.py +++ b/test/unittests/audio/services/working/__init__.py @@ -16,8 +16,9 @@ from mycroft.audio.services import AudioBackend class WorkingBackend(AudioBackend): - def __init__(self, config, emitter, name='Working'): - pass + def __init__(self, config, bus, name='Working'): + super(WorkingBackend, self).__init__(config, bus) + self.name = name def supported_uris(self): return ['file', 'http'] @@ -35,6 +36,6 @@ class WorkingBackend(AudioBackend): pass -def load_service(base_config, emitter): - instances = [WorkingBackend(base_config, emitter)] +def load_service(base_config, bus): + instances = [WorkingBackend(base_config, bus)] return instances