From e2e4c072f851458a0d2328924ae1e2e6d36130b8 Mon Sep 17 00:00:00 2001 From: Ruben van de Ven Date: Tue, 15 Jan 2019 21:40:44 +0100 Subject: [PATCH] Initial WIP version --- hugvey/__init__.py | 0 hugvey/__pycache__/__init__.cpython-36.pyc | Bin 0 -> 152 bytes hugvey/__pycache__/client.cpython-36.pyc | Bin 0 -> 6047 bytes .../__pycache__/communication.cpython-36.pyc | Bin 0 -> 930 bytes hugvey/client.py | 178 ++++++++++++++++++ hugvey/communication.py | 20 ++ hugvey_client.py | 47 +++++ requirements.txt | 3 + 8 files changed, 248 insertions(+) create mode 100644 hugvey/__init__.py create mode 100644 hugvey/__pycache__/__init__.cpython-36.pyc create mode 100644 hugvey/__pycache__/client.cpython-36.pyc create mode 100644 hugvey/__pycache__/communication.cpython-36.pyc create mode 100644 hugvey/client.py create mode 100644 hugvey/communication.py create mode 100644 hugvey_client.py create mode 100644 requirements.txt diff --git a/hugvey/__init__.py b/hugvey/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hugvey/__pycache__/__init__.cpython-36.pyc b/hugvey/__pycache__/__init__.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b34ad433aabc382517139dc84efad5f08e782679 GIT binary patch literal 152 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 A-v9sr literal 0 HcmV?d00001 diff --git a/hugvey/__pycache__/communication.cpython-36.pyc b/hugvey/__pycache__/communication.cpython-36.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c37ea72557ca0f5cfe7928cbcb25ad9b0d89caf8 GIT binary patch literal 930 zcmZXT&2H2%5XbGr`PfCflpZP}afrkz7gCCZ5JDAFD^w6#A7(y+`?Kiv7<~@RZ|yPY6@9r6r(WE z<1t5IdI-X4Nm{bTrOsr@E*Ceem|Firswy=w8(cQ|LR_C0{3}r&Ecp2*xs;ViRP#~P z$+66uO4LRt@02V>W*~O3|M*#Q(k)B+)xX_eIX#+R+b|dAgPa$ckNIF?gsK2Eb8!v> zk7Qt7!p~u1NJ(4ZuyT#lQt-^UC4(}&mO&ieP$;PMTUG{Z(z>WQMEe3CoQj%9`_)xs zWOTENGFh9nsEay>r#Q5%)Uvi7Z>qWO60!bF=DARo6*WM?wCq^JCg)W%GsQer#`>bp zBp0z~eJ!(3!YItxx|PnS0myM`QtKJ4vqRVmmTZG%b%iNmD|84#=#YkVm#TX(|I;?y z5A7S6egy&*k~Mh^x8sDIIV-vX7A0LnJ30B{+$TtbpvmxUe57+>Y3kQg} zlUszoJa{iMQ7lA{rV?7GxzG_59QBQi9{vX_uwx~%#Wn{nkb}Zv)EyMWrLZjP&1A}T z%+we|J1BQiu&RHd3<1g}A^dQ|WtZ;IJ*xH~5(oXJ0rm!ZX9sYP-gbTJ563Ii5$rzS e+8?iz>d1#LD#@FfIK*F~`xQbOIzuO9q5liUOxw!< literal 0 HcmV?d00001 diff --git a/hugvey/client.py b/hugvey/client.py new file mode 100644 index 0000000..4cc907a --- /dev/null +++ b/hugvey/client.py @@ -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') diff --git a/hugvey/communication.py b/hugvey/communication.py new file mode 100644 index 0000000..d9a3eed --- /dev/null +++ b/hugvey/communication.py @@ -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) diff --git a/hugvey_client.py b/hugvey_client.py new file mode 100644 index 0000000..4a3ca89 --- /dev/null +++ b/hugvey_client.py @@ -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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d4002e1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pyzmq +pyaudio +coloredlogs