最終更新 1772128893

Use this script to send vban text requests over a network

sendtext.py Raw
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
66
67@dataclass
68class RTHeader:
69 """RTHeader represents the header of a VBAN packet for sending text messages.
70 It includes fields for the stream name, bits per second, channel, and frame counter,
71 as well as methods to convert these fields into the appropriate byte format for transmission.
72
73 Attributes:
74 name (str): The name of the VBAN stream (max 16 characters).
75 bps (int): The bits per second for the VBAN stream, must be one of the predefined options.
76 channel (int): The channel number for the VBAN stream.
77 framecounter (int): A counter for the frames being sent, default is 0.
78 BPS_OPTS (list[int]): A list of valid bits per second options for VBAN streams.
79
80 Methods:
81 __post_init__(): Validates the stream name length and bits per second value.
82 vban(): Returns the VBAN header as bytes.
83 sr(): Returns the sample rate byte based on the bits per second index.
84 nbs(): Returns the number of bits per sample byte (currently set to 0).
85 nbc(): Returns the number of channels byte based on the channel attribute.
86 bit(): Returns the bit depth byte (currently set to 0x10).
87 streamname(): Returns the stream name as bytes, padded to 16 bytes.
88 to_bytes(name, bps, channel, framecounter): Class method to create a byte representation of the RTHeader with the given parameters.
89
90 Raises:
91 ValueError: If the stream name exceeds 16 characters or if the bits per second value is not in the predefined options.
92 """
93
94 name: str
95 bps: int
96 channel: int
97 framecounter: int = 0
98 # fmt: off
99 BPS_OPTS: list[int] = field(default_factory=lambda: [
100 0, 110, 150, 300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 31250,
101 38400, 57600, 115200, 128000, 230400, 250000, 256000, 460800, 921600,
102 1000000, 1500000, 2000000, 3000000
103 ])
104 # fmt: on
105
106 def __post_init__(self):
107 if len(self.name) > 16:
108 raise ValueError(
109 f"Stream name got: '{self.name}', want: must be 16 characters or fewer"
110 )
111 try:
112 self.bps_index = self.BPS_OPTS.index(self.bps)
113 except ValueError as e:
114 ERR_MSG = f'Invalid bps: {self.bps}, must be one of {self.BPS_OPTS}'
115 e.add_note(ERR_MSG)
116 raise
117
118 @property
119 def vban(self) -> bytes:
120 return b'VBAN'
121
122 @property
123 def sr(self) -> bytes:
124 return (0x40 + self.bps_index).to_bytes(1, 'little')
125
126 @property
127 def nbs(self) -> bytes:
128 return (0).to_bytes(1, 'little')
129
130 @property
131 def nbc(self) -> bytes:
132 return (self.channel).to_bytes(1, 'little')
133
134 @property
135 def bit(self) -> bytes:
136 return (0x10).to_bytes(1, 'little')
137
138 @property
139 def streamname(self) -> bytes:
140 return self.name.encode() + bytes(16 - len(self.name))
141
142 @classmethod
143 def to_bytes(cls, name: str, bps: int, channel: int, framecounter: int) -> bytes:
144 header = cls(name=name, bps=bps, channel=channel, framecounter=framecounter)
145
146 data = bytearray()
147 data.extend(header.vban)
148 data.extend(header.sr)
149 data.extend(header.nbs)
150 data.extend(header.nbc)
151 data.extend(header.bit)
152 data.extend(header.streamname)
153 data.extend(header.framecounter.to_bytes(4, 'little'))
154 return bytes(data)
155
156
157def ratelimit(func):
158 """
159 Decorator to enforce a rate limit on a function.
160
161 This decorator extracts the rate limit value from the 'args.ratelimit' parameter
162 of the decorated function and ensures that the function is not called more
163 frequently than the specified rate limit.
164
165 Args:
166 func: The function to be rate limited. Must accept 'args' as a parameter
167 with a 'ratelimit' attribute.
168
169 Returns:
170 The decorated function with rate limiting applied.
171 """
172 last_call_time = [0.0] # Use list to make it mutable in closure
173
174 @functools.wraps(func)
175 def wrapper(*args, **kwargs):
176 _, args_namespace, _, _ = args
177 ratelimit = args_namespace.ratelimit
178
179 current_time = time.time()
180 time_since_last_call = current_time - last_call_time[0]
181
182 if time_since_last_call < ratelimit:
183 sleep_time = ratelimit - time_since_last_call
184 logger.debug(f'Rate limiting: sleeping for {sleep_time:.3f} seconds')
185 time.sleep(sleep_time)
186
187 last_call_time[0] = time.time()
188 return func(*args, **kwargs)
189
190 return wrapper
191
192
193@ratelimit
194def send(
195 sock: socket.socket, args: argparse.Namespace, cmd: str, framecounter: int
196) -> None:
197 """
198 Send a text message using the provided socket and packet.
199
200 Args:
201 sock (socket.socket): The socket to use for sending the message.
202 args (argparse.Namespace): The command-line arguments containing the stream name, bits per second, and channel information.
203 cmd (str): The text command to send.
204 framecounter (int): The frame counter to include in the VBAN header.
205
206 Returns:
207 None
208 """
209
210 raw_packet = RTHeader.to_bytes(
211 name=args.streamname,
212 bps=args.bps,
213 channel=args.channel,
214 framecounter=framecounter,
215 ) + cmd.encode('utf-8')
216 logger.debug('Sending packet: %s', raw_packet)
217 sock.sendto(raw_packet, (args.host, args.port))
218
219
220def parse_args() -> argparse.Namespace:
221 """
222 Parse command-line arguments.
223 Returns:
224 argparse.Namespace: Parsed command-line arguments.
225 Command-line arguments:
226 --host, -H: VBAN host to send to (default: localhost)
227 --port, -P: VBAN port to send to (default: 6980)
228 --streamname, -s: VBAN stream name (default: Command1)
229 --bps, -b: Bits per second for VBAN stream (default: 256000)
230 --channel, -c: Channel number for VBAN stream (default: 1)
231 text: Text to send (positional argument)
232 """
233
234 parser = argparse.ArgumentParser(description='Voicemeeter VBAN Send Text CLI')
235 parser.add_argument(
236 '--host',
237 '-H',
238 type=str,
239 default='localhost',
240 help='VBAN host to send to (default: localhost)',
241 )
242 parser.add_argument(
243 '--port',
244 '-P',
245 type=int,
246 default=6980,
247 help='VBAN port to send to (default: 6980)',
248 )
249 parser.add_argument(
250 '--streamname',
251 '-s',
252 type=str,
253 default='Command1',
254 help='VBAN stream name (default: Command1)',
255 )
256 parser.add_argument(
257 '--bps',
258 '-b',
259 type=int,
260 default=256000,
261 help='Bits per second for VBAN stream (default: 256000)',
262 )
263 parser.add_argument(
264 '--channel',
265 '-c',
266 type=int,
267 default=1,
268 help='Channel number for VBAN stream (default: 1)',
269 )
270 parser.add_argument(
271 '--ratelimit',
272 '-r',
273 type=float,
274 default=0.02,
275 help='Minimum time in seconds between sending messages (default: 0.02)',
276 )
277 parser.add_argument(
278 '--loglevel',
279 '-l',
280 type=str,
281 default='info',
282 choices=['debug', 'info', 'warning', 'error', 'critical'],
283 help='Set the logging level (default: info)',
284 )
285 parser.add_argument(
286 'cmds', nargs='+', type=str, help='Text to send (positional argument)'
287 )
288 return parser.parse_args()
289
290
291def main(args: argparse.Namespace):
292 """
293 Main function to send text using VBAN.
294 Args:
295 args (argparse.Namespace): The command-line arguments containing
296 host, port, stream name, bits per second, channel, and text commands to send.
297 Behavior:
298 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.
299 """
300
301 with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
302 for n, cmd in enumerate(args.cmds):
303 logger.debug(f'Sending: {cmd}')
304 send(sock, args, cmd, n)
305
306
307if __name__ == '__main__':
308 args = parse_args()
309
310 logging.basicConfig(level=getattr(logging, args.loglevel.upper()))
311
312 main(args)
313