From 6a63c753fa1725bc5fa962b0d691136202138394 Mon Sep 17 00:00:00 2001 From: Samuel Date: Mon, 4 Mar 2024 23:34:44 +0100 Subject: [PATCH] Refactored the script to a real library. Related to #9 --- .gitignore | 44 ++++++- README.md | 159 +++++++++++++++-------- main.py | 40 ++++++ poetry.lock | 29 +++++ pyVBAN.py | 170 ------------------------- pyproject.toml | 20 +++ pyvban/__init__.py | 8 ++ pyvban/const.py | 20 +++ pyvban/packet.py | 43 +++++++ pyvban/subprotocols/__init__.py | 4 + pyvban/subprotocols/audio/__init__.py | 2 + pyvban/subprotocols/audio/const.py | 81 ++++++++++++ pyvban/subprotocols/audio/header.py | 51 ++++++++ pyvban/subprotocols/serial/__init__.py | 2 + pyvban/subprotocols/serial/const.py | 89 +++++++++++++ pyvban/subprotocols/serial/header.py | 62 +++++++++ pyvban/subprotocols/text/__init__.py | 2 + pyvban/subprotocols/text/const.py | 21 +++ pyvban/subprotocols/text/header.py | 53 ++++++++ pyvban/utils/__init__.py | 5 + pyvban/utils/device_list.py | 23 ++++ pyvban/utils/receiver.py | 101 +++++++++++++++ pyvban/utils/send_text.py | 59 +++++++++ pyvban/utils/sender.py | 93 ++++++++++++++ 24 files changed, 954 insertions(+), 227 deletions(-) create mode 100644 main.py create mode 100644 poetry.lock delete mode 100644 pyVBAN.py create mode 100644 pyproject.toml create mode 100644 pyvban/__init__.py create mode 100644 pyvban/const.py create mode 100644 pyvban/packet.py create mode 100644 pyvban/subprotocols/__init__.py create mode 100644 pyvban/subprotocols/audio/__init__.py create mode 100644 pyvban/subprotocols/audio/const.py create mode 100644 pyvban/subprotocols/audio/header.py create mode 100644 pyvban/subprotocols/serial/__init__.py create mode 100644 pyvban/subprotocols/serial/const.py create mode 100644 pyvban/subprotocols/serial/header.py create mode 100644 pyvban/subprotocols/text/__init__.py create mode 100644 pyvban/subprotocols/text/const.py create mode 100644 pyvban/subprotocols/text/header.py create mode 100644 pyvban/utils/__init__.py create mode 100644 pyvban/utils/device_list.py create mode 100644 pyvban/utils/receiver.py create mode 100644 pyvban/utils/send_text.py create mode 100644 pyvban/utils/sender.py diff --git a/.gitignore b/.gitignore index ed18421..01540e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,42 @@ +*.pyc -test2.py -test.py -__pycache__/pyVBAN.cpython-37.pyc +# Packages +*.egg +!/tests/**/*.egg +/*.egg-info +/dist/* +dist +build +_build +.cache +*.so + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.pytest_cache + +.DS_Store +.idea/* +.python-version +.vscode/* + +/test.py +/test_*.* + +/setup.cfg +MANIFEST.in +/setup.py +/docs/site/* +/tests/fixtures/simple_project/setup.py +/tests/fixtures/project_with_extras/setup.py +.mypy_cache + +.venv +/releases/* +pip-wheel-metadata +/poetry.toml + +poetry/core/* \ No newline at end of file diff --git a/README.md b/README.md index d1c960f..88ee017 100644 --- a/README.md +++ b/README.md @@ -1,77 +1,128 @@ # pyVBAN -python implementation of the VBAN (VB Audio network) protocol -Specification here: https://www.vb-audio.com/Voicemeeter/VBANProtocol_Specifications.pdf -The module supports audio receiving and transmitting + text transmitting -## Requierments -Need of the pyaudio module +Python implementation of the VBAN (VB Audio network) protocol -## pyVBAN.VBAN_Recv() usage: -```python -cl = VBAN_Recv("IP-FROM","StreamName",Port,OutputDeviceIndex,verbose=False) -cl.runforever() -``` +Original specifications here: https://www.vb-audio.com/Voicemeeter/VBANProtocol_Specifications.pdf -Example: -```python -cl = VBAN_Recv("127.0.0.1","Stream7",6981,10,verbose=False) -cl.runforever() -``` -or +Supported sub-protocols: + - [x] Audio + - [x] Serial **(To verify)** + - [x] Text + - [ ] Service + +As I'm currently not using VBAN nor VB-Audio products I have no plans to implement this further. +I will do my best to maintain it! +For any feature request, the specs are open, feel free to open a PR 😀 + +## Using the built-in utilities +### List devices +```bash +$ python -m pyvban.utils.device_list +``` ```python -cl = VBAN_Recv("127.0.0.1","Stream7",6981,10,verbose=False) -while True: - cl.runonce() +import logging +import pyvban +logging.basicConfig(level=logging.DEBUG) +pyvban.utils.device_list() ``` -## pyVBAN.VBAN_Send() usage: +### Receiver +```bash +$ python -m pyvban.utils.receiver --address 127.0.0.1 --port 6980 --stream Stream1 --device 11 +``` ```python -cl = VBAN_Send("IP-TO",PORT,"StreamName",SampleRate,DeviceInId,verbose=False) -cl.runforever() +import logging +import pyvban +logging.basicConfig(level=logging.DEBUG) +receiver = pyvban.utils.VBAN_Receiver( + sender_ip="127.0.0.1", + stream_name="Stream1", + port=6980, + device_index=11 +) +receiver.run() ``` -Example: +### Sender +```bash +$ python -m pyvban.utils.sender --address 127.0.0.1 --port 6980 --stream Stream1 --rate 48000 --channels 2 --device 1 +``` ```python -cl = VBAN_Send("127.0.0.1",6980,"Stream8",48000,3,verbose=True) -cl.runforever() +import logging +import pyvban +logging.basicConfig(level=logging.DEBUG) +sender = pyvban.utils.VBAN_Sender( + receiver_ip="127.0.0.1", + receiver_port=6980, + stream_name="Stream1", + sample_rate=48000, + channels=2, + device_index=1 +) +sender.run() ``` -or +### Send text +```bash +$ python -m pyvban.utils.send_text --address 127.0.0.1 --port 6980 --stream Command1 "Strip[5].Mute = 0" +``` ```python -cl = VBAN_Send("127.0.0.1",6980,"Stream8",48000,3,verbose=True) +import logging +import pyvban +import time +logging.basicConfig(level=logging.DEBUG) +send_text = pyvban.utils.VBAN_SendText( + receiver_ip="127.0.0.1", + receiver_port=6980, + stream_name="Command1" +) +state = False while True: - cl.runonce() + state = not state + if state: + send_text.send("Strip[5].Mute = 0") + else: + send_text.send("Strip[5].Mute = 1") + time.sleep(1) ``` -## pyVBAN.VBAN_SendText() usage: -```python -cl = VBAN_SendText("IP-TO",PORT,BAUDRATE,"StreamName") -cl.send("Command") -``` -Example: +## Craft a packet manually + ```python -cl = VBAN_SendText("127.0.0.1",6980,9600,"Command1") -cl.send("Strip[0].Gain = -6") -``` +import pyvban -## Help -To find the output device index you can use this code: +pcm_data = b"" # 128 sample stereo data +header = pyvban.VBANAudioHeader( + sample_rate=pyvban.subprotocols.audio.VBANSampleRates.RATE_48000, + samples_per_frame=128, + channels=2, + format=pyvban.subprotocols.audio.VBANBitResolution.VBAN_BITFMT_16_INT, + codec=pyvban.subprotocols.audio.VBANCodec.VBAN_CODEC_PCM, + stream_name="Stream1", + frame_counter=0, +) -```python -import pyaudio -p = pyaudio.PyAudio() -info = p.get_host_api_info_by_index(0) -numdevices = info.get('deviceCount') - -print("--- INPUT DEVICES ---") -for i in range(0, numdevices): - if (p.get_device_info_by_host_api_device_index(0, i).get('maxInputChannels')) > 0: - print("Input Device id ", i, " - ", p.get_device_info_by_host_api_device_index(0, i).get('name')) - -print("--- OUTPUT DEVICES ---") -for i in range(0, numdevices): - if (p.get_device_info_by_host_api_device_index(0, i).get('maxOutputChannels')) > 0: - print("Input Device id ", i, " - ", p.get_device_info_by_host_api_device_index(0, i).get('name')) +vban_packet = header.to_bytes() + pcm_data +vban_packet = vban_packet[:pyvban.const.VBAN_PROTOCOL_MAX_SIZE] ``` + +## Parse a packet manually +```python +import pyvban + +def run_once(self): + data, addr = socket.recvfrom(pyvban.const.VBAN_PROTOCOL_MAX_SIZE) + packet = pyvban.VBANPacket(data) + if packet.header: + if packet.header.sub_protocol != pyvban.const.VBANProtocols.VBAN_PROTOCOL_AUDIO: + print(f"Received non audio packet {packet}") + return + if packet.header.stream_name != self._stream_name: + print(f"Unexpected stream name \"{packet.header.stream_name}\" != \"{self._stream_name}\"") + return + if addr[0] != self._sender_ip: + print(f"Unexpected sender \"{addr[0]}\" != \"{self._sender_ip}\"") + return +``` \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..90b1b79 --- /dev/null +++ b/main.py @@ -0,0 +1,40 @@ +# import logging +# import time + +# import pyvban + +# logging.basicConfig(level=logging.DEBUG) + +# pyvban.utils.device_list() + +# receiver = pyvban.utils.VBAN_Receiver( +# sender_ip="127.0.0.1", +# stream_name="Stream1", +# port=6980, +# device_index=11 +# ) +# receiver.run() + +# sender = pyvban.utils.VBAN_Sender( +# receiver_ip="127.0.0.1", +# receiver_port=6980, +# stream_name="Stream1", +# sample_rate=48000, +# channels=2, +# device_index=1 +# ) +# sender.run() + +# send_text = pyvban.utils.VBAN_SendText( +# receiver_ip="127.0.0.1", +# receiver_port=6980, +# stream_name="Command1" +# ) +# state = False +# while True: +# state = not state +# if state: +# send_text.send("Strip[5].Mute = 0") +# else: +# send_text.send("Strip[5].Mute = 1") +# time.sleep(1) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..8047d86 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,29 @@ +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "pyaudio" +version = "0.2.14" +description = "Cross-platform audio I/O with PortAudio" +optional = false +python-versions = "*" +files = [ + {file = "PyAudio-0.2.14-cp310-cp310-win32.whl", hash = "sha256:126065b5e82a1c03ba16e7c0404d8f54e17368836e7d2d92427358ad44fefe61"}, + {file = "PyAudio-0.2.14-cp310-cp310-win_amd64.whl", hash = "sha256:2a166fc88d435a2779810dd2678354adc33499e9d4d7f937f28b20cc55893e83"}, + {file = "PyAudio-0.2.14-cp311-cp311-win32.whl", hash = "sha256:506b32a595f8693811682ab4b127602d404df7dfc453b499c91a80d0f7bad289"}, + {file = "PyAudio-0.2.14-cp311-cp311-win_amd64.whl", hash = "sha256:bbeb01d36a2f472ae5ee5e1451cacc42112986abe622f735bb870a5db77cf903"}, + {file = "PyAudio-0.2.14-cp312-cp312-win32.whl", hash = "sha256:5fce4bcdd2e0e8c063d835dbe2860dac46437506af509353c7f8114d4bacbd5b"}, + {file = "PyAudio-0.2.14-cp312-cp312-win_amd64.whl", hash = "sha256:12f2f1ba04e06ff95d80700a78967897a489c05e093e3bffa05a84ed9c0a7fa3"}, + {file = "PyAudio-0.2.14-cp38-cp38-win32.whl", hash = "sha256:858caf35b05c26d8fc62f1efa2e8f53d5fa1a01164842bd622f70ddc41f55000"}, + {file = "PyAudio-0.2.14-cp38-cp38-win_amd64.whl", hash = "sha256:2dac0d6d675fe7e181ba88f2de88d321059b69abd52e3f4934a8878e03a7a074"}, + {file = "PyAudio-0.2.14-cp39-cp39-win32.whl", hash = "sha256:f745109634a7c19fa4d6b8b7d6967c3123d988c9ade0cd35d4295ee1acdb53e9"}, + {file = "PyAudio-0.2.14-cp39-cp39-win_amd64.whl", hash = "sha256:009f357ee5aa6bc8eb19d69921cd30e98c42cddd34210615d592a71d09c4bd57"}, + {file = "PyAudio-0.2.14.tar.gz", hash = "sha256:78dfff3879b4994d1f4fc6485646a57755c6ee3c19647a491f790a0895bd2f87"}, +] + +[package.extras] +test = ["numpy"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "e94c207bf1b967956be761127ae07e80f1cfdd692a5ee6d4734523b734c2e54a" diff --git a/pyVBAN.py b/pyVBAN.py deleted file mode 100644 index 33f623c..0000000 --- a/pyVBAN.py +++ /dev/null @@ -1,170 +0,0 @@ -import socket -import struct -import pyaudio - - -class VBAN_Recv(object): - """docstring for VBAN_Recv""" - def __init__(self, senderIp, streamName, port, outDeviceIndex ,verbose=False): - super(VBAN_Recv, self).__init__() - self.streamName = streamName - self.senderIp = senderIp - self.const_VBAN_SRList = [6000, 12000, 24000, 48000, 96000, 192000, 384000, 8000, 16000, 32000, 64000, 128000, 256000, 512000,11025, 22050, 44100, 88200, 176400, 352800, 705600] - self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP - self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.sock.bind(("0.0.0.0", port)) - self.sampRate = 48000 - self.channels = 2 - self.outDeviceIndex=outDeviceIndex - self.stream_magicString = "" - self.stream_sampRate = 0 - self.stream_sampNum = 0 - self.stream_chanNum = 0 - self.stream_dataFormat = 0 - self.stream_streamName = "" - self.stream_frameCounter = 0 - self.p = pyaudio.PyAudio() - self.stream = self.p.open(format = self.p.get_format_from_width(2), channels = self.channels, rate = self.sampRate, output = True, output_device_index=self.outDeviceIndex) - self.rawPcm = None - self.running = True - self.verbose = verbose - self.rawData = None - self.subprotocol = 0 - print("pyVBAN-Recv Started") - print("Hint: Remeber that pyVBAN only support's PCM 16bits") - - def _correctPyAudioStream(self): - self.channels = self.stream_chanNum - self.sampRate = self.stream_sampRate - self.stream.close() - self.stream = self.p.open(format = self.p.get_format_from_width(2), channels = self.channels, rate = self.sampRate, output = True, output_device_index=self.outDeviceIndex) - - def _cutAtNullByte(self,stri): - return stri.decode('utf-8').split("\x00")[0] - - def _parseHeader(self,data): - self.stream_magicString = data[0:4].decode('utf-8') - sampRateIndex = data[4] & 0x1F - self.subprotocol = (data[4] & 0xE0) >> 5 - self.stream_sampRate = self.const_VBAN_SRList[sampRateIndex] - self.stream_sampNum = data[5] + 1 - self.stream_chanNum = data[6] + 1 - self.stream_dataFormat = data[7] - self.stream_streamName = self._cutAtNullByte(b''.join(struct.unpack("cccccccccccccccc",data[8:24]))) - self.stream_frameCounter = struct.unpack("" +] +readme = "README.md" + +packages = [ + {include = "pyvban"} +] + +[tool.poetry.dependencies] +python = "^3.10" +pyaudio = "^0.2.14" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/pyvban/__init__.py b/pyvban/__init__.py new file mode 100644 index 0000000..de6ae95 --- /dev/null +++ b/pyvban/__init__.py @@ -0,0 +1,8 @@ + +from .packet import VBANPacket +from .subprotocols.audio import VBANAudioHeader +from .subprotocols.serial import VBANSerialHeader +from .subprotocols.text import VBANTextHeader + +from . import utils +from . import const diff --git a/pyvban/const.py b/pyvban/const.py new file mode 100644 index 0000000..6f9c796 --- /dev/null +++ b/pyvban/const.py @@ -0,0 +1,20 @@ +import enum + +VBAN_HEADER_SIZE = (4 + 4 + 16 + 4) +VBAN_HEADER_FOURC = 'NABV' +VBAN_STREAM_NAME_SIZE = 16 +VBAN_PROTOCOL_MAX_SIZE = 1464 +VBAN_DATA_MAX_SIZE = (VBAN_PROTOCOL_MAX_SIZE - VBAN_HEADER_SIZE) +VBAN_CHANNELS_MAX_NB = 256 +VBAN_SAMPLES_MAX_NB = 256 + +VBAN_PROTOCOL_MASK = 0xE0 +class VBANProtocols(enum.Enum): + VBAN_PROTOCOL_AUDIO = 0x00 + VBAN_PROTOCOL_SERIAL = 0x20 + VBAN_PROTOCOL_TXT = 0x40 + VBAN_PROTOCOL_SERVICE = 0x60 + VBAN_PROTOCOL_UNDEFINED_1 = 0x80 + VBAN_PROTOCOL_UNDEFINED_2 = 0xA0 + VBAN_PROTOCOL_UNDEFINED_3 = 0xC0 + VBAN_PROTOCOL_UNDEFINED_4 = 0xE0 diff --git a/pyvban/packet.py b/pyvban/packet.py new file mode 100644 index 0000000..3d18841 --- /dev/null +++ b/pyvban/packet.py @@ -0,0 +1,43 @@ +import typing +from .const import * +from .subprotocols.audio import VBANAudioHeader +from .subprotocols.serial import VBANSerialHeader +from .subprotocols.text import VBANTextHeader + + +def parse_header(data: bytes) -> typing.Union[VBANAudioHeader, VBANSerialHeader, VBANTextHeader]: + if len(data) != VBAN_HEADER_SIZE: + raise Exception(f"Invalid header size provided expected {VBAN_HEADER_SIZE} got {len(data)}") + + if data[:4] != b"VBAN": + raise Exception(f"Invalid fourcc in header expected b\"VBAN\" got {data[:4]}") + + sub_protocol = VBANProtocols(data[4] & VBAN_PROTOCOL_MASK) + + if sub_protocol == VBANProtocols.VBAN_PROTOCOL_AUDIO: + return VBANAudioHeader.from_bytes(data) + if sub_protocol == VBANProtocols.VBAN_PROTOCOL_SERIAL: + return VBANSerialHeader.from_bytes(data) + if sub_protocol == VBANProtocols.VBAN_PROTOCOL_TXT: + return VBANTextHeader.from_bytes(data) + # TODO: VBANProtocols.VBAN_PROTOCOL_SERVICE + + +class VBANPacket: + def __init__(self, data: bytes): + self._header = data[:VBAN_HEADER_SIZE] + self._data = data[VBAN_HEADER_SIZE:] + + self._header_o = parse_header(self._header) + + def __repr__(self): + return f"VBANPacket(proto={self._header_o.sub_protocol})" + + @property + def header(self): + return self._header_o + + @property + def data(self): + return self._data + diff --git a/pyvban/subprotocols/__init__.py b/pyvban/subprotocols/__init__.py new file mode 100644 index 0000000..8c4b739 --- /dev/null +++ b/pyvban/subprotocols/__init__.py @@ -0,0 +1,4 @@ + +from . import audio +from . import serial +from . import text diff --git a/pyvban/subprotocols/audio/__init__.py b/pyvban/subprotocols/audio/__init__.py new file mode 100644 index 0000000..3159f90 --- /dev/null +++ b/pyvban/subprotocols/audio/__init__.py @@ -0,0 +1,2 @@ +from .header import VBANAudioHeader +from .const import * \ No newline at end of file diff --git a/pyvban/subprotocols/audio/const.py b/pyvban/subprotocols/audio/const.py new file mode 100644 index 0000000..8aad341 --- /dev/null +++ b/pyvban/subprotocols/audio/const.py @@ -0,0 +1,81 @@ +import enum + +VBAN_SR_MASK = 0x1F +class VBANSampleRates(enum.Enum): + RATE_6000 = 0 + RATE_8000 = 7 + RATE_11025 = 14 + RATE_12000 = 1 + RATE_16000 = 8 + RATE_22050 = 15 + RATE_24000 = 2 + RATE_32000 = 9 + RATE_44100 = 16 + RATE_48000 = 3 + RATE_64000 = 10 + RATE_88200 = 17 + RATE_96000 = 4 + RATE_128000 = 11 + RATE_176400 = 18 + RATE_192000 = 5 + RATE_256000 = 12 + RATE_352800 = 19 + RATE_384000 = 6 + RATE_512000 = 13 + RATE_705600 = 20 +VBANSampleRatesEnum2SR = { + VBANSampleRates.RATE_6000: 6000, + VBANSampleRates.RATE_8000: 8000, + VBANSampleRates.RATE_11025: 11025, + VBANSampleRates.RATE_12000: 12000, + VBANSampleRates.RATE_16000: 16000, + VBANSampleRates.RATE_22050: 22050, + VBANSampleRates.RATE_24000: 24000, + VBANSampleRates.RATE_32000: 32000, + VBANSampleRates.RATE_44100: 44100, + VBANSampleRates.RATE_48000: 48000, + VBANSampleRates.RATE_64000: 64000, + VBANSampleRates.RATE_88200: 88200, + VBANSampleRates.RATE_96000: 96000, + VBANSampleRates.RATE_128000: 128000, + VBANSampleRates.RATE_176400: 176400, + VBANSampleRates.RATE_192000: 192000, + VBANSampleRates.RATE_256000: 256000, + VBANSampleRates.RATE_352800: 352800, + VBANSampleRates.RATE_384000: 384000, + VBANSampleRates.RATE_512000: 512000, + VBANSampleRates.RATE_705600: 705600 +} +VBANSampleRatesSR2Enum = {v: k for k, v in VBANSampleRatesEnum2SR.items()} + +VBAN_BIT_RESOLUTION_MASK = 0x07 +class VBANBitResolution(enum.Enum): + VBAN_BITFMT_8_INT = 0 + VBAN_BITFMT_16_INT = 1 + VBAN_BITFMT_24_INT = 2 + VBAN_BITFMT_32_INT = 3 + VBAN_BITFMT_32_FLOAT = 4 + VBAN_BITFMT_64_FLOAT = 5 + VBAN_BITFMT_12_INT = 6 + VBAN_BITFMT_10_INT = 7 + VBAN_BIT_RESOLUTION_MAX = 8 + +VBAN_CODEC_MASK = 0xF0 +class VBANCodec(enum.Enum): + VBAN_CODEC_PCM = 0x00 + VBAN_CODEC_VBCA = 0x10 + VBAN_CODEC_VBCV = 0x20 + VBAN_CODEC_UNDEFINED_3 = 0x30 + VBAN_CODEC_UNDEFINED_4 = 0x40 + VBAN_CODEC_UNDEFINED_5 = 0x50 + VBAN_CODEC_UNDEFINED_6 = 0x60 + VBAN_CODEC_UNDEFINED_7 = 0x70 + VBAN_CODEC_UNDEFINED_8 = 0x80 + VBAN_CODEC_UNDEFINED_9 = 0x90 + VBAN_CODEC_UNDEFINED_10 = 0xA0 + VBAN_CODEC_UNDEFINED_11 = 0xB0 + VBAN_CODEC_UNDEFINED_12 = 0xC0 + VBAN_CODEC_UNDEFINED_13 = 0xD0 + VBAN_CODEC_UNDEFINED_14 = 0xE0 + VBAN_CODEC_USER = 0xF0 + diff --git a/pyvban/subprotocols/audio/header.py b/pyvban/subprotocols/audio/header.py new file mode 100644 index 0000000..82ce31e --- /dev/null +++ b/pyvban/subprotocols/audio/header.py @@ -0,0 +1,51 @@ +import struct +from dataclasses import dataclass + +from ...const import * +from .const import * + + +@dataclass +class VBANAudioHeader: + sample_rate: VBANSampleRates + samples_per_frame: int + channels: int + format: VBANBitResolution + codec: VBANCodec + stream_name: str + frame_counter: int + + sub_protocol: VBANProtocols = VBANProtocols.VBAN_PROTOCOL_AUDIO + + @classmethod + def from_bytes(cls, data: bytes): + if len(data) != VBAN_HEADER_SIZE: + raise Exception(f"Invalid header size provided expected {VBAN_HEADER_SIZE} got {len(data)}") + + if data[:4] != b"VBAN": + raise Exception(f"Invalid fourcc in header expected b\"VBAN\" got {data[:4]}") + + proto = VBANProtocols(data[4] & VBAN_PROTOCOL_MASK) + if proto != VBANProtocols.VBAN_PROTOCOL_AUDIO: + raise Exception(f"Invalid sub protocol VBAN_PROTOCOL_AUDIO got {proto}") + + return VBANAudioHeader( + sample_rate=VBANSampleRates(data[4] & VBAN_SR_MASK), + samples_per_frame=data[5] + 1, + channels=data[6] + 1, + format=VBANBitResolution(data[7] & VBAN_BIT_RESOLUTION_MASK), + codec=VBANCodec(data[7] & VBAN_CODEC_MASK), + stream_name=b''.join(struct.unpack("cccccccccccccccc", data[8:24])).decode("utf-8").split('\x00', 1)[0], + frame_counter=struct.unpack("> VBAN_SERIAL_START_BIT_SHIFT), + parity=VBANSerialParityCheck((data[5] & VBAN_SERIAL_PARITY_CHECK_MASK) >> VBAN_SERIAL_PARITY_CHECK_SHIFT), + multipart=data[5] & VBAN_SERIAL_MULTIPART_MASK, + channel=data[6] + 1, + data_format=VBANSerialDataType(data[7] & VBAN_SERIAL_DATA_TYPE_MASK), + serial_type=VBANSerialStreamType(data[7] & VBAN_SERIAL_STREAM_TYPE_MASK), + stream_name=b''.join(struct.unpack("cccccccccccccccc", data[8:24])).decode("utf-8").split('\x00', 1)[0], + frame_counter=struct.unpack(" 0: + logger.info(f"\t{i} - {dev.get('name')}") + logger.info(f"Output:") + for i in range(info.get('deviceCount')): + dev = p.get_device_info_by_host_api_device_index(0, i) + if dev["maxOutputChannels"] > 0: + logger.info(f"\t{i} - {dev.get('name')}") + + +if __name__ == "__main__": + import logging + logging.basicConfig(level=logging.INFO) + device_list() diff --git a/pyvban/utils/receiver.py b/pyvban/utils/receiver.py new file mode 100644 index 0000000..b305350 --- /dev/null +++ b/pyvban/utils/receiver.py @@ -0,0 +1,101 @@ +import logging +import socket + +import pyaudio + +from ..packet import VBANPacket +from ..const import * +from ..subprotocols.audio.const import VBANSampleRatesEnum2SR + + +class VBAN_Receiver: + def __init__(self, sender_ip: str, stream_name: str, port: int, device_index: int): + self._logger = logging.getLogger(f"VBAN_Receiver_{sender_ip}_{port}_{stream_name}") + self._logger.info("Hellow world") + + self._sender_ip = sender_ip + self._stream_name = stream_name + self._device_index = device_index + + self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._socket.bind(("0.0.0.0", port)) + + self._p = pyaudio.PyAudio() + self._current_pyaudio_config = { + "channels": 2, + "rate": 48000 + } + self._stream = self._p.open( + format=self._p.get_format_from_width(2), + channels=self._current_pyaudio_config["channels"], + rate=self._current_pyaudio_config["rate"], + output=True, + output_device_index=self._device_index + ) + + self._running = True + + def _check_pyaudio(self, header): + if VBANSampleRatesEnum2SR[header.sample_rate] != self._current_pyaudio_config["rate"] or header.channels != self._current_pyaudio_config["channels"]: + self._logger.info("Re-Configuring PyAudio") + self._current_pyaudio_config["rate"] = VBANSampleRatesEnum2SR[header.sample_rate] + self._current_pyaudio_config["channels"] = header.channels + self._stream.close() + self._stream = self._p.open( + format=self._p.get_format_from_width(2), + channels=self._current_pyaudio_config["channels"], + rate=self._current_pyaudio_config["rate"], + output=True, + output_device_index=self._device_index + ) + + def run_once(self): + try: + data, addr = self._socket.recvfrom(VBAN_PROTOCOL_MAX_SIZE) + packet = VBANPacket(data) + if packet.header: + if packet.header.sub_protocol != VBANProtocols.VBAN_PROTOCOL_AUDIO: + self._logger.debug(f"Received non audio packet {packet}") + return + if packet.header.stream_name != self._stream_name: + self._logger.debug(f"Unexpected stream name \"{packet.header.stream_name}\" != \"{self._stream_name}\"") + return + if addr[0] != self._sender_ip: + self._logger.debug(f"Unexpected sender \"{addr[0]}\" != \"{self._sender_ip}\"") + return + + self._check_pyaudio(packet.header) + + self._stream.write(packet.data) + except Exception as e: + self._logger.error(f"An exception occurred: {e}") + + def run(self): + self._running = True + while self._running: + self.run_once() + self.stop() + + def stop(self): + self._running = False + self._stream.close() + self._stream = None + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser(prog='VBAN_Receiver', description='Python based VBAN receiver') + parser.add_argument('-a', '--address', required=True, type=str) + parser.add_argument('-p', '--port', default=6980, type=int) + parser.add_argument('-s', '--stream', default="Stream1", type=str) + parser.add_argument('-d', '--device', default=-1, required=True, type=int) + args = parser.parse_args() + + logging.basicConfig(level=logging.DEBUG) + sender = VBAN_Receiver( + sender_ip=args.address, + stream_name=args.stream, + port=args.port, + device_index=args.device + ) + sender.run() diff --git a/pyvban/utils/send_text.py b/pyvban/utils/send_text.py new file mode 100644 index 0000000..1a9450d --- /dev/null +++ b/pyvban/utils/send_text.py @@ -0,0 +1,59 @@ +import logging +import socket + +from .. import VBANTextHeader +from ..const import VBAN_PROTOCOL_MAX_SIZE +from ..subprotocols.serial.const import VBANBaudRate, VBANSerialDataType +from ..subprotocols.text.const import VBANTextStreamType + + +class VBAN_SendText: + def __init__(self, receiver_ip: str, receiver_port: int, stream_name: str): + self._logger = logging.getLogger(f"VBAN_Sender_{receiver_ip}:{receiver_port}_{stream_name}") + self._logger.info("Hellow world") + + self._receiver = (receiver_ip, receiver_port) + self._stream_name = stream_name + + self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._socket.connect(self._receiver) + + self._frame_counter = 0 + + self._running = True + + def send(self, text: str): + try: + self._frame_counter += 1 + header = VBANTextHeader( + baud_rate=VBANBaudRate.RATE_115200, + channel=1, + data_format=VBANSerialDataType.VBAN_SERIAL_DATA_TYPE_8BIT, + text_type=VBANTextStreamType.VBAN_TEXT_STREAM_TYPE_ASCII, + stream_name=self._stream_name, + frame_counter=self._frame_counter, + ) + + data = header.to_bytes() + text.encode("utf-8") + data = data[:VBAN_PROTOCOL_MAX_SIZE] + self._socket.sendto(data, self._receiver) + except Exception as e: + self._logger.error(f"An exception occurred: {e}") + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser(prog='VBAN_SendText', description='Python based VBAN text sender') + parser.add_argument('-a', '--address', required=True, type=str) + parser.add_argument('-p', '--port', default=6980, type=int) + parser.add_argument('-s', '--stream', default="Command1", type=str) + parser.add_argument('text', type=str) + args = parser.parse_args() + + logging.basicConfig(level=logging.DEBUG) + sender = VBAN_SendText( + receiver_ip=args.address, + receiver_port=args.port, + stream_name=args.stream, + ) + sender.send(args.text) diff --git a/pyvban/utils/sender.py b/pyvban/utils/sender.py new file mode 100644 index 0000000..b2cda06 --- /dev/null +++ b/pyvban/utils/sender.py @@ -0,0 +1,93 @@ +import logging +import socket + +import pyaudio + +from .. import VBANAudioHeader +from ..const import * +from ..subprotocols.audio.const import VBANSampleRatesSR2Enum, VBANBitResolution, VBANCodec + + +class VBAN_Sender: + def __init__(self, receiver_ip: str, receiver_port: int, stream_name: str, sample_rate: int, channels: int, device_index: int): + self._logger = logging.getLogger(f"VBAN_Sender_{receiver_ip}:{receiver_port}_{stream_name}") + self._logger.info("Hellow world") + + self._receiver = (receiver_ip, receiver_port) + self._stream_name = stream_name + self._sample_rate = sample_rate + self._vban_sample_rate = VBANSampleRatesSR2Enum[sample_rate] + self._channels = channels + self._device_index = device_index + + self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._socket.connect(self._receiver) + + self._samples_per_frame = 128 + + self._p = pyaudio.PyAudio() + self._stream = self._p.open( + format=self._p.get_format_from_width(2), + channels=self._channels, + rate=self._sample_rate, + input=True, + input_device_index=self._device_index + ) + + self._frame_counter = 0 + + self._running = True + + def run_once(self): + try: + self._frame_counter += 1 + header = VBANAudioHeader( + sample_rate=self._vban_sample_rate, + samples_per_frame=self._samples_per_frame, + channels=self._channels, + format=VBANBitResolution.VBAN_BITFMT_16_INT, + codec=VBANCodec.VBAN_CODEC_PCM, + stream_name=self._stream_name, + frame_counter=self._frame_counter, + ) + + data = header.to_bytes() + self._stream.read(self._samples_per_frame) + data = data[:VBAN_PROTOCOL_MAX_SIZE] + + self._socket.sendto(data, self._receiver) + except Exception as e: + self._logger.error(f"An exception occurred: {e}") + + def run(self): + self._running = True + while self._running: + self.run_once() + self.stop() + + def stop(self): + self._running = False + self._stream.close() + self._stream = None + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser(prog='VBAN_Sender', description='Python based VBAN streamer') + parser.add_argument('-a', '--address', required=True, type=str) + parser.add_argument('-p', '--port', default=6980, type=int) + parser.add_argument('-s', '--stream', default="Stream1", type=str) + parser.add_argument('-r', '--rate', default=48000, type=int) + parser.add_argument('-c', '--channels', default=2, type=int) + parser.add_argument('-d', '--device', default=-1, required=True, type=int) + args = parser.parse_args() + + logging.basicConfig(level=logging.DEBUG) + sender = VBAN_Sender( + receiver_ip=args.address, + receiver_port=args.port, + stream_name=args.stream, + sample_rate=args.rate, + channels=args.channels, + device_index=args.device + ) + sender.run()