Ver código fonte

Merge pull request #277 from kalliope-project/pyaudio

#234 manage players modularity
Nicolas Marcq 8 anos atrás
pai
commit
4c1c488efd
54 arquivos alterados com 775 adições e 75 exclusões
  1. 1 1
      Docs/installation/debian_jessie.md
  2. 1 1
      Docs/installation/raspbian_jessie.md
  3. 1 1
      Docs/installation/ubuntu_14.04.md
  4. 1 1
      Docs/installation/ubuntu_16.04.md
  5. 20 0
      Docs/player_list.md
  6. 53 0
      Docs/settings.md
  7. 1 1
      Docs/stt.md
  8. 17 0
      Tests/settings/settings_test.yml
  9. 1 1
      Tests/test_dynamic_loading.py
  10. 34 4
      Tests/test_models.py
  11. 11 2
      Tests/test_neuron_module.py
  12. 44 0
      Tests/test_player_launcher.py
  13. 16 1
      Tests/test_settings_loader.py
  14. 21 5
      Tests/test_trigger_launcher.py
  15. 1 0
      install/files/deb-packages_requirements.txt
  16. 3 0
      install/files/python_requirements.txt
  17. 1 1
      kalliope/_version.py
  18. 73 3
      kalliope/core/ConfigurationManager/SettingLoader.py
  19. 15 16
      kalliope/core/MainController.py
  20. 27 0
      kalliope/core/Models/Player.py
  21. 6 0
      kalliope/core/Models/Settings.py
  22. 30 0
      kalliope/core/PlayerLauncher.py
  23. 41 0
      kalliope/core/PlayerModule.py
  24. 0 1
      kalliope/core/Players/__init__.py
  25. 8 4
      kalliope/core/TTS/TTSModule.py
  26. 17 11
      kalliope/core/TriggerLauncher.py
  27. 5 4
      kalliope/core/Utils/Utils.py
  28. 1 0
      kalliope/core/__init__.py
  29. 1 0
      kalliope/neurons/uri/__init__.py
  30. 0 0
      kalliope/players/__init__.py
  31. 11 0
      kalliope/players/mplayer/README.md
  32. 1 0
      kalliope/players/mplayer/__init__.py
  33. 3 2
      kalliope/players/mplayer/mplayer.py
  34. 0 0
      kalliope/players/mplayer/tests/__init__.py
  35. 10 0
      kalliope/players/mplayer/tests/test_mplayer.py
  36. 15 0
      kalliope/players/pyalsaaudio/README.md
  37. 1 0
      kalliope/players/pyalsaaudio/__init__.py
  38. 89 0
      kalliope/players/pyalsaaudio/pyalsaaudio.py
  39. 0 0
      kalliope/players/pyalsaaudio/tests/__init__.py
  40. 11 0
      kalliope/players/pyalsaaudio/tests/test_pyalsaaudio.py
  41. 13 0
      kalliope/players/pyaudioplayer/README.md
  42. 1 0
      kalliope/players/pyaudioplayer/__init__.py
  43. 61 0
      kalliope/players/pyaudioplayer/pyaudioplayer.py
  44. 0 0
      kalliope/players/pyaudioplayer/tests/__init__.py
  45. 10 0
      kalliope/players/pyaudioplayer/tests/test_pyaudioplayer.py
  46. 13 0
      kalliope/players/sounddeviceplayer/README.md
  47. 1 0
      kalliope/players/sounddeviceplayer/__init__.py
  48. 31 0
      kalliope/players/sounddeviceplayer/sounddeviceplayer.py
  49. 0 0
      kalliope/players/sounddeviceplayer/tests/__init__.py
  50. 10 0
      kalliope/players/sounddeviceplayer/tests/test_sounddeviceplayer.py
  51. 37 12
      kalliope/settings.yml
  52. 0 2
      kalliope/trigger/snowboy/snowboy.py
  53. 2 0
      kalliope/tts/acapela/README.md
  54. 4 1
      setup.py

+ 1 - 1
Docs/installation/debian_jessie.md

@@ -12,7 +12,7 @@ Install some required system libraries and softwares:
 
 ```bash
 sudo apt-get update
-sudo apt-get install git python-dev libsmpeg0 libttspico-utils libsmpeg0 flac dialog libffi-dev libffi-dev libssl-dev portaudio19-dev build-essential libssl-dev libffi-dev sox libatlas3-base mplayer
+sudo apt-get install git python-dev libsmpeg0 libttspico-utils libsmpeg0 flac dialog libffi-dev libffi-dev libssl-dev portaudio19-dev build-essential libssl-dev libffi-dev sox libatlas3-base mplayer libav-tools
 ```
 
 Let's install the last release of python-pip

+ 1 - 1
Docs/installation/raspbian_jessie.md

@@ -28,7 +28,7 @@ Install some required system libraries and software:
 
 ```bash
 sudo apt-get update
-sudo apt-get install git python-dev libsmpeg0 libttspico-utils libsmpeg0 flac dialog libffi-dev libffi-dev libssl-dev portaudio19-dev build-essential libssl-dev libffi-dev sox libatlas3-base mplayer libyaml-dev libpython2.7-dev
+sudo apt-get install git python-dev libsmpeg0 libttspico-utils libsmpeg0 flac dialog libffi-dev libffi-dev libssl-dev portaudio19-dev build-essential libssl-dev libffi-dev sox libatlas3-base mplayer libyaml-dev libpython2.7-dev libav-tools
 ```
 
 Let's install the last release of python-pip

+ 1 - 1
Docs/installation/ubuntu_14.04.md

@@ -6,7 +6,7 @@ Install some required system libraries and softwares:
 
 ```bash
 sudo apt-get update
-sudo apt-get install git python-dev libsmpeg0 libttspico-utils libsmpeg0 flac dialog libffi-dev libffi-dev libssl-dev libjack0 libjack-dev portaudio19-dev build-essential libssl-dev libffi-dev sox libatlas3-base mplayer
+sudo apt-get install git python-dev libsmpeg0 libttspico-utils libsmpeg0 flac dialog libffi-dev libffi-dev libssl-dev libjack0 libjack-dev portaudio19-dev build-essential libssl-dev libffi-dev sox libatlas3-base mplayer libav-tools
 ```
 
 Let's install the last release of python-pip

+ 1 - 1
Docs/installation/ubuntu_16.04.md

@@ -6,7 +6,7 @@ Install some required system libraries and softwares:
 
 ```bash
 sudo apt-get update
-sudo apt-get install git python-dev libsmpeg0 libttspico-utils libsmpeg0 flac dialog libffi-dev libffi-dev libssl-dev portaudio19-dev build-essential libssl-dev libffi-dev sox libatlas3-base mplayer
+sudo apt-get install git python-dev libsmpeg0 libttspico-utils libsmpeg0 flac dialog libffi-dev libffi-dev libssl-dev portaudio19-dev build-essential libssl-dev libffi-dev sox libatlas3-base mplayer libav-tools
 ```
 Let's install the last release of python-pip
 ```bash

+ 20 - 0
Docs/player_list.md

@@ -0,0 +1,20 @@
+# List of available Player
+
+A player is a module that Kalliope will use when playing a sound generated from a TTS engine.
+You can define them in your [settings.yml file](settings.md). 
+
+## Core Players
+Core Players are already packaged with the installation of Kalliope an can be used out of the box.
+
+|        Name       | Description                                                                    | Note                |
+|-------------------|--------------------------------------------------------------------------------|---------------------|
+| mplayer           | [mplayer](http://www.mplayerhq.hu/design7/news.html)                           |                     |
+| AlsaAudioPlayer   | [AlsaAudioPlayer](https://larsimmisch.github.io/pyalsaaudio/libalsaaudio.html) | Does not handle mp3 |
+| PyAudioPlayer     | [PyAudioPlayer](https://people.csail.mit.edu/hubert/pyaudio/)                  | Does not handle mp3 |
+| SoundDevicePlayer | [SoundDevicePlayer](https://pypi.python.org/pypi/sounddevice)                  | Does not handle mp3 |
+
+## Community Players
+Community Players need to be installed manually.
+
+Wanna add your Player in the list? Open [an issue](../../issues) and send a pull request to update the list directly.
+

+ 53 - 0
Docs/settings.md

@@ -36,6 +36,59 @@ triggers:
 
 See the complete list of [available triggers here](trigger.md).
 
+## Players configuration
+
+#### default_player
+
+The player is the module managing the sound in Kalliope.
+
+Specify the name of the player module you want to use.
+```yml
+default_player: "player_name"
+```
+E.g
+```yml
+default_player: "mplayer"
+```
+
+#### players
+The player is the engine in charge of running sounds in Kalliope.
+
+Each Players has it own configuration. 
+This configuration is passed as argument following the syntax bellow
+```yml
+players:
+  - player_name:
+      parameter_name: "value"
+```
+
+E.g
+```yml
+players:
+  - mplayer: {}
+  - pyalsaaudio:
+     device: "default"
+     convert_to_wav: True
+  - pyaudioplayer:
+     convert_to_wav: True
+  - sounddeviceplayer:
+     convert_to_wav: True
+```
+
+See the complete list of [available players here](player_list.md).
+
+When no parameters are required set an empty object:
+```yml
+players:
+  - mplayer: {}
+```
+
+Sometime, parameters will be necessary to use an engine. See the [complete list here](player_list.md) to know which parameter are required.
+Core players are already packaged with the installation of Kalliope an can be used out of the box. 
+
+>**Note:** Most cloud based TTS generate a file in MP3 format. Some players are not able to read this format and then a conversion to wav is needed.
+
+
 ## Speech to text configuration
 
 #### default_speech_to_text

+ 1 - 1
Docs/stt.md

@@ -23,7 +23,7 @@ speech_to_text:
    - STT2:
       STT2parameter1: "value option1"
 ```
-Sometime, an API key will be necessary to use an engine. Click on a TTS engine link in the `Current Available STT` section to know which parameter are required.
+Sometime, an API key will be necessary to use an engine. Click on a TTS engine link in the `Current CORE Available STT` section to know which parameter are required.
 
 ## Current CORE Available STT
 

+ 17 - 0
Tests/settings/settings_test.yml

@@ -46,6 +46,23 @@ text_to_speech:
       voice: "Agnes"
       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"
+
 # ---------------------------
 # Wake up answers
 # ---------------------------

+ 1 - 1
Tests/test_dynamic_loading.py

@@ -15,7 +15,7 @@ class TestDynamicLoading(unittest.TestCase):
     """
 
     def setUp(self):
-        # get current script directory path. We are in /an/unknown/path/kalliope/core/Tests
+        # get current script directory path. We are in /an/unknown/path/kalliope/core/tests
         cur_script_directory = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
         # get parent dir. Now we are in /an/unknown/path/kalliope
         root_dir = os.path.normpath(cur_script_directory + os.sep + os.pardir)

+ 34 - 4
Tests/test_models.py

@@ -1,6 +1,8 @@
 import unittest
 import ast
 import mock
+
+from kalliope.core.Models.Player import Player
 from kalliope.core.Models.Tts import Tts
 
 from kalliope.core.Models.Trigger import Trigger
@@ -14,7 +16,7 @@ from kalliope.core.Models.Dna import Dna
 from kalliope.core import LIFOBuffer
 from kalliope.core.Models.Settings import Settings
 
-from kalliope.core.Models import Neuron, Order, Synapse, Brain, Event, Resources
+from kalliope.core.Models import Neuron, Order, Synapse, Brain, Event, Resources, Singleton
 
 from kalliope.core.Models.APIResponse import APIResponse
 from kalliope.core.Models.MatchedSynapse import MatchedSynapse
@@ -23,6 +25,9 @@ from kalliope.core.Models.MatchedSynapse import MatchedSynapse
 class TestModels(unittest.TestCase):
 
     def setUp(self):
+        # Kill the singleton
+        Singleton._instances = dict()
+
         # Init
         neuron1 = Neuron(name='neurone1', parameters={'var1': 'val1'})
         neuron2 = Neuron(name='neurone2', parameters={'var2': 'val2'})
@@ -273,12 +278,16 @@ class TestModels(unittest.TestCase):
 
     def test_Settings(self):
         with mock.patch('platform.machine', return_value='pumpkins'):
-            rest_api1 = RestAPI(password_protected=True, login="admin", password="password", active=True,
+            rest_api1 = RestAPI(password_protected=True,
+                                login="admin",
+                                password="password",
+                                active=True,
                                 port=5000, allowed_cors_origin="*")
 
             setting1 = Settings(default_tts_name="pico2wav",
                                 default_stt_name="google",
                                 default_trigger_name="swoyboy",
+                                default_player_name="mplayer",
                                 ttss=["ttts"],
                                 stts=["stts"],
                                 random_wake_up_answers=["yes"],
@@ -287,15 +296,18 @@ class TestModels(unittest.TestCase):
                                 on_ready_answers=None,
                                 on_ready_sounds=None,
                                 triggers=["snowboy"],
+                                players=["mplayer"],
                                 rest_api=rest_api1,
                                 cache_path="/tmp/kalliope",
                                 default_synapse="default_synapse",
                                 resources=None,
                                 variables={"key1": "val1"})
+            setting1.kalliope_version = "0.4.5"
 
             setting2 = Settings(default_tts_name="accapela",
                                 default_stt_name="bing",
                                 default_trigger_name="swoyboy",
+                                default_player_name="mplayer",
                                 ttss=["ttts"],
                                 stts=["stts"],
                                 random_wake_up_answers=["no"],
@@ -309,10 +321,12 @@ class TestModels(unittest.TestCase):
                                 default_synapse="my_default_synapse",
                                 resources=None,
                                 variables={"key1": "val1"})
+            setting2.kalliope_version = "0.4.5"
 
             setting3 = Settings(default_tts_name="pico2wav",
                                 default_stt_name="google",
                                 default_trigger_name="swoyboy",
+                                default_player_name="mplayer",
                                 ttss=["ttts"],
                                 stts=["stts"],
                                 random_wake_up_answers=["yes"],
@@ -321,11 +335,13 @@ class TestModels(unittest.TestCase):
                                 on_ready_answers=None,
                                 on_ready_sounds=None,
                                 triggers=["snowboy"],
+                                players=["mplayer"],
                                 rest_api=rest_api1,
                                 cache_path="/tmp/kalliope",
                                 default_synapse="default_synapse",
                                 resources=None,
                                 variables={"key1": "val1"})
+            setting3.kalliope_version = "0.4.5"
 
             expected_result_serialize = {
                 'default_synapse': 'default_synapse',
@@ -341,10 +357,11 @@ class TestModels(unittest.TestCase):
                     },
                 'play_on_ready_notification': False,
                 'default_stt_name': 'google',
-                'kalliope_version': '0.4.4b',
+                'kalliope_version': '0.4.5',
                 'random_wake_up_sounds': None,
                 'on_ready_answers': None,
                 'default_trigger_name': 'swoyboy',
+                'default_player_name': 'mplayer',
                 'cache_path': '/tmp/kalliope',
                 'stts': ['stts'],
                 'machine': 'pumpkins',
@@ -354,7 +371,8 @@ class TestModels(unittest.TestCase):
                 'variables': {'key1': 'val1'},
                 'resources': None,
                 'triggers': ['snowboy'],
-                'rpi_settings': None
+                'rpi_settings': None,
+                'players': ['mplayer']
             }
 
             self.assertDictEqual(expected_result_serialize, setting1.serialize())
@@ -428,6 +446,18 @@ class TestModels(unittest.TestCase):
         self.assertTrue(trigger1.__eq__(trigger3))
         self.assertFalse(trigger1.__eq__(trigger2))
 
+    def test_Player(self):
+        player1 = Player(name="player1", parameters={"key1": "val1"})
+        player2 = Player(name="player2", parameters={"key2": "val2"})
+        player3 = Player(name="player1", parameters={"key1": "val1"})
+
+        expected_result_serialize = {'name': 'player1', 'parameters': {'key1': 'val1'}}
+
+        self.assertDictEqual(expected_result_serialize, player1.serialize())
+
+        self.assertTrue(player1.__eq__(player3))
+        self.assertFalse(player1.__eq__(player2))
+
     def test_Tts(self):
         tts1 = Tts(name="tts1", parameters={"key1": "val1"})
         tts2 = Tts(name="tts2", parameters={"key2": "val2"})

+ 11 - 2
Tests/test_neuron_module.py

@@ -1,6 +1,8 @@
 import os
 import unittest
 import mock
+
+from kalliope.core.Models import Singleton
 from kalliope.core.Models.Tts import Tts
 
 from kalliope import SettingLoader
@@ -10,8 +12,11 @@ from kalliope.core.NeuronModule import NeuronModule, TemplateFileNotFoundExcepti
 class TestNeuronModule(unittest.TestCase):
 
     def setUp(self):
+        # kill singleton
+        Singleton._instances = dict()
+
         self.expected_result = "hello, this is a replaced word"
-        # this allow us to run the test from an IDE and from the root with python -m unittest Tests.TestNeuronModule
+        # this allow us to run the test from an IDE and from the root with python -m unittest tests.TestNeuronModule
         if "/Tests" in os.getcwd():
             self.file_template = "templates/template_test.j2"
         else:
@@ -22,7 +27,11 @@ class TestNeuronModule(unittest.TestCase):
         }
         self.neuron_module_test = NeuronModule()
 
-        self.settings = SettingLoader(file_path="settings_test.yml").settings
+        if "/Tests" in os.getcwd():
+            self.file_settings = "settings/settings_test.yml"
+        else:
+            self.file_settings = "Tests/settings/settings_test.yml"
+        self.settings = SettingLoader(file_path=self.file_settings).settings
 
     def tearDown(self):
         del self.neuron_module_test

+ 44 - 0
Tests/test_player_launcher.py

@@ -0,0 +1,44 @@
+import unittest
+import mock
+
+from kalliope.core.Models.Player import Player
+from kalliope.core.Models.Settings import Settings
+from kalliope.core.PlayerLauncher import PlayerLauncher
+
+
+class TestPlayerLauncher(unittest.TestCase):
+    """
+    Class to test Launchers Classes (PlayerLauncher) and methods
+    """
+
+    def setUp(self):
+        pass
+
+    ####
+    # Player Launcher
+    def test_get_player(self):
+        """
+        Test the PlayerLauncher trying to run the Player
+        """
+        player1 = Player("Player", {})
+        player2 = Player("Player2", {'test': "hitheparamtest"})
+        settings = Settings()
+        settings.players = [player1, player2]
+        with mock.patch("kalliope.core.Utils.get_dynamic_class_instantiation") as mock_get_class_instantiation:
+            # Get the player1
+            settings.default_player_name = "Player"
+            PlayerLauncher.get_player(settings=settings)
+
+            mock_get_class_instantiation.assert_called_once_with(package_name="players",
+                                                                 module_name=player1.name,
+                                                                 parameters=player1.parameters)
+            mock_get_class_instantiation.reset_mock()
+
+            # Get the player 2
+            settings.default_player_name = "Player2"
+            PlayerLauncher.get_player(settings=settings)
+
+            mock_get_class_instantiation.assert_called_once_with(package_name="players",
+                                                                 module_name=player2.name,
+                                                                 parameters=player2.parameters)
+            mock_get_class_instantiation.reset_mock()

+ 16 - 1
Tests/test_settings_loader.py

@@ -7,6 +7,7 @@ import unittest
 from kalliope.core.ConfigurationManager import SettingLoader
 from kalliope.core.Models import Singleton
 from kalliope.core.Models import Resources
+from kalliope.core.Models.Player import Player
 from kalliope.core.Models.RestAPI import RestAPI
 from kalliope.core.Models.Settings import Settings
 from kalliope.core.Models.Stt import Stt
@@ -17,7 +18,7 @@ from kalliope.core.Models.Tts import Tts
 class TestSettingLoader(unittest.TestCase):
 
     def setUp(self):
-        # get current script directory path. We are in /an/unknown/path/kalliope/core/Tests
+        # get current script directory path. We are in /an/unknown/path/kalliope/core/tests
         cur_script_directory = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
         # get parent dir. Now we are in /an/unknown/path/kalliope
         root_dir = os.path.normpath(cur_script_directory + os.sep + os.pardir)
@@ -33,8 +34,10 @@ class TestSettingLoader(unittest.TestCase):
                  'password_protected': True,
                  'password': 'secret', 'port': 5000},
             'default_trigger': 'snowboy',
+            'default_player': 'mplayer',
             'play_on_ready_notification': 'never',
             'triggers': [{'snowboy': {'pmdl_file': 'trigger/snowboy/resources/kalliope-FR-6samples.pmdl'}}],
+            'players': [{'mplayer': {}}, {'pyalsaaudio': {"device": "default"}}],
             'speech_to_text': [{'google': {'language': 'fr-FR'}}],
             'on_ready_answers': ['Kalliope is ready'],
             'cache_path': '/tmp/kalliope_tts_cache',
@@ -83,6 +86,7 @@ class TestSettingLoader(unittest.TestCase):
         settings_object.default_tts_name = "pico2wave"
         settings_object.default_stt_name = "google"
         settings_object.default_trigger_name = "snowboy"
+        settings_object.default_player_name = "mplayer"
         tts1 = Tts(name="pico2wave", parameters={'cache': True, 'language': 'fr-FR'})
         tts2 = Tts(name="voxygen", parameters={'voice': 'Agnes', 'cache': True})
         settings_object.ttss = [tts1, tts2]
@@ -96,6 +100,9 @@ class TestSettingLoader(unittest.TestCase):
         trigger1 = Trigger(name="snowboy",
                            parameters={'pmdl_file': 'trigger/snowboy/resources/kalliope-FR-6samples.pmdl'})
         settings_object.triggers = [trigger1]
+        player1 = Player(name="mplayer", parameters={})
+        player2 = Player(name="pyalsaaudio", parameters={"device": "default"})
+        settings_object.players = [player1, player2]
         settings_object.rest_api = RestAPI(password_protected=True, active=True,
                                            login="admin", password="secret", port=5000,
                                            allowed_cors_origin=False)
@@ -150,6 +157,14 @@ class TestSettingLoader(unittest.TestCase):
         sl = SettingLoader(file_path=self.settings_file_to_test)
         self.assertEqual([trigger1], sl._get_triggers(self.settings_dict))
 
+    def test_get_players(self):
+        player1 = Player(name="mplayer",
+                         parameters={})
+        player2 = Player(name="pyalsaaudio",
+                         parameters={'device': 'default'})
+        sl = SettingLoader(file_path=self.settings_file_to_test)
+        self.assertEqual([player1, player2], sl._get_players(self.settings_dict))
+
     def test_get_random_wake_up_answers(self):
         expected_random_wake_up_answers = ['Oui monsieur?']
         sl = SettingLoader(file_path=self.settings_file_to_test)

+ 21 - 5
Tests/test_trigger_launcher.py

@@ -1,6 +1,7 @@
 import unittest
 import mock
 
+from kalliope.core.Models.Settings import Settings
 from kalliope.core.TriggerLauncher import TriggerLauncher
 
 from kalliope.core.Models.Trigger import Trigger
@@ -8,7 +9,7 @@ from kalliope.core.Models.Trigger import Trigger
 
 class TestTriggerLauncher(unittest.TestCase):
     """
-    Class to test Launchers Classes (TriggerLauncher, SynapseLauncher, NeuronLauncher) and methods
+    Class to test Launchers Classes (TriggerLauncher) and methods
     """
 
     def setUp(self):
@@ -20,12 +21,27 @@ class TestTriggerLauncher(unittest.TestCase):
         """
         Test the Trigger Launcher trying to run the trigger
         """
-        trigger = Trigger("Trigger", {})
+        trigger1 = Trigger("Trigger", {})
+        trigger2 = Trigger("Trigger2", {'pmdl_file': "trigger/snowboy/resources/kalliope-FR-6samples.pmdl"})
+        settings = Settings()
+        settings.triggers = [trigger1, trigger2]
         with mock.patch("kalliope.core.Utils.get_dynamic_class_instantiation") as mock_get_class_instantiation:
-            TriggerLauncher.get_trigger(trigger=trigger,
+            # Get the trigger 1
+            settings.default_trigger_name = "Trigger"
+            TriggerLauncher.get_trigger(settings=settings,
                                         callback=None)
 
             mock_get_class_instantiation.assert_called_once_with(package_name="trigger",
-                                                                 module_name=trigger.name,
-                                                                 parameters=trigger.parameters)
+                                                                 module_name=trigger1.name,
+                                                                 parameters=trigger1.parameters)
+            mock_get_class_instantiation.reset_mock()
+
+            # Get the trigger 2
+            settings.default_trigger_name = "Trigger2"
+            TriggerLauncher.get_trigger(settings=settings,
+                                        callback=None)
+
+            mock_get_class_instantiation.assert_called_once_with(package_name="trigger",
+                                                                 module_name=trigger2.name,
+                                                                 parameters=trigger2.parameters)
             mock_get_class_instantiation.reset_mock()

+ 1 - 0
install/files/deb-packages_requirements.txt

@@ -20,3 +20,4 @@ build-essential
 sox
 libatlas3-base
 mplayer
+libav-tools

+ 3 - 0
install/files/python_requirements.txt

@@ -19,5 +19,8 @@ apscheduler==3.3.0
 GitPython==2.1.1
 packaging>=16.8
 transitions>=0.4.3
+sounddevice>=0.3.7
+SoundFile>=0.9.0
+pyalsaaudio>=0.8.4
 RPi.GPIO>=0.6.3
 

+ 1 - 1
kalliope/_version.py

@@ -1,2 +1,2 @@
 # https://www.python.org/dev/peps/pep-0440/
-version_str = "0.4.4b"
+version_str = "0.4.4"

+ 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.Stt import Stt
 from kalliope.core.Models.Trigger import Trigger
+from kalliope.core.Models.Player import Player
 from kalliope.core.Models.Tts import Tts
 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_tts_name = self._get_default_text_to_speech(settings)
         default_trigger_name = self._get_default_trigger(settings)
+        default_player_name = self._get_default_player(settings)
         stts = self._get_stts(settings)
         ttss = self._get_ttss(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_sound = self._get_random_wake_up_sounds(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_stt_name = default_stt_name
         setting_object.default_trigger_name = default_trigger_name
+        setting_object.default_player_name = default_player_name
         setting_object.stts = stts
         setting_object.ttss = ttss
         setting_object.triggers = triggers
+        setting_object.players = players
         setting_object.random_wake_up_answers = random_wake_up_answers
         setting_object.random_wake_up_sounds = random_wake_up_sound
         setting_object.play_on_ready_notification = play_on_ready_notification
@@ -220,6 +225,33 @@ class SettingLoader(with_metaclass(Singleton, object)):
         except KeyError as 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
     def _get_stts(settings):
         """
@@ -236,7 +268,7 @@ class SettingLoader(with_metaclass(Singleton, object)):
 
         .. seealso:: Stt
         .. raises:: SettingNotFound
-        .. warnings:: Class Method and Private
+        .. warnings:: Static Method and Private
         """
 
         try:
@@ -275,7 +307,7 @@ class SettingLoader(with_metaclass(Singleton, object)):
 
         .. seealso:: Tts
         .. raises:: SettingNotFound
-        .. warnings:: Class Method and Private
+        .. warnings:: Static Method and Private
         """
 
         try:
@@ -313,7 +345,7 @@ class SettingLoader(with_metaclass(Singleton, object)):
 
         .. seealso:: Trigger
         .. raises:: SettingNotFound
-        .. warnings:: Class Method and Private
+        .. warnings:: Static Method and Private
         """
 
         try:
@@ -335,6 +367,44 @@ class SettingLoader(with_metaclass(Singleton, object)):
                 triggers.append(new_trigger)
         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
     def _get_random_wake_up_answers(settings):
         """

+ 15 - 16
kalliope/core/MainController.py

@@ -2,17 +2,22 @@ import logging
 import random
 from time import sleep
 
-from flask import Flask
 from transitions import Machine
-
 from kalliope.core import Utils
 from kalliope.core.ConfigurationManager import SettingLoader
 from kalliope.core.OrderListener import OrderListener
-from kalliope.core.Players import Mplayer
+
+# API
+from flask import Flask
 from kalliope.core.RestAPI.FlaskAPI import FlaskAPI
+
+# Launchers
 from kalliope.core.SynapseLauncher import SynapseLauncher
 from kalliope.core.TriggerLauncher import TriggerLauncher
 from kalliope.core.Utils.RpiUtils import RpiUtils
+from kalliope.core.PlayerLauncher import PlayerLauncher
+
+# Neurons
 from kalliope.neurons.say.say import Say
 
 logging.basicConfig()
@@ -44,6 +49,7 @@ class MainController:
         # Starting the rest API
         self._start_rest_api()
 
+        # rpi setting for led and mute button
         self.rpi_utils = None
         if self.settings.rpi_settings:
             # 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")
                 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
         self.trigger_instance = None
         self.trigger_callback_called = False
@@ -100,7 +109,7 @@ class MainController:
         This function will start the trigger thread that listen for the hotword
         """
         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_instance.daemon = True
         # Wait that the kalliope trigger is pronounced by the user
@@ -121,7 +130,7 @@ class MainController:
                 Say(message=self.settings.on_ready_answers)
             elif self.settings.on_ready_sounds is not None:
                 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()
 
     def waiting_for_trigger_callback_thread(self):
@@ -188,8 +197,7 @@ class MainController:
             Say(message=self.settings.random_wake_up_answers)
         else:
             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()
 
     def order_listener_callback(self, order):
@@ -215,15 +223,6 @@ class MainController:
         # return to the state "unpausing_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
     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_stt_name=None,
                  default_trigger_name=None,
+                 default_player_name=None,
                  ttss=None,
                  stts=None,
                  random_wake_up_answers=None,
@@ -20,6 +21,7 @@ class Settings(object):
                  on_ready_answers=None,
                  on_ready_sounds=None,
                  triggers=None,
+                 players=None,
                  rest_api=None,
                  cache_path=None,
                  default_synapse=None,
@@ -30,6 +32,7 @@ class Settings(object):
         self.default_tts_name = default_tts_name
         self.default_stt_name = default_stt_name
         self.default_trigger_name = default_trigger_name
+        self.default_player_name = default_player_name
         self.ttss = ttss
         self.stts = stts
         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_sounds = on_ready_sounds
         self.triggers = triggers
+        self.players = players
         self.rest_api = rest_api
         self.cache_path = cache_path
         self.default_synapse = default_synapse
@@ -59,6 +63,7 @@ class Settings(object):
             'default_tts_name': self.default_tts_name,
             'default_stt_name': self.default_stt_name,
             'default_trigger_name': self.default_trigger_name,
+            'default_player_name': self.default_player_name,
             'ttss': self.ttss,
             'stts': self.stts,
             '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_sounds': self.on_ready_sounds,
             'triggers': self.triggers,
+            'players': self.players,
             'rest_api': self.rest_api.serialize(),
             'cache_path': self.cache_path,
             '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

+ 41 - 0
kalliope/core/PlayerModule.py

@@ -0,0 +1,41 @@
+# coding: utf8
+import logging
+import os
+import subprocess
+
+from kalliope.core.Utils.FileManager import FileManager
+
+logging.basicConfig()
+logger = logging.getLogger("kalliope")
+
+
+class PlayerModule(object):
+    """
+    Mother class of Players. 
+    Ability to convert mp3 to wave format.
+    """
+
+    def __init__(self, **kwargs):
+
+        # set parameter from what we receive from the settings
+        self.convert = kwargs.get('convert_to_wav', True)
+
+    @staticmethod
+    def convert_mp3_to_wav(file_path_mp3):
+        """ 
+        PyAudio, AlsaPlayer, sounddevices  do 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(['avconv', '-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)

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

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

+ 8 - 4
kalliope/core/TTS/TTSModule.py

@@ -2,12 +2,14 @@
 import hashlib
 import logging
 import os
-import sys
+import subprocess
+
 import six
 
 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 import Utils
 
 logging.basicConfig()
 logger = logging.getLogger("kalliope")
@@ -60,6 +62,7 @@ class TTSModule(object):
         # load settings
         sl = SettingLoader()
         self.settings = sl.settings
+        self.player = PlayerLauncher.get_player(settings=self.settings)
 
         # create the path in the tmp folder
         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
         """
-        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):
         """
@@ -153,4 +157,4 @@ class TTSModule(object):
             logger.debug("TTSModule, File already in cache: %s" % file_path)
         else:
             logger.debug("TTSModule, File not yet in cache: %s" % file_path)
-        return exist_in_cache
+        return exist_in_cache

+ 17 - 11
kalliope/core/TriggerLauncher.py

@@ -10,19 +10,25 @@ class TriggerLauncher(object):
     def __init__(self):
         pass
 
-    @classmethod
-    def get_trigger(cls, trigger, callback):
+    @staticmethod
+    def get_trigger(settings, callback):
         """
         Start a trigger module
         :param trigger: trigger object to instantiate
         :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):
-
     color_list = dict(
         PURPLE='\033[95m',
         BLUE='\033[94m',
@@ -114,6 +113,7 @@ class Utils(object):
         :return:
         """
         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:
             neuron_resource_path = resources_dir + os.sep + module_name.lower() \
                                    + os.sep + module_name.lower() + ".py"
@@ -121,7 +121,7 @@ class Utils(object):
                 imp.load_source(module_name.capitalize(), neuron_resource_path)
                 package_path = module_name.capitalize()
                 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()])
 
@@ -129,7 +129,8 @@ class Utils(object):
             klass = getattr(mod, module_name.capitalize())
         except AttributeError:
             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:
             # run the plugin
@@ -299,4 +300,4 @@ class Utils(object):
         if sys.version_info[0] < 3:
             if isinstance(text, unicode):
                 text = text.encode("utf-8")
-        return text
+        return text

+ 1 - 0
kalliope/core/__init__.py

@@ -9,5 +9,6 @@ from kalliope.core.SynapseLauncher import SynapseLauncher
 from kalliope.core.LIFOBuffer import LIFOBuffer
 from kalliope.core.NeuronParameterLoader import NeuronParameterLoader
 from kalliope.core.NeuronModule import NeuronModule
+from kalliope.core.PlayerModule import PlayerModule
 from kalliope.core.MainController import MainController
 from kalliope.core.EventManager import EventManager

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

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

+ 0 - 0
kalliope/players/__init__.py


+ 11 - 0
kalliope/players/mplayer/README.md

@@ -0,0 +1,11 @@
+### Mplayer
+
+This Player is based on the [mplayer engine](http://www.mplayerhq.hu/design7/news.html)
+
+| Parameters | Required | Default | Choices    | Comment |
+|------------|----------|---------|------------|---------|
+|
+#### Notes
+
+This Player handles mp3 and wav format.
+

+ 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.
     """
 
-    def __init__(self):
-        pass
+    def __init__(self, **kwargs):
+        logger.debug("[Mplayer.__init__] instance")
+        logger.debug("[Mplayer.__init__] args : %s " % str(kwargs))
 
     @classmethod
     def play(cls, filepath):

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


+ 10 - 0
kalliope/players/mplayer/tests/test_mplayer.py

@@ -0,0 +1,10 @@
+import unittest
+
+
+class TestMplayer(unittest.TestCase):
+
+    def test_play(self):
+        pass
+
+if __name__ == '__main__':
+    unittest.main()

+ 15 - 0
kalliope/players/pyalsaaudio/README.md

@@ -0,0 +1,15 @@
+### PyAlsaAudio
+
+This Player is based on the [alsa engine](https://larsimmisch.github.io/pyalsaaudio/libalsaaudio.html)
+
+| AlsaAudioPlayer |          |           |             |                                   |
+|-----------------|----------|-----------|-------------|-----------------------------------|
+| Parameters      | Required | Default   | Choices     | Comment                           |
+| device          | No       | "default" |             | Select the device to use for alsa |
+| convert_to_wav  | No       | TRUE      | True, False | convert the file into wav         |
+
+#### Notes
+
+Ability to define the device to use.
+This Player does not handle mp3 format, converting mp3 to wav might be required.
+

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

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

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

@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+import alsaaudio
+import logging
+import wave
+
+from kalliope.core.PlayerModule import PlayerModule
+
+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(PlayerModule):
+    """
+    This Class is representing the Player Object used to play the all sound of the system.
+    """
+
+    def __init__(self, **kwargs):
+        super(Pyalsaaudio, self).__init__(**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):
+
+        if self.convert:
+            self.convert_mp3_to_wav(file_path_mp3=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)
+        # 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()

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


+ 11 - 0
kalliope/players/pyalsaaudio/tests/test_pyalsaaudio.py

@@ -0,0 +1,11 @@
+import unittest
+
+
+class TestPyAlsaAudio(unittest.TestCase):
+
+    def test_play(self):
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()

+ 13 - 0
kalliope/players/pyaudioplayer/README.md

@@ -0,0 +1,13 @@
+### PyAudioPlayer
+
+This Player is based on the [pyaudio engine](https://people.csail.mit.edu/hubert/pyaudio/)
+
+| PyAudioPlayer |          |           |             |                           |
+|-----------------|----------|-----------|-------------|---------------------------|
+| Parameters      | Required | Default   | Choices     | Comment                   |
+| convert_to_wav  | No       | TRUE      | True, False | convert the file into wav |
+
+#### Notes
+
+This Player does not handle mp3 format, converting mp3 to wav might be required.
+

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

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

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

@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+import logging
+import wave
+
+import pyaudio
+
+from kalliope.core.PlayerModule import PlayerModule
+
+logging.basicConfig()
+logger = logging.getLogger("kalliope")
+
+CHUNK = 1024
+
+
+class Pyaudioplayer(PlayerModule):
+    """
+    This Class is representing the Player Object used to play the all sound of the system.
+    """
+
+    def __init__(self, **kwargs):
+        super(Pyaudioplayer, self).__init__(**kwargs)
+        logger.debug("[Pyaudioplayer.__init__] instance")
+        logger.debug("[Pyaudioplayer.__init__] args : %s " % str(kwargs))
+
+    def play(self, 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              
+        """
+        if self.convert:
+            self.convert_mp3_to_wav(file_path_mp3=file_path)
+        # 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()

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


+ 10 - 0
kalliope/players/pyaudioplayer/tests/test_pyaudioplayer.py

@@ -0,0 +1,10 @@
+import unittest
+
+
+class TestPyAudioPlayer(unittest.TestCase):
+    def test_play(self):
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()

+ 13 - 0
kalliope/players/sounddeviceplayer/README.md

@@ -0,0 +1,13 @@
+### SoundDevicePlayer
+
+This Player is based on the [sounddevice and soundfile engines](https://pypi.python.org/pypi/sounddevice)
+
+| SoundDevicePlayer |          |           |             |                           |
+|-----------------|----------|-----------|-------------|---------------------------|
+| Parameters      | Required | Default   | Choices     | Comment                   |
+| convert_to_wav  | No       | TRUE      | True, False | convert the file into wav |
+
+#### Notes
+
+This Player does not handle mp3 format, converting mp3 to wav might be required.
+

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

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

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

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

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


+ 10 - 0
kalliope/players/sounddeviceplayer/tests/test_sounddeviceplayer.py

@@ -0,0 +1,10 @@
+import unittest
+
+
+class TestSoundDevicePlayer(unittest.TestCase):
+    def test_play(self):
+        pass
+
+
+if __name__ == '__main__':
+    unittest.main()

+ 37 - 12
kalliope/settings.yml

@@ -25,23 +25,27 @@ triggers:
 # This is the STT that will be used by default
 default_speech_to_text: "google"
 
-# Spreech to Text engines configuration
+# Speech to Text engines configuration
 # Available engine are:
 # - google (via SpeechRecognition)
-# -
+# - wit
+# - bing
+# - apiai
+# - houndify
+# - cmusphinx (must be installed first)
 speech_to_text:
   - google:
       language: "fr-FR"
   - wit:
-      key: "B5JI3YUSLYOYWNIDBINBVM34XUODME2K"
+      key: "fakekey" # example : "B5JI3YUSLYOYWNIDBINBVM34XUODME2K"
   - bing: # API not working : credential fails ..
-      key: "9e48ddaf75904838bedc11aea6b36fb0"
+      key: "fakekey" # example : "9e48ddaf75904838bedc11aea6b36fb0"
   - apiai:
-      key: "e0cbff154af44944a6b9f82c0668b527"
+      key: "fakekey" # example : "e0cbff154af44944a6b9f82c0668b527"
       language: "fr"
   - houndify:
-      key: "7zj90T7qAV74OYXk4X4vI2Xhk7wPsJu4aEZ0G5Ll-BMmV1JGtFpCxtSH9SmTY4G3bpEJ7a5y_GTQid-CAKI6vw=="
-      client_id: "lN4JXeaSticbSo9-llczbA=="
+      key: "fakekey" # example : "7zj90T7qAV74OYXk4X4vI2Xhk7wPsJu4aEZ0G5Ll-BMmV1JGtFpCxtSH9SmTY4G3bpEJ7a5y_GTQid-CAKI6vw=="
+      client_id: "fakeclientid" # example : "lN4JXeaSticbSo9-llczbA=="
   #- cmusphinx
 
 
@@ -53,13 +57,12 @@ default_text_to_speech: "pico2wave"
 # where we store generated audio files from TTS engine to reuse them
 cache_path: "/tmp/kalliope_tts_cache"
 
-# Text to Spreech engines configuration
+# Text to Speech engines configuration
 # Available engine are:
 # - pico2wave
-# - acapela
-# - pico2wave
-# - googletts
-# - voicerss
+# - acapela     # MP3
+# - googletts   # MP3
+# - voicerss    # MP3
 text_to_speech:
   - pico2wave:
       language: "fr-FR"
@@ -75,6 +78,28 @@ text_to_speech:
       language: "fr-fr"
       cache: True
 
+# ---------------------------
+# players
+# ---------------------------
+# This is the sound player that will be used by default
+default_player: "mplayer"
+
+# players configuration
+# Available engine are:
+# - mplayer
+# - pyalsaaudio       # no mp3
+# - pyaudioplayer     # no mp3
+# - sounddeviceplayer # no mp3
+players:
+  - mplayer: {}
+  - pyalsaaudio:
+     device: "default"
+     convert_to_wav: True
+  - pyaudioplayer:
+     convert_to_wav: True
+  - sounddeviceplayer:
+     convert_to_wav: True
+
 # ---------------------------
 # Wake up answers
 # ---------------------------

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

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

+ 2 - 0
kalliope/tts/acapela/README.md

@@ -13,3 +13,5 @@ This TTS is based on the [Acapela engine](http://www.acapela-group.com/)
 
 The voice name is attached to a specific language.
 To test and get the name of a voice, please refer to [this website(https://acapela-box.com/AcaBox/index.php].
+
+The generated file is mp3. 

+ 4 - 1
setup.py

@@ -67,7 +67,7 @@ setup(
         'markupsafe>=1.0',
         'pyaudio>=0.2.10',
         'pyasn1>=0.2.3',
-        'ansible>=2.2',
+        'ansible>=2.3',
         py2_prefix + 'pythondialog>=3.4.0',
         'jinja2>=2.8,<=2.9.6',
         'cffi>=1.9.1',
@@ -83,6 +83,9 @@ setup(
         'GitPython>=2.1.3',
         'packaging>=16.8',
         'transitions>=0.4.3',
+        'sounddevice>=0.3.7',
+        'SoundFile>=0.9.0',
+        'pyalsaaudio>=0.8.4',
         'RPi.GPIO>=0.6.3'
     ],