15 Commits

Author SHA1 Message Date
a1da5c7256 patch bump 2026-03-02 12:04:21 +00:00
642337d987 pause-recording command added, to differentiate from playback pausing. 2026-03-02 12:04:01 +00:00
51002edb39 upd IN 4 2026-03-02 12:02:14 +00:00
d30c9f828d upd recorder examples
add implementation note 4
2026-03-02 11:59:56 +00:00
a9c3168542 add uv lock pre-commit config 2026-03-02 02:27:49 +00:00
fba3eddea8 upd link to packet ident:1 2026-03-02 01:39:50 +00:00
6c2c924a48 upd implementation notes. 2026-03-02 01:36:46 +00:00
27290e1a0e md fix 2026-03-02 01:26:33 +00:00
6188da4f51 minor bump 2026-03-02 01:26:03 +00:00
230e537414 add gainlayer examples 2026-03-02 01:25:54 +00:00
e6ebf86c86 add gainlayer command group
add GainlayerHelpFormatter
2026-03-02 01:24:18 +00:00
1a0fb979e0 minor bump 2026-03-02 00:29:02 +00:00
080e26f75f add more eq cell commands 2026-03-02 00:28:32 +00:00
f6d82c5064 remove duplicate entries 2026-03-01 22:08:41 +00:00
627ada3b09 md fix 2026-03-01 22:06:07 +00:00
10 changed files with 211 additions and 34 deletions

10
.gitignore vendored
View File

@@ -1,13 +1,3 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
# Generated by ignr: github.com/onyx-and-iris/ignr
## Python ##

7
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,7 @@
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
rev: 0.10.7
hooks:
# Update the uv lockfile
- id: uv-lock

View File

@@ -85,10 +85,28 @@ examples:
```console
vban-cli strip 0 eq cell 0 on false
vban-cli strip 3 eq cell 2 freq 1500
vban-cli strip 4 eq cell 5 type 5
```
see `vban-cli strip eq cell --help` for more info.
#### Strip Gainlayer Command
Usage: vban-cli strip \<index> gainlayer \<gainlayer_index> COMMAND [OPTIONS] [ARGS]
examples:
```console
vban-cli strip 0 gainlayer 0 level
vban-cli strip 3 gainlayer 2 level -13.5
```
see `vban-cli strip gainlayer --help` for more info.
### Bus Command
Usage: vban-cli bus \<index> COMMAND [ARGS]
@@ -126,9 +144,9 @@ examples:
```console
vban-cli recorder play
vban-cli recorder rew
vban-cli recorder pause
vban-cli recorder replay
vban-cli recorder goto "00:01:30"
```
see `vban-cli recorder --help` for more info.
@@ -137,10 +155,10 @@ see `vban-cli recorder --help` for more info.
Usage: vban-cli sendtext TEXT
*To Voicemeeter*
examples:
*To Voicemeeter*
```console
vban-cli sendtext 'Strip[0].Mute=1;Bus[0].Mono=2'
```
@@ -159,9 +177,10 @@ see `vban-cli sendtext --help` for more info.
## Implementation Notes
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.
1. The VBAN RT SERVICE subprotocol defines two packet structures [ident:0][ident-0] and [ident:1][ident-1]. Neither of them contain the data for Bus EQ parameters.
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 current 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.
3. Packet structure with [ident:1][ident-1] defines parameteric EQ data only for the [first channel][ident-1-peq].
4. There doesn't appear to be any way to retrieve the current recorder status, ie, recording, playing, stopped etc. I don't see the data available in either packet structures [ident:0][ident-0] or [ident:1][ident-1].
---
@@ -182,5 +201,5 @@ If there's something missing that you would like to see added the best bet is to
[ident-0]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L896
[ident-1]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L982
[ident-1]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L1053
[ident-1-peq]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L995

View File

@@ -1,6 +1,6 @@
[project]
name = "vban-cli"
version = "0.7.1"
version = "0.9.1"
description = "A command-line interface for Voicemeeter leveraging VBAN."
readme = "README.md"
license = { text = "LICENSE" }

View File

@@ -64,7 +64,7 @@ def cell_launcher(
Only channel 0 is supported, see https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 3.
"""
additional_kwargs = {}
command, bound, _ = app.parse_args(tokens)
command, bound, _ = cell_app.parse_args(tokens)
additional_kwargs['target'] = target.channel[0].cell[band]
return command(*bound.args, **bound.kwargs, **additional_kwargs)
@@ -88,3 +88,83 @@ def cell_on(
# console.out.print(target.on)
return
target.on = new_state
@cell_app.command(name='freq')
def cell_freq(
new_freq: Annotated[float, Argument()] = None,
*,
target: Annotated[object, Parameter(show=False)] = None,
):
"""Get or set the frequency of the specified EQ cell.
Parameters
----------
new_freq : float
If provided, sets the frequency to this value. If not provided, the current frequency is printed.
"""
if new_freq is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# console.out.print(target.f)
return
target.f = new_freq
@cell_app.command(name='gain')
def cell_gain(
new_gain: Annotated[float, Argument()] = None,
*,
target: Annotated[object, Parameter(show=False)] = None,
):
"""Get or set the gain of the specified EQ cell.
Parameters
----------
new_gain : float
If provided, sets the gain to this value. If not provided, the current gain is printed.
"""
if new_gain is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# console.out.print(target.gain)
return
target.gain = new_gain
@cell_app.command(name='quality')
def cell_q(
new_q: Annotated[float, Argument()] = None,
*,
target: Annotated[object, Parameter(show=False)] = None,
):
"""Get or set the Q of the specified EQ cell.
Parameters
----------
new_q : float
If provided, sets the Q to this value. If not provided, the current Q is printed.
"""
if new_q is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# console.out.print(target.q)
return
target.q = new_q
@cell_app.command(name='type')
def cell_type(
new_type: Annotated[int, Argument()] = None,
*,
target: Annotated[object, Parameter(show=False)] = None,
):
"""Get or set the type of the specified EQ cell.
Parameters
----------
new_type : int
If provided, sets the type to this value. If not provided, the current type is printed.
"""
if new_type is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# console.out.print(target.type)
return
target.type = new_type

51
src/vban_cli/gainlayer.py Normal file
View File

@@ -0,0 +1,51 @@
from typing import Annotated
from cyclopts import App, Argument, Parameter
from . import console
from .context import Context
from .help import GainlayerHelpFormatter
app = App(name='gainlayer', help_formatter=GainlayerHelpFormatter())
@app.meta.default
def launcher(
gainlayer_index: Annotated[int, Argument()] = None,
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
index: Annotated[int, Argument()] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
):
"""Control the gainlayers."""
additional_kwargs = {}
command, bound, _ = app.parse_args(tokens)
if index is not None and gainlayer_index is not None:
additional_kwargs['strip_index'] = index
additional_kwargs['gainlayer_index'] = gainlayer_index
else:
raise ValueError('Both gainlayer_index and index must be provided.')
if ctx is not None:
additional_kwargs['ctx'] = ctx
return command(*bound.args, **bound.kwargs, **additional_kwargs)
@app.command(name='level')
def level(
new_level: Annotated[float, Argument()] = None,
*,
strip_index: Annotated[int, Parameter(show=False)] = None,
gainlayer_index: Annotated[int, Parameter(show=False)] = None,
ctx: Annotated[Context, Parameter(show=False)] = None,
):
"""Get or set the level of the specified gainlayer.
Parameters
----------
new_level : float
If provided, sets the level to this value. If not provided, the current level is printed.
"""
if new_level is None:
console.out.print(ctx.client.strip[strip_index].gainlayer[gainlayer_index].gain)
return
ctx.client.strip[strip_index].gainlayer[gainlayer_index].gain = new_level

View File

@@ -95,6 +95,22 @@ class EqHelpFormatter(BaseHelpFormatter):
console.print(f'[bold]Usage:[/bold] {modified_usage}')
class GainlayerHelpFormatter(BaseHelpFormatter):
"""Help formatter for gainlayer commands that works with strip commands.
Injects <index> after 'strip' and <gainlayer_index> after 'gainlayer'."""
def render_usage(self, console: Console, options: ConsoleOptions, usage) -> None:
"""Render the usage line with proper <index> placement for strip commands."""
if usage:
modified_usage = re.sub(
r'(\S+\s+strip)(\s+gainlayer\s+)(COMMAND)',
r'\1 <index>\2<[cyan]gainlayer_index[/cyan]> \3',
str(usage),
)
console.print(f'[bold]Usage:[/bold] {modified_usage}')
class CellHelpFormatter(BaseHelpFormatter):
"""Help formatter for cell commands that works with both strip and bus commands.
@@ -105,7 +121,7 @@ class CellHelpFormatter(BaseHelpFormatter):
if usage:
modified_usage = re.sub(
r'(\S+\s+)(\w+)(\s+eq\s+cell\s+)(COMMAND)',
r'\1\2 <index>\3<band> \4',
r'\1\2 <index>\3<[cyan]band[/cyan]> \4',
str(usage),
)
console.print(f'[bold]Usage:[/bold] {modified_usage}')

View File

@@ -20,26 +20,29 @@ def play(
console.out.print('Recorder playback started.')
@app.command(name='stop')
def stop(
*,
ctx: Annotated[Context, Parameter(show=False)] = None,
):
"""Stop the recorder playback."""
ctx.client.recorder.stop()
console.out.print('Recorder playback stopped.')
@app.command(name='pause')
def pause(
*,
ctx: Annotated[Context, Parameter(show=False)] = None,
):
"""Pause the recorder playback."""
ctx.client.recorder.pause()
ctx.client.recorder.stop()
console.out.print('Recorder playback paused.')
@app.command(name='stop')
def stop(
*,
ctx: Annotated[Context, Parameter(show=False)] = None,
):
"""Stop the recorder playback/recording and reset to the beginning."""
ctx.client.recorder.stop()
ctx.client.recorder.goto('00:00:00')
# We have no way of knowing if the recorder was playing or recording, so we print a generic message.
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 4.
console.out.print('Recorder stopped.')
@app.command(name='replay')
def replay(
*,
@@ -57,7 +60,17 @@ def record(
):
"""Start recording."""
ctx.client.recorder.record()
console.out.print('Recording started.')
console.out.print('Recorder recording started.')
@app.command(name='pause-recording')
def pause_recording(
*,
ctx: Annotated[Context, Parameter(show=False)] = None,
):
"""Pause the recorder recording."""
ctx.client.recorder.pause()
console.out.print('Recorder recording paused.')
@app.command(name='ff')

View File

@@ -2,7 +2,7 @@ from typing import Annotated, Optional
from cyclopts import App, Argument, Parameter
from . import comp, console, denoiser, eq, gate
from . import comp, console, denoiser, eq, gainlayer, gate
from .context import Context
from .help import StripHelpFormatter
@@ -11,6 +11,7 @@ app.command(eq.app.meta, name='eq')
app.command(comp.app.meta, name='comp')
app.command(gate.app.meta, name='gate')
app.command(denoiser.app.meta, name='denoiser')
app.command(gainlayer.app.meta, name='gainlayer')
@app.meta.default

4
uv.lock generated
View File

@@ -124,7 +124,7 @@ wheels = [
[[package]]
name = "vban-cli"
version = "0.7.0"
version = "0.9.1"
source = { editable = "." }
dependencies = [
{ name = "cyclopts" },
@@ -141,7 +141,7 @@ requires-dist = [
[[package]]
name = "vban-cmd"
version = "2.8.0"
version = "2.8.1"
source = { editable = "../vban-cmd-python" }
[package.metadata]