feat: stream audio to CB
Uses PyAudio and ZeroMQ to publish audio chunks. ref: N25B-119
This commit is contained in:
@@ -24,7 +24,9 @@ python -m virtualenv .venv
|
|||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
Install the required packages with
|
Before installing the Python packages, you'll need to have the `portaudio` system package installed.
|
||||||
|
|
||||||
|
Then you can install the required packages with
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|||||||
44
main.py
44
main.py
@@ -1,27 +1,51 @@
|
|||||||
import qi
|
import qi
|
||||||
import zmq
|
import zmq
|
||||||
import time
|
|
||||||
|
from src.audio_streaming import AudioStreaming
|
||||||
|
from state import state
|
||||||
|
|
||||||
|
|
||||||
def say(session, message):
|
def say(session, message):
|
||||||
tts = session.service("ALTextToSpeech")
|
tts = session.service("ALTextToSpeech")
|
||||||
tts.say(message)
|
tts.say(message)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
app = qi.Application()
|
|
||||||
app.start()
|
|
||||||
session = app.session
|
|
||||||
|
|
||||||
|
def listen_for_messages(session):
|
||||||
context = zmq.Context()
|
context = zmq.Context()
|
||||||
socket = context.socket(zmq.SUB)
|
socket = context.socket(zmq.SUB)
|
||||||
socket.connect("tcp://localhost:5556")
|
socket.connect("tcp://localhost:5556")
|
||||||
socket.setsockopt_string(zmq.SUBSCRIBE, u"") # u because Python 2 shenanigans
|
socket.setsockopt_string(zmq.SUBSCRIBE, u"") # u because Python 2 shenanigans
|
||||||
|
|
||||||
while True:
|
poller = zmq.Poller()
|
||||||
print("Listening for message")
|
poller.register(socket, zmq.POLLIN)
|
||||||
|
|
||||||
|
print("Listening for messages")
|
||||||
|
while not state.exit_event.is_set():
|
||||||
|
if not poller.poll(200): continue # At most 200 ms delay after CTRL+C
|
||||||
|
# We now know there's a message waiting for us
|
||||||
message = socket.recv_string()
|
message = socket.recv_string()
|
||||||
print("Received message: {}".format(message))
|
print("Received message: {}".format(message))
|
||||||
|
|
||||||
say(session, message)
|
if session: say(session, message)
|
||||||
|
|
||||||
time.sleep(1)
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
app = qi.Application()
|
||||||
|
app.start()
|
||||||
|
session = app.session
|
||||||
|
except RuntimeError:
|
||||||
|
session = None
|
||||||
|
|
||||||
|
audio_streamer = AudioStreaming()
|
||||||
|
audio_streamer.run()
|
||||||
|
|
||||||
|
listen_for_messages(session) # Runs indefinitely, until CTRL+C
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
state.initialize()
|
||||||
|
main()
|
||||||
|
finally:
|
||||||
|
state.deinitialize()
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
pyzmq<16
|
pyzmq<16
|
||||||
|
pyaudio<=0.2.11
|
||||||
0
src/__init__.py
Normal file
0
src/__init__.py
Normal file
93
src/audio_streaming.py
Normal file
93
src/audio_streaming.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import threading
|
||||||
|
|
||||||
|
import pyaudio
|
||||||
|
import zmq
|
||||||
|
|
||||||
|
from state import state
|
||||||
|
|
||||||
|
|
||||||
|
def choose_mic_interactive(audio):
|
||||||
|
"""Choose a microphone to use. The `audio` parameter is an instance of PyAudio. Returns a dict."""
|
||||||
|
device_count = audio.get_device_count()
|
||||||
|
print("Found {} audio devices:".format(device_count))
|
||||||
|
for i in range(device_count):
|
||||||
|
print("- {}: {}".format(i, audio.get_device_info_by_index(i)["name"]))
|
||||||
|
|
||||||
|
microphone_index = None
|
||||||
|
while microphone_index is None:
|
||||||
|
chosen = input("Which device would you like to use?\n> ")
|
||||||
|
try:
|
||||||
|
chosen = int(chosen)
|
||||||
|
if chosen < 0 or chosen > device_count: raise ValueError()
|
||||||
|
microphone_index = chosen
|
||||||
|
except ValueError:
|
||||||
|
print("Please enter a number between 0 and {}".format(device_count))
|
||||||
|
|
||||||
|
chosen_microphone = audio.get_device_info_by_index(microphone_index)
|
||||||
|
print("Chose microphone \"{}\"".format(chosen_microphone["name"]))
|
||||||
|
return chosen_microphone
|
||||||
|
|
||||||
|
|
||||||
|
def choose_mic_default(audio):
|
||||||
|
"""Choose a microphone to use based on defaults. The `audio` parameter is a PyAudio. Returns a dict."""
|
||||||
|
default_device = audio.get_default_input_device_info()
|
||||||
|
return default_device
|
||||||
|
|
||||||
|
|
||||||
|
class AudioStreaming:
|
||||||
|
def __init__(self, port=5557):
|
||||||
|
self.port = port
|
||||||
|
self.audio = pyaudio.PyAudio()
|
||||||
|
self.microphone = choose_mic_default(self.audio)
|
||||||
|
self.thread = None
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.thread = threading.Thread(target=self._stream)
|
||||||
|
self.thread.start()
|
||||||
|
|
||||||
|
def wait_until_done(self):
|
||||||
|
if not self.thread: return
|
||||||
|
self.thread.join()
|
||||||
|
|
||||||
|
def _stream(self):
|
||||||
|
context = zmq.Context()
|
||||||
|
socket = context.socket(zmq.PUB)
|
||||||
|
socket.bind("tcp://*:{}".format(self.port))
|
||||||
|
|
||||||
|
chunk = 512 # 320 at 16000 Hz is 20ms, 512 is required for Silero-VAD
|
||||||
|
|
||||||
|
stream = self.audio.open(
|
||||||
|
format=pyaudio.paFloat32,
|
||||||
|
channels=self.microphone["maxInputChannels"],
|
||||||
|
rate=16000,
|
||||||
|
input=True,
|
||||||
|
input_device_index=self.microphone["index"],
|
||||||
|
frames_per_buffer=chunk,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
while not state.exit_event.is_set():
|
||||||
|
data = stream.read(chunk)
|
||||||
|
socket.send(data)
|
||||||
|
finally:
|
||||||
|
stream.stop_stream()
|
||||||
|
stream.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
state.initialize()
|
||||||
|
try:
|
||||||
|
audio = AudioStreaming()
|
||||||
|
print("Starting audio streaming...")
|
||||||
|
audio.run()
|
||||||
|
|
||||||
|
import time
|
||||||
|
end = time.time() + 10
|
||||||
|
while not state.exit_event.is_set() and time.time() < end:
|
||||||
|
print "\rExiting in {:.2f} seconds".format(end - time.time()),
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
state.exit_event.set()
|
||||||
|
audio.wait_until_done()
|
||||||
|
finally:
|
||||||
|
state.deinitialize()
|
||||||
47
state.py
Normal file
47
state.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import signal
|
||||||
|
import threading
|
||||||
|
|
||||||
|
|
||||||
|
class State(object):
|
||||||
|
"""
|
||||||
|
Do not create an instance of this class directly: use the instance `state` below. This state must be initiated once,
|
||||||
|
probably when your program starts.
|
||||||
|
|
||||||
|
This class is used to share state between threads. For example, when the program is quit, that all threads can
|
||||||
|
detect this via the `exit_event` property being set.
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.is_initialized = False
|
||||||
|
self.exit_event = None
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
if self.is_initialized:
|
||||||
|
print("Already initialized")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.exit_event = threading.Event()
|
||||||
|
def handle_exit(_, __):
|
||||||
|
print("Exiting.")
|
||||||
|
self.exit_event.set()
|
||||||
|
signal.signal(signal.SIGINT, handle_exit)
|
||||||
|
signal.signal(signal.SIGTERM, handle_exit)
|
||||||
|
|
||||||
|
self.is_initialized = True
|
||||||
|
|
||||||
|
def deinitialize(self):
|
||||||
|
if not self.is_initialized: return
|
||||||
|
self.is_initialized = False
|
||||||
|
|
||||||
|
def __getattribute__(self, name):
|
||||||
|
# Enforce that the state is initialized before accessing any property (aside from the basic ones)
|
||||||
|
if name in ("initialize", "deinitialize", "is_initialized", "__dict__", "__class__"):
|
||||||
|
return object.__getattribute__(self, name)
|
||||||
|
|
||||||
|
if not self.is_initialized:
|
||||||
|
raise RuntimeError("State must be initialized before accessing '%s'" % name)
|
||||||
|
|
||||||
|
return object.__getattribute__(self, name)
|
||||||
|
|
||||||
|
|
||||||
|
# Must call `.initialize` before use
|
||||||
|
state = State()
|
||||||
Reference in New Issue
Block a user