vban-cli 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
vban_cli-0.3.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Onyx and Iris
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: vban-cli
3
+ Version: 0.3.0
4
+ Summary: A command-line interface for Voicemeeter leveraging VBAN.
5
+ License: LICENSE
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Programming Language :: Python
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: Implementation :: CPython
13
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
14
+ Requires-Python: >=3.13
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: cyclopts>=4.6.0
18
+ Requires-Dist: vban-cmd>=2.6.0
19
+ Dynamic: license-file
20
+
21
+ # vban-cli
22
+
23
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
24
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
25
+
26
+ ---
27
+
28
+ ## Install
29
+
30
+ #### With uv
31
+
32
+ ```console
33
+ uv tool install vban-cli
34
+ ```
35
+
36
+ #### With pipx
37
+
38
+ ```console
39
+ pipx install vban-cli
40
+ ```
41
+
42
+ The CLI should now be discoverable as `vban-cli`
43
+
44
+ ---
45
+
46
+ ## Configuration
47
+
48
+ ### Flags
49
+
50
+ ```console
51
+ vban-cli --host=localhost --port=6980 --streamname=Command1
52
+ ```
53
+
54
+ ### Environment Variables
55
+
56
+ example .envrc:
57
+
58
+ ```env
59
+ #!/usr/bin/env bash
60
+
61
+ export VBAN_CLI_HOST="localhost"
62
+ export VBAN_CLI_PORT=6980
63
+ export VBAN_CLI_STREAMNAME=Command1
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Use
69
+
70
+ ```console
71
+ Usage: vban-cli COMMAND
72
+
73
+ ╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────╮
74
+ │ bus Control the bus parameters. │
75
+ │ strip Control the strip parameters. │
76
+ │ --help (-h) Display this message and exit. │
77
+ │ --version Display application version. │
78
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
79
+ ╭─ Parameters ─────────────────────────────────────────────────────────────────────────────────────╮
80
+ │ --kind Kind of Voicemeeter [env var: VBAN_CLI_KIND] [default: potato] │
81
+ │ --host VBAN host [env var: VBAN_CLI_HOST] [default: localhost] │
82
+ │ --port VBAN port [env var: VBAN_CLI_PORT] [default: 6980] │
83
+ │ --streamname VBAN stream name [env var: VBAN_CLI_STREAMNAME] [default: Command1] │
84
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
85
+ ```
86
+
87
+ For every command and subcommand there exists a `--help` flag for further usage information.
88
+
89
+ ---
90
+
91
+ ## Implementation Notes
92
+
93
+ 1. The VBAN TEXT subprotocol defines two packet structures [ident:0][ident-0] and [ident:1][ident-1]. Neither of them contain the data for Bus EQ parameters.
94
+ 2. Packet structure with [ident:1][ident-1] is emitted by the VBAN server only on pdirty events. This means we do not receive the initial state of those parameters on initial subscription. Therefore any commands which are intended to fetch the value of parameters defined in packet [ident:1][ident-1] will not work in this CLI.
95
+ 3. Packet structure with [ident:1][ident-1] defines parameteric EQ data only for the [first channel][ident-1-peq].
96
+
97
+
98
+ ---
99
+
100
+ ## Further Notes
101
+
102
+ I've made the effort to set up the basic skeletal structure of the CLI as well as demonstrate how to combine subcommand groups with subcommand groups so more can be implemented, it just needs doing. There may be restrictions on some things however, for example, retrieving values is only possible for parameters [defined in the protocol](https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L787). Setting parameters can be done for anything possible by a string request.
103
+
104
+ If there's something missing that you would like to see added the best bet is to submit a PR. You may raise an issue and if it's quick and simple to do I may (or may not) do it.
105
+
106
+ ---
107
+
108
+ ## License
109
+
110
+ `vban-cli` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
111
+
112
+
113
+
114
+ [ident-0]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L896
115
+ [ident-1]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L982
116
+ [ident-1-peq]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L995
@@ -0,0 +1,96 @@
1
+ # vban-cli
2
+
3
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
4
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
5
+
6
+ ---
7
+
8
+ ## Install
9
+
10
+ #### With uv
11
+
12
+ ```console
13
+ uv tool install vban-cli
14
+ ```
15
+
16
+ #### With pipx
17
+
18
+ ```console
19
+ pipx install vban-cli
20
+ ```
21
+
22
+ The CLI should now be discoverable as `vban-cli`
23
+
24
+ ---
25
+
26
+ ## Configuration
27
+
28
+ ### Flags
29
+
30
+ ```console
31
+ vban-cli --host=localhost --port=6980 --streamname=Command1
32
+ ```
33
+
34
+ ### Environment Variables
35
+
36
+ example .envrc:
37
+
38
+ ```env
39
+ #!/usr/bin/env bash
40
+
41
+ export VBAN_CLI_HOST="localhost"
42
+ export VBAN_CLI_PORT=6980
43
+ export VBAN_CLI_STREAMNAME=Command1
44
+ ```
45
+
46
+ ---
47
+
48
+ ## Use
49
+
50
+ ```console
51
+ Usage: vban-cli COMMAND
52
+
53
+ ╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────╮
54
+ │ bus Control the bus parameters. │
55
+ │ strip Control the strip parameters. │
56
+ │ --help (-h) Display this message and exit. │
57
+ │ --version Display application version. │
58
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
59
+ ╭─ Parameters ─────────────────────────────────────────────────────────────────────────────────────╮
60
+ │ --kind Kind of Voicemeeter [env var: VBAN_CLI_KIND] [default: potato] │
61
+ │ --host VBAN host [env var: VBAN_CLI_HOST] [default: localhost] │
62
+ │ --port VBAN port [env var: VBAN_CLI_PORT] [default: 6980] │
63
+ │ --streamname VBAN stream name [env var: VBAN_CLI_STREAMNAME] [default: Command1] │
64
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
65
+ ```
66
+
67
+ For every command and subcommand there exists a `--help` flag for further usage information.
68
+
69
+ ---
70
+
71
+ ## Implementation Notes
72
+
73
+ 1. The VBAN TEXT subprotocol defines two packet structures [ident:0][ident-0] and [ident:1][ident-1]. Neither of them contain the data for Bus EQ parameters.
74
+ 2. Packet structure with [ident:1][ident-1] is emitted by the VBAN server only on pdirty events. This means we do not receive the initial state of those parameters on initial subscription. Therefore any commands which are intended to fetch the value of parameters defined in packet [ident:1][ident-1] will not work in this CLI.
75
+ 3. Packet structure with [ident:1][ident-1] defines parameteric EQ data only for the [first channel][ident-1-peq].
76
+
77
+
78
+ ---
79
+
80
+ ## Further Notes
81
+
82
+ I've made the effort to set up the basic skeletal structure of the CLI as well as demonstrate how to combine subcommand groups with subcommand groups so more can be implemented, it just needs doing. There may be restrictions on some things however, for example, retrieving values is only possible for parameters [defined in the protocol](https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L787). Setting parameters can be done for anything possible by a string request.
83
+
84
+ If there's something missing that you would like to see added the best bet is to submit a PR. You may raise an issue and if it's quick and simple to do I may (or may not) do it.
85
+
86
+ ---
87
+
88
+ ## License
89
+
90
+ `vban-cli` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
91
+
92
+
93
+
94
+ [ident-0]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L896
95
+ [ident-1]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L982
96
+ [ident-1-peq]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L995
@@ -0,0 +1,27 @@
1
+ [project]
2
+ name = "vban-cli"
3
+ version = "0.3.0"
4
+ description = "A command-line interface for Voicemeeter leveraging VBAN."
5
+ readme = "README.md"
6
+ license = { text = "LICENSE" }
7
+ requires-python = ">=3.13"
8
+ dependencies = ["cyclopts>=4.6.0", "vban-cmd>=2.6.0"]
9
+ classifiers = [
10
+ "Development Status :: 3 - Alpha",
11
+ "Programming Language :: Python",
12
+ "Programming Language :: Python :: 3.10",
13
+ "Programming Language :: Python :: 3.11",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Programming Language :: Python :: 3.13",
16
+ "Programming Language :: Python :: Implementation :: CPython",
17
+ "Programming Language :: Python :: Implementation :: PyPy",
18
+ ]
19
+
20
+ [project.scripts]
21
+ vban-cli = "vban_cli.app:run"
22
+
23
+ [tool.uv]
24
+ package = true
25
+
26
+ [tool.uv.sources]
27
+ vban-cmd = { path = "../vban-cmd-python" }
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ from importlib.metadata import version
2
+
3
+ __version__ = version('vban-cli')
@@ -0,0 +1,53 @@
1
+ from dataclasses import dataclass
2
+ from typing import Annotated
3
+
4
+ import vban_cmd
5
+ from cyclopts import App, Parameter, config
6
+
7
+ from . import __version__ as version
8
+ from . import bus, console, strip
9
+ from .context import Context
10
+
11
+ app = App(
12
+ config=config.Env(
13
+ 'VBAN_CLI_',
14
+ ), # Environment variable prefix for configuration parameters
15
+ version=version,
16
+ )
17
+ app.command(strip.app.meta, name='strip')
18
+ app.command(bus.app.meta, name='bus')
19
+
20
+
21
+ @Parameter(name='*')
22
+ @dataclass
23
+ class VBANConfig:
24
+ kind: Annotated[str, Parameter(help='Kind of Voicemeeter')] = 'potato'
25
+ host: Annotated[str, Parameter(help='VBAN host')] = 'localhost'
26
+ port: Annotated[int, Parameter(help='VBAN port')] = 6980
27
+ streamname: Annotated[str, Parameter(help='VBAN stream name')] = 'Command1'
28
+
29
+
30
+ @app.meta.default
31
+ def launcher(
32
+ *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
33
+ vban_config: Annotated[VBANConfig, Parameter()] = VBANConfig(),
34
+ ):
35
+ with vban_cmd.api(
36
+ vban_config.kind,
37
+ ip=vban_config.host,
38
+ port=vban_config.port,
39
+ streamname=vban_config.streamname,
40
+ ) as client:
41
+ additional_kwargs = {}
42
+ command, bound, _ = app.parse_args(tokens)
43
+ additional_kwargs['ctx'] = Context(client=client)
44
+
45
+ return command(*bound.args, **bound.kwargs, **additional_kwargs)
46
+
47
+
48
+ def run():
49
+ try:
50
+ app.meta()
51
+ except Exception as e:
52
+ console.err.print(f'Error: {e}')
53
+ return e.code
@@ -0,0 +1,106 @@
1
+ from typing import Annotated, Literal, Optional
2
+
3
+ from cyclopts import App, Argument, Parameter
4
+
5
+ from . import console
6
+ from .context import Context
7
+ from .help import CustomHelpFormatter
8
+
9
+ app = App(name='bus', help_formatter=CustomHelpFormatter())
10
+ # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 1.
11
+ # app.command(eq.app.meta, name='eq')
12
+
13
+
14
+ @app.meta.default
15
+ def launcher(
16
+ index: Annotated[int, Argument()] = None,
17
+ *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
18
+ ctx: Annotated[Context, Parameter(show=False)] = None,
19
+ ):
20
+ """Control the bus parameters."""
21
+ additional_kwargs = {}
22
+ command, bound, _ = app.parse_args(tokens)
23
+ if index is not None:
24
+ additional_kwargs['index'] = index
25
+ if ctx is not None:
26
+ additional_kwargs['ctx'] = ctx
27
+
28
+ return command(*bound.args, **bound.kwargs, **additional_kwargs)
29
+
30
+
31
+ @app.command(name='mono')
32
+ def mono(
33
+ new_value: Annotated[Optional[bool], Argument()] = None,
34
+ *,
35
+ index: Annotated[int, Parameter(show=False)] = None,
36
+ ctx: Annotated[Context, Parameter(show=False)] = None,
37
+ ):
38
+ """Get or set the mono state of the specified bus.
39
+
40
+ Parameters
41
+ ----------
42
+ new_value : bool, optional
43
+ If provided, sets the mono state to this value. If not provided, the current mono state is printed.
44
+ """
45
+ if new_value is None:
46
+ console.out.print(ctx.client.bus[index].mono)
47
+ return
48
+ ctx.client.bus[index].mono = new_value
49
+
50
+
51
+ @app.command(name='mute')
52
+ def mute(
53
+ new_value: Annotated[Optional[bool], Argument()] = None,
54
+ *,
55
+ index: Annotated[int, Parameter(show=False)] = None,
56
+ ctx: Annotated[Context, Parameter(show=False)] = None,
57
+ ):
58
+ """Get or set the mute state of the specified bus.
59
+
60
+ Parameters
61
+ ----------
62
+ new_value : bool, optional
63
+ If provided, sets the mute state to this value. If not provided, the current mute state is printed.
64
+ """
65
+ if new_value is None:
66
+ console.out.print(ctx.client.bus[index].mute)
67
+ return
68
+ ctx.client.bus[index].mute = new_value
69
+
70
+
71
+ @app.command(name='mode')
72
+ def mode(
73
+ type_: Annotated[
74
+ Optional[
75
+ Literal[
76
+ 'normal',
77
+ 'amix',
78
+ 'bmix',
79
+ 'repeat',
80
+ 'composite',
81
+ 'tvmix',
82
+ 'upmix21',
83
+ 'upmix41',
84
+ 'upmix61',
85
+ 'centeronly',
86
+ 'lfeonly',
87
+ 'rearonly',
88
+ ]
89
+ ],
90
+ Argument(),
91
+ ] = None,
92
+ *,
93
+ index: Annotated[int, Parameter(show=False)] = None,
94
+ ctx: Annotated[Context, Parameter(show=False)] = None,
95
+ ):
96
+ """Get or set the bus mode of the specified bus.
97
+
98
+ Parameters
99
+ ----------
100
+ type_ : str, optional
101
+ If provided, sets the bus mode to this value. If not provided, the current bus mode is printed.
102
+ """
103
+ if type_ is None:
104
+ console.out.print(ctx.client.bus[index].mode.get())
105
+ return
106
+ setattr(ctx.client.bus[index].mode, type_, True)
@@ -0,0 +1,67 @@
1
+ from typing import Annotated
2
+
3
+ from cyclopts import App, Argument, Parameter
4
+
5
+ from .context import Context
6
+ from .help import CustomHelpFormatter
7
+
8
+ app = App(name='comp', help_formatter=CustomHelpFormatter())
9
+
10
+
11
+ @app.meta.default
12
+ def launcher(
13
+ *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
14
+ index: Annotated[int, Argument()] = None,
15
+ ctx: Annotated[Context, Parameter(show=False)] = None,
16
+ ):
17
+ """Control the compressor parameters."""
18
+ additional_kwargs = {}
19
+ command, bound, _ = app.parse_args(tokens)
20
+ if index is not None:
21
+ additional_kwargs['index'] = index
22
+ if ctx is not None:
23
+ additional_kwargs['ctx'] = ctx
24
+
25
+ return command(*bound.args, **bound.kwargs, **additional_kwargs)
26
+
27
+
28
+ @app.command(name='knob')
29
+ def knob(
30
+ new_knob: Annotated[float, Argument()] = None,
31
+ *,
32
+ index: Annotated[int, Parameter(show=False)] = None,
33
+ ctx: Annotated[Context, Parameter(show=False)] = None,
34
+ ):
35
+ """Get or set the knob of the specified compressor.
36
+
37
+ Parameters
38
+ ----------
39
+ new_knob : int, optional
40
+ If provided, sets the knob to this value. If not provided, the current knob is printed.
41
+ """
42
+ if new_knob is None:
43
+ # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
44
+ # console.out.print(ctx.client.strip[index].comp.knob)
45
+ return
46
+ ctx.client.strip[index].comp.knob = new_knob
47
+
48
+
49
+ @app.command(name='input-gain')
50
+ def input_gain(
51
+ new_gain: Annotated[float, Argument()] = None,
52
+ *,
53
+ index: Annotated[int, Parameter(show=False)] = None,
54
+ ctx: Annotated[Context, Parameter(show=False)] = None,
55
+ ):
56
+ """Get or set the input gain of the specified compressor.
57
+
58
+ Parameters
59
+ ----------
60
+ new_gain : float, optional
61
+ If provided, sets the input gain to this value. If not provided, the current input gain is printed.
62
+ """
63
+ if new_gain is None:
64
+ # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
65
+ # console.out.print(ctx.client.strip[index].comp.gainin)
66
+ return
67
+ ctx.client.strip[index].comp.gainin = new_gain
@@ -0,0 +1,4 @@
1
+ from rich.console import Console
2
+
3
+ out = Console()
4
+ err = Console(stderr=True)
@@ -0,0 +1,8 @@
1
+ from dataclasses import dataclass
2
+
3
+ from vban_cmd.vbancmd import VbanCmd
4
+
5
+
6
+ @dataclass
7
+ class Context:
8
+ client: VbanCmd
@@ -0,0 +1,46 @@
1
+ from typing import Annotated
2
+
3
+ from cyclopts import App, Argument, Parameter
4
+
5
+ from .context import Context
6
+ from .help import CustomHelpFormatter
7
+
8
+ app = App(name='denoiser', help_formatter=CustomHelpFormatter())
9
+
10
+
11
+ @app.meta.default
12
+ def launcher(
13
+ *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
14
+ index: Annotated[int, Argument()] = None,
15
+ ctx: Annotated[Context, Parameter(show=False)] = None,
16
+ ):
17
+ """Control the denoiser parameters."""
18
+ additional_kwargs = {}
19
+ command, bound, _ = app.parse_args(tokens)
20
+ if index is not None:
21
+ additional_kwargs['index'] = index
22
+ if ctx is not None:
23
+ additional_kwargs['ctx'] = ctx
24
+
25
+ return command(*bound.args, **bound.kwargs, **additional_kwargs)
26
+
27
+
28
+ @app.command(name='knob')
29
+ def knob(
30
+ new_knob: Annotated[float, Argument()] = None,
31
+ *,
32
+ index: Annotated[int, Parameter(show=False)] = None,
33
+ ctx: Annotated[Context, Parameter(show=False)] = None,
34
+ ):
35
+ """Get or set the knob of the specified denoiser.
36
+
37
+ Parameters
38
+ ----------
39
+ new_knob : int, optional
40
+ If provided, sets the knob to this value. If not provided, the current knob is printed.
41
+ """
42
+ if new_knob is None:
43
+ # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
44
+ # console.out.print(ctx.client.strip[index].denoiser.knob)
45
+ return
46
+ ctx.client.strip[index].denoiser.knob = new_knob
@@ -0,0 +1,53 @@
1
+ from typing import Annotated
2
+
3
+ from cyclopts import App, Argument, Parameter
4
+
5
+ from .context import Context
6
+ from .help import CustomHelpFormatter
7
+
8
+ app = App(name='eq', help_formatter=CustomHelpFormatter())
9
+
10
+
11
+ @app.meta.default
12
+ def launcher(
13
+ band: Annotated[int, Argument()] = None,
14
+ *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
15
+ index: Annotated[int, Argument()] = None,
16
+ ctx: Annotated[Context, Parameter(show=False)] = None,
17
+ ):
18
+ """Control the EQ parameters.
19
+
20
+ Only channel 0 is supported, see https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 3.
21
+ """
22
+ additional_kwargs = {}
23
+ command, bound, _ = app.parse_args(tokens)
24
+ if index is not None:
25
+ additional_kwargs['index'] = index
26
+ if band is not None:
27
+ additional_kwargs['band'] = band
28
+ if ctx is not None:
29
+ additional_kwargs['ctx'] = ctx
30
+
31
+ return command(*bound.args, **bound.kwargs, **additional_kwargs)
32
+
33
+
34
+ @app.command(name='on')
35
+ def on(
36
+ new_state: Annotated[bool, Argument()] = None,
37
+ *,
38
+ index: Annotated[int, Parameter(show=False)] = None,
39
+ band: Annotated[int, Parameter(show=False)] = None,
40
+ ctx: Annotated[Context, Parameter(show=False)] = None,
41
+ ):
42
+ """Get or set the on state of the specified EQ band.
43
+
44
+ Parameters
45
+ ----------
46
+ new_state : bool
47
+ If provided, sets the on state to this value. If not provided, the current on state is printed.
48
+ """
49
+ if new_state is None:
50
+ # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
51
+ # console.out.print(ctx.client.strip[index].eq.channel[0].cell[band].on)
52
+ return
53
+ ctx.client.strip[index].eq.channel[0].cell[band].on = new_state
@@ -0,0 +1,67 @@
1
+ from typing import Annotated
2
+
3
+ from cyclopts import App, Argument, Parameter
4
+
5
+ from .context import Context
6
+ from .help import CustomHelpFormatter
7
+
8
+ app = App(name='gate', help_formatter=CustomHelpFormatter())
9
+
10
+
11
+ @app.meta.default
12
+ def launcher(
13
+ *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
14
+ index: Annotated[int, Argument()] = None,
15
+ ctx: Annotated[Context, Parameter(show=False)] = None,
16
+ ):
17
+ """Control the compressor parameters."""
18
+ additional_kwargs = {}
19
+ command, bound, _ = app.parse_args(tokens)
20
+ if index is not None:
21
+ additional_kwargs['index'] = index
22
+ if ctx is not None:
23
+ additional_kwargs['ctx'] = ctx
24
+
25
+ return command(*bound.args, **bound.kwargs, **additional_kwargs)
26
+
27
+
28
+ @app.command(name='knob')
29
+ def knob(
30
+ new_knob: Annotated[float, Argument()] = None,
31
+ *,
32
+ index: Annotated[int, Parameter(show=False)] = None,
33
+ ctx: Annotated[Context, Parameter(show=False)] = None,
34
+ ):
35
+ """Get or set the knob of the specified gate.
36
+
37
+ Parameters
38
+ ----------
39
+ new_knob : int, optional
40
+ If provided, sets the knob to this value. If not provided, the current knob is printed.
41
+ """
42
+ if new_knob is None:
43
+ # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
44
+ # console.out.print(ctx.client.strip[index].gate.knob)
45
+ return
46
+ ctx.client.strip[index].gate.knob = new_knob
47
+
48
+
49
+ @app.command(name='threshold')
50
+ def threshold(
51
+ new_threshold: Annotated[float, Argument()] = None,
52
+ *,
53
+ index: Annotated[int, Parameter(show=False)] = None,
54
+ ctx: Annotated[Context, Parameter(show=False)] = None,
55
+ ):
56
+ """Get or set the threshold of the specified gate.
57
+
58
+ Parameters
59
+ ----------
60
+ new_threshold : float, optional
61
+ If provided, sets the threshold to this value. If not provided, the current threshold is printed.
62
+ """
63
+ if new_threshold is None:
64
+ # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
65
+ # console.out.print(ctx.client.strip[index].gate.threshold)
66
+ return
67
+ ctx.client.strip[index].gate.threshold = new_threshold
@@ -0,0 +1,43 @@
1
+ import re
2
+
3
+ from cyclopts.help import DefaultFormatter, HelpPanel
4
+ from rich.console import Console, ConsoleOptions
5
+
6
+
7
+ class CustomHelpFormatter(DefaultFormatter):
8
+ """Custom help formatter that injects an index argument into the usage line and filters it out from the parameters list.
9
+
10
+ This formatter modifies the usage line to include an <index> argument after the 'strip' command,
11
+ and filters out any parameters related to 'index' from the Parameters section of the help output.
12
+ """
13
+
14
+ def render_usage(self, console: Console, options: ConsoleOptions, usage) -> None:
15
+ """Render the usage line with index argument injected."""
16
+ if usage:
17
+ modified_usage = re.sub(
18
+ r'(\S+\s+[a-z]+)\s+(COMMAND)', r'\1 <index> \2', str(usage)
19
+ )
20
+ console.print(f'[bold]Usage:[/bold] {modified_usage}')
21
+
22
+ def __call__(
23
+ self, console: Console, options: ConsoleOptions, panel: HelpPanel
24
+ ) -> None:
25
+ """Render a help panel, filtering out the index parameter from Parameters sections."""
26
+ if panel.title == 'Parameters':
27
+ filtered_entries = [
28
+ entry
29
+ for entry in panel.entries
30
+ if not (
31
+ entry.names and any('index' in name.lower() for name in entry.names)
32
+ )
33
+ ]
34
+
35
+ filtered_panel = HelpPanel(
36
+ title=panel.title,
37
+ entries=filtered_entries,
38
+ description=panel.description,
39
+ format=panel.format,
40
+ )
41
+ super().__call__(console, options, filtered_panel)
42
+ else:
43
+ super().__call__(console, options, panel)
@@ -0,0 +1,270 @@
1
+ from typing import Annotated, Optional
2
+
3
+ from cyclopts import App, Argument, Parameter
4
+
5
+ from . import comp, console, denoiser, eq, gate
6
+ from .context import Context
7
+ from .help import CustomHelpFormatter
8
+
9
+ app = App(name='strip', help_formatter=CustomHelpFormatter())
10
+ app.command(eq.app.meta, name='eq')
11
+ app.command(comp.app.meta, name='comp')
12
+ app.command(gate.app.meta, name='gate')
13
+ app.command(denoiser.app.meta, name='denoiser')
14
+
15
+
16
+ @app.meta.default
17
+ def launcher(
18
+ index: Annotated[int, Argument()] = None,
19
+ *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
20
+ ctx: Annotated[Context, Parameter(show=False)] = None,
21
+ ):
22
+ """Control the strip parameters."""
23
+ additional_kwargs = {}
24
+ command, bound, _ = app.parse_args(tokens)
25
+ if index is not None:
26
+ additional_kwargs['index'] = index
27
+ if ctx is not None:
28
+ additional_kwargs['ctx'] = ctx
29
+
30
+ return command(*bound.args, **bound.kwargs, **additional_kwargs)
31
+
32
+
33
+ @app.command(name='mono')
34
+ def mono(
35
+ new_state: Annotated[Optional[bool], Argument()] = None,
36
+ *,
37
+ index: Annotated[int, Parameter(show=False)] = None,
38
+ ctx: Annotated[Context, Parameter(show=False)] = None,
39
+ ):
40
+ """Get or set the mono state of the specified strip.
41
+
42
+ Parameters
43
+ ----------
44
+ new_state : bool, optional
45
+ If provided, sets the mono state to this value. If not provided, the current mono state is printed.
46
+ """
47
+ if new_state is None:
48
+ console.out.print(ctx.client.strip[index].mono)
49
+ return
50
+ ctx.client.strip[index].mono = new_state
51
+
52
+
53
+ @app.command(name='solo')
54
+ def solo(
55
+ new_state: Annotated[Optional[bool], Argument()] = None,
56
+ *,
57
+ index: Annotated[int, Parameter(show=False)] = None,
58
+ ctx: Annotated[Context, Parameter(show=False)] = None,
59
+ ):
60
+ """Get or set the solo state of the specified strip.
61
+
62
+ Parameters
63
+ ----------
64
+ new_state : bool, optional
65
+ If provided, sets the solo state to this value. If not provided, the current solo state is printed.
66
+ """
67
+ if new_state is None:
68
+ console.out.print(ctx.client.strip[index].solo)
69
+ return
70
+ ctx.client.strip[index].solo = new_state
71
+
72
+
73
+ @app.command(name='mute')
74
+ def mute(
75
+ new_state: Annotated[Optional[bool], Argument()] = None,
76
+ *,
77
+ index: Annotated[int, Parameter(show=False)] = None,
78
+ ctx: Annotated[Context, Parameter(show=False)] = None,
79
+ ):
80
+ """Get or set the mute state of the specified strip.
81
+
82
+ Parameters
83
+ ----------
84
+ new_state : bool, optional
85
+ If provided, sets the mute state to this value. If not provided, the current mute state is printed.
86
+ """
87
+ if new_state is None:
88
+ console.out.print(ctx.client.strip[index].mute)
89
+ return
90
+ ctx.client.strip[index].mute = new_state
91
+
92
+
93
+ @app.command(name='gain')
94
+ def gain(
95
+ new_value: Annotated[Optional[float], Argument()] = None,
96
+ *,
97
+ index: Annotated[int, Parameter(show=False)] = None,
98
+ ctx: Annotated[Context, Parameter(show=False)] = None,
99
+ ):
100
+ """Get or set the gain of the specified strip.
101
+
102
+ Parameters
103
+ ----------
104
+ new_value : float, optional
105
+ If provided, sets the gain to this value. If not provided, the current gain is printed.
106
+ """
107
+ if new_value is None:
108
+ console.out.print(ctx.client.strip[index].gain)
109
+ return
110
+ ctx.client.strip[index].gain = new_value
111
+
112
+
113
+ @app.command(name='A1')
114
+ def a1(
115
+ new_value: Annotated[Optional[bool], Argument()] = None,
116
+ *,
117
+ index: Annotated[int, Parameter(show=False)] = None,
118
+ ctx: Annotated[Context, Parameter(show=False)] = None,
119
+ ):
120
+ """Get or set the A1 state of the specified strip.
121
+
122
+ Parameters
123
+ ----------
124
+ new_value : bool, optional
125
+ If provided, sets the A1 state to this value. If not provided, the current A1 state is printed.
126
+ """
127
+ if new_value is None:
128
+ console.out.print(ctx.client.strip[index].A1)
129
+ return
130
+ ctx.client.strip[index].A1 = new_value
131
+
132
+
133
+ @app.command(name='A2')
134
+ def a2(
135
+ new_value: Annotated[Optional[bool], Argument()] = None,
136
+ *,
137
+ index: Annotated[int, Parameter(show=False)] = None,
138
+ ctx: Annotated[Context, Parameter(show=False)] = None,
139
+ ):
140
+ """Get or set the A2 state of the specified strip.
141
+
142
+ Parameters
143
+ ----------
144
+ new_value : bool, optional
145
+ If provided, sets the A2 state to this value. If not provided, the current A2 state is printed.
146
+ """
147
+ if new_value is None:
148
+ console.out.print(ctx.client.strip[index].A2)
149
+ return
150
+ ctx.client.strip[index].A2 = new_value
151
+
152
+
153
+ @app.command(name='A3')
154
+ def a3(
155
+ new_value: Annotated[Optional[bool], Argument()] = None,
156
+ *,
157
+ index: Annotated[int, Parameter(show=False)] = None,
158
+ ctx: Annotated[Context, Parameter(show=False)] = None,
159
+ ):
160
+ """Get or set the A3 state of the specified strip.
161
+
162
+ Parameters
163
+ ----------
164
+ new_value : bool, optional
165
+ If provided, sets the A3 state to this value. If not provided, the current A3 state is printed.
166
+ """
167
+ if new_value is None:
168
+ console.out.print(ctx.client.strip[index].A3)
169
+ return
170
+ ctx.client.strip[index].A3 = new_value
171
+
172
+
173
+ @app.command(name='A4')
174
+ def a4(
175
+ new_value: Annotated[Optional[bool], Argument()] = None,
176
+ *,
177
+ index: Annotated[int, Parameter(show=False)] = None,
178
+ ctx: Annotated[Context, Parameter(show=False)] = None,
179
+ ):
180
+ """Get or set the A4 state of the specified strip.
181
+
182
+ Parameters
183
+ ----------
184
+ new_value : bool, optional
185
+ If provided, sets the A4 state to this value. If not provided, the current A4 state is printed.
186
+ """
187
+ if new_value is None:
188
+ console.out.print(ctx.client.strip[index].A4)
189
+ return
190
+ ctx.client.strip[index].A4 = new_value
191
+
192
+
193
+ @app.command(name='A5')
194
+ def a5(
195
+ new_value: Annotated[Optional[bool], Argument()] = None,
196
+ *,
197
+ index: Annotated[int, Parameter(show=False)] = None,
198
+ ctx: Annotated[Context, Parameter(show=False)] = None,
199
+ ):
200
+ """Get or set the A5 state of the specified strip.
201
+
202
+ Parameters
203
+ ----------
204
+ new_value : bool, optional
205
+ If provided, sets the A5 state to this value. If not provided, the current A5 state is printed.
206
+ """
207
+ if new_value is None:
208
+ console.out.print(ctx.client.strip[index].A5)
209
+ return
210
+ ctx.client.strip[index].A5 = new_value
211
+
212
+
213
+ @app.command(name='B1')
214
+ def b1(
215
+ new_value: Annotated[Optional[bool], Argument()] = None,
216
+ *,
217
+ index: Annotated[int, Parameter(show=False)] = None,
218
+ ctx: Annotated[Context, Parameter(show=False)] = None,
219
+ ):
220
+ """Get or set the B1 state of the specified strip.
221
+
222
+ Parameters
223
+ ----------
224
+ new_value : bool, optional
225
+ If provided, sets the B1 state to this value. If not provided, the current B1 state is printed.
226
+ """
227
+ if new_value is None:
228
+ console.out.print(ctx.client.strip[index].B1)
229
+ return
230
+ ctx.client.strip[index].B1 = new_value
231
+
232
+
233
+ @app.command(name='B2')
234
+ def b2(
235
+ new_value: Annotated[Optional[bool], Argument()] = None,
236
+ *,
237
+ index: Annotated[int, Parameter(show=False)] = None,
238
+ ctx: Annotated[Context, Parameter(show=False)] = None,
239
+ ):
240
+ """Get or set the B2 state of the specified strip.
241
+
242
+ Parameters
243
+ ----------
244
+ new_value : bool, optional
245
+ If provided, sets the B2 state to this value. If not provided, the current B2 state is printed.
246
+ """
247
+ if new_value is None:
248
+ console.out.print(ctx.client.strip[index].B2)
249
+ return
250
+ ctx.client.strip[index].B2 = new_value
251
+
252
+
253
+ @app.command(name='B3')
254
+ def b3(
255
+ new_value: Annotated[Optional[bool], Argument()] = None,
256
+ *,
257
+ index: Annotated[int, Parameter(show=False)] = None,
258
+ ctx: Annotated[Context, Parameter(show=False)] = None,
259
+ ):
260
+ """Get or set the B3 state of the specified strip.
261
+
262
+ Parameters
263
+ ----------
264
+ new_value : bool, optional
265
+ If provided, sets the B3 state to this value. If not provided, the current B3 state is printed.
266
+ """
267
+ if new_value is None:
268
+ console.out.print(ctx.client.strip[index].B3)
269
+ return
270
+ ctx.client.strip[index].B3 = new_value
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: vban-cli
3
+ Version: 0.3.0
4
+ Summary: A command-line interface for Voicemeeter leveraging VBAN.
5
+ License: LICENSE
6
+ Classifier: Development Status :: 3 - Alpha
7
+ Classifier: Programming Language :: Python
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: Implementation :: CPython
13
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
14
+ Requires-Python: >=3.13
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: cyclopts>=4.6.0
18
+ Requires-Dist: vban-cmd>=2.6.0
19
+ Dynamic: license-file
20
+
21
+ # vban-cli
22
+
23
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
24
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
25
+
26
+ ---
27
+
28
+ ## Install
29
+
30
+ #### With uv
31
+
32
+ ```console
33
+ uv tool install vban-cli
34
+ ```
35
+
36
+ #### With pipx
37
+
38
+ ```console
39
+ pipx install vban-cli
40
+ ```
41
+
42
+ The CLI should now be discoverable as `vban-cli`
43
+
44
+ ---
45
+
46
+ ## Configuration
47
+
48
+ ### Flags
49
+
50
+ ```console
51
+ vban-cli --host=localhost --port=6980 --streamname=Command1
52
+ ```
53
+
54
+ ### Environment Variables
55
+
56
+ example .envrc:
57
+
58
+ ```env
59
+ #!/usr/bin/env bash
60
+
61
+ export VBAN_CLI_HOST="localhost"
62
+ export VBAN_CLI_PORT=6980
63
+ export VBAN_CLI_STREAMNAME=Command1
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Use
69
+
70
+ ```console
71
+ Usage: vban-cli COMMAND
72
+
73
+ ╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────╮
74
+ │ bus Control the bus parameters. │
75
+ │ strip Control the strip parameters. │
76
+ │ --help (-h) Display this message and exit. │
77
+ │ --version Display application version. │
78
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
79
+ ╭─ Parameters ─────────────────────────────────────────────────────────────────────────────────────╮
80
+ │ --kind Kind of Voicemeeter [env var: VBAN_CLI_KIND] [default: potato] │
81
+ │ --host VBAN host [env var: VBAN_CLI_HOST] [default: localhost] │
82
+ │ --port VBAN port [env var: VBAN_CLI_PORT] [default: 6980] │
83
+ │ --streamname VBAN stream name [env var: VBAN_CLI_STREAMNAME] [default: Command1] │
84
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
85
+ ```
86
+
87
+ For every command and subcommand there exists a `--help` flag for further usage information.
88
+
89
+ ---
90
+
91
+ ## Implementation Notes
92
+
93
+ 1. The VBAN TEXT subprotocol defines two packet structures [ident:0][ident-0] and [ident:1][ident-1]. Neither of them contain the data for Bus EQ parameters.
94
+ 2. Packet structure with [ident:1][ident-1] is emitted by the VBAN server only on pdirty events. This means we do not receive the initial state of those parameters on initial subscription. Therefore any commands which are intended to fetch the value of parameters defined in packet [ident:1][ident-1] will not work in this CLI.
95
+ 3. Packet structure with [ident:1][ident-1] defines parameteric EQ data only for the [first channel][ident-1-peq].
96
+
97
+
98
+ ---
99
+
100
+ ## Further Notes
101
+
102
+ I've made the effort to set up the basic skeletal structure of the CLI as well as demonstrate how to combine subcommand groups with subcommand groups so more can be implemented, it just needs doing. There may be restrictions on some things however, for example, retrieving values is only possible for parameters [defined in the protocol](https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L787). Setting parameters can be done for anything possible by a string request.
103
+
104
+ If there's something missing that you would like to see added the best bet is to submit a PR. You may raise an issue and if it's quick and simple to do I may (or may not) do it.
105
+
106
+ ---
107
+
108
+ ## License
109
+
110
+ `vban-cli` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
111
+
112
+
113
+
114
+ [ident-0]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L896
115
+ [ident-1]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L982
116
+ [ident-1-peq]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L995
@@ -0,0 +1,20 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/vban_cli/__init__.py
5
+ src/vban_cli/app.py
6
+ src/vban_cli/bus.py
7
+ src/vban_cli/comp.py
8
+ src/vban_cli/console.py
9
+ src/vban_cli/context.py
10
+ src/vban_cli/denoiser.py
11
+ src/vban_cli/eq.py
12
+ src/vban_cli/gate.py
13
+ src/vban_cli/help.py
14
+ src/vban_cli/strip.py
15
+ src/vban_cli.egg-info/PKG-INFO
16
+ src/vban_cli.egg-info/SOURCES.txt
17
+ src/vban_cli.egg-info/dependency_links.txt
18
+ src/vban_cli.egg-info/entry_points.txt
19
+ src/vban_cli.egg-info/requires.txt
20
+ src/vban_cli.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ vban-cli = vban_cli.app:run
@@ -0,0 +1,2 @@
1
+ cyclopts>=4.6.0
2
+ vban-cmd>=2.6.0
@@ -0,0 +1 @@
1
+ vban_cli