snowboydecoder.py 6.3 KB

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