Utoljára aktív 1772903254

Use this script to send vban text requests over a network

sendtext.py Eredeti
1#!/usr/bin/env python3
2
3# MIT License
4#
5# Copyright (c) 2025 Onyx and Iris
6#
7# Permission is hereby granted, free of charge, to any person obtaining a copy
8# of this software and associated documentation files (the "Software"), to deal
9# in the Software without restriction, including without limitation the rights
10# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11# copies of the Software, and to permit persons to whom the Software is
12# furnished to do so, subject to the following conditions:
13#
14# The above copyright notice and this permission notice shall be included in all
15# copies or substantial portions of the Software.
16#
17# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23# SOFTWARE.
24
25"""
26sendtext.py
27
28This script provides functionality to send text messages over a network using the VBAN protocol.
29It includes classes and functions to construct and send VBAN packets with text data, read configuration
30from a TOML file, and handle command-line arguments for customization.
31
32Classes:
33 RTHeader: A dataclass representing the header of a VBAN packet, with methods to convert it to bytes.
34
35Functions:
36 ratelimit(func): A decorator to enforce a rate limit on a function.
37 send(sock, args, cmd, framecounter): Sends a text message using the provided socket and packet.
38 parse_args(): Parses command-line arguments and returns them as an argparse.Namespace object.
39 main(args): Main function to send text using VbanSendText.
40
41Usage:
42 Run the script with appropriate command-line arguments to send text messages.
43 Example:
44 To send a single message:
45 python sendtext.py --host localhost --port 6980 --streamname Command1 --bps 256000 --channel 1 "strip[0].mute=1;strip[1].mute=1;strip[2].mute=1"
46 To send multiple messages:
47 python sendtext.py --host localhost --port 6980 --streamname Command1 --bps 256000 --channel 1 "strip[0].mute=1;strip[1].mute=1;strip[2].mute=1" "bus[0].mute=1;bus[1].mute=1;bus[2].mute=1"
48 To send commands stored in a text file:
49 bash:
50 xargs ./sendtext.py --host localhost --port 6980 --streamname Command1 --bps 256000 --channel 1 < commands.txt
51 powershell:
52 ./sendtext.py --host localhost --port 6980 --streamname Command1 --bps 256000 --channel 1 (Get-Content commands.txt)
53 Messages to Matrix are also possible:
54 python sendtext.py --host localhost --port 6980 --streamname Command1 --bps 256000 --channel 1 "Point(ASIO128.IN[2],ASIO128.OUT[1]).dBGain = -8"
55"""
56
57import argparse
58import functools
59import logging
60import socket
61import time
62from dataclasses import dataclass, field
63
64logger = logging.getLogger(__name__)
65
66SUBPROTOCOL_TXT = 0x40
67STREAMTYPE_UTF8 = 0x10
68STREAMNAME_MAXLEN = 16
69
70
71@dataclass
72class RTHeader:
73 """RTHeader represents the header of a VBAN packet for sending text messages.
74 It includes fields for the stream name, bits per second, channel, and frame counter,
75 as well as methods to convert these fields into the appropriate byte format for transmission.
76
77 Attributes:
78 name (str): The name of the VBAN stream (max 16 characters).
79 bps (int): The bits per second for the VBAN stream, must be one of the predefined options.
80 channel (int): The channel number for the VBAN stream.
81 framecounter (int): A counter for the frames being sent, default is 0.
82 BPS_OPTS (list[int]): A list of valid bits per second options for VBAN streams.
83
84 Methods:
85 __post_init__(): Validates the stream name length and bits per second value.
86 vban(): Returns the VBAN header as bytes.
87 sr(): Returns the sample rate byte based on the bits per second index.
88 nbs(): Returns the number of bits per sample byte (currently set to 0).
89 nbc(): Returns the number of channels byte based on the channel attribute.
90 bit(): Returns the bit depth byte (currently set to 0x10).
91 streamname(): Returns the stream name as bytes, padded to 16 bytes.
92 to_bytes(name, bps, channel, framecounter): Class method to create a byte representation of the RTHeader with the given parameters.
93
94 Raises:
95 ValueError: If the stream name exceeds 16 characters or if the bits per second value is not in the predefined options.
96 """
97
98 name: str
99 bps: int
100 channel: int
101 framecounter: int = 0
102 # fmt: off
103 BPS_OPTS: list[int] = field(default_factory=lambda: [
104 0, 110, 150, 300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 31250,
105 38400, 57600, 115200, 128000, 230400, 250000, 256000, 460800, 921600,
106 1000000, 1500000, 2000000, 3000000
107 ])
108 # fmt: on
109
110 def __post_init__(self):
111 if len(self.name) > STREAMNAME_MAXLEN:
112 raise ValueError(
113 f"Stream name got: '{self.name}', want: must be {STREAMNAME_MAXLEN} characters or fewer"
114 )
115 try:
116 self.bps_index = self.BPS_OPTS.index(self.bps)
117 except ValueError as e:
118 ERR_MSG = f'Invalid bps: {self.bps}, must be one of {self.BPS_OPTS}'
119 e.add_note(ERR_MSG)
120 raise
121
122 @property
123 def vban(self) -> bytes:
124 return b'VBAN'
125
126 @property
127 def sr(self) -> bytes:
128 return (self.bps_index | SUBPROTOCOL_TXT).to_bytes(1, 'little')
129
130 @property
131 def nbs(self) -> bytes:
132 return (0).to_bytes(1, 'little')
133
134 @property
135 def nbc(self) -> bytes:
136 return (self.channel).to_bytes(1, 'little')
137
138 @property
139 def bit(self) -> bytes:
140 return (STREAMTYPE_UTF8).to_bytes(1, 'little')
141
142 @property
143 def streamname(self) -> bytes:
144 return self.name.encode().ljust(STREAMNAME_MAXLEN, b'\x00')
145
146 @classmethod
147 def to_bytes(cls, name: str, bps: int, channel: int, framecounter: int) -> bytes:
148 header = cls(name=name, bps=bps, channel=channel, framecounter=framecounter)
149
150 data = bytearray()
151 data.extend(header.vban)
152 data.extend(header.sr)
153 data.extend(header.nbs)
154 data.extend(header.nbc)
155 data.extend(header.bit)
156 data.extend(header.streamname)
157 data.extend(header.framecounter.to_bytes(4, 'little'))
158 return bytes(data)
159
160 @classmethod
161 def encode_with_cmd(
162 cls, name: str, bps: int, channel: int, framecounter: int, cmd: str
163 ) -> bytes:
164 header_bytes = cls.to_bytes(name, bps, channel, framecounter)
165 cmd_bytes = cmd.encode('utf-8')
166 return header_bytes + cmd_bytes
167
168
169def ratelimit(func):
170 """
171 Decorator to enforce a rate limit on a function.
172
173 This decorator extracts the rate limit value from the 'args.ratelimit' parameter
174 of the decorated function and ensures that the function is not called more
175 frequently than the specified rate limit.
176
177 Args:
178 func: The function to be rate limited. Must accept 'args' as a parameter
179 with a 'ratelimit' attribute.
180
181 Returns:
182 The decorated function with rate limiting applied.
183 """
184 last_call_time = [0.0] # Use list to make it mutable in closure
185
186 @functools.wraps(func)
187 def wrapper(*args, **kwargs):
188 _, args_namespace, _, _ = args
189 ratelimit = args_namespace.ratelimit
190
191 current_time = time.time()
192 time_since_last_call = current_time - last_call_time[0]
193
194 if time_since_last_call < ratelimit:
195 sleep_time = ratelimit - time_since_last_call
196 logger.debug(f'Rate limiting: sleeping for {sleep_time:.3f} seconds')
197 time.sleep(sleep_time)
198
199 last_call_time[0] = time.time()
200 return func(*args, **kwargs)
201
202 return wrapper
203
204
205@ratelimit
206def send(
207 sock: socket.socket, args: argparse.Namespace, cmd: str, framecounter: int
208) -> None:
209 """
210 Send a text message using the provided socket and packet.
211
212 Args:
213 sock (socket.socket): The socket to use for sending the message.
214 args (argparse.Namespace): The command-line arguments containing the stream name, bits per second, and channel information.
215 cmd (str): The text command to send.
216 framecounter (int): The frame counter to include in the VBAN header.
217
218 Returns:
219 None
220 """
221
222 sock.sendto(
223 RTHeader.encode_with_cmd(
224 name=args.streamname,
225 bps=args.bps,
226 channel=args.channel,
227 framecounter=framecounter,
228 cmd=cmd,
229 ),
230 (args.host, args.port),
231 )
232
233
234def parse_args() -> argparse.Namespace:
235 """
236 Parse command-line arguments.
237 Returns:
238 argparse.Namespace: Parsed command-line arguments.
239 Command-line arguments:
240 --host, -H: VBAN host to send to (default: localhost)
241 --port, -P: VBAN port to send to (default: 6980)
242 --streamname, -s: VBAN stream name (default: Command1)
243 --bps, -b: Bits per second for VBAN stream (default: 256000)
244 --channel, -c: Channel number for VBAN stream (default: 1)
245 text: Text to send (positional argument)
246 """
247
248 parser = argparse.ArgumentParser(description='Voicemeeter VBAN Send Text CLI')
249 parser.add_argument(
250 '--host',
251 '-H',
252 type=str,
253 default='localhost',
254 help='VBAN host to send to (default: localhost)',
255 )
256 parser.add_argument(
257 '--port',
258 '-P',
259 type=int,
260 default=6980,
261 help='VBAN port to send to (default: 6980)',
262 )
263 parser.add_argument(
264 '--streamname',
265 '-s',
266 type=str,
267 default='Command1',
268 help='VBAN stream name (default: Command1)',
269 )
270 parser.add_argument(
271 '--bps',
272 '-b',
273 type=int,
274 default=256000,
275 help='Bits per second for VBAN stream (default: 256000)',
276 )
277 parser.add_argument(
278 '--channel',
279 '-c',
280 type=int,
281 default=1,
282 help='Channel number for VBAN stream (default: 1)',
283 )
284 parser.add_argument(
285 '--ratelimit',
286 '-r',
287 type=float,
288 default=0.02,
289 help='Minimum time in seconds between sending messages (default: 0.02)',
290 )
291 parser.add_argument(
292 '--loglevel',
293 '-l',
294 type=str,
295 default='info',
296 choices=['debug', 'info', 'warning', 'error', 'critical'],
297 help='Set the logging level (default: info)',
298 )
299 parser.add_argument(
300 'cmds', nargs='+', type=str, help='Text to send (positional argument)'
301 )
302 return parser.parse_args()
303
304
305def main(args: argparse.Namespace):
306 """
307 Main function to send text using VBAN.
308 Args:
309 args (argparse.Namespace): The command-line arguments containing
310 host, port, stream name, bits per second, channel, and text commands to send.
311 Behavior:
312 Creates a UDP socket and sends each command in 'args.cmds' to the specified VBAN host and port using the 'send' function, with rate limiting applied.
313 """
314
315 with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
316 for n, cmd in enumerate(args.cmds):
317 send(sock, args, cmd, n)
318
319
320if __name__ == '__main__':
321 args = parse_args()
322
323 logging.basicConfig(level=getattr(logging, args.loglevel.upper()))
324
325 main(args)
326