浏览代码

fix ctrl-c. Snowboy is now a real thread. Child daemon of kalliope main program

nico 8 年之前
父节点
当前提交
8437bcbbb9
共有 3 个文件被更改,包括 77 次插入72 次删除
  1. 1 2
      core/MainController.py
  2. 30 11
      trigger/snowboy/snowboy.py
  3. 46 59
      trigger/snowboy/snowboydecoder.py

+ 1 - 2
core/MainController.py

@@ -31,6 +31,7 @@ class MainController:
             Utils.print_info("Starting REST API Listening port: %s" % self.settings.rest_api.port)
             app = Flask(__name__)
             flask_api = FlaskAPI(app, port=self.settings.rest_api.port, brain=self.brain)
+            flask_api.daemon = True
             flask_api.start()
 
         # create an order listener object. This last will the trigger callback before starting
@@ -71,8 +72,6 @@ class MainController:
         self.trigger_instance.unpause()
         # create a new order listener that will wait for start
         self.order_listener = OrderListener(self.analyse_order)
-        # restart the trigger to catch the hotword
-        self.trigger_instance.start()
 
     def _get_default_trigger(self):
         """

+ 30 - 11
trigger/snowboy/snowboy.py

@@ -1,15 +1,22 @@
+import logging
+import time
+
 from trigger.snowboy import snowboydecoder
 
 
 class MissingParameterException(Exception):
     pass
 
+logging.basicConfig()
+logger = logging.getLogger("kalliope")
+
 
 class Snowboy(object):
 
     def __init__(self, **kwargs):
         # pause listening boolean
         self.interrupted = False
+        self.kill_received = False
 
         # callback function to call when hotword caught
         self.callback = kwargs.get('callback', None)
@@ -22,6 +29,10 @@ class Snowboy(object):
         if self.pmdl is None:
             raise MissingParameterException("Pmdl file is required with snowboy")
 
+        self.detector = snowboydecoder.HotwordDetector(self.pmdl, sensitivity=0.5, detected_callback=self.callback,
+                                                       interrupt_check=self.interrupt_callback,
+                                                       sleep_time=0.03)
+
     def interrupt_callback(self):
         """
         This function will be passed to snowboy to stop the main thread
@@ -34,22 +45,30 @@ class Snowboy(object):
         Start the snowboy thread and wait for a Kalliope trigger word
         :return:
         """
-        detector = snowboydecoder.HotwordDetector(self.pmdl, sensitivity=0.5)
-
         # start snowboy loop
-        detector.start(detected_callback=self.callback,
-                       interrupt_check=self.interrupt_callback,
-                       sleep_time=0.03)
-
+        self.detector.daemon = True
+        try:
+            self.detector.start()
+            while not self.kill_received:
+                #  once the main thread has started child thread, there's nothing else for it to do.
+                # So it exits, and the threads are destroyed instantly. So let's keep the main thread alive
+                time.sleep(1)
+        except KeyboardInterrupt:
+            self.kill_received = True
+            self.detector.kill_received = True
         # we wait that a callback
-        detector.terminate()
+        self.detector.terminate()
 
     def pause(self):
         """
-        Stop the Snowboy main thread
-        :return:
+        pause the Snowboy main thread
         """
-        self.interrupted = True
+        logger.debug("Pausing snowboy process")
+        self.detector.paused = True
 
     def unpause(self):
-        self.interrupted = False
+        """
+        unpause the Snowboy main thread
+        """
+        logger.debug("Unpausing snowboy process")
+        self.detector.paused = False

+ 46 - 59
trigger/snowboy/snowboydecoder.py

@@ -1,7 +1,13 @@
 #!/usr/bin/env python
 
 import collections
+from threading import Thread
+
 import pyaudio
+import sys
+
+import signal
+
 import snowboydetect
 import time
 import wave
@@ -9,8 +15,7 @@ import os
 import logging
 
 logging.basicConfig()
-logger = logging.getLogger("snowboy")
-logger.setLevel(logging.INFO)
+logger = logging.getLogger("kalliope")
 TOP_DIR = os.path.dirname(os.path.abspath(__file__))
 
 RESOURCE_FILE = os.path.join(TOP_DIR, "resources/common.res")
@@ -34,29 +39,7 @@ class RingBuffer(object):
         return tmp
 
 
-def play_audio_file(fname=DETECT_DING):
-    """Simple callback function to play a wave file. By default it plays
-    a Ding sound.
-
-    :param str fname: wave file name
-    :return: None
-    """
-    ding_wav = wave.open(fname, 'rb')
-    ding_data = ding_wav.readframes(ding_wav.getnframes())
-    audio = pyaudio.PyAudio()
-    stream_out = audio.open(
-        format=audio.get_format_from_width(ding_wav.getsampwidth()),
-        channels=ding_wav.getnchannels(),
-        rate=ding_wav.getframerate(), input=False, output=True)
-    stream_out.start_stream()
-    stream_out.write(ding_data)
-    time.sleep(0.2)
-    stream_out.stop_stream()
-    stream_out.close()
-    audio.terminate()
-
-
-class HotwordDetector(object):
+class HotwordDetector(Thread):
     """
     Snowboy decoder to detect whether a keyword specified by `decoder_model`
     exists in a microphone input stream.
@@ -69,10 +52,15 @@ class HotwordDetector(object):
                               default sensitivity in the model will be used.
     :param audio_gain: multiply input volume by this factor.
     """
-    def __init__(self, decoder_model,
-                 resource=RESOURCE_FILE,
-                 sensitivity=[],
-                 audio_gain=1):
+    def __init__(self, decoder_model, resource=RESOURCE_FILE, sensitivity=[], audio_gain=1, detected_callback=None,
+                 interrupt_check=lambda: False, sleep_time=0.03):
+
+        super(HotwordDetector, self).__init__()
+        self.detected_callback = detected_callback
+        self.interrupt_check = interrupt_check
+        self.sleep_time = sleep_time
+        self.kill_received = False
+        self.paused = False
 
         def audio_callback(in_data, frame_count, time_info, status):
             self.ring_buffer.extend(in_data)
@@ -114,9 +102,7 @@ class HotwordDetector(object):
             frames_per_buffer=2048,
             stream_callback=audio_callback)
 
-    def start(self, detected_callback=play_audio_file,
-              interrupt_check=lambda: False,
-              sleep_time=0.03):
+    def run(self):
         """
         Start the voice detector. For every `sleep_time` second it checks the
         audio buffer for triggering keywords. If detected, then call
@@ -133,42 +119,43 @@ class HotwordDetector(object):
         :param float sleep_time: how much time in second every loop waits.
         :return: None
         """
-        if interrupt_check():
+        if self.interrupt_check():
             logger.debug("detect voice return")
             return
 
-        tc = type(detected_callback)
+        tc = type(self.detected_callback)
         if tc is not list:
-            detected_callback = [detected_callback]
-        if len(detected_callback) == 1 and self.num_hotwords > 1:
-            detected_callback *= self.num_hotwords
+            self.detected_callback = [self.detected_callback]
+        if len(self.detected_callback) == 1 and self.num_hotwords > 1:
+            self.detected_callback *= self.num_hotwords
 
-        assert self.num_hotwords == len(detected_callback), \
+        assert self.num_hotwords == len(self.detected_callback), \
             "Error: hotwords in your models (%d) do not match the number of " \
-            "callbacks (%d)" % (self.num_hotwords, len(detected_callback))
+            "callbacks (%d)" % (self.num_hotwords, len(self.detected_callback))
 
         logger.debug("detecting...")
 
-        while True:
-            if interrupt_check():
-                logger.debug("detect voice break")
-                break
-            data = self.ring_buffer.get()
-            if len(data) == 0:
-                time.sleep(sleep_time)
-                continue
-
-            ans = self.detector.RunDetection(data)
-            if ans == -1:
-                logger.warning("Error initializing streams or reading audio data")
-            elif ans > 0:
-                message = "Keyword " + str(ans) + " detected at time: "
-                message += time.strftime("%Y-%m-%d %H:%M:%S",
-                                         time.localtime(time.time()))
-                logger.info(message)
-                callback = detected_callback[ans-1]
-                if callback is not None:
-                    callback()
+        while not self.kill_received:
+            if not self.paused:
+                if self.interrupt_check():
+                    logger.debug("detect voice break")
+                    break
+                data = self.ring_buffer.get()
+                if len(data) == 0:
+                    time.sleep(self.sleep_time)
+                    continue
+
+                ans = self.detector.RunDetection(data)
+                if ans == -1:
+                    logger.warning("Error initializing streams or reading audio data")
+                elif ans > 0:
+                    message = "Keyword " + str(ans) + " detected at time: "
+                    message += time.strftime("%Y-%m-%d %H:%M:%S",
+                                             time.localtime(time.time()))
+                    logger.info(message)
+                    callback = self.detected_callback[ans-1]
+                    if callback is not None:
+                        callback()
 
         logger.debug("finished.")