audio/audioservice.py
Go to the documentation of this file.
1 # Copyright 2017 Mycroft AI Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 
15 import imp
16 import sys
17 import time
18 from os import listdir
19 from os.path import abspath, dirname, basename, isdir, join
20 from threading import Lock
21 
22 from mycroft.configuration import Configuration
23 from mycroft.messagebus.message import Message
24 from mycroft.util.log import LOG
25 
26 from .services import RemoteAudioBackend
27 
28 try:
29  import pulsectl
30 except ImportError:
31  pulsectl = None
32 
33 MAINMODULE = '__init__'
34 sys.path.append(abspath(dirname(__file__)))
35 
36 
37 def create_service_descriptor(service_folder):
38  """Prepares a descriptor that can be used together with imp.
39 
40  Args:
41  service_folder: folder that shall be imported.
42 
43  Returns:
44  Dict with import information
45  """
46  info = imp.find_module(MAINMODULE, [service_folder])
47  return {"name": basename(service_folder), "info": info}
48 
49 
50 def get_services(services_folder):
51  """
52  Load and initialize services from all subfolders.
53 
54  Args:
55  services_folder: base folder to look for services in.
56 
57  Returns:
58  Sorted list of audio services.
59  """
60  LOG.info("Loading services from " + services_folder)
61  services = []
62  possible_services = listdir(services_folder)
63  for i in possible_services:
64  location = join(services_folder, i)
65  if (isdir(location) and
66  not MAINMODULE + ".py" in listdir(location)):
67  for j in listdir(location):
68  name = join(location, j)
69  if (not isdir(name) or
70  not MAINMODULE + ".py" in listdir(name)):
71  continue
72  try:
73  services.append(create_service_descriptor(name))
74  except Exception:
75  LOG.error('Failed to create service from ' + name,
76  exc_info=True)
77  if (not isdir(location) or
78  not MAINMODULE + ".py" in listdir(location)):
79  continue
80  try:
81  services.append(create_service_descriptor(location))
82  except Exception:
83  LOG.error('Failed to create service from ' + location,
84  exc_info=True)
85  return sorted(services, key=lambda p: p.get('name'))
86 
87 
88 def load_services(config, bus, path=None):
89  """
90  Search though the service directory and load any services.
91 
92  Args:
93  config: configuration dict for the audio backends.
94  bus: Mycroft messagebus
95 
96  Returns:
97  List of started services.
98  """
99  if path is None:
100  path = dirname(abspath(__file__)) + '/services/'
101  service_directories = get_services(path)
102  service = []
103  for descriptor in service_directories:
104  LOG.info('Loading ' + descriptor['name'])
105  try:
106  service_module = imp.load_module(descriptor["name"] + MAINMODULE,
107  *descriptor["info"])
108  except Exception as e:
109  LOG.error('Failed to import module ' + descriptor['name'] + '\n' +
110  repr(e))
111  continue
112 
113  if (hasattr(service_module, 'autodetect') and
114  callable(service_module.autodetect)):
115  try:
116  s = service_module.autodetect(config, bus)
117  service += s
118  except Exception as e:
119  LOG.error('Failed to autodetect. ' + repr(e))
120  if hasattr(service_module, 'load_service'):
121  try:
122  s = service_module.load_service(config, bus)
123  service += s
124  except Exception as e:
125  LOG.error('Failed to load service. ' + repr(e))
126 
127  return service
128 
129 
131  """ Audio Service class.
132  Handles playback of audio and selecting proper backend for the uri
133  to be played.
134  """
135 
136  def __init__(self, bus):
137  """
138  Args:
139  bus: Mycroft messagebus
140  """
141  self.bus = bus
142  self.config = Configuration.get().get("Audio")
143  self.service_lock = Lock()
144 
145  self.default = None
146  self.service = []
147  self.current = None
149  self.volume_is_low = False
150  self.pulse = None
151  self.pulse_quiet = None
152  self.pulse_restore = None
153 
154  self.muted_sinks = []
155  # Setup control of pulse audio
156  self.setup_pulseaudio_handlers(self.config.get('pulseaudio'))
157  bus.once('open', self.load_services_callback)
158 
160  """
161  Main callback function for loading services. Sets up the globals
162  service and default and registers the event handlers for the
163  subsystem.
164  """
165 
166  services = load_services(self.config, self.bus)
167  # Sort services so local services are checked first
168  local = [s for s in services if not isinstance(s, RemoteAudioBackend)]
169  remote = [s for s in services if isinstance(s, RemoteAudioBackend)]
170  self.service = local + remote
171 
172  # Register end of track callback
173  for s in self.service:
174  s.set_track_start_callback(self.track_start)
175 
176  # Find default backend
177  default_name = self.config.get('default-backend', '')
178  LOG.info('Finding default backend...')
179  for s in self.service:
180  if s.name == default_name:
181  self.default = s
182  LOG.info('Found ' + self.default.name)
183  break
184  else:
185  self.default = None
186  LOG.info('no default found')
187 
188  # Setup event handlers
189  self.bus.on('mycroft.audio.service.play', self._play)
190  self.bus.on('mycroft.audio.service.queue', self._queue)
191  self.bus.on('mycroft.audio.service.pause', self._pause)
192  self.bus.on('mycroft.audio.service.resume', self._resume)
193  self.bus.on('mycroft.audio.service.stop', self._stop)
194  self.bus.on('mycroft.audio.service.next', self._next)
195  self.bus.on('mycroft.audio.service.prev', self._prev)
196  self.bus.on('mycroft.audio.service.track_info', self._track_info)
197  self.bus.on('mycroft.audio.service.list_backends', self._list_backends)
198  self.bus.on('mycroft.audio.service.seek_forward', self._seek_forward)
199  self.bus.on('mycroft.audio.service.seek_backward', self._seek_backward)
200  self.bus.on('recognizer_loop:audio_output_start', self._lower_volume)
201  self.bus.on('recognizer_loop:record_begin', self._lower_volume)
202  self.bus.on('recognizer_loop:audio_output_end', self._restore_volume)
203  self.bus.on('recognizer_loop:record_end', self._restore_volume)
204 
205  def track_start(self, track):
206  """
207  Callback method called from the services to indicate start of
208  playback of a track.
209  """
210  self.bus.emit(Message('mycroft.audio.playing_track',
211  data={'track': track}))
212 
213  def _pause(self, message=None):
214  """
215  Handler for mycroft.audio.service.pause. Pauses the current audio
216  service.
217 
218  Args:
219  message: message bus message, not used but required
220  """
221  if self.current:
222  self.current.pause()
223 
224  def _resume(self, message=None):
225  """
226  Handler for mycroft.audio.service.resume.
227 
228  Args:
229  message: message bus message, not used but required
230  """
231  if self.current:
232  self.current.resume()
233 
234  def _next(self, message=None):
235  """
236  Handler for mycroft.audio.service.next. Skips current track and
237  starts playing the next.
238 
239  Args:
240  message: message bus message, not used but required
241  """
242  if self.current:
243  self.current.next()
244 
245  def _prev(self, message=None):
246  """
247  Handler for mycroft.audio.service.prev. Starts playing the previous
248  track.
249 
250  Args:
251  message: message bus message, not used but required
252  """
253  if self.current:
254  self.current.previous()
255 
256  def _stop(self, message=None):
257  """
258  Handler for mycroft.stop. Stops any playing service.
259 
260  Args:
261  message: message bus message, not used but required
262  """
263  if time.monotonic() - self.play_start_time > 1:
264  LOG.debug('stopping all playing services')
265  with self.service_lock:
266  if self.current:
267  name = self.current.name
268  if self.current.stop():
269  self.bus.emit(Message("mycroft.stop.handled",
270  {"by": "audio:" + name}))
271 
272  self.current = None
273 
274  def _lower_volume(self, message=None):
275  """
276  Is triggered when mycroft starts to speak and reduces the volume.
277 
278  Args:
279  message: message bus message, not used but required
280  """
281  if self.current:
282  LOG.debug('lowering volume')
283  self.current.lower_volume()
284  self.volume_is_low = True
285  try:
286  if self.pulse_quiet:
287  self.pulse_quiet()
288  except Exception as exc:
289  LOG.error(exc)
290 
291  def pulse_mute(self):
292  """
293  Mute all pulse audio input sinks except for the one named
294  'mycroft-voice'.
295  """
296  for sink in self.pulse.sink_input_list():
297  if sink.name != 'mycroft-voice':
298  self.pulse.sink_input_mute(sink.index, 1)
299  self.muted_sinks.append(sink.index)
300 
301  def pulse_unmute(self):
302  """
303  Unmute all pulse audio input sinks.
304  """
305  for sink in self.pulse.sink_input_list():
306  if sink.index in self.muted_sinks:
307  self.pulse.sink_input_mute(sink.index, 0)
308  self.muted_sinks = []
309 
311  """
312  Lower volume of all pulse audio input sinks except the one named
313  'mycroft-voice'.
314  """
315  for sink in self.pulse.sink_input_list():
316  if sink.name != 'mycroft-voice':
317  volume = sink.volume
318  volume.value_flat *= 0.3
319  self.pulse.volume_set(sink, volume)
320 
322  """
323  Restore volume of all pulse audio input sinks except the one named
324  'mycroft-voice'.
325  """
326  for sink in self.pulse.sink_input_list():
327  if sink.name != 'mycroft-voice':
328  volume = sink.volume
329  volume.value_flat /= 0.3
330  self.pulse.volume_set(sink, volume)
331 
332  def _restore_volume(self, message):
333  """
334  Is triggered when mycroft is done speaking and restores the volume
335 
336  Args:
337  message: message bus message, not used but required
338  """
339  if self.current:
340  LOG.debug('restoring volume')
341  self.volume_is_low = False
342  time.sleep(2)
343  if not self.volume_is_low:
344  self.current.restore_volume()
345  if self.pulse_restore:
346  self.pulse_restore()
347 
348  def play(self, tracks, prefered_service, repeat=False):
349  """
350  play starts playing the audio on the prefered service if it
351  supports the uri. If not the next best backend is found.
352 
353  Args:
354  tracks: list of tracks to play.
355  repeat: should the playlist repeat
356  prefered_service: indecates the service the user prefer to play
357  the tracks.
358  """
359  self._stop()
360 
361  if isinstance(tracks[0], str):
362  uri_type = tracks[0].split(':')[0]
363  else:
364  uri_type = tracks[0][0].split(':')[0]
365 
366  # check if user requested a particular service
367  if prefered_service and uri_type in prefered_service.supported_uris():
368  selected_service = prefered_service
369  # check if default supports the uri
370  elif self.default and uri_type in self.default.supported_uris():
371  LOG.debug("Using default backend ({})".format(self.default.name))
372  selected_service = self.default
373  else: # Check if any other service can play the media
374  LOG.debug("Searching the services")
375  for s in self.service:
376  if uri_type in s.supported_uris():
377  LOG.debug("Service {} supports URI {}".format(s, uri_type))
378  selected_service = s
379  break
380  else:
381  LOG.info('No service found for uri_type: ' + uri_type)
382  return
383  if not selected_service.supports_mime_hints:
384  tracks = [t[0] if isinstance(t, list) else t for t in tracks]
385  selected_service.clear_list()
386  selected_service.add_list(tracks)
387  selected_service.play(repeat)
388  self.current = selected_service
389  self.play_start_time = time.monotonic()
390 
391  def _queue(self, message):
392  if self.current:
393  tracks = message.data['tracks']
394  self.current.add_list(tracks)
395  else:
396  self._play(message)
397 
398  def _play(self, message):
399  """
400  Handler for mycroft.audio.service.play. Starts playback of a
401  tracklist. Also determines if the user requested a special
402  service.
403 
404  Args:
405  message: message bus message, not used but required
406  """
407  tracks = message.data['tracks']
408  repeat = message.data.get('repeat', False)
409  # Find if the user wants to use a specific backend
410  for s in self.service:
411  if ('utterance' in message.data and
412  s.name in message.data['utterance']):
413  prefered_service = s
414  LOG.debug(s.name + ' would be prefered')
415  break
416  else:
417  prefered_service = None
418  self.play(tracks, prefered_service, repeat)
419 
420  def _track_info(self, message):
421  """
422  Returns track info on the message bus.
423 
424  Args:
425  message: message bus message, not used but required
426  """
427  if self.current:
428  track_info = self.current.track_info()
429  else:
430  track_info = {}
431  self.bus.emit(Message('mycroft.audio.service.track_info_reply',
432  data=track_info))
433 
434  def _list_backends(self, message):
435  """ Return a dict of available backends. """
436  data = {}
437  for s in self.service:
438  info = {
439  'supported_uris': s.supported_uris(),
440  'default': s == self.default,
441  'remote': isinstance(s, RemoteAudioBackend)
442  }
443  data[s.name] = info
444  self.bus.emit(message.response(data))
445 
446  def _seek_forward(self, message):
447  """
448  Handle message bus command to skip X seconds
449 
450  Args:
451  message: message bus message
452  """
453  seconds = message.data.get("seconds", 1)
454  if self.current:
455  self.current.seek_forward(seconds)
456 
457  def _seek_backward(self, message):
458  """
459  Handle message bus command to rewind X seconds
460 
461  Args:
462  message: message bus message
463  """
464  seconds = message.data.get("seconds", 1)
465  if self.current:
466  self.current.seek_backward(seconds)
467 
468  def setup_pulseaudio_handlers(self, pulse_choice=None):
469  """
470  Select functions for handling lower volume/restore of
471  pulse audio input sinks.
472 
473  Args:
474  pulse_choice: method selection, can be eithe 'mute' or 'lower'
475  """
476  if pulsectl and pulse_choice:
477  self.pulse = pulsectl.Pulse('Mycroft-audio-service')
478  if pulse_choice == 'mute':
479  self.pulse_quiet = self.pulse_mute
480  self.pulse_restore = self.pulse_unmute
481  elif pulse_choice == 'lower':
482  self.pulse_quiet = self.pulse_lower_volume
484 
485  def shutdown(self):
486  for s in self.service:
487  try:
488  LOG.info('shutting down ' + s.name)
489  s.shutdown()
490  except Exception as e:
491  LOG.error('shutdown of ' + s.name + ' failed: ' + repr(e))
492 
493  # remove listeners
494  self.bus.remove('mycroft.audio.service.play', self._play)
495  self.bus.remove('mycroft.audio.service.queue', self._queue)
496  self.bus.remove('mycroft.audio.service.pause', self._pause)
497  self.bus.remove('mycroft.audio.service.resume', self._resume)
498  self.bus.remove('mycroft.audio.service.stop', self._stop)
499  self.bus.remove('mycroft.audio.service.next', self._next)
500  self.bus.remove('mycroft.audio.service.prev', self._prev)
501  self.bus.remove('mycroft.audio.service.track_info', self._track_info)
502  self.bus.remove('mycroft.audio.service.seek_forward',
503  self._seek_forward)
504  self.bus.remove('mycroft.audio.service.seek_backward',
505  self._seek_backward)
506  self.bus.remove('recognizer_loop:audio_output_start',
507  self._lower_volume)
508  self.bus.remove('recognizer_loop:record_begin', self._lower_volume)
509  self.bus.remove('recognizer_loop:audio_output_end',
510  self._restore_volume)
511  self.bus.remove('recognizer_loop:record_end', self._restore_volume)
def get_services(services_folder)
def play(self, tracks, prefered_service, repeat=False)
def setup_pulseaudio_handlers(self, pulse_choice=None)
def load_services(config, bus, path=None)
def create_service_descriptor(service_folder)
def get(phrase, lang=None, context=None)


mycroft_ros
Author(s):
autogenerated on Mon Apr 26 2021 02:35:40