99 lines
3.2 KiB
Python
99 lines
3.2 KiB
Python
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)
|
|
if (state.is_speaking): continue # Do not send audio while the robot is speaking
|
|
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()
|