Initial WIP version
This commit is contained in:
commit
e2e4c072f8
8 changed files with 248 additions and 0 deletions
0
hugvey/__init__.py
Normal file
0
hugvey/__init__.py
Normal file
BIN
hugvey/__pycache__/__init__.cpython-36.pyc
Normal file
BIN
hugvey/__pycache__/__init__.cpython-36.pyc
Normal file
Binary file not shown.
BIN
hugvey/__pycache__/client.cpython-36.pyc
Normal file
BIN
hugvey/__pycache__/client.cpython-36.pyc
Normal file
Binary file not shown.
BIN
hugvey/__pycache__/communication.cpython-36.pyc
Normal file
BIN
hugvey/__pycache__/communication.cpython-36.pyc
Normal file
Binary file not shown.
178
hugvey/client.py
Normal file
178
hugvey/client.py
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
import pyaudio
|
||||||
|
import socket
|
||||||
|
import select
|
||||||
|
import audioop
|
||||||
|
import threading
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import zmq
|
||||||
|
import asyncio
|
||||||
|
from zmq.asyncio import Context
|
||||||
|
from .communication import zmqReceive, zmqSend, getTopic
|
||||||
|
|
||||||
|
logger = logging.getLogger("client")
|
||||||
|
|
||||||
|
class VoiceServer(object):
|
||||||
|
"""A UDP server, providing mic data at 16 kHz"""
|
||||||
|
def __init__(self, voice_port, input_rate, input_name = None, target_rate = 16000):
|
||||||
|
self.voice_port = voice_port
|
||||||
|
self.input_rate = input_rate
|
||||||
|
self.target_rate = target_rate
|
||||||
|
self.stopped = True
|
||||||
|
self.clients = []
|
||||||
|
self.laststate = None
|
||||||
|
self.input_name = input_name
|
||||||
|
|
||||||
|
def get_input_idx(self):
|
||||||
|
input_device_idx = None
|
||||||
|
# input_device_idx = 6
|
||||||
|
# input_device_idx = 0
|
||||||
|
devices_count = self.p.get_device_count()
|
||||||
|
for i in range(devices_count):
|
||||||
|
dev = self.p.get_device_info_by_index(i)
|
||||||
|
if input_device_idx is None and dev['maxInputChannels'] > 0:
|
||||||
|
if (self.input_name and self.input_name in dev['name']) or \
|
||||||
|
(not self.input_name and dev['name'] != 'default'):
|
||||||
|
input_device_idx = dev['index']
|
||||||
|
logger.info("Use device {0}: {1}".format(dev['index'],dev['name']))
|
||||||
|
logger.debug("{} {:0d} {}".format("* " if input_device_idx == i else "- ", i, dev['name']))
|
||||||
|
return input_device_idx
|
||||||
|
|
||||||
|
|
||||||
|
def onBuffer(self, in_data, frame_count, time_info, status):
|
||||||
|
if self.input_rate == self.target_rate:
|
||||||
|
f = in_data
|
||||||
|
else:
|
||||||
|
f, self.laststate = audioop.ratecv(in_data, 2, 1, self.input_rate, self.target_rate, self.laststate)
|
||||||
|
|
||||||
|
for s in self.clients:
|
||||||
|
try:
|
||||||
|
s.send(f)
|
||||||
|
except Exception as e:
|
||||||
|
self.clients.remove(s)
|
||||||
|
logger.warn("Error sending to {}".format(s.getsockname()))
|
||||||
|
pass
|
||||||
|
return (None, pyaudio.paContinue)
|
||||||
|
|
||||||
|
async def start(self):
|
||||||
|
FORMAT = pyaudio.paInt16
|
||||||
|
CHANNELS = 1
|
||||||
|
CHUNK = 4096
|
||||||
|
|
||||||
|
self.p = pyaudio.PyAudio()
|
||||||
|
self.stopped = False
|
||||||
|
|
||||||
|
stream = self.p.open(
|
||||||
|
format=FORMAT,
|
||||||
|
channels=CHANNELS,
|
||||||
|
rate=self.input_rate,
|
||||||
|
input=True,
|
||||||
|
frames_per_buffer=CHUNK,
|
||||||
|
stream_callback=self.onBuffer,
|
||||||
|
input_device_index=self.get_input_idx()
|
||||||
|
)
|
||||||
|
|
||||||
|
while not self.stopped:
|
||||||
|
try:
|
||||||
|
self.voice_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self.voice_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
self.voice_socket.bind(('', self.voice_port))
|
||||||
|
self.voice_socket.listen(5)
|
||||||
|
|
||||||
|
read_list = [self.voice_socket]
|
||||||
|
logger.info( "Waiting for connections")
|
||||||
|
while not self.stopped:
|
||||||
|
(clientsocket, address) = self.voice_socket.accept()
|
||||||
|
self.clients.append(clientsocket)
|
||||||
|
|
||||||
|
logger.info( "Stop recording & streaming")
|
||||||
|
self.voice_socket.close()
|
||||||
|
# stop Recording
|
||||||
|
stream.stop_stream()
|
||||||
|
stream.close()
|
||||||
|
self.p.terminate()
|
||||||
|
except Exception as e:
|
||||||
|
logging.critical("Socket Exception {}".format(e))
|
||||||
|
self.voice_socket.close()
|
||||||
|
time.sleep(.5)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.stopped = True
|
||||||
|
|
||||||
|
class CommandHandler(object):
|
||||||
|
def __init__(self, hugvey_id, event_address = "tcp://127.0.0.1:5555"):
|
||||||
|
self.eventQueue = []
|
||||||
|
self.ctx = Context.instance()
|
||||||
|
self.hugvey_id = hugvey_id
|
||||||
|
self.event_address = event_address
|
||||||
|
|
||||||
|
def handle(self, cmd):
|
||||||
|
if not 'action' in cmd:
|
||||||
|
logger.critical("Invalid command: {}".format(cmd))
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Received {}".format(cmd))
|
||||||
|
if cmd['action'] == 'play':
|
||||||
|
self.cmdPlay(cmd['id'], cmd['msg'])
|
||||||
|
|
||||||
|
def cmdPlay(self, msgId, msgText):
|
||||||
|
# espeak(msgText)
|
||||||
|
logger.inof("Play: {}".format(msgText))
|
||||||
|
time.sleep(2)
|
||||||
|
sendMessage({
|
||||||
|
'event': 'playbackFinish',
|
||||||
|
'msgId': msgId
|
||||||
|
})
|
||||||
|
|
||||||
|
def sendMessage(self, msg):
|
||||||
|
self.eventQueue.append(msg)
|
||||||
|
|
||||||
|
async def command_listener(self):
|
||||||
|
s = self.ctx.socket(zmq.SUB)
|
||||||
|
s.connect(self.event_address)
|
||||||
|
queueName = 'hv{}'.format(self.hugvey_id)
|
||||||
|
s.subscribe(queueName)
|
||||||
|
logger.info("Subscribed to commands on {}".format(queueName))
|
||||||
|
while True:
|
||||||
|
hugvey_id, msg = await zmqReceive(s)
|
||||||
|
# topic, msg = await s.recv_multipart()
|
||||||
|
print('received', msg)
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
async def event_sender(port):
|
||||||
|
s = self.ctx.socket(zmq.PUB)
|
||||||
|
s.connect(self.event_send_address)
|
||||||
|
topic = getTopic(self.hugvey_id)
|
||||||
|
s.subscribe(topic)
|
||||||
|
|
||||||
|
logger.info("Subscribed to commands on {}".format(topic))
|
||||||
|
while True:
|
||||||
|
for i in range(len(self.eventQueue)):
|
||||||
|
hugvey_id, msg = await zmqSend(s, self.hugvey_id, self.eventQueue.pop(0))
|
||||||
|
if len(self.eventQueue) == 0:
|
||||||
|
await asyncio.sleep(0.05)
|
||||||
|
|
||||||
|
s.close()
|
||||||
|
|
||||||
|
class Hugvey(object):
|
||||||
|
"""The Hugvey client, to be ran on the Raspberry Pi's
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def loadConfig(self, filename):
|
||||||
|
# filename
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def startCommandListener():
|
||||||
|
return await self.cmd_server.command_listener()
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.voice_server = VoiceServer(4444, 44100)
|
||||||
|
self.cmd_server = CommandHandler(1)
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
logger.info('start')
|
||||||
|
loop.run_until_complete(self.voice_server.start())
|
||||||
|
loop.run_until_complete(self.cmd_server.command_listener())
|
||||||
|
loop.run_until_complete(self.cmd_server.command_listener())
|
||||||
|
logger.info('done')
|
20
hugvey/communication.py
Normal file
20
hugvey/communication.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger("communication")
|
||||||
|
|
||||||
|
def getTopic(hugvey_id):
|
||||||
|
return "hv{}".format(hugvey_id)
|
||||||
|
|
||||||
|
|
||||||
|
def zmqSend(socket, hugvey_id, msg):
|
||||||
|
msgData = json.dumps(msg)
|
||||||
|
topic = getTopic(hugvey_id)
|
||||||
|
logger.info("Send 0mq to {} containing {}".format(topic, msg))
|
||||||
|
return socket.send_multipart([topic.encode(), msgData.encode()])
|
||||||
|
|
||||||
|
async def zmqReceive(socket):
|
||||||
|
topic, msg = await socket.recv_multipart()
|
||||||
|
hugvey_id = topic.decode()[2:]
|
||||||
|
logger.info("Received 0mq messages for Hugvey #{} containing {}".format(hugvey_id, msg.decode()))
|
||||||
|
return hugvey_id, json.loads(msg)
|
47
hugvey_client.py
Normal file
47
hugvey_client.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
from hugvey.client import Hugvey
|
||||||
|
import coloredlogs, logging
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
argParser = argparse.ArgumentParser(description='Start up a Hugvey pillow. Mic stream becomes available on TCP Socket, and starts listening + emitting events')
|
||||||
|
# argParser.add_argument(
|
||||||
|
# '--voice-port',
|
||||||
|
# required=True,
|
||||||
|
# type=int,
|
||||||
|
# help='The port on which to listen for TCP connections (listens on 0.0.0.0) for audio receivers'
|
||||||
|
# )
|
||||||
|
# argParser.add_argument(
|
||||||
|
# '--event-address',
|
||||||
|
# type=str,
|
||||||
|
# default="127.0.0.1",
|
||||||
|
# help='The ip to which to set up the TCP connection for sending events. Can also be an existing unix file socket.'
|
||||||
|
# )
|
||||||
|
# argParser.add_argument(
|
||||||
|
# '--event-port',
|
||||||
|
# type=int,
|
||||||
|
# help='The port on which to set up the TCP connection for sending events. Ignored if --event-address points to a file socket'
|
||||||
|
# )
|
||||||
|
# argParser.add_argument(
|
||||||
|
# '--language-code',
|
||||||
|
# default="en-US",
|
||||||
|
# type=str,
|
||||||
|
# help='Language code for Speech to Text (BCP-47 language tag)'
|
||||||
|
# )
|
||||||
|
argParser.add_argument(
|
||||||
|
'--verbose',
|
||||||
|
'-v',
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = argParser.parse_args()
|
||||||
|
|
||||||
|
coloredlogs.install(
|
||||||
|
level=logging.DEBUG if args.verbose else logging.INFO,
|
||||||
|
)
|
||||||
|
|
||||||
|
# server = VoiceServer(voice_port=4444, input_rate=44100)
|
||||||
|
hv = Hugvey()
|
||||||
|
hv.start()
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pyzmq
|
||||||
|
pyaudio
|
||||||
|
coloredlogs
|
Loading…
Reference in a new issue