Compare commits

..

23 Commits
v0.9.0 ... main

Author SHA1 Message Date
a2ae240605 upd call to vban_cmd
minor bump
2026-03-02 23:27:11 +00:00
18ed3ea7fb bump development status classifier 2026-03-02 22:11:30 +00:00
d5ca50e9bd patch bump 2026-03-02 22:08:07 +00:00
0f8be6de48 bp-sidechain should be float 2026-03-02 22:07:53 +00:00
aa6b156eff minor bump 2026-03-02 22:04:05 +00:00
5c60a382fd add Strip Gate Command to README
update  the usage lines
2026-03-02 22:03:51 +00:00
d7fef4f26a remove duplicates from help outputs.
this bug was introduced due to changes in the meta app launchers.
2026-03-02 22:03:32 +00:00
e6a17d5772 add more gate commands 2026-03-02 22:02:25 +00:00
5c3452bff5 md upd 2026-03-02 21:20:43 +00:00
b0a524d125 add Strip Comp Command to README 2026-03-02 21:18:05 +00:00
2693fcce2c bump vban-cmd dep version
minor bump
2026-03-02 21:13:39 +00:00
bf058e910e add more compressor commands 2026-03-02 21:13:12 +00:00
e84accd37a patch bump 2026-03-02 12:55:49 +00:00
d9810ce270 enable rich tracebacks
console, error_console are now app attributes.
2026-03-02 12:55:33 +00:00
4fff581c95 patch bump 2026-03-02 12:30:35 +00:00
1f6811d5a0 general parser improvements.
index and ctx parameters now show as required in help output
2026-03-02 12:30:22 +00:00
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
17 changed files with 534 additions and 204 deletions

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

@ -5,8 +5,6 @@
--- ---
This CLI is still in an early stage of development with many more things that could be implemented. However, the commands that are implemented should be working without issues.
## Install ## Install
#### With uv #### With uv
@ -51,7 +49,7 @@ export VBAN_CLI_STREAMNAME=Command1
### Strip Command ### Strip Command
Usage: vban-cli strip \<index> COMMAND [ARGS] *Usage: vban-cli strip \<index> COMMAND [OPTIONS]*
examples: examples:
@ -65,9 +63,9 @@ vban-cli strip 2 gain -18.7
see `vban-cli strip --help` for more info. see `vban-cli strip --help` for more info.
#### Strip EQ ##### Strip EQ
Usage: vban-cli strip \<index> eq COMMAND [OPTIONS] *Usage: vban-cli strip \<index> eq COMMAND [OPTIONS]*
examples: examples:
@ -77,9 +75,9 @@ vban-cli strip 0 eq on true
see `vban-cli strip eq --help` for more info. see `vban-cli strip eq --help` for more info.
#### Strip EQ Cell Command ##### Strip EQ Cell Command
Usage: vban-cli strip \<index> eq cell \<band> COMMAND [ARGS] *Usage: vban-cli strip \<index> eq cell \<band> COMMAND [ARGS]*
examples: examples:
@ -93,9 +91,37 @@ vban-cli strip 4 eq cell 5 type 5
see `vban-cli strip eq cell --help` for more info. see `vban-cli strip eq cell --help` for more info.
#### Strip Gainlayer Command ##### Strip Comp Command
Usage: vban-cli strip \<index> gainlayer \<gainlayer_index> COMMAND [OPTIONS] [ARGS] *Usage: vban-cli strip \<index> comp COMMAND [OPTIONS]*
examples:
```console
vban-cli strip 0 comp attack 2.0
vban-cli strip 3 comp auto-makeup true
```
see `vban-cli strip comp --help` for more info.
##### Strip Gate Command
*Usage: vban-cli strip \<index> gate COMMAND [OPTIONS]*
examples:
```console
vban-cli strip 2 gate attack 634
vban-cli strip 5 gate hold 2088.7
```
see `vban-cli strip comp --help` for more info.
##### Strip Gainlayer Command
*Usage: vban-cli strip \<index> gainlayer \<gainlayer_index> COMMAND [OPTIONS] [ARGS]*
examples: examples:
@ -109,7 +135,7 @@ see `vban-cli strip gainlayer --help` for more info.
### Bus Command ### Bus Command
Usage: vban-cli bus \<index> COMMAND [ARGS] *Usage: vban-cli bus \<index> COMMAND [OPTIONS]*
examples: examples:
@ -123,7 +149,7 @@ see `vban-cli bus --help` for more info.
### Command Command ### Command Command
Usage: vban-cli command COMMAND *Usage: vban-cli command COMMAND*
examples: examples:
@ -137,23 +163,23 @@ see `vban-cli command --help` for more info.
### Recorder Command ### Recorder Command
Usage: vban-cli recorder COMMAND *Usage: vban-cli recorder COMMAND*
examples: examples:
```console ```console
vban-cli recorder play 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. see `vban-cli recorder --help` for more info.
### Sendtext Command ### Sendtext Command
Usage: vban-cli sendtext TEXT *Usage: vban-cli sendtext TEXT*
examples: examples:
@ -177,9 +203,10 @@ see `vban-cli sendtext --help` for more info.
## Implementation Notes ## 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. 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]. 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].
--- ---
@ -200,5 +227,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-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 [ident-1-peq]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L995

View File

@ -1,13 +1,13 @@
[project] [project]
name = "vban-cli" name = "vban-cli"
version = "0.9.0" version = "0.12.0"
description = "A command-line interface for Voicemeeter leveraging VBAN." description = "A command-line interface for Voicemeeter leveraging VBAN."
readme = "README.md" readme = "README.md"
license = { text = "LICENSE" } license = { text = "LICENSE" }
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = ["cyclopts>=4.6.0", "loguru>=0.7.3", "vban-cmd>=2.8.1"] dependencies = ["cyclopts>=4.6.0", "loguru>=0.7.3", "vban-cmd>=2.9.1"]
classifiers = [ classifiers = [
"Development Status :: 3 - Alpha", "Development Status :: 4 - Beta",
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",

View File

@ -3,16 +3,21 @@ from typing import Annotated
import vban_cmd import vban_cmd
from cyclopts import App, Argument, Parameter, config from cyclopts import App, Argument, Parameter, config
from rich.traceback import install as install_rich_traceback
from . import __version__ as version from . import __version__ as version
from . import bus, command, console, recorder, strip from . import bus, command, console, recorder, strip
from .context import Context from .context import Context
from .error import VbanCLIConnectionError
app = App( app = App(
config=config.Env( config=config.Env(
'VBAN_CLI_', 'VBAN_CLI_',
), # Environment variable prefix for configuration parameters ), # Environment variable prefix for configuration parameters
version=version, version=version,
console=console.out,
error_console=console.err,
exit_on_error=True,
) )
app.command(strip.app.meta, name='strip') app.command(strip.app.meta, name='strip')
app.command(bus.app.meta, name='bus') app.command(bus.app.meta, name='bus')
@ -20,6 +25,8 @@ app.command(command.app, name='command')
app.command(recorder.app, name='recorder') app.command(recorder.app, name='recorder')
app.register_install_completion_command() app.register_install_completion_command()
install_rich_traceback(console=console.err)
@Parameter(name='*') @Parameter(name='*')
@dataclass @dataclass
@ -43,14 +50,17 @@ def launcher(
if command.__name__ == 'sendtext': if command.__name__ == 'sendtext':
disable_rt_listeners = True disable_rt_listeners = True
try:
with vban_cmd.api( with vban_cmd.api(
vban_config.kind, vban_config.kind,
ip=vban_config.host, host=vban_config.host,
port=vban_config.port, port=vban_config.port,
streamname=vban_config.streamname, streamname=vban_config.streamname,
disable_rt_listeners=disable_rt_listeners, disable_rt_listeners=disable_rt_listeners,
) as client: ) as client:
return command(*bound.args, **bound.kwargs, ctx=Context(client=client)) return command(*bound.args, **bound.kwargs, ctx=Context(client=client))
except vban_cmd.error.VBANCMDConnectionError as e:
raise VbanCLIConnectionError(str(e)) from e
@app.command(name='sendtext') @app.command(name='sendtext')
@ -58,16 +68,12 @@ def sendtext(
text: Annotated[str, Argument()], text: Annotated[str, Argument()],
/, /,
*, *,
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Send a text command to the current Voicemeeter/Matrix instance.""" """Send a text command to the current Voicemeeter/Matrix instance."""
if resp := ctx.client.sendtext(text): if resp := ctx.client.sendtext(text):
console.out.print(resp) app.console.print(resp)
def run(): def run():
try:
app.meta() app.meta()
except Exception as e:
console.err.print(f'Error: {e}')
return e.code

View File

@ -2,7 +2,6 @@ from typing import Annotated, Literal, Optional
from cyclopts import App, Argument, Parameter from cyclopts import App, Argument, Parameter
from . import console
from .context import Context from .context import Context
from .help import BusHelpFormatter from .help import BusHelpFormatter
@ -13,18 +12,17 @@ app = App(name='bus', help_formatter=BusHelpFormatter())
@app.meta.default @app.meta.default
def launcher( def launcher(
index: Annotated[int, Argument()] = None, index: Annotated[int, Argument()],
/,
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)], *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Control the bus parameters.""" """Control the bus parameters."""
additional_kwargs = {} additional_kwargs = {}
command, bound, _ = app.parse_args(tokens) command, bound, _ = app.parse_args(tokens)
if tokens[0] == 'eq': if tokens[0] == 'eq':
additional_kwargs['eq_kind'] = app.name[0] additional_kwargs['eq_kind'] = app.name[0]
if index is not None:
additional_kwargs['index'] = index additional_kwargs['index'] = index
if ctx is not None:
additional_kwargs['ctx'] = ctx additional_kwargs['ctx'] = ctx
return command(*bound.args, **bound.kwargs, **additional_kwargs) return command(*bound.args, **bound.kwargs, **additional_kwargs)
@ -36,8 +34,8 @@ def mono(
Optional[Literal['off', 'mono', 'stereoreverse']], Argument() Optional[Literal['off', 'mono', 'stereoreverse']], Argument()
] = None, ] = None,
*, *,
index: Annotated[int, Parameter(show=False)] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Get or set the mono state of the specified bus. """Get or set the mono state of the specified bus.
@ -47,7 +45,7 @@ def mono(
If provided, sets the mono state to this value. If not provided, the current mono state is printed. If provided, sets the mono state to this value. If not provided, the current mono state is printed.
""" """
if new_value is None: if new_value is None:
console.out.print(['off', 'mono', 'stereoreverse'][ctx.client.bus[index].mono]) app.console.print(['off', 'mono', 'stereoreverse'][ctx.client.bus[index].mono])
return return
ctx.client.bus[index].mono = ['off', 'mono', 'stereoreverse'].index(new_value) ctx.client.bus[index].mono = ['off', 'mono', 'stereoreverse'].index(new_value)
@ -56,8 +54,8 @@ def mono(
def mute( def mute(
new_value: Annotated[Optional[bool], Argument()] = None, new_value: Annotated[Optional[bool], Argument()] = None,
*, *,
index: Annotated[int, Parameter(show=False)] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Get or set the mute state of the specified bus. """Get or set the mute state of the specified bus.
@ -67,7 +65,7 @@ def mute(
If provided, sets the mute state to this value. If not provided, the current mute state is printed. If provided, sets the mute state to this value. If not provided, the current mute state is printed.
""" """
if new_value is None: if new_value is None:
console.out.print(ctx.client.bus[index].mute) app.console.print(ctx.client.bus[index].mute)
return return
ctx.client.bus[index].mute = new_value ctx.client.bus[index].mute = new_value
@ -94,8 +92,8 @@ def mode(
Argument(), Argument(),
] = None, ] = None,
*, *,
index: Annotated[int, Parameter(show=False)] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Get or set the bus mode of the specified bus. """Get or set the bus mode of the specified bus.
@ -105,6 +103,6 @@ def mode(
If provided, sets the bus mode to this value. If not provided, the current bus mode is printed. If provided, sets the bus mode to this value. If not provided, the current bus mode is printed.
""" """
if type_ is None: if type_ is None:
console.out.print(ctx.client.bus[index].mode.get()) app.console.print(ctx.client.bus[index].mode.get())
return return
setattr(ctx.client.bus[index].mode, type_, True) setattr(ctx.client.bus[index].mode, type_, True)

View File

@ -2,7 +2,6 @@ from typing import Annotated
from cyclopts import App, Parameter from cyclopts import App, Parameter
from . import console
from .context import Context from .context import Context
from .help import BaseHelpFormatter from .help import BaseHelpFormatter
@ -12,38 +11,38 @@ app = App(name='command', help_formatter=BaseHelpFormatter())
@app.command(name='show') @app.command(name='show')
def show( def show(
*, *,
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)] = None,
): ):
"""Bring the Voicemeeter GUI to the foreground.""" """Bring the Voicemeeter GUI to the foreground."""
ctx.client.command.show() ctx.client.command.show()
console.out.print('Voicemeeter GUI should now be in the foreground.') app.console.print('Voicemeeter GUI should now be in the foreground.')
@app.command(name='hide') @app.command(name='hide')
def hide( def hide(
*, *,
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Send the Voicemeeter GUI to the background.""" """Send the Voicemeeter GUI to the background."""
ctx.client.command.hide() ctx.client.command.hide()
console.out.print('Voicemeeter GUI should now be in the background.') app.console.print('Voicemeeter GUI should now be in the background.')
@app.command(name='shutdown') @app.command(name='shutdown')
def shutdown( def shutdown(
*, *,
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Shut down Voicemeeter.""" """Shut down Voicemeeter."""
ctx.client.command.shutdown() ctx.client.command.shutdown()
console.out.print('Voicemeeter should now be shut down.') app.console.print('Voicemeeter should now be shut down.')
@app.command(name='restart') @app.command(name='restart')
def restart( def restart(
*, *,
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Restart the Voicemeeter engine.""" """Restart the Voicemeeter engine."""
ctx.client.command.restart() ctx.client.command.restart()
console.out.print('Voicemeeter engine should now be restarting.') app.console.print('Voicemeeter engine should now be restarting.')

View File

@ -11,15 +11,13 @@ app = App(name='comp', help_formatter=StripHelpFormatter())
@app.meta.default @app.meta.default
def launcher( def launcher(
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)], *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
index: Annotated[int, Argument()] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Control the compressor parameters.""" """Control the compressor parameters."""
additional_kwargs = {} additional_kwargs = {}
command, bound, _ = app.parse_args(tokens) command, bound, _ = app.parse_args(tokens)
if index is not None:
additional_kwargs['index'] = index additional_kwargs['index'] = index
if ctx is not None:
additional_kwargs['ctx'] = ctx additional_kwargs['ctx'] = ctx
return command(*bound.args, **bound.kwargs, **additional_kwargs) return command(*bound.args, **bound.kwargs, **additional_kwargs)
@ -29,8 +27,8 @@ def launcher(
def knob( def knob(
new_knob: Annotated[float, Argument()] = None, new_knob: Annotated[float, Argument()] = None,
*, *,
index: Annotated[int, Parameter(show=False)] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Get or set the knob of the specified compressor. """Get or set the knob of the specified compressor.
@ -41,7 +39,7 @@ def knob(
""" """
if new_knob is None: if new_knob is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# console.out.print(ctx.client.strip[index].comp.knob) # app.console.print(ctx.client.strip[index].comp.knob)
return return
ctx.client.strip[index].comp.knob = new_knob ctx.client.strip[index].comp.knob = new_knob
@ -50,8 +48,8 @@ def knob(
def input_gain( def input_gain(
new_gain: Annotated[float, Argument()] = None, new_gain: Annotated[float, Argument()] = None,
*, *,
index: Annotated[int, Parameter(show=False)] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Get or set the input gain of the specified compressor. """Get or set the input gain of the specified compressor.
@ -62,6 +60,153 @@ def input_gain(
""" """
if new_gain is None: if new_gain is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# console.out.print(ctx.client.strip[index].comp.gainin) # app.console.print(ctx.client.strip[index].comp.gainin)
return return
ctx.client.strip[index].comp.gainin = new_gain ctx.client.strip[index].comp.gainin = new_gain
@app.command(name='ratio')
def ratio(
new_ratio: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the ratio of the specified compressor.
Parameters
----------
new_ratio : float, optional
If provided, sets the ratio to this value. If not provided, the current ratio is printed.
"""
if new_ratio is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(ctx.client.strip[index].comp.ratio)
return
ctx.client.strip[index].comp.ratio = new_ratio
@app.command(name='threshold')
def threshold(
new_threshold: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the threshold of the specified compressor.
Parameters
----------
new_threshold : float, optional
If provided, sets the threshold to this value. If not provided, the current threshold is printed.
"""
if new_threshold is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(ctx.client.strip[index].comp.threshold)
return
ctx.client.strip[index].comp.threshold = new_threshold
@app.command(name='attack')
def attack(
new_attack: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the attack of the specified compressor.
Parameters
----------
new_attack : float, optional
If provided, sets the attack to this value. If not provided, the current attack is printed.
"""
if new_attack is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(ctx.client.strip[index].comp.attack)
return
ctx.client.strip[index].comp.attack = new_attack
@app.command(name='release')
def release(
new_release: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the release of the specified compressor.
Parameters
----------
new_release : float, optional
If provided, sets the release to this value. If not provided, the current release is printed.
"""
if new_release is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(ctx.client.strip[index].comp.release)
return
ctx.client.strip[index].comp.release = new_release
@app.command(name='knee')
def knee(
new_knee: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the knee of the specified compressor.
Parameters
----------
new_knee : float, optional
If provided, sets the knee to this value. If not provided, the current knee is printed.
"""
if new_knee is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(ctx.client.strip[index].comp.knee)
return
ctx.client.strip[index].comp.knee = new_knee
@app.command(name='output-gain')
def output_gain(
new_gain: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the output gain of the specified compressor.
Parameters
----------
new_gain : float, optional
If provided, sets the output gain to this value. If not provided, the current output gain is printed.
"""
if new_gain is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(ctx.client.strip[index].comp.gainout)
return
ctx.client.strip[index].comp.gainout = new_gain
@app.command(name='auto-makeup')
def makeup(
new_makeup: Annotated[bool, Argument()] = None,
*,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the auto-makeup of the specified compressor.
Parameters
----------
new_makeup : bool, optional
If provided, sets the auto-makeup to this value. If not provided, the current auto-makeup is printed.
"""
if new_makeup is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(ctx.client.strip[index].comp.makeup)
return
ctx.client.strip[index].comp.makeup = new_makeup

View File

@ -11,15 +11,13 @@ app = App(name='denoiser', help_formatter=StripHelpFormatter())
@app.meta.default @app.meta.default
def launcher( def launcher(
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)], *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
index: Annotated[int, Argument()] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Control the denoiser parameters.""" """Control the denoiser parameters."""
additional_kwargs = {} additional_kwargs = {}
command, bound, _ = app.parse_args(tokens) command, bound, _ = app.parse_args(tokens)
if index is not None:
additional_kwargs['index'] = index additional_kwargs['index'] = index
if ctx is not None:
additional_kwargs['ctx'] = ctx additional_kwargs['ctx'] = ctx
return command(*bound.args, **bound.kwargs, **additional_kwargs) return command(*bound.args, **bound.kwargs, **additional_kwargs)
@ -29,8 +27,8 @@ def launcher(
def knob( def knob(
new_knob: Annotated[float, Argument()] = None, new_knob: Annotated[float, Argument()] = None,
*, *,
index: Annotated[int, Parameter(show=False)] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Get or set the knob of the specified denoiser. """Get or set the knob of the specified denoiser.
@ -41,6 +39,6 @@ def knob(
""" """
if new_knob is None: if new_knob is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# console.out.print(ctx.client.strip[index].denoiser.knob) # app.console.print(ctx.client.strip[index].denoiser.knob)
return return
ctx.client.strip[index].denoiser.knob = new_knob ctx.client.strip[index].denoiser.knob = new_knob

View File

@ -14,9 +14,9 @@ app.command(cell_app.meta, name='cell')
@app.meta.default @app.meta.default
def launcher( def launcher(
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)], *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
eq_kind: Annotated[str, Parameter(show=False)] = None, eq_kind: Annotated[str, Parameter(parse=False)],
index: Annotated[int, Argument()] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Control the EQ parameters.""" """Control the EQ parameters."""
additional_kwargs = {} additional_kwargs = {}
@ -37,7 +37,7 @@ def launcher(
def on( def on(
new_state: Annotated[bool, Argument()] = None, new_state: Annotated[bool, Argument()] = None,
*, *,
target: Annotated[object, Parameter(show=False)] = None, target: Annotated[object, Parameter(parse=False)],
): ):
"""Get or set the on state of the specified EQ band. """Get or set the on state of the specified EQ band.
@ -48,16 +48,17 @@ def on(
""" """
if new_state is None: if new_state is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# console.out.print(target.on) # app.console.print(target.on)
return return
target.on = new_state target.on = new_state
@cell_app.meta.default @cell_app.meta.default
def cell_launcher( def cell_launcher(
band: Annotated[int, Argument()] = None, band: Annotated[int, Argument()],
/,
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)], *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
target: Annotated[object, Parameter(show=False)] = None, target: Annotated[object, Parameter(parse=False)],
): ):
"""Control the EQ Cell parameters. """Control the EQ Cell parameters.
@ -74,7 +75,7 @@ def cell_launcher(
def cell_on( def cell_on(
new_state: Annotated[bool, Argument()] = None, new_state: Annotated[bool, Argument()] = None,
*, *,
target: Annotated[object, Parameter(show=False)] = None, target: Annotated[object, Parameter(parse=False)],
): ):
"""Get or set the on state of the specified EQ cell. """Get or set the on state of the specified EQ cell.
@ -85,7 +86,7 @@ def cell_on(
""" """
if new_state is None: if new_state is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# console.out.print(target.on) # app.console.print(target.on)
return return
target.on = new_state target.on = new_state
@ -94,7 +95,7 @@ def cell_on(
def cell_freq( def cell_freq(
new_freq: Annotated[float, Argument()] = None, new_freq: Annotated[float, Argument()] = None,
*, *,
target: Annotated[object, Parameter(show=False)] = None, target: Annotated[object, Parameter(parse=False)],
): ):
"""Get or set the frequency of the specified EQ cell. """Get or set the frequency of the specified EQ cell.
@ -105,7 +106,7 @@ def cell_freq(
""" """
if new_freq is None: if new_freq is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# console.out.print(target.f) # app.console.print(target.f)
return return
target.f = new_freq target.f = new_freq
@ -114,7 +115,7 @@ def cell_freq(
def cell_gain( def cell_gain(
new_gain: Annotated[float, Argument()] = None, new_gain: Annotated[float, Argument()] = None,
*, *,
target: Annotated[object, Parameter(show=False)] = None, target: Annotated[object, Parameter(parse=False)],
): ):
"""Get or set the gain of the specified EQ cell. """Get or set the gain of the specified EQ cell.
@ -125,7 +126,7 @@ def cell_gain(
""" """
if new_gain is None: if new_gain is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# console.out.print(target.gain) # app.console.print(target.gain)
return return
target.gain = new_gain target.gain = new_gain
@ -134,7 +135,7 @@ def cell_gain(
def cell_q( def cell_q(
new_q: Annotated[float, Argument()] = None, new_q: Annotated[float, Argument()] = None,
*, *,
target: Annotated[object, Parameter(show=False)] = None, target: Annotated[object, Parameter(parse=False)],
): ):
"""Get or set the Q of the specified EQ cell. """Get or set the Q of the specified EQ cell.
@ -145,7 +146,7 @@ def cell_q(
""" """
if new_q is None: if new_q is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# console.out.print(target.q) # app.console.print(target.q)
return return
target.q = new_q target.q = new_q
@ -154,7 +155,7 @@ def cell_q(
def cell_type( def cell_type(
new_type: Annotated[int, Argument()] = None, new_type: Annotated[int, Argument()] = None,
*, *,
target: Annotated[object, Parameter(show=False)] = None, target: Annotated[object, Parameter(parse=False)],
): ):
"""Get or set the type of the specified EQ cell. """Get or set the type of the specified EQ cell.
@ -165,6 +166,6 @@ def cell_type(
""" """
if new_type is None: if new_type is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# console.out.print(target.type) # app.console.print(target.type)
return return
target.type = new_type target.type = new_type

14
src/vban_cli/error.py Normal file
View File

@ -0,0 +1,14 @@
class VbanCLIError(Exception):
"""Base class for exceptions in this module."""
def __init__(self, message: str, code: int = 1):
super().__init__(message)
self.code = code
class VbanCLIConnectionError(VbanCLIError):
"""Exception raised for connection errors."""
class VbanCLIValidationError(VbanCLIError):
"""Exception raised for validation errors."""

View File

@ -2,7 +2,6 @@ from typing import Annotated
from cyclopts import App, Argument, Parameter from cyclopts import App, Argument, Parameter
from . import console
from .context import Context from .context import Context
from .help import GainlayerHelpFormatter from .help import GainlayerHelpFormatter
@ -11,20 +10,17 @@ app = App(name='gainlayer', help_formatter=GainlayerHelpFormatter())
@app.meta.default @app.meta.default
def launcher( def launcher(
gainlayer_index: Annotated[int, Argument()] = None, gainlayer_index: Annotated[int, Argument()],
/,
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)], *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
index: Annotated[int, Argument()] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Control the gainlayers.""" """Control the gainlayers."""
additional_kwargs = {} additional_kwargs = {}
command, bound, _ = app.parse_args(tokens) command, bound, _ = app.parse_args(tokens)
if index is not None and gainlayer_index is not None:
additional_kwargs['strip_index'] = index additional_kwargs['strip_index'] = index
additional_kwargs['gainlayer_index'] = gainlayer_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 additional_kwargs['ctx'] = ctx
return command(*bound.args, **bound.kwargs, **additional_kwargs) return command(*bound.args, **bound.kwargs, **additional_kwargs)
@ -34,9 +30,9 @@ def launcher(
def level( def level(
new_level: Annotated[float, Argument()] = None, new_level: Annotated[float, Argument()] = None,
*, *,
strip_index: Annotated[int, Parameter(show=False)] = None, strip_index: Annotated[int, Parameter(parse=False)],
gainlayer_index: Annotated[int, Parameter(show=False)] = None, gainlayer_index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Get or set the level of the specified gainlayer. """Get or set the level of the specified gainlayer.
@ -46,6 +42,6 @@ def level(
If provided, sets the level to this value. If not provided, the current level is printed. If provided, sets the level to this value. If not provided, the current level is printed.
""" """
if new_level is None: if new_level is None:
console.out.print(ctx.client.strip[strip_index].gainlayer[gainlayer_index].gain) app.console.print(ctx.client.strip[strip_index].gainlayer[gainlayer_index].gain)
return return
ctx.client.strip[strip_index].gainlayer[gainlayer_index].gain = new_level ctx.client.strip[strip_index].gainlayer[gainlayer_index].gain = new_level

View File

@ -11,15 +11,13 @@ app = App(name='gate', help_formatter=StripHelpFormatter())
@app.meta.default @app.meta.default
def launcher( def launcher(
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)], *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
index: Annotated[int, Argument()] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Control the compressor parameters.""" """Control the compressor parameters."""
additional_kwargs = {} additional_kwargs = {}
command, bound, _ = app.parse_args(tokens) command, bound, _ = app.parse_args(tokens)
if index is not None:
additional_kwargs['index'] = index additional_kwargs['index'] = index
if ctx is not None:
additional_kwargs['ctx'] = ctx additional_kwargs['ctx'] = ctx
return command(*bound.args, **bound.kwargs, **additional_kwargs) return command(*bound.args, **bound.kwargs, **additional_kwargs)
@ -29,8 +27,8 @@ def launcher(
def knob( def knob(
new_knob: Annotated[float, Argument()] = None, new_knob: Annotated[float, Argument()] = None,
*, *,
index: Annotated[int, Parameter(show=False)] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Get or set the knob of the specified gate. """Get or set the knob of the specified gate.
@ -41,7 +39,7 @@ def knob(
""" """
if new_knob is None: if new_knob is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# console.out.print(ctx.client.strip[index].gate.knob) # app.console.print(ctx.client.strip[index].gate.knob)
return return
ctx.client.strip[index].gate.knob = new_knob ctx.client.strip[index].gate.knob = new_knob
@ -50,8 +48,8 @@ def knob(
def threshold( def threshold(
new_threshold: Annotated[float, Argument()] = None, new_threshold: Annotated[float, Argument()] = None,
*, *,
index: Annotated[int, Parameter(show=False)] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Get or set the threshold of the specified gate. """Get or set the threshold of the specified gate.
@ -62,6 +60,111 @@ def threshold(
""" """
if new_threshold is None: if new_threshold is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2. # See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# console.out.print(ctx.client.strip[index].gate.threshold) # app.console.print(ctx.client.strip[index].gate.threshold)
return return
ctx.client.strip[index].gate.threshold = new_threshold ctx.client.strip[index].gate.threshold = new_threshold
@app.command(name='damping-max')
def damping_max(
new_damping_max: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the damping max of the specified gate.
Parameters
----------
new_damping_max : float, optional
If provided, sets the damping max to this value. If not provided, the current damping max is printed.
"""
if new_damping_max is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(ctx.client.strip[index].gate.damping)
return
ctx.client.strip[index].gate.damping = new_damping_max
@app.command(name='bp-sidechain')
def bp_sidechain(
new_bp_sidechain: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the BP sidechain of the specified gate.
Parameters
----------
new_bp_sidechain : float, optional
If provided, sets the BP sidechain to this value. If not provided, the current BP sidechain is printed.
"""
if new_bp_sidechain is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(ctx.client.strip[index].gate.bpsidechain)
return
ctx.client.strip[index].gate.bpsidechain = new_bp_sidechain
@app.command(name='attack')
def attack(
new_attack: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the attack of the specified gate.
Parameters
----------
new_attack : float, optional
If provided, sets the attack to this value. If not provided, the current attack is printed.
"""
if new_attack is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(ctx.client.strip[index].gate.attack)
return
ctx.client.strip[index].gate.attack = new_attack
@app.command(name='hold')
def hold(
new_hold: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the hold of the specified gate.
Parameters
----------
new_hold : float, optional
If provided, sets the hold to this value. If not provided, the current hold is printed.
"""
if new_hold is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(ctx.client.strip[index].gate.hold)
return
ctx.client.strip[index].gate.hold = new_hold
@app.command(name='release')
def release(
new_release: Annotated[float, Argument()] = None,
*,
index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Get or set the release of the specified gate.
Parameters
----------
new_release : float, optional
If provided, sets the release to this value. If not provided, the current release is printed.
"""
if new_release is None:
# See https://github.com/onyx-and-iris/vban-cli?tab=readme-ov-file#implementation-notes - 2.
# app.console.print(ctx.client.strip[index].gate.release)
return
ctx.client.strip[index].gate.release = new_release

View File

@ -56,8 +56,16 @@ class StripHelpFormatter(BaseHelpFormatter):
) )
if modified_usage == str(usage): if modified_usage == str(usage):
modified_usage = re.sub( modified_usage = re.sub(
r'(\S+\s+strip)\s+(\w+)', r'\1 <index> \2', str(usage) r'(\S+\s+strip)\s+(\w+)',
r'\1 <index> \2 [OPTIONS] [ARGS]',
str(usage),
) )
# Handle main strip command and subcommand groups
modified_usage = re.sub(
r'\bCOMMAND\b(?!\s+\[)', 'COMMAND [OPTIONS]', modified_usage
)
# Remove the duplicate INDEX that gets automatically added by cyclopts
modified_usage = re.sub(r'\s+INDEX$', '', modified_usage)
console.print(f'[bold]Usage:[/bold] {modified_usage}') console.print(f'[bold]Usage:[/bold] {modified_usage}')
@ -76,8 +84,16 @@ class BusHelpFormatter(BaseHelpFormatter):
) )
if modified_usage == str(usage): if modified_usage == str(usage):
modified_usage = re.sub( modified_usage = re.sub(
r'(\S+\s+bus)\s+(\w+)', r'\1 <index> \2', str(usage) r'(\S+\s+bus)\s+(\w+)',
r'\1 <index> \2 [OPTIONS] [ARGS]',
str(usage),
) )
# Handle main bus command and subcommand groups
modified_usage = re.sub(
r'\bCOMMAND\b(?!\s+\[)', 'COMMAND [OPTIONS]', modified_usage
)
# Remove the duplicate INDEX that gets automatically added by cyclopts
modified_usage = re.sub(r'\s+INDEX$', '', modified_usage)
console.print(f'[bold]Usage:[/bold] {modified_usage}') console.print(f'[bold]Usage:[/bold] {modified_usage}')
@ -90,7 +106,9 @@ class EqHelpFormatter(BaseHelpFormatter):
"""Render the usage line with proper <index> placement for both strip and bus commands.""" """Render the usage line with proper <index> placement for both strip and bus commands."""
if usage: if usage:
modified_usage = re.sub( modified_usage = re.sub(
r'(\S+\s+)(\w+)(\s+eq\s+)(COMMAND)', r'\1\2 <index>\3\4', str(usage) r'(\S+\s+)(\w+)(\s+eq\s+)(COMMAND)',
r'\1\2 <index>\3\4 [OPTIONS]',
str(usage),
) )
console.print(f'[bold]Usage:[/bold] {modified_usage}') console.print(f'[bold]Usage:[/bold] {modified_usage}')
@ -105,9 +123,11 @@ class GainlayerHelpFormatter(BaseHelpFormatter):
if usage: if usage:
modified_usage = re.sub( modified_usage = re.sub(
r'(\S+\s+strip)(\s+gainlayer\s+)(COMMAND)', r'(\S+\s+strip)(\s+gainlayer\s+)(COMMAND)',
r'\1 <index>\2<[cyan]gainlayer_index[/cyan]> \3', r'\1 <index>\2<[cyan]gainlayer_index[/cyan]> \3 [OPTIONS] [ARGS]',
str(usage), str(usage),
) )
# Remove the duplicate GAINLAYER_INDEX that gets automatically added by cyclopts
modified_usage = re.sub(r'\s+GAINLAYER_INDEX$', '', modified_usage)
console.print(f'[bold]Usage:[/bold] {modified_usage}') console.print(f'[bold]Usage:[/bold] {modified_usage}')
@ -121,7 +141,9 @@ class CellHelpFormatter(BaseHelpFormatter):
if usage: if usage:
modified_usage = re.sub( modified_usage = re.sub(
r'(\S+\s+)(\w+)(\s+eq\s+cell\s+)(COMMAND)', r'(\S+\s+)(\w+)(\s+eq\s+cell\s+)(COMMAND)',
r'\1\2 <index>\3<[cyan]band[/cyan]> \4', r'\1\2 <index>\3<[cyan]band[/cyan]> \4 [ARGS]',
str(usage), str(usage),
) )
# Remove the duplicate BAND that gets automatically added by cyclopts
modified_usage = re.sub(r'\s+BAND$', '', modified_usage)
console.print(f'[bold]Usage:[/bold] {modified_usage}') console.print(f'[bold]Usage:[/bold] {modified_usage}')

View File

@ -3,7 +3,7 @@ from typing import Annotated
from cyclopts import App, Parameter, validators from cyclopts import App, Parameter, validators
from . import console, validation from . import validation
from .context import Context from .context import Context
from .help import BaseHelpFormatter from .help import BaseHelpFormatter
@ -13,71 +13,84 @@ app = App(name='recorder', help_formatter=BaseHelpFormatter())
@app.command(name='play') @app.command(name='play')
def play( def play(
*, *,
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Start the recorder playback.""" """Start the recorder playback."""
ctx.client.recorder.play() ctx.client.recorder.play()
console.out.print('Recorder playback started.') app.console.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') @app.command(name='pause')
def pause( def pause(
*, *,
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Pause the recorder playback.""" """Pause the recorder playback."""
ctx.client.recorder.pause() ctx.client.recorder.stop()
console.out.print('Recorder playback paused.') app.console.print('Recorder playback paused.')
@app.command(name='stop')
def stop(
*,
ctx: Annotated[Context, Parameter(parse=False)],
):
"""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.
app.console.print('Recorder stopped.')
@app.command(name='replay') @app.command(name='replay')
def replay( def replay(
*, *,
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Replay the recorder playback.""" """Replay the recorder playback."""
ctx.client.recorder.replay() ctx.client.recorder.replay()
console.out.print('Recorder playback replay started.') app.console.print('Recorder playback replay started.')
@app.command(name='record') @app.command(name='record')
def record( def record(
*, *,
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Start recording.""" """Start recording."""
ctx.client.recorder.record() ctx.client.recorder.record()
console.out.print('Recording started.') app.console.print('Recorder recording started.')
@app.command(name='pause-recording')
def pause_recording(
*,
ctx: Annotated[Context, Parameter(parse=False)],
):
"""Pause the recorder recording."""
ctx.client.recorder.pause()
app.console.print('Recorder recording paused.')
@app.command(name='ff') @app.command(name='ff')
def ff( def ff(
*, *,
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Fast forward the recorder playback.""" """Fast forward the recorder playback."""
ctx.client.recorder.ff() ctx.client.recorder.ff()
console.out.print('Recorder playback fast forwarded.') app.console.print('Recorder playback fast forwarded.')
@app.command(name='rew') @app.command(name='rew')
def rew( def rew(
*, *,
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Rewind the recorder playback.""" """Rewind the recorder playback."""
ctx.client.recorder.rew() ctx.client.recorder.rew()
console.out.print('Recorder playback rewound.') app.console.print('Recorder playback rewound.')
@app.command(name='load') @app.command(name='load')
@ -91,13 +104,13 @@ def load(
], ],
/, /,
*, *,
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Load a file into the recorder. """Load a file into the recorder.
note: This command may only work if vban-cli is running on localhost and may not work if vban-cli is running on a remote server.""" note: This command may only work if vban-cli is running on localhost and may not work if vban-cli is running on a remote server."""
ctx.client.recorder.load(file_path) ctx.client.recorder.load(file_path)
console.out.print(f'Loaded file: {file_path}') app.console.print(f'Loaded file: {file_path}')
@app.command(name='goto') @app.command(name='goto')
@ -111,8 +124,8 @@ def goto(
], ],
/, /,
*, *,
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Go to a specific timestamp in the recorder playback.""" """Go to a specific timestamp in the recorder playback."""
ctx.client.recorder.goto(time_string) ctx.client.recorder.goto(time_string)
console.out.print(f'Went to timestamp {time_string} in recorder playback.') app.console.print(f'Went to timestamp {time_string} in recorder playback.')

View File

@ -2,7 +2,7 @@ from typing import Annotated, Optional
from cyclopts import App, Argument, Parameter from cyclopts import App, Argument, Parameter
from . import comp, console, denoiser, eq, gainlayer, gate from . import comp, denoiser, eq, gainlayer, gate
from .context import Context from .context import Context
from .help import StripHelpFormatter from .help import StripHelpFormatter
@ -16,18 +16,17 @@ app.command(gainlayer.app.meta, name='gainlayer')
@app.meta.default @app.meta.default
def launcher( def launcher(
index: Annotated[int, Argument()] = None, index: Annotated[int, Argument()],
/,
*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)], *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Control the strip parameters.""" """Control the strip parameters."""
additional_kwargs = {} additional_kwargs = {}
command, bound, _ = app.parse_args(tokens) command, bound, _ = app.parse_args(tokens)
if tokens[0] == 'eq': if tokens[0] == 'eq':
additional_kwargs['eq_kind'] = app.name[0] additional_kwargs['eq_kind'] = app.name[0]
if index is not None:
additional_kwargs['index'] = index additional_kwargs['index'] = index
if ctx is not None:
additional_kwargs['ctx'] = ctx additional_kwargs['ctx'] = ctx
return command(*bound.args, **bound.kwargs, **additional_kwargs) return command(*bound.args, **bound.kwargs, **additional_kwargs)
@ -37,8 +36,8 @@ def launcher(
def mono( def mono(
new_state: Annotated[Optional[bool], Argument()] = None, new_state: Annotated[Optional[bool], Argument()] = None,
*, *,
index: Annotated[int, Parameter(show=False)] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Get or set the mono state of the specified strip. """Get or set the mono state of the specified strip.
@ -48,7 +47,7 @@ def mono(
If provided, sets the mono state to this value. If not provided, the current mono state is printed. If provided, sets the mono state to this value. If not provided, the current mono state is printed.
""" """
if new_state is None: if new_state is None:
console.out.print(ctx.client.strip[index].mono) app.console.print(ctx.client.strip[index].mono)
return return
ctx.client.strip[index].mono = new_state ctx.client.strip[index].mono = new_state
@ -57,8 +56,8 @@ def mono(
def solo( def solo(
new_state: Annotated[Optional[bool], Argument()] = None, new_state: Annotated[Optional[bool], Argument()] = None,
*, *,
index: Annotated[int, Parameter(show=False)] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Get or set the solo state of the specified strip. """Get or set the solo state of the specified strip.
@ -68,7 +67,7 @@ def solo(
If provided, sets the solo state to this value. If not provided, the current solo state is printed. If provided, sets the solo state to this value. If not provided, the current solo state is printed.
""" """
if new_state is None: if new_state is None:
console.out.print(ctx.client.strip[index].solo) app.console.print(ctx.client.strip[index].solo)
return return
ctx.client.strip[index].solo = new_state ctx.client.strip[index].solo = new_state
@ -77,8 +76,8 @@ def solo(
def mute( def mute(
new_state: Annotated[Optional[bool], Argument()] = None, new_state: Annotated[Optional[bool], Argument()] = None,
*, *,
index: Annotated[int, Parameter(show=False)] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Get or set the mute state of the specified strip. """Get or set the mute state of the specified strip.
@ -88,7 +87,7 @@ def mute(
If provided, sets the mute state to this value. If not provided, the current mute state is printed. If provided, sets the mute state to this value. If not provided, the current mute state is printed.
""" """
if new_state is None: if new_state is None:
console.out.print(ctx.client.strip[index].mute) app.console.print(ctx.client.strip[index].mute)
return return
ctx.client.strip[index].mute = new_state ctx.client.strip[index].mute = new_state
@ -97,8 +96,8 @@ def mute(
def gain( def gain(
new_value: Annotated[Optional[float], Argument()] = None, new_value: Annotated[Optional[float], Argument()] = None,
*, *,
index: Annotated[int, Parameter(show=False)] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Get or set the gain of the specified strip. """Get or set the gain of the specified strip.
@ -108,7 +107,7 @@ def gain(
If provided, sets the gain to this value. If not provided, the current gain is printed. If provided, sets the gain to this value. If not provided, the current gain is printed.
""" """
if new_value is None: if new_value is None:
console.out.print(ctx.client.strip[index].gain) app.console.print(ctx.client.strip[index].gain)
return return
ctx.client.strip[index].gain = new_value ctx.client.strip[index].gain = new_value
@ -117,8 +116,8 @@ def gain(
def a1( def a1(
new_value: Annotated[Optional[bool], Argument()] = None, new_value: Annotated[Optional[bool], Argument()] = None,
*, *,
index: Annotated[int, Parameter(show=False)] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Get or set the A1 state of the specified strip. """Get or set the A1 state of the specified strip.
@ -128,7 +127,7 @@ def a1(
If provided, sets the A1 state to this value. If not provided, the current A1 state is printed. If provided, sets the A1 state to this value. If not provided, the current A1 state is printed.
""" """
if new_value is None: if new_value is None:
console.out.print(ctx.client.strip[index].A1) app.console.print(ctx.client.strip[index].A1)
return return
ctx.client.strip[index].A1 = new_value ctx.client.strip[index].A1 = new_value
@ -137,8 +136,8 @@ def a1(
def a2( def a2(
new_value: Annotated[Optional[bool], Argument()] = None, new_value: Annotated[Optional[bool], Argument()] = None,
*, *,
index: Annotated[int, Parameter(show=False)] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Get or set the A2 state of the specified strip. """Get or set the A2 state of the specified strip.
@ -148,7 +147,7 @@ def a2(
If provided, sets the A2 state to this value. If not provided, the current A2 state is printed. If provided, sets the A2 state to this value. If not provided, the current A2 state is printed.
""" """
if new_value is None: if new_value is None:
console.out.print(ctx.client.strip[index].A2) app.console.print(ctx.client.strip[index].A2)
return return
ctx.client.strip[index].A2 = new_value ctx.client.strip[index].A2 = new_value
@ -157,8 +156,8 @@ def a2(
def a3( def a3(
new_value: Annotated[Optional[bool], Argument()] = None, new_value: Annotated[Optional[bool], Argument()] = None,
*, *,
index: Annotated[int, Parameter(show=False)] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Get or set the A3 state of the specified strip. """Get or set the A3 state of the specified strip.
@ -168,7 +167,7 @@ def a3(
If provided, sets the A3 state to this value. If not provided, the current A3 state is printed. If provided, sets the A3 state to this value. If not provided, the current A3 state is printed.
""" """
if new_value is None: if new_value is None:
console.out.print(ctx.client.strip[index].A3) app.console.print(ctx.client.strip[index].A3)
return return
ctx.client.strip[index].A3 = new_value ctx.client.strip[index].A3 = new_value
@ -177,8 +176,8 @@ def a3(
def a4( def a4(
new_value: Annotated[Optional[bool], Argument()] = None, new_value: Annotated[Optional[bool], Argument()] = None,
*, *,
index: Annotated[int, Parameter(show=False)] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Get or set the A4 state of the specified strip. """Get or set the A4 state of the specified strip.
@ -188,7 +187,7 @@ def a4(
If provided, sets the A4 state to this value. If not provided, the current A4 state is printed. If provided, sets the A4 state to this value. If not provided, the current A4 state is printed.
""" """
if new_value is None: if new_value is None:
console.out.print(ctx.client.strip[index].A4) app.console.print(ctx.client.strip[index].A4)
return return
ctx.client.strip[index].A4 = new_value ctx.client.strip[index].A4 = new_value
@ -197,8 +196,8 @@ def a4(
def a5( def a5(
new_value: Annotated[Optional[bool], Argument()] = None, new_value: Annotated[Optional[bool], Argument()] = None,
*, *,
index: Annotated[int, Parameter(show=False)] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Get or set the A5 state of the specified strip. """Get or set the A5 state of the specified strip.
@ -208,7 +207,7 @@ def a5(
If provided, sets the A5 state to this value. If not provided, the current A5 state is printed. If provided, sets the A5 state to this value. If not provided, the current A5 state is printed.
""" """
if new_value is None: if new_value is None:
console.out.print(ctx.client.strip[index].A5) app.console.print(ctx.client.strip[index].A5)
return return
ctx.client.strip[index].A5 = new_value ctx.client.strip[index].A5 = new_value
@ -217,8 +216,8 @@ def a5(
def b1( def b1(
new_value: Annotated[Optional[bool], Argument()] = None, new_value: Annotated[Optional[bool], Argument()] = None,
*, *,
index: Annotated[int, Parameter(show=False)] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Get or set the B1 state of the specified strip. """Get or set the B1 state of the specified strip.
@ -228,7 +227,7 @@ def b1(
If provided, sets the B1 state to this value. If not provided, the current B1 state is printed. If provided, sets the B1 state to this value. If not provided, the current B1 state is printed.
""" """
if new_value is None: if new_value is None:
console.out.print(ctx.client.strip[index].B1) app.console.print(ctx.client.strip[index].B1)
return return
ctx.client.strip[index].B1 = new_value ctx.client.strip[index].B1 = new_value
@ -237,8 +236,8 @@ def b1(
def b2( def b2(
new_value: Annotated[Optional[bool], Argument()] = None, new_value: Annotated[Optional[bool], Argument()] = None,
*, *,
index: Annotated[int, Parameter(show=False)] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Get or set the B2 state of the specified strip. """Get or set the B2 state of the specified strip.
@ -248,7 +247,7 @@ def b2(
If provided, sets the B2 state to this value. If not provided, the current B2 state is printed. If provided, sets the B2 state to this value. If not provided, the current B2 state is printed.
""" """
if new_value is None: if new_value is None:
console.out.print(ctx.client.strip[index].B2) app.console.print(ctx.client.strip[index].B2)
return return
ctx.client.strip[index].B2 = new_value ctx.client.strip[index].B2 = new_value
@ -257,8 +256,8 @@ def b2(
def b3( def b3(
new_value: Annotated[Optional[bool], Argument()] = None, new_value: Annotated[Optional[bool], Argument()] = None,
*, *,
index: Annotated[int, Parameter(show=False)] = None, index: Annotated[int, Parameter(parse=False)],
ctx: Annotated[Context, Parameter(show=False)] = None, ctx: Annotated[Context, Parameter(parse=False)],
): ):
"""Get or set the B3 state of the specified strip. """Get or set the B3 state of the specified strip.
@ -268,6 +267,6 @@ def b3(
If provided, sets the B3 state to this value. If not provided, the current B3 state is printed. If provided, sets the B3 state to this value. If not provided, the current B3 state is printed.
""" """
if new_value is None: if new_value is None:
console.out.print(ctx.client.strip[index].B3) app.console.print(ctx.client.strip[index].B3)
return return
ctx.client.strip[index].B3 = new_value ctx.client.strip[index].B3 = new_value

View File

@ -1,9 +1,11 @@
import re import re
from .error import VbanCLIValidationError
def is_valid_time_string(type_, value: str) -> str: def is_valid_time_string(type_, value: str) -> str:
"""Validate if the given string is a valid time format (HH:MM:SS).""" """Validate if the given string is a valid time format (HH:MM:SS)."""
pattern = r'^(?:[01]\d|2[0123]):(?:[012345]\d):(?:[012345]\d)$' pattern = r'^(?:[01]\d|2[0123]):(?:[012345]\d):(?:[012345]\d)$'
if not re.match(pattern, value): if not re.match(pattern, value):
raise ValueError('Invalid time format. Expected HH:MM:SS.') raise VbanCLIValidationError('Invalid time format. Expected HH:MM:SS.')
return value return value

4
uv.lock generated
View File

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