Browse Source

[Feature] #234 Player modularity

monf 8 years ago
parent
commit
3e5c2b0fa9

+ 3 - 0
Docs/player.md

@@ -0,0 +1,3 @@
+# Players
+
+With Kalliope project, you can set whatever Sound Player you want to use.

+ 73 - 3
kalliope/core/ConfigurationManager/SettingLoader.py

@@ -11,6 +11,7 @@ from kalliope.core.Models.RestAPI import RestAPI
 from kalliope.core.Models.Settings import Settings
 from kalliope.core.Models.Settings import Settings
 from kalliope.core.Models.Stt import Stt
 from kalliope.core.Models.Stt import Stt
 from kalliope.core.Models.Trigger import Trigger
 from kalliope.core.Models.Trigger import Trigger
+from kalliope.core.Models.Player import Player
 from kalliope.core.Models.Tts import Tts
 from kalliope.core.Models.Tts import Tts
 from kalliope.core.Utils.FileManager import FileManager
 from kalliope.core.Utils.FileManager import FileManager
 
 
@@ -101,9 +102,11 @@ class SettingLoader(with_metaclass(Singleton, object)):
         default_stt_name = self._get_default_speech_to_text(settings)
         default_stt_name = self._get_default_speech_to_text(settings)
         default_tts_name = self._get_default_text_to_speech(settings)
         default_tts_name = self._get_default_text_to_speech(settings)
         default_trigger_name = self._get_default_trigger(settings)
         default_trigger_name = self._get_default_trigger(settings)
+        default_player_name = self._get_default_player(settings)
         stts = self._get_stts(settings)
         stts = self._get_stts(settings)
         ttss = self._get_ttss(settings)
         ttss = self._get_ttss(settings)
         triggers = self._get_triggers(settings)
         triggers = self._get_triggers(settings)
+        players = self._get_players(settings)
         random_wake_up_answers = self._get_random_wake_up_answers(settings)
         random_wake_up_answers = self._get_random_wake_up_answers(settings)
         random_wake_up_sound = self._get_random_wake_up_sounds(settings)
         random_wake_up_sound = self._get_random_wake_up_sounds(settings)
         play_on_ready_notification = self._get_play_on_ready_notification(settings)
         play_on_ready_notification = self._get_play_on_ready_notification(settings)
@@ -120,9 +123,11 @@ class SettingLoader(with_metaclass(Singleton, object)):
         setting_object.default_tts_name = default_tts_name
         setting_object.default_tts_name = default_tts_name
         setting_object.default_stt_name = default_stt_name
         setting_object.default_stt_name = default_stt_name
         setting_object.default_trigger_name = default_trigger_name
         setting_object.default_trigger_name = default_trigger_name
+        setting_object.default_player_name = default_player_name
         setting_object.stts = stts
         setting_object.stts = stts
         setting_object.ttss = ttss
         setting_object.ttss = ttss
         setting_object.triggers = triggers
         setting_object.triggers = triggers
+        setting_object.players = players
         setting_object.random_wake_up_answers = random_wake_up_answers
         setting_object.random_wake_up_answers = random_wake_up_answers
         setting_object.random_wake_up_sounds = random_wake_up_sound
         setting_object.random_wake_up_sounds = random_wake_up_sound
         setting_object.play_on_ready_notification = play_on_ready_notification
         setting_object.play_on_ready_notification = play_on_ready_notification
@@ -220,6 +225,33 @@ class SettingLoader(with_metaclass(Singleton, object)):
         except KeyError as e:
         except KeyError as e:
             raise SettingNotFound("%s setting not found" % e)
             raise SettingNotFound("%s setting not found" % e)
 
 
+    @staticmethod
+    def _get_default_player(settings):
+        """
+        Get the default player defined in the settings.yml file
+        :param settings: The YAML settings file
+        :type settings: dict
+        :return: the default player
+        :rtype: str
+
+        :Example:
+
+            default_player_name = cls._get_default_player(settings)
+
+        .. seealso:: Player
+        .. raises:: NullSettingException, SettingNotFound
+        .. warnings:: Static and Private
+        """
+
+        try:
+            default_player = settings["default_player"]
+            if default_player is None:
+                raise NullSettingException("Attribute default_player is null")
+            logger.debug("Default Player name: %s" % default_player)
+            return default_player
+        except KeyError as e:
+            raise SettingNotFound("%s setting not found" % e)
+
     @staticmethod
     @staticmethod
     def _get_stts(settings):
     def _get_stts(settings):
         """
         """
@@ -236,7 +268,7 @@ class SettingLoader(with_metaclass(Singleton, object)):
 
 
         .. seealso:: Stt
         .. seealso:: Stt
         .. raises:: SettingNotFound
         .. raises:: SettingNotFound
-        .. warnings:: Class Method and Private
+        .. warnings:: Static Method and Private
         """
         """
 
 
         try:
         try:
@@ -275,7 +307,7 @@ class SettingLoader(with_metaclass(Singleton, object)):
 
 
         .. seealso:: Tts
         .. seealso:: Tts
         .. raises:: SettingNotFound
         .. raises:: SettingNotFound
-        .. warnings:: Class Method and Private
+        .. warnings:: Static Method and Private
         """
         """
 
 
         try:
         try:
@@ -313,7 +345,7 @@ class SettingLoader(with_metaclass(Singleton, object)):
 
 
         .. seealso:: Trigger
         .. seealso:: Trigger
         .. raises:: SettingNotFound
         .. raises:: SettingNotFound
-        .. warnings:: Class Method and Private
+        .. warnings:: Static Method and Private
         """
         """
 
 
         try:
         try:
@@ -335,6 +367,44 @@ class SettingLoader(with_metaclass(Singleton, object)):
                 triggers.append(new_trigger)
                 triggers.append(new_trigger)
         return triggers
         return triggers
 
 
+    @staticmethod
+    def _get_players(settings):
+        """
+        Return a list of Player object
+
+        :param settings: The YAML settings file
+        :type settings: dict
+        :return: List of Player
+        :rtype: list
+
+        :Example:
+
+            players = cls._get_players(settings)
+
+        .. seealso:: players
+        .. raises:: SettingNotFound
+        .. warnings:: Static Method and Private
+        """
+
+        try:
+            players_list = settings["players"]
+        except KeyError as e:
+            raise SettingNotFound("%s setting not found" % e)
+
+        players = list()
+        for player_el in players_list:
+            if isinstance(player_el, dict):
+                for player_name in player_el:
+                    name = player_name
+                    parameters = player_el[name]
+                    new_player = Player(name=name, parameters=parameters)
+                    players.append(new_player)
+            else:
+                # the player does not have parameters
+                new_player = Player(name=player_el)
+                players.append(new_player)
+        return players
+
     @staticmethod
     @staticmethod
     def _get_random_wake_up_answers(settings):
     def _get_random_wake_up_answers(settings):
         """
         """

+ 15 - 16
kalliope/core/MainController.py

@@ -2,17 +2,22 @@ import logging
 import random
 import random
 from time import sleep
 from time import sleep
 
 
-from flask import Flask
 from transitions import Machine
 from transitions import Machine
-
 from kalliope.core import Utils
 from kalliope.core import Utils
 from kalliope.core.ConfigurationManager import SettingLoader
 from kalliope.core.ConfigurationManager import SettingLoader
 from kalliope.core.OrderListener import OrderListener
 from kalliope.core.OrderListener import OrderListener
-from kalliope.core.Players import Mplayer
+
+# API
+from flask import Flask
 from kalliope.core.RestAPI.FlaskAPI import FlaskAPI
 from kalliope.core.RestAPI.FlaskAPI import FlaskAPI
+
+# Launchers
 from kalliope.core.SynapseLauncher import SynapseLauncher
 from kalliope.core.SynapseLauncher import SynapseLauncher
 from kalliope.core.TriggerLauncher import TriggerLauncher
 from kalliope.core.TriggerLauncher import TriggerLauncher
 from kalliope.core.Utils.RpiUtils import RpiUtils
 from kalliope.core.Utils.RpiUtils import RpiUtils
+from kalliope.core.PlayerLauncher import PlayerLauncher
+
+# Neurons
 from kalliope.neurons.say.say import Say
 from kalliope.neurons.say.say import Say
 
 
 logging.basicConfig()
 logging.basicConfig()
@@ -44,6 +49,7 @@ class MainController:
         # Starting the rest API
         # Starting the rest API
         self._start_rest_api()
         self._start_rest_api()
 
 
+        # rpi setting for led and mute button
         self.rpi_utils = None
         self.rpi_utils = None
         if self.settings.rpi_settings:
         if self.settings.rpi_settings:
             # the useer set GPIO pin, we need to instantiate the RpiUtils class in order to setup GPIO
             # the useer set GPIO pin, we need to instantiate the RpiUtils class in order to setup GPIO
@@ -58,6 +64,9 @@ class MainController:
                 logger.debug("[MainController] Switching pin_led_started to ON")
                 logger.debug("[MainController] Switching pin_led_started to ON")
                 RpiUtils.switch_pin_to_on(self.settings.rpi_settings.pin_led_started)
                 RpiUtils.switch_pin_to_on(self.settings.rpi_settings.pin_led_started)
 
 
+        # get the player instance
+        self.player_instance = PlayerLauncher.get_player(settings=self.settings)
+
         # save an instance of the trigger
         # save an instance of the trigger
         self.trigger_instance = None
         self.trigger_instance = None
         self.trigger_callback_called = False
         self.trigger_callback_called = False
@@ -100,7 +109,7 @@ class MainController:
         This function will start the trigger thread that listen for the hotword
         This function will start the trigger thread that listen for the hotword
         """
         """
         logger.debug("[MainController] Entering state: %s" % self.state)
         logger.debug("[MainController] Entering state: %s" % self.state)
-        self.trigger_instance = self._get_default_trigger()
+        self.trigger_instance = TriggerLauncher.get_trigger(settings=self.settings, callback=self.trigger_callback)
         self.trigger_callback_called = False
         self.trigger_callback_called = False
         self.trigger_instance.daemon = True
         self.trigger_instance.daemon = True
         # Wait that the kalliope trigger is pronounced by the user
         # Wait that the kalliope trigger is pronounced by the user
@@ -121,7 +130,7 @@ class MainController:
                 Say(message=self.settings.on_ready_answers)
                 Say(message=self.settings.on_ready_answers)
             elif self.settings.on_ready_sounds is not None:
             elif self.settings.on_ready_sounds is not None:
                 random_sound_to_play = self._get_random_sound(self.settings.on_ready_sounds)
                 random_sound_to_play = self._get_random_sound(self.settings.on_ready_sounds)
-                Mplayer.play(random_sound_to_play)
+                self.player_instance.play(random_sound_to_play)
         self.next_state()
         self.next_state()
 
 
     def waiting_for_trigger_callback_thread(self):
     def waiting_for_trigger_callback_thread(self):
@@ -188,8 +197,7 @@ class MainController:
             Say(message=self.settings.random_wake_up_answers)
             Say(message=self.settings.random_wake_up_answers)
         else:
         else:
             random_sound_to_play = self._get_random_sound(self.settings.random_wake_up_sounds)
             random_sound_to_play = self._get_random_sound(self.settings.random_wake_up_sounds)
-            Mplayer.play(random_sound_to_play)
-
+            self.player_instance.play(random_sound_to_play)
         self.next_state()
         self.next_state()
 
 
     def order_listener_callback(self, order):
     def order_listener_callback(self, order):
@@ -215,15 +223,6 @@ class MainController:
         # return to the state "unpausing_trigger"
         # return to the state "unpausing_trigger"
         self.start_trigger()
         self.start_trigger()
 
 
-    def _get_default_trigger(self):
-        """
-        Return an instance of the default trigger
-        :return: Trigger
-        """
-        for trigger in self.settings.triggers:
-            if trigger.name == self.settings.default_trigger_name:
-                return TriggerLauncher.get_trigger(trigger, callback=self.trigger_callback)
-
     @staticmethod
     @staticmethod
     def _get_random_sound(random_wake_up_sounds):
     def _get_random_sound(random_wake_up_sounds):
         """
         """

+ 27 - 0
kalliope/core/Models/Player.py

@@ -0,0 +1,27 @@
+class Player(object):
+    """
+    This Class is representing a Player with its name and parameters
+
+    .. note:: must be defined in the settings.yml
+    """
+
+    def __init__(self, name=None, parameters=None):
+        self.name = name
+        self.parameters = parameters
+
+    def __str__(self):
+        return str(self.serialize())
+
+    def serialize(self):
+        return {
+            'name': self.name,
+            'parameters': self.parameters
+        }
+
+    def __eq__(self, other):
+        """
+        This is used to compare 2 Player objects
+        :param other: the Player to compare
+        :return: True if both players are similar, False otherwise
+        """
+        return self.__dict__ == other.__dict__

+ 6 - 0
kalliope/core/Models/Settings.py

@@ -12,6 +12,7 @@ class Settings(object):
                  default_tts_name=None,
                  default_tts_name=None,
                  default_stt_name=None,
                  default_stt_name=None,
                  default_trigger_name=None,
                  default_trigger_name=None,
+                 default_player_name=None,
                  ttss=None,
                  ttss=None,
                  stts=None,
                  stts=None,
                  random_wake_up_answers=None,
                  random_wake_up_answers=None,
@@ -20,6 +21,7 @@ class Settings(object):
                  on_ready_answers=None,
                  on_ready_answers=None,
                  on_ready_sounds=None,
                  on_ready_sounds=None,
                  triggers=None,
                  triggers=None,
+                 players=None,
                  rest_api=None,
                  rest_api=None,
                  cache_path=None,
                  cache_path=None,
                  default_synapse=None,
                  default_synapse=None,
@@ -30,6 +32,7 @@ class Settings(object):
         self.default_tts_name = default_tts_name
         self.default_tts_name = default_tts_name
         self.default_stt_name = default_stt_name
         self.default_stt_name = default_stt_name
         self.default_trigger_name = default_trigger_name
         self.default_trigger_name = default_trigger_name
+        self.default_player_name = default_player_name
         self.ttss = ttss
         self.ttss = ttss
         self.stts = stts
         self.stts = stts
         self.random_wake_up_answers = random_wake_up_answers
         self.random_wake_up_answers = random_wake_up_answers
@@ -38,6 +41,7 @@ class Settings(object):
         self.on_ready_answers = on_ready_answers
         self.on_ready_answers = on_ready_answers
         self.on_ready_sounds = on_ready_sounds
         self.on_ready_sounds = on_ready_sounds
         self.triggers = triggers
         self.triggers = triggers
+        self.players = players
         self.rest_api = rest_api
         self.rest_api = rest_api
         self.cache_path = cache_path
         self.cache_path = cache_path
         self.default_synapse = default_synapse
         self.default_synapse = default_synapse
@@ -59,6 +63,7 @@ class Settings(object):
             'default_tts_name': self.default_tts_name,
             'default_tts_name': self.default_tts_name,
             'default_stt_name': self.default_stt_name,
             'default_stt_name': self.default_stt_name,
             'default_trigger_name': self.default_trigger_name,
             'default_trigger_name': self.default_trigger_name,
+            'default_player_name': self.default_player_name,
             'ttss': self.ttss,
             'ttss': self.ttss,
             'stts': self.stts,
             'stts': self.stts,
             'random_wake_up_answers': self.random_wake_up_answers,
             'random_wake_up_answers': self.random_wake_up_answers,
@@ -67,6 +72,7 @@ class Settings(object):
             'on_ready_answers': self.on_ready_answers,
             'on_ready_answers': self.on_ready_answers,
             'on_ready_sounds': self.on_ready_sounds,
             'on_ready_sounds': self.on_ready_sounds,
             'triggers': self.triggers,
             'triggers': self.triggers,
+            'players': self.players,
             'rest_api': self.rest_api.serialize(),
             'rest_api': self.rest_api.serialize(),
             'cache_path': self.cache_path,
             'cache_path': self.cache_path,
             'default_synapse': self.default_synapse,
             'default_synapse': self.default_synapse,

+ 30 - 0
kalliope/core/PlayerLauncher.py

@@ -0,0 +1,30 @@
+import logging
+
+from kalliope.core import Utils
+
+logging.basicConfig()
+logger = logging.getLogger("kalliope")
+
+
+class PlayerLauncher(object):
+    def __init__(self):
+        pass
+
+    @staticmethod
+    def get_player(settings):
+        """
+        Instantiate a Player
+        :param settings: setting object
+        :type settings: Settings
+        :return: the Player instance
+        :rtype: Player
+        """
+        player_instance = None
+        for player in settings.players:
+            if player.name == settings.default_player_name:
+                logger.debug("PlayerLauncher: Start player %s with parameters: %s" % (player.name, player.parameters))
+                player_instance = Utils.get_dynamic_class_instantiation(package_name="players",
+                                                                        module_name=player.name,
+                                                                        parameters=player.parameters)
+                break
+        return player_instance

+ 0 - 1
kalliope/core/Players/__init__.py

@@ -1 +0,0 @@
-from .Mplayer import Mplayer

+ 27 - 3
kalliope/core/TTS/TTSModule.py

@@ -2,12 +2,14 @@
 import hashlib
 import hashlib
 import logging
 import logging
 import os
 import os
-import sys
+import subprocess
+
 import six
 import six
 
 
 from kalliope.core.ConfigurationManager import SettingLoader
 from kalliope.core.ConfigurationManager import SettingLoader
-from kalliope.core.Players import Mplayer
+from kalliope.core.PlayerLauncher import PlayerLauncher
 from kalliope.core.Utils.FileManager import FileManager
 from kalliope.core.Utils.FileManager import FileManager
+from kalliope.core import Utils
 
 
 logging.basicConfig()
 logging.basicConfig()
 logger = logging.getLogger("kalliope")
 logger = logging.getLogger("kalliope")
@@ -60,6 +62,7 @@ class TTSModule(object):
         # load settings
         # load settings
         sl = SettingLoader()
         sl = SettingLoader()
         self.settings = sl.settings
         self.settings = sl.settings
+        self.player = PlayerLauncher.get_player(settings=self.settings)
 
 
         # create the path in the tmp folder
         # create the path in the tmp folder
         base_path = os.path.join(self.settings.cache_path, self.tts_caller_name, self.language, self.voice)
         base_path = os.path.join(self.settings.cache_path, self.tts_caller_name, self.language, self.voice)
@@ -74,7 +77,8 @@ class TTSModule(object):
         """
         """
         Play the audio file
         Play the audio file
         """
         """
-        Mplayer.play(self.file_path)
+        # Mplayer.play(self.file_path)
+        self.player.play(self.file_path)
 
 
     def generate_and_play(self, words, generate_audio_function_from_child=None):
     def generate_and_play(self, words, generate_audio_function_from_child=None):
         """
         """
@@ -154,3 +158,23 @@ class TTSModule(object):
         else:
         else:
             logger.debug("TTSModule, File not yet in cache: %s" % file_path)
             logger.debug("TTSModule, File not yet in cache: %s" % file_path)
         return exist_in_cache
         return exist_in_cache
+
+    @staticmethod
+    def convert_mp3_to_wav(file_path_mp3):
+        """ 
+        PyAudio does not support mp3 files 
+        MP3 files must be converted to a wave in order to be played
+        This function assumes ffmpeg is available on the system
+        :param file_path_mp3: the file path to convert from mp3 to wav
+        """
+        logger.debug("Converting mp3 file to wav file: %s" % file_path_mp3)
+        fnull = open(os.devnull, 'w')
+        # temp file
+        tmp_file_wav = file_path_mp3 + ".wav"
+        # Convert mp3 to wave
+        subprocess.call(['ffmpeg', '-y', '-i', file_path_mp3, tmp_file_wav],
+                           stdout=fnull, stderr=fnull)
+        # remove the original file
+        FileManager.remove_file(file_path_mp3)
+        # rename the temp file with the same name as the original file
+        os.rename(tmp_file_wav, file_path_mp3)

+ 17 - 11
kalliope/core/TriggerLauncher.py

@@ -10,19 +10,25 @@ class TriggerLauncher(object):
     def __init__(self):
     def __init__(self):
         pass
         pass
 
 
-    @classmethod
-    def get_trigger(cls, trigger, callback):
+    @staticmethod
+    def get_trigger(settings, callback):
         """
         """
         Start a trigger module
         Start a trigger module
         :param trigger: trigger object to instantiate
         :param trigger: trigger object to instantiate
         :type trigger: Trigger
         :type trigger: Trigger
-        :param callback: Callback function to call when the trigger
-        catch the magic word
-        :return:
+        :param callback: Callback function to call when the trigger catch the magic word
+        :return: The instance of Trigger 
+        :rtype: Trigger
         """
         """
-        # add the callback method to parameters
-        trigger.parameters["callback"] = callback
-        logger.debug("TriggerLauncher: Start trigger %s with parameters: %s" % (trigger.name, trigger.parameters))
-        return Utils.get_dynamic_class_instantiation(package_name="trigger",
-                                                     module_name=trigger.name,
-                                                     parameters=trigger.parameters)
+        trigger_instance = None
+        for trigger in settings.triggers:
+            if trigger.name == settings.default_trigger_name:
+                # add the callback method to parameters
+                trigger.parameters["callback"] = callback
+                logger.debug(
+                    "TriggerLauncher: Start trigger %s with parameters: %s" % (trigger.name, trigger.parameters))
+                trigger_instance = Utils.get_dynamic_class_instantiation(package_name="trigger",
+                                                                         module_name=trigger.name,
+                                                                         parameters=trigger.parameters)
+                break
+        return trigger_instance

+ 5 - 4
kalliope/core/Utils/Utils.py

@@ -25,7 +25,6 @@ class ModuleNotFoundError(Exception):
 
 
 
 
 class Utils(object):
 class Utils(object):
-
     color_list = dict(
     color_list = dict(
         PURPLE='\033[95m',
         PURPLE='\033[95m',
         BLUE='\033[94m',
         BLUE='\033[94m',
@@ -114,6 +113,7 @@ class Utils(object):
         :return:
         :return:
         """
         """
         package_path = "kalliope." + package_name + "." + module_name.lower() + "." + module_name.lower()
         package_path = "kalliope." + package_name + "." + module_name.lower() + "." + module_name.lower()
+        logger.debug("[Utils]-> get_dynamic_class_instantiation : package path : %s" % (package_path))
         if resources_dir is not None:
         if resources_dir is not None:
             neuron_resource_path = resources_dir + os.sep + module_name.lower() \
             neuron_resource_path = resources_dir + os.sep + module_name.lower() \
                                    + os.sep + module_name.lower() + ".py"
                                    + os.sep + module_name.lower() + ".py"
@@ -121,7 +121,7 @@ class Utils(object):
                 imp.load_source(module_name.capitalize(), neuron_resource_path)
                 imp.load_source(module_name.capitalize(), neuron_resource_path)
                 package_path = module_name.capitalize()
                 package_path = module_name.capitalize()
                 logger.debug("[Utils]-> get_dynamic_class_instantiation : loading path : %s, as package %s" % (
                 logger.debug("[Utils]-> get_dynamic_class_instantiation : loading path : %s, as package %s" % (
-                                                                                neuron_resource_path, package_path))
+                    neuron_resource_path, package_path))
 
 
         mod = __import__(package_path, fromlist=[module_name.capitalize()])
         mod = __import__(package_path, fromlist=[module_name.capitalize()])
 
 
@@ -129,7 +129,8 @@ class Utils(object):
             klass = getattr(mod, module_name.capitalize())
             klass = getattr(mod, module_name.capitalize())
         except AttributeError:
         except AttributeError:
             logger.debug("Error: No module named %s " % module_name.capitalize())
             logger.debug("Error: No module named %s " % module_name.capitalize())
-            raise ModuleNotFoundError("The module %s does not exist in package %s" % (module_name.capitalize(), package_name))
+            raise ModuleNotFoundError(
+                "The module %s does not exist in package %s" % (module_name.capitalize(), package_name))
 
 
         if klass is not None:
         if klass is not None:
             # run the plugin
             # run the plugin
@@ -299,4 +300,4 @@ class Utils(object):
         if sys.version_info[0] < 3:
         if sys.version_info[0] < 3:
             if isinstance(text, unicode):
             if isinstance(text, unicode):
                 text = text.encode("utf-8")
                 text = text.encode("utf-8")
-        return text
+        return text

+ 1 - 0
kalliope/neurons/uri/__init__.py

@@ -0,0 +1 @@
+from .uri import Uri

+ 0 - 0
kalliope/players/__init__.py


+ 1 - 0
kalliope/players/mplayer/__init__.py

@@ -0,0 +1 @@
+from .mplayer import Mplayer

+ 3 - 2
kalliope/core/Players/Mplayer.py → kalliope/players/mplayer/mplayer.py

@@ -13,8 +13,9 @@ class Mplayer(object):
     This Class is representing the MPlayer Object used to play the all sound of the system.
     This Class is representing the MPlayer Object used to play the all sound of the system.
     """
     """
 
 
-    def __init__(self):
-        pass
+    def __init__(self, **kwargs):
+        logger.debug("[Mplayer.__init__] instance")
+        logger.debug("[Mplayer.__init__] args : %s " % str(kwargs))
 
 
     @classmethod
     @classmethod
     def play(cls, filepath):
     def play(cls, filepath):

+ 1 - 0
kalliope/players/pyalsaaudio/__init__.py

@@ -0,0 +1 @@
+from .pyalsaaudio import Pyalsaaudio

+ 86 - 0
kalliope/players/pyalsaaudio/pyalsaaudio.py

@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+import alsaaudio
+import logging
+import wave
+
+logging.basicConfig()
+logger = logging.getLogger("kalliope")
+
+CHUNK = 1024
+
+ALSAAUDIO_BIT_MAPPING = {8:  alsaaudio.PCM_FORMAT_S8,
+                         16: alsaaudio.PCM_FORMAT_S16_LE,
+                         24: alsaaudio.PCM_FORMAT_S24_LE,
+                         32: alsaaudio.PCM_FORMAT_S32_LE}
+
+STANDARD_SAMPLE_RATES = (
+    8000, 9600, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, 88200,
+    96000, 192000
+)
+
+DEVICE_TYPE_ALL = 'all'
+DEVICE_TYPE_INPUT = 'input'
+DEVICE_TYPE_OUTPUT = 'output'
+
+
+def bits_to_samplefmt(bits):
+    if bits in ALSAAUDIO_BIT_MAPPING.keys():
+        return ALSAAUDIO_BIT_MAPPING[bits]
+    else: 
+        raise ValueError('Unsupported format')
+
+
+class Pyalsaaudio(object):
+    """
+    This Class is representing the Player Object used to play the all sound of the system.
+    """
+
+    def __init__(self, **kwargs):
+        # List devices
+        logger.debug("[pyalsaaudio.__init__] instance")
+        logger.debug("[pyalsaaudio.__init__] devices : %s " % (str(self.get_devices(DEVICE_TYPE_OUTPUT))))
+        logger.debug("[pyalsaaudio.__init__] args : %s " % str(kwargs))
+        self.device = kwargs.get('device', 'default')
+
+    @staticmethod
+    def get_devices(device_type=DEVICE_TYPE_ALL):
+        devices = set()
+        if device_type in (DEVICE_TYPE_ALL,
+                           DEVICE_TYPE_OUTPUT):
+            devices.update(set(alsaaudio.pcms(alsaaudio.PCM_PLAYBACK)))
+        if device_type in (DEVICE_TYPE_ALL,
+                           DEVICE_TYPE_INPUT):
+            devices.update(set(alsaaudio.pcms(alsaaudio.PCM_CAPTURE)))
+        device_names = sorted(list(devices))
+        num_devices = len(device_names)
+        logger.debug('Found %d ALSA devices', num_devices)
+        return device_names
+
+    def play(self, file_path):
+
+        f = wave.open(file_path, 'rb')
+        pcm_type = alsaaudio.PCM_PLAYBACK
+        stream = alsaaudio.PCM(type=pcm_type,
+                               mode=alsaaudio.PCM_NORMAL,
+                               device=self.device)  # this is just for testing
+                               # device='sysdefault:CARD=ALSA')  # this is just for testing
+                                # on RPI3 this is fine; pulse (usually also default) is not working
+                                # device should be configurable; default schould be "default"
+        # Set attributes
+        stream.setchannels(f.getnchannels())
+        stream.setrate(f.getframerate())
+        bits = f.getsampwidth()*8
+        stream.setformat(bits_to_samplefmt(bits))        
+        stream.setperiodsize(CHUNK)
+        
+        logger.debug("[PyAlsaAudioPlayer] %d channels, %d sampling rate, %d bit" % (f.getnchannels(),
+                                                                            f.getframerate(),  bits))
+        
+        data = f.readframes(CHUNK)
+        while data:
+            # Read data from stdin
+            stream.write(data)
+            data = f.readframes(CHUNK)
+     
+        f.close()
+        stream.close()

+ 1 - 0
kalliope/players/pyaudioplayer/__init__.py

@@ -0,0 +1 @@
+from .pyaudioplayer import Pyaudioplayer

+ 57 - 0
kalliope/players/pyaudioplayer/pyaudioplayer.py

@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+import logging
+import wave
+
+import pyaudio
+
+logging.basicConfig()
+logger = logging.getLogger("kalliope")
+
+CHUNK = 1024
+
+
+class Pyaudioplayer(object):
+    """
+    This Class is representing the Player Object used to play the all sound of the system.
+    """
+
+    def __init__(self, **kwargs):
+        logger.debug("[Pyaudioplayer.__init__] instance")
+        logger.debug("[Pyaudioplayer.__init__] args : %s " % str(kwargs))
+
+    @classmethod
+    def play(cls, file_path):
+        """
+        Play the sound located in the provided file_path
+        :param file_path: The file path of the sound to play. Must be wav format
+        :type file_path: str              
+        """
+        # open the wave file
+        wf = wave.open(file_path, 'rb')
+
+        # instantiate PyAudio
+        p = pyaudio.PyAudio()
+
+        # open stream (2)
+        stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
+                        channels=wf.getnchannels(),
+                        rate=wf.getframerate(),
+                        # frames_per_buffer=CHUNK,
+                        output=True)
+
+        # read data
+        data = wf.readframes(CHUNK)
+
+        logger.debug("Pyplayer file: %s" % str(file_path))
+
+        # play stream (3)
+        while len(data) > 0:
+            stream.write(data)
+            data = wf.readframes(CHUNK)
+
+        # stop stream (4)
+        stream.stop_stream()
+        stream.close()
+
+        # close PyAudio
+        p.terminate()

+ 1 - 0
kalliope/players/sounddeviceplayer/__init__.py

@@ -0,0 +1 @@
+from .sounddeviceplayer import Sounddeviceplayer

+ 26 - 0
kalliope/players/sounddeviceplayer/sounddeviceplayer.py

@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+import sounddevice as sd
+import soundfile as sf
+import logging
+
+logging.basicConfig()
+logger = logging.getLogger("kalliope")
+
+FS = 48000
+
+
+class Sounddeviceplayer(object):
+    """
+    This Class is representing the Player Object used to play the all sound of the system.
+    """
+
+    def __init__(self, **kwargs):
+        logger.debug("[Sounddeviceplayer.__init__] instance")
+        logger.debug("[Sounddeviceplayer.__init__] args : %s " % str(kwargs))
+
+    @classmethod
+    def play(cls, file_path):
+
+        data, fs = sf.read(file_path)
+        sd.play(data, fs)
+        sd.wait()

+ 26 - 3
kalliope/settings.yml

@@ -25,10 +25,14 @@ triggers:
 # This is the STT that will be used by default
 # This is the STT that will be used by default
 default_speech_to_text: "google"
 default_speech_to_text: "google"
 
 
-# Spreech to Text engines configuration
+# Speech to Text engines configuration
 # Available engine are:
 # Available engine are:
 # - google (via SpeechRecognition)
 # - google (via SpeechRecognition)
-# -
+# - wit
+# - bing
+# - apiai
+# - houndify
+# - cmusphinx (must be installed first)
 speech_to_text:
 speech_to_text:
   - google:
   - google:
       language: "fr-FR"
       language: "fr-FR"
@@ -53,7 +57,7 @@ default_text_to_speech: "pico2wave"
 # where we store generated audio files from TTS engine to reuse them
 # where we store generated audio files from TTS engine to reuse them
 cache_path: "/tmp/kalliope_tts_cache"
 cache_path: "/tmp/kalliope_tts_cache"
 
 
-# Text to Spreech engines configuration
+# Text to Speech engines configuration
 # Available engine are:
 # Available engine are:
 # - pico2wave
 # - pico2wave
 # - acapela
 # - acapela
@@ -75,6 +79,25 @@ text_to_speech:
       language: "fr-fr"
       language: "fr-fr"
       cache: True
       cache: True
 
 
+# ---------------------------
+# players
+# ---------------------------
+# This is the sound player that will be used by default
+default_player: "mplayer"
+
+# players configuration
+# Available engine are:
+# - mplayer
+# - pyalsaaudio
+# - pyaudioplayer
+# - sounddeviceplayer
+players:
+  - mplayer: {}
+  - pyalsaaudio:
+     device: "default"
+  - pyaudioplayer: {}
+  - sounddeviceplayer: {}
+
 # ---------------------------
 # ---------------------------
 # Wake up answers
 # Wake up answers
 # ---------------------------
 # ---------------------------

+ 0 - 2
kalliope/trigger/snowboy/snowboy.py

@@ -1,7 +1,5 @@
-import inspect
 import logging
 import logging
 import os
 import os
-import time
 from threading import Thread
 from threading import Thread
 
 
 from kalliope import Utils
 from kalliope import Utils

+ 3 - 0
kalliope/tts/acapela/acapela.py

@@ -77,6 +77,9 @@ class Acapela(TTSModule):
         # OK we get the audio we can write the sound file
         # OK we get the audio we can write the sound file
         FileManager.write_in_file(self.file_path, r.content)
         FileManager.write_in_file(self.file_path, r.content)
 
 
+        # the received file use MP3 format. It must be converted to wav in order to be played by pyaudio
+        self.convert_mp3_to_wav(self.file_path)
+
     def get_payload(self):
     def get_payload(self):
         """
         """
         Generic method used load the payload used to access the remote api
         Generic method used load the payload used to access the remote api

+ 2 - 0
setup.py

@@ -83,6 +83,8 @@ setup(
         'GitPython>=2.1.3',
         'GitPython>=2.1.3',
         'packaging>=16.8',
         'packaging>=16.8',
         'transitions>=0.4.3',
         'transitions>=0.4.3',
+        'sounddevice>=0.3.7',
+        'SoundFile>=0.9.0'
         'RPi.GPIO>=0.6.3'
         'RPi.GPIO>=0.6.3'
     ],
     ],