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 stream = 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, ) try: while not state.exit_event.is_set(): data = stream.read(chunk) self.socket.send(data) except IOError as e: logger.error("Stopped listening: failed to get audio from microphone.", exc_info=e) finally: stream.stop_stream() stream.close()