snowboydecoder.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. #!/usr/bin/env python
  2. import collections
  3. from threading import Thread
  4. import pyaudio
  5. import sys
  6. import signal
  7. import snowboydetect
  8. import time
  9. import wave
  10. import os
  11. import logging
  12. logging.basicConfig()
  13. logger = logging.getLogger("kalliope")
  14. TOP_DIR = os.path.dirname(os.path.abspath(__file__))
  15. RESOURCE_FILE = os.path.join(TOP_DIR, "resources/common.res")
  16. DETECT_DING = os.path.join(TOP_DIR, "resources/ding.wav")
  17. DETECT_DONG = os.path.join(TOP_DIR, "resources/dong.wav")
  18. class RingBuffer(object):
  19. """Ring buffer to hold audio from PortAudio"""
  20. def __init__(self, size = 4096):
  21. self._buf = collections.deque(maxlen=size)
  22. def extend(self, data):
  23. """Adds data to the end of buffer"""
  24. self._buf.extend(data)
  25. def get(self):
  26. """Retrieves data from the beginning of buffer and clears it"""
  27. tmp = bytes(bytearray(self._buf))
  28. self._buf.clear()
  29. return tmp
  30. class HotwordDetector(Thread):
  31. """
  32. Snowboy decoder to detect whether a keyword specified by `decoder_model`
  33. exists in a microphone input stream.
  34. :param decoder_model: decoder model file path, a string or a list of strings
  35. :param resource: resource file path.
  36. :param sensitivity: decoder sensitivity, a float of a list of floats.
  37. The bigger the value, the more senstive the
  38. decoder. If an empty list is provided, then the
  39. default sensitivity in the model will be used.
  40. :param audio_gain: multiply input volume by this factor.
  41. """
  42. def __init__(self, decoder_model, resource=RESOURCE_FILE, sensitivity=[], audio_gain=1, detected_callback=None,
  43. interrupt_check=lambda: False, sleep_time=0.03):
  44. super(HotwordDetector, self).__init__()
  45. self.detected_callback = detected_callback
  46. self.interrupt_check = interrupt_check
  47. self.sleep_time = sleep_time
  48. self.kill_received = False
  49. self.paused = False
  50. def audio_callback(in_data, frame_count, time_info, status):
  51. self.ring_buffer.extend(in_data)
  52. play_data = chr(0) * len(in_data)
  53. return play_data, pyaudio.paContinue
  54. tm = type(decoder_model)
  55. ts = type(sensitivity)
  56. if tm is not list:
  57. decoder_model = [decoder_model]
  58. if ts is not list:
  59. sensitivity = [sensitivity]
  60. model_str = ",".join(decoder_model)
  61. self.detector = snowboydetect.SnowboyDetect(
  62. resource_filename=resource.encode(), model_str=model_str.encode())
  63. self.detector.SetAudioGain(audio_gain)
  64. self.num_hotwords = self.detector.NumHotwords()
  65. if len(decoder_model) > 1 and len(sensitivity) == 1:
  66. sensitivity = sensitivity*self.num_hotwords
  67. if len(sensitivity) != 0:
  68. assert self.num_hotwords == len(sensitivity), \
  69. "number of hotwords in decoder_model (%d) and sensitivity " \
  70. "(%d) does not match" % (self.num_hotwords, len(sensitivity))
  71. sensitivity_str = ",".join([str(t) for t in sensitivity])
  72. if len(sensitivity) != 0:
  73. self.detector.SetSensitivity(sensitivity_str.encode())
  74. self.ring_buffer = RingBuffer(
  75. self.detector.NumChannels() * self.detector.SampleRate() * 5)
  76. self.audio = pyaudio.PyAudio()
  77. self.stream_in = self.audio.open(
  78. input=True, output=False,
  79. format=self.audio.get_format_from_width(
  80. self.detector.BitsPerSample() / 8),
  81. channels=self.detector.NumChannels(),
  82. rate=self.detector.SampleRate(),
  83. frames_per_buffer=2048,
  84. stream_callback=audio_callback)
  85. def run(self):
  86. """
  87. Start the voice detector. For every `sleep_time` second it checks the
  88. audio buffer for triggering keywords. If detected, then call
  89. corresponding function in `detected_callback`, which can be a single
  90. function (single model) or a list of callback functions (multiple
  91. models). Every loop it also calls `interrupt_check` -- if it returns
  92. True, then breaks from the loop and return.
  93. :param detected_callback: a function or list of functions. The number of
  94. items must match the number of models in
  95. `decoder_model`.
  96. :param interrupt_check: a function that returns True if the main loop
  97. needs to stop.
  98. :param float sleep_time: how much time in second every loop waits.
  99. :return: None
  100. """
  101. if self.interrupt_check():
  102. logger.debug("detect voice return")
  103. return
  104. tc = type(self.detected_callback)
  105. if tc is not list:
  106. self.detected_callback = [self.detected_callback]
  107. if len(self.detected_callback) == 1 and self.num_hotwords > 1:
  108. self.detected_callback *= self.num_hotwords
  109. assert self.num_hotwords == len(self.detected_callback), \
  110. "Error: hotwords in your models (%d) do not match the number of " \
  111. "callbacks (%d)" % (self.num_hotwords, len(self.detected_callback))
  112. logger.debug("detecting...")
  113. while not self.kill_received:
  114. if not self.paused:
  115. if self.interrupt_check():
  116. logger.debug("detect voice break")
  117. break
  118. data = self.ring_buffer.get()
  119. if len(data) == 0:
  120. time.sleep(self.sleep_time)
  121. continue
  122. ans = self.detector.RunDetection(data)
  123. if ans == -1:
  124. logger.warning("Error initializing streams or reading audio data")
  125. elif ans > 0:
  126. message = "Keyword " + str(ans) + " detected at time: "
  127. message += time.strftime("%Y-%m-%d %H:%M:%S",
  128. time.localtime(time.time()))
  129. logger.info(message)
  130. callback = self.detected_callback[ans-1]
  131. if callback is not None:
  132. callback()
  133. logger.debug("finished.")
  134. def terminate(self):
  135. """
  136. Terminate audio stream. Users cannot call start() again to detect.
  137. :return: None
  138. """
  139. self.stream_in.stop_stream()
  140. self.stream_in.close()
  141. self.audio.terminate()