Compare commits
4 Commits
feat/cb2ri
...
feat/ri2cb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c634e4b516 | ||
|
|
2132a74321 | ||
|
|
d21c7fa423 | ||
|
|
afae6fc331 |
17
README.md
17
README.md
@@ -24,7 +24,13 @@ python -m virtualenv .venv
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
Install the required packages with
|
||||
To be able to install the PyAudio Python package, you'll need to have the `portaudio` system package installed. On Debian or Ubuntu:
|
||||
|
||||
```shell
|
||||
sudo apt install portaudio19-dev
|
||||
```
|
||||
|
||||
Then you can install the required packages with
|
||||
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
@@ -59,7 +65,14 @@ python -c "import qi; print(qi)"
|
||||
You should now be able to run this project.
|
||||
|
||||
### MacOS
|
||||
...
|
||||
|
||||
On ARM CPU's, pyenv doesn't want to install Python 2. You can download and install it from [the Python website](https://www.python.org/downloads/release/python-2718/).
|
||||
|
||||
Create a virtual environment as described in the Linux section.
|
||||
|
||||
Then build `portaudio` for x86_64 CPU's.
|
||||
|
||||
Then follow the remaining installation instructions in the Linux section.
|
||||
|
||||
## Running
|
||||
Assuming you have the virtual environment activated (`source .venv/bin/activate` on Linux) and that you have a virtual robot running on localhost you should be able to run this project by typing
|
||||
|
||||
66
main.py
66
main.py
@@ -1,27 +1,69 @@
|
||||
import qi
|
||||
import sys
|
||||
import logging
|
||||
logging.
|
||||
|
||||
import zmq
|
||||
import time
|
||||
|
||||
from src.audio_streaming import AudioStreaming
|
||||
from state import state
|
||||
|
||||
|
||||
def say(session, message):
|
||||
tts = session.service("ALTextToSpeech")
|
||||
tts.say(message)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = qi.Application()
|
||||
app.start()
|
||||
session = app.session
|
||||
|
||||
def listen_for_messages(session):
|
||||
context = zmq.Context()
|
||||
socket = context.socket(zmq.SUB)
|
||||
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:
|
||||
print("Listening for message")
|
||||
poller = zmq.Poller()
|
||||
poller.register(socket, zmq.POLLIN)
|
||||
|
||||
logging.info("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()
|
||||
print("Received message: {}".format(message))
|
||||
logging.debug("Received message: {}".format(message))
|
||||
|
||||
say(session, message)
|
||||
if session: say(session, message)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
def get_session():
|
||||
if "--qi-url" not in sys.argv:
|
||||
logging.info("No Qi URL argument given. Running in stand-alone mode.")
|
||||
return None
|
||||
|
||||
try:
|
||||
import qi
|
||||
except ImportError:
|
||||
logging.info("Unable to import qi. Running in stand-alone mode.")
|
||||
return None
|
||||
|
||||
try:
|
||||
app = qi.Application()
|
||||
app.start()
|
||||
return app.session
|
||||
except RuntimeError:
|
||||
logging.info("Unable to connect to the robot. Running in stand-alone mode.")
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
session = get_session()
|
||||
|
||||
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=1,
|
||||
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()
|
||||
53
state.py
Normal file
53
state.py
Normal file
@@ -0,0 +1,53 @@
|
||||
import logging
|
||||
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:
|
||||
logging.warn("Already initialized")
|
||||
return
|
||||
|
||||
self.exit_event = threading.Event()
|
||||
def handle_exit(_, __):
|
||||
logging.info("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 object.__getattribute__(self, "is_initialized"):
|
||||
# Special case for the exit_event: if the event is set, return it without an error
|
||||
if name == "exit_event":
|
||||
exit_event = object.__getattribute__(self, "exit_event")
|
||||
if exit_event and exit_event.is_set(): return exit_event
|
||||
|
||||
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