from __future__ import unicode_literals # So that `logging` can use Unicode characters in names import threading import logging import pyaudio import zmq from robot_interface.endpoints.socket_base import SocketBase from robot_interface.state import state from robot_interface.utils.microphone import choose_mic from robot_interface.core.config import settings logger = logging.getLogger(__name__) class AudioSender(SocketBase): """ Audio sender endpoint, responsible for sending microphone audio data. :param zmq_context: The ZeroMQ context to use. :type zmq_context: zmq.Context :param port: The port to use. :type port: int :ivar thread: Thread used for sending audio. :vartype thread: threading.Thread | None :ivar audio: PyAudio instance. :vartype audio: pyaudio.PyAudio | None :ivar microphone: Selected microphone information. :vartype microphone: dict | None """ def __init__(self, zmq_context, port=settings.agent_settings.audio_sender_port): super(AudioSender, self).__init__(str("audio")) # Convert future's unicode_literal to str self.create_socket(zmq_context, zmq.PUB, port) self.thread = None try: self.audio = pyaudio.PyAudio() self.microphone = choose_mic(self.audio) except IOError as e: logger.warning("PyAudio is not available.", exc_info=e) self.audio = None self.microphone = None def start(self): """ Start sending audio in a different thread. Will not start if no microphone is available. """ if not self.microphone: logger.info("Not listening: no microphone available.") return logger.info("Listening with microphone \"{}\".".format(self.microphone["name"])) self.thread = threading.Thread(target=self._stream) self.thread.start() def wait_until_done(self): """ Wait until the audio thread is done. Will block until `state.exit_event` is set. If the thread is not running, does nothing. """ if not self.thread: return self.thread.join() self.thread = None def _stream(self): """ Internal method to continuously read audio from the microphone and send it over the socket. """ audio_settings = settings.audio_config chunk = audio_settings.chunk_size # 320 at 16000 Hz is 20ms, 512 is required for Silero-VAD # Docs say this only raises an error if neither `input` nor `output` is True def open_stream(): return self.audio.open( format=pyaudio.paFloat32, channels=audio_settings.channels, rate=audio_settings.sample_rate, input=True, input_device_index=self.microphone["index"], frames_per_buffer=chunk, ) stream = None try: state.active_event.wait() # Wait until the system is not paused # Test in case exit_event was set while waiting if not state.exit_event.is_set(): stream = open_stream() while not state.exit_event.is_set(): if not state.active_event.is_set(): # when paused # Stop and close the stream if it is open to prevent buffer overflow if stream: try: stream.stop_stream() stream.close() except IOError: pass # Ignore errors on closing stream = None state.active_event.wait() # Wait until unpaused # Check if exit_event was set while waiting if state.exit_event.is_set(): break stream = open_stream() if stream: try: data = stream.read(chunk) self.socket.send(data) except IOError as e: logger.warn("Audio read error occurred.", exc_info=e) if stream: stream.close() stream = open_stream() except IOError as e: logger.error("Stopped listening: failed to get audio from microphone.", exc_info=e) finally: if stream: try: stream.stop_stream() stream.close() except IOError: pass # Ignore errors on closing