Последняя активность 1772128893

Use this script to send vban text requests over a network

Версия b585d6a7d9a6acc6d4ed913cf3612a66ed2b0b7c

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