Browse Source

Merge pull request #277 from kalliope-project/pyaudio

#234 manage players modularity
Nicolas Marcq 8 years ago
parent
commit
4c1c488efd
54 changed files with 775 additions and 75 deletions
  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
 ```bash
 sudo apt-get update
 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
 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
 ```bash
 sudo apt-get update
 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
 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
 ```bash
 sudo apt-get update
 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
 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
 ```bash
 sudo apt-get update
 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
 Let's install the last release of python-pip
 ```bash
 ```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).
 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
 ## Speech to text configuration
 
 
 #### default_speech_to_text
 #### default_speech_to_text

+ 1 - 1
Docs/stt.md

@@ -23,7 +23,7 @@ speech_to_text:
    - STT2:
    - STT2:
       STT2parameter1: "value option1"
       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
 ## Current CORE Available STT
 
 

+ 17 - 0
Tests/settings/settings_test.yml

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

+ 1 - 1
Tests/test_dynamic_loading.py

@@ -15,7 +15,7 @@ class TestDynamicLoading(unittest.TestCase):
     """
     """
 
 
     def setUp(self):
     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())))
         cur_script_directory = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
         # get parent dir. Now we are in /an/unknown/path/kalliope
         # get parent dir. Now we are in /an/unknown/path/kalliope
         root_dir = os.path.normpath(cur_script_directory + os.sep + os.pardir)
         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 unittest
 import ast
 import ast
 import mock
 import mock
+
+from kalliope.core.Models.Player import Player
 from kalliope.core.Models.Tts import Tts
 from kalliope.core.Models.Tts import Tts
 
 
 from kalliope.core.Models.Trigger import Trigger
 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 import LIFOBuffer
 from kalliope.core.Models.Settings import Settings
 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.APIResponse import APIResponse
 from kalliope.core.Models.MatchedSynapse import MatchedSynapse
 from kalliope.core.Models.MatchedSynapse import MatchedSynapse
@@ -23,6 +25,9 @@ from kalliope.core.Models.MatchedSynapse import MatchedSynapse
 class TestModels(unittest.TestCase):
 class TestModels(unittest.TestCase):
 
 
     def setUp(self):
     def setUp(self):
+        # Kill the singleton
+        Singleton._instances = dict()
+
         # Init
         # Init
         neuron1 = Neuron(name='neurone1', parameters={'var1': 'val1'})
         neuron1 = Neuron(name='neurone1', parameters={'var1': 'val1'})
         neuron2 = Neuron(name='neurone2', parameters={'var2': 'val2'})
         neuron2 = Neuron(name='neurone2', parameters={'var2': 'val2'})
@@ -273,12 +278,16 @@ class TestModels(unittest.TestCase):
 
 
     def test_Settings(self):
     def test_Settings(self):
         with mock.patch('platform.machine', return_value='pumpkins'):
         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="*")
                                 port=5000, allowed_cors_origin="*")
 
 
             setting1 = Settings(default_tts_name="pico2wav",
             setting1 = Settings(default_tts_name="pico2wav",
                                 default_stt_name="google",
                                 default_stt_name="google",
                                 default_trigger_name="swoyboy",
                                 default_trigger_name="swoyboy",
+                                default_player_name="mplayer",
                                 ttss=["ttts"],
                                 ttss=["ttts"],
                                 stts=["stts"],
                                 stts=["stts"],
                                 random_wake_up_answers=["yes"],
                                 random_wake_up_answers=["yes"],
@@ -287,15 +296,18 @@ class TestModels(unittest.TestCase):
                                 on_ready_answers=None,
                                 on_ready_answers=None,
                                 on_ready_sounds=None,
                                 on_ready_sounds=None,
                                 triggers=["snowboy"],
                                 triggers=["snowboy"],
+                                players=["mplayer"],
                                 rest_api=rest_api1,
                                 rest_api=rest_api1,
                                 cache_path="/tmp/kalliope",
                                 cache_path="/tmp/kalliope",
                                 default_synapse="default_synapse",
                                 default_synapse="default_synapse",
                                 resources=None,
                                 resources=None,
                                 variables={"key1": "val1"})
                                 variables={"key1": "val1"})
+            setting1.kalliope_version = "0.4.5"
 
 
             setting2 = Settings(default_tts_name="accapela",
             setting2 = Settings(default_tts_name="accapela",
                                 default_stt_name="bing",
                                 default_stt_name="bing",
                                 default_trigger_name="swoyboy",
                                 default_trigger_name="swoyboy",
+                                default_player_name="mplayer",
                                 ttss=["ttts"],
                                 ttss=["ttts"],
                                 stts=["stts"],
                                 stts=["stts"],
                                 random_wake_up_answers=["no"],
                                 random_wake_up_answers=["no"],
@@ -309,10 +321,12 @@ class TestModels(unittest.TestCase):
                                 default_synapse="my_default_synapse",
                                 default_synapse="my_default_synapse",
                                 resources=None,
                                 resources=None,
                                 variables={"key1": "val1"})
                                 variables={"key1": "val1"})
+            setting2.kalliope_version = "0.4.5"
 
 
             setting3 = Settings(default_tts_name="pico2wav",
             setting3 = Settings(default_tts_name="pico2wav",
                                 default_stt_name="google",
                                 default_stt_name="google",
                                 default_trigger_name="swoyboy",
                                 default_trigger_name="swoyboy",
+                                default_player_name="mplayer",
                                 ttss=["ttts"],
                                 ttss=["ttts"],
                                 stts=["stts"],
                                 stts=["stts"],
                                 random_wake_up_answers=["yes"],
                                 random_wake_up_answers=["yes"],
@@ -321,11 +335,13 @@ class TestModels(unittest.TestCase):
                                 on_ready_answers=None,
                                 on_ready_answers=None,
                                 on_ready_sounds=None,
                                 on_ready_sounds=None,
                                 triggers=["snowboy"],
                                 triggers=["snowboy"],
+                                players=["mplayer"],
                                 rest_api=rest_api1,
                                 rest_api=rest_api1,
                                 cache_path="/tmp/kalliope",
                                 cache_path="/tmp/kalliope",
                                 default_synapse="default_synapse",
                                 default_synapse="default_synapse",
                                 resources=None,
                                 resources=None,
                                 variables={"key1": "val1"})
                                 variables={"key1": "val1"})
+            setting3.kalliope_version = "0.4.5"
 
 
             expected_result_serialize = {
             expected_result_serialize = {
                 'default_synapse': 'default_synapse',
                 'default_synapse': 'default_synapse',
@@ -341,10 +357,11 @@ class TestModels(unittest.TestCase):
                     },
                     },
                 'play_on_ready_notification': False,
                 'play_on_ready_notification': False,
                 'default_stt_name': 'google',
                 'default_stt_name': 'google',
-                'kalliope_version': '0.4.4b',
+                'kalliope_version': '0.4.5',
                 'random_wake_up_sounds': None,
                 'random_wake_up_sounds': None,
                 'on_ready_answers': None,
                 'on_ready_answers': None,
                 'default_trigger_name': 'swoyboy',
                 'default_trigger_name': 'swoyboy',
+                'default_player_name': 'mplayer',
                 'cache_path': '/tmp/kalliope',
                 'cache_path': '/tmp/kalliope',
                 'stts': ['stts'],
                 'stts': ['stts'],
                 'machine': 'pumpkins',
                 'machine': 'pumpkins',
@@ -354,7 +371,8 @@ class TestModels(unittest.TestCase):
                 'variables': {'key1': 'val1'},
                 'variables': {'key1': 'val1'},
                 'resources': None,
                 'resources': None,
                 'triggers': ['snowboy'],
                 'triggers': ['snowboy'],
-                'rpi_settings': None
+                'rpi_settings': None,
+                'players': ['mplayer']
             }
             }
 
 
             self.assertDictEqual(expected_result_serialize, setting1.serialize())
             self.assertDictEqual(expected_result_serialize, setting1.serialize())
@@ -428,6 +446,18 @@ class TestModels(unittest.TestCase):
         self.assertTrue(trigger1.__eq__(trigger3))
         self.assertTrue(trigger1.__eq__(trigger3))
         self.assertFalse(trigger1.__eq__(trigger2))
         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):
     def test_Tts(self):
         tts1 = Tts(name="tts1", parameters={"key1": "val1"})
         tts1 = Tts(name="tts1", parameters={"key1": "val1"})
         tts2 = Tts(name="tts2", parameters={"key2": "val2"})
         tts2 = Tts(name="tts2", parameters={"key2": "val2"})

+ 11 - 2
Tests/test_neuron_module.py

@@ -1,6 +1,8 @@
 import os
 import os
 import unittest
 import unittest
 import mock
 import mock
+
+from kalliope.core.Models import Singleton
 from kalliope.core.Models.Tts import Tts
 from kalliope.core.Models.Tts import Tts
 
 
 from kalliope import SettingLoader
 from kalliope import SettingLoader
@@ -10,8 +12,11 @@ from kalliope.core.NeuronModule import NeuronModule, TemplateFileNotFoundExcepti
 class TestNeuronModule(unittest.TestCase):
 class TestNeuronModule(unittest.TestCase):
 
 
     def setUp(self):
     def setUp(self):
+        # kill singleton
+        Singleton._instances = dict()
+
         self.expected_result = "hello, this is a replaced word"
         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():
         if "/Tests" in os.getcwd():
             self.file_template = "templates/template_test.j2"
             self.file_template = "templates/template_test.j2"
         else:
         else:
@@ -22,7 +27,11 @@ class TestNeuronModule(unittest.TestCase):
         }
         }
         self.neuron_module_test = NeuronModule()
         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):
     def tearDown(self):
         del self.neuron_module_test
         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.ConfigurationManager import SettingLoader
 from kalliope.core.Models import Singleton
 from kalliope.core.Models import Singleton
 from kalliope.core.Models import Resources
 from kalliope.core.Models import Resources
+from kalliope.core.Models.Player import Player
 from kalliope.core.Models.RestAPI import RestAPI
 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
@@ -17,7 +18,7 @@ from kalliope.core.Models.Tts import Tts
 class TestSettingLoader(unittest.TestCase):
 class TestSettingLoader(unittest.TestCase):
 
 
     def setUp(self):
     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())))
         cur_script_directory = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
         # get parent dir. Now we are in /an/unknown/path/kalliope
         # get parent dir. Now we are in /an/unknown/path/kalliope
         root_dir = os.path.normpath(cur_script_directory + os.sep + os.pardir)
         root_dir = os.path.normpath(cur_script_directory + os.sep + os.pardir)
@@ -33,8 +34,10 @@ class TestSettingLoader(unittest.TestCase):
                  'password_protected': True,
                  'password_protected': True,
                  'password': 'secret', 'port': 5000},
                  'password': 'secret', 'port': 5000},
             'default_trigger': 'snowboy',
             'default_trigger': 'snowboy',
+            'default_player': 'mplayer',
             'play_on_ready_notification': 'never',
             'play_on_ready_notification': 'never',
             'triggers': [{'snowboy': {'pmdl_file': 'trigger/snowboy/resources/kalliope-FR-6samples.pmdl'}}],
             'triggers': [{'snowboy': {'pmdl_file': 'trigger/snowboy/resources/kalliope-FR-6samples.pmdl'}}],
+            'players': [{'mplayer': {}}, {'pyalsaaudio': {"device": "default"}}],
             'speech_to_text': [{'google': {'language': 'fr-FR'}}],
             'speech_to_text': [{'google': {'language': 'fr-FR'}}],
             'on_ready_answers': ['Kalliope is ready'],
             'on_ready_answers': ['Kalliope is ready'],
             'cache_path': '/tmp/kalliope_tts_cache',
             'cache_path': '/tmp/kalliope_tts_cache',
@@ -83,6 +86,7 @@ class TestSettingLoader(unittest.TestCase):
         settings_object.default_tts_name = "pico2wave"
         settings_object.default_tts_name = "pico2wave"
         settings_object.default_stt_name = "google"
         settings_object.default_stt_name = "google"
         settings_object.default_trigger_name = "snowboy"
         settings_object.default_trigger_name = "snowboy"
+        settings_object.default_player_name = "mplayer"
         tts1 = Tts(name="pico2wave", parameters={'cache': True, 'language': 'fr-FR'})
         tts1 = Tts(name="pico2wave", parameters={'cache': True, 'language': 'fr-FR'})
         tts2 = Tts(name="voxygen", parameters={'voice': 'Agnes', 'cache': True})
         tts2 = Tts(name="voxygen", parameters={'voice': 'Agnes', 'cache': True})
         settings_object.ttss = [tts1, tts2]
         settings_object.ttss = [tts1, tts2]
@@ -96,6 +100,9 @@ class TestSettingLoader(unittest.TestCase):
         trigger1 = Trigger(name="snowboy",
         trigger1 = Trigger(name="snowboy",
                            parameters={'pmdl_file': 'trigger/snowboy/resources/kalliope-FR-6samples.pmdl'})
                            parameters={'pmdl_file': 'trigger/snowboy/resources/kalliope-FR-6samples.pmdl'})
         settings_object.triggers = [trigger1]
         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,
         settings_object.rest_api = RestAPI(password_protected=True, active=True,
                                            login="admin", password="secret", port=5000,
                                            login="admin", password="secret", port=5000,
                                            allowed_cors_origin=False)
                                            allowed_cors_origin=False)
@@ -150,6 +157,14 @@ class TestSettingLoader(unittest.TestCase):
         sl = SettingLoader(file_path=self.settings_file_to_test)
         sl = SettingLoader(file_path=self.settings_file_to_test)
         self.assertEqual([trigger1], sl._get_triggers(self.settings_dict))
         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):
     def test_get_random_wake_up_answers(self):
         expected_random_wake_up_answers = ['Oui monsieur?']
         expected_random_wake_up_answers = ['Oui monsieur?']
         sl = SettingLoader(file_path=self.settings_file_to_test)
         sl = SettingLoader(file_path=self.settings_file_to_test)

+ 21 - 5
Tests/test_trigger_launcher.py

@@ -1,6 +1,7 @@
 import unittest
 import unittest
 import mock
 import mock
 
 
+from kalliope.core.Models.Settings import Settings
 from kalliope.core.TriggerLauncher import TriggerLauncher
 from kalliope.core.TriggerLauncher import TriggerLauncher
 
 
 from kalliope.core.Models.Trigger import Trigger
 from kalliope.core.Models.Trigger import Trigger
@@ -8,7 +9,7 @@ from kalliope.core.Models.Trigger import Trigger
 
 
 class TestTriggerLauncher(unittest.TestCase):
 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):
     def setUp(self):
@@ -20,12 +21,27 @@ class TestTriggerLauncher(unittest.TestCase):
         """
         """
         Test the Trigger Launcher trying to run the trigger
         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:
         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)
                                         callback=None)
 
 
             mock_get_class_instantiation.assert_called_once_with(package_name="trigger",
             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()
             mock_get_class_instantiation.reset_mock()

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

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

+ 3 - 0
install/files/python_requirements.txt

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

+ 1 - 1
kalliope/_version.py

@@ -1,2 +1,2 @@
 # https://www.python.org/dev/peps/pep-0440/
 # 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.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

+ 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 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):
         """
         """
@@ -153,4 +157,4 @@ class TTSModule(object):
             logger.debug("TTSModule, File already in cache: %s" % file_path)
             logger.debug("TTSModule, File already in cache: %s" % file_path)
         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

+ 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/core/__init__.py

@@ -9,5 +9,6 @@ from kalliope.core.SynapseLauncher import SynapseLauncher
 from kalliope.core.LIFOBuffer import LIFOBuffer
 from kalliope.core.LIFOBuffer import LIFOBuffer
 from kalliope.core.NeuronParameterLoader import NeuronParameterLoader
 from kalliope.core.NeuronParameterLoader import NeuronParameterLoader
 from kalliope.core.NeuronModule import NeuronModule
 from kalliope.core.NeuronModule import NeuronModule
+from kalliope.core.PlayerModule import PlayerModule
 from kalliope.core.MainController import MainController
 from kalliope.core.MainController import MainController
 from kalliope.core.EventManager import EventManager
 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.
     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):

+ 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
 # 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"
   - wit:
   - wit:
-      key: "B5JI3YUSLYOYWNIDBINBVM34XUODME2K"
+      key: "fakekey" # example : "B5JI3YUSLYOYWNIDBINBVM34XUODME2K"
   - bing: # API not working : credential fails ..
   - bing: # API not working : credential fails ..
-      key: "9e48ddaf75904838bedc11aea6b36fb0"
+      key: "fakekey" # example : "9e48ddaf75904838bedc11aea6b36fb0"
   - apiai:
   - apiai:
-      key: "e0cbff154af44944a6b9f82c0668b527"
+      key: "fakekey" # example : "e0cbff154af44944a6b9f82c0668b527"
       language: "fr"
       language: "fr"
   - houndify:
   - houndify:
-      key: "7zj90T7qAV74OYXk4X4vI2Xhk7wPsJu4aEZ0G5Ll-BMmV1JGtFpCxtSH9SmTY4G3bpEJ7a5y_GTQid-CAKI6vw=="
-      client_id: "lN4JXeaSticbSo9-llczbA=="
+      key: "fakekey" # example : "7zj90T7qAV74OYXk4X4vI2Xhk7wPsJu4aEZ0G5Ll-BMmV1JGtFpCxtSH9SmTY4G3bpEJ7a5y_GTQid-CAKI6vw=="
+      client_id: "fakeclientid" # example : "lN4JXeaSticbSo9-llczbA=="
   #- cmusphinx
   #- cmusphinx
 
 
 
 
@@ -53,13 +57,12 @@ 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
-# - pico2wave
-# - googletts
-# - voicerss
+# - acapela     # MP3
+# - googletts   # MP3
+# - voicerss    # MP3
 text_to_speech:
 text_to_speech:
   - pico2wave:
   - pico2wave:
       language: "fr-FR"
       language: "fr-FR"
@@ -75,6 +78,28 @@ 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       # 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
 # 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

+ 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.
 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].
 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',
         'markupsafe>=1.0',
         'pyaudio>=0.2.10',
         'pyaudio>=0.2.10',
         'pyasn1>=0.2.3',
         'pyasn1>=0.2.3',
-        'ansible>=2.2',
+        'ansible>=2.3',
         py2_prefix + 'pythondialog>=3.4.0',
         py2_prefix + 'pythondialog>=3.4.0',
         'jinja2>=2.8,<=2.9.6',
         'jinja2>=2.8,<=2.9.6',
         'cffi>=1.9.1',
         'cffi>=1.9.1',
@@ -83,6 +83,9 @@ 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',
+        'pyalsaaudio>=0.8.4',
         'RPi.GPIO>=0.6.3'
         'RPi.GPIO>=0.6.3'
     ],
     ],