mirror of
https://github.com/onyx-and-iris/vban-cli.git
synced 2026-04-16 02:23:30 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27290e1a0e | |||
| 6188da4f51 | |||
| 230e537414 | |||
| e6ebf86c86 | |||
| 1a0fb979e0 | |||
| 080e26f75f | |||
| f6d82c5064 | |||
| 627ada3b09 | |||
| f389eb53b8 | |||
| 341c81fde1 | |||
| e062da51ed | |||
| c82a021708 |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -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 ##
|
||||
|
||||
38
README.md
38
README.md
@@ -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]
|
||||
@@ -117,14 +135,30 @@ vban-cli command restart
|
||||
|
||||
see `vban-cli command --help` for more info.
|
||||
|
||||
### Recorder Command
|
||||
|
||||
Usage: vban-cli recorder COMMAND
|
||||
|
||||
examples:
|
||||
|
||||
```console
|
||||
vban-cli recorder play
|
||||
|
||||
vban-cli recorder rew
|
||||
|
||||
vban-cli recorder replay
|
||||
```
|
||||
|
||||
see `vban-cli recorder --help` for more info.
|
||||
|
||||
### Sendtext Command
|
||||
|
||||
Usage: vban-cli sendtext TEXT
|
||||
|
||||
*To Voicemeeter*
|
||||
|
||||
examples:
|
||||
|
||||
*To Voicemeeter*
|
||||
|
||||
```console
|
||||
vban-cli sendtext 'Strip[0].Mute=1;Bus[0].Mono=2'
|
||||
```
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
[project]
|
||||
name = "vban-cli"
|
||||
version = "0.6.0"
|
||||
version = "0.9.0"
|
||||
description = "A command-line interface for Voicemeeter leveraging VBAN."
|
||||
readme = "README.md"
|
||||
license = { text = "LICENSE" }
|
||||
requires-python = ">=3.13"
|
||||
dependencies = ["cyclopts>=4.6.0", "loguru>=0.7.3", "vban-cmd>=2.7.1"]
|
||||
dependencies = ["cyclopts>=4.6.0", "loguru>=0.7.3", "vban-cmd>=2.8.1"]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Programming Language :: Python",
|
||||
|
||||
@@ -5,7 +5,7 @@ import vban_cmd
|
||||
from cyclopts import App, Argument, Parameter, config
|
||||
|
||||
from . import __version__ as version
|
||||
from . import bus, command, console, strip
|
||||
from . import bus, command, console, recorder, strip
|
||||
from .context import Context
|
||||
|
||||
app = App(
|
||||
@@ -17,6 +17,7 @@ app = App(
|
||||
app.command(strip.app.meta, name='strip')
|
||||
app.command(bus.app.meta, name='bus')
|
||||
app.command(command.app, name='command')
|
||||
app.command(recorder.app, name='recorder')
|
||||
app.register_install_completion_command()
|
||||
|
||||
|
||||
|
||||
@@ -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
51
src/vban_cli/gainlayer.py
Normal 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
|
||||
@@ -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}')
|
||||
|
||||
118
src/vban_cli/recorder.py
Normal file
118
src/vban_cli/recorder.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
from cyclopts import App, Parameter, validators
|
||||
|
||||
from . import console, validation
|
||||
from .context import Context
|
||||
from .help import BaseHelpFormatter
|
||||
|
||||
app = App(name='recorder', help_formatter=BaseHelpFormatter())
|
||||
|
||||
|
||||
@app.command(name='play')
|
||||
def play(
|
||||
*,
|
||||
ctx: Annotated[Context, Parameter(show=False)] = None,
|
||||
):
|
||||
"""Start the recorder playback."""
|
||||
ctx.client.recorder.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()
|
||||
console.out.print('Recorder playback paused.')
|
||||
|
||||
|
||||
@app.command(name='replay')
|
||||
def replay(
|
||||
*,
|
||||
ctx: Annotated[Context, Parameter(show=False)] = None,
|
||||
):
|
||||
"""Replay the recorder playback."""
|
||||
ctx.client.recorder.replay()
|
||||
console.out.print('Recorder playback replay started.')
|
||||
|
||||
|
||||
@app.command(name='record')
|
||||
def record(
|
||||
*,
|
||||
ctx: Annotated[Context, Parameter(show=False)] = None,
|
||||
):
|
||||
"""Start recording."""
|
||||
ctx.client.recorder.record()
|
||||
console.out.print('Recording started.')
|
||||
|
||||
|
||||
@app.command(name='ff')
|
||||
def ff(
|
||||
*,
|
||||
ctx: Annotated[Context, Parameter(show=False)] = None,
|
||||
):
|
||||
"""Fast forward the recorder playback."""
|
||||
ctx.client.recorder.ff()
|
||||
console.out.print('Recorder playback fast forwarded.')
|
||||
|
||||
|
||||
@app.command(name='rew')
|
||||
def rew(
|
||||
*,
|
||||
ctx: Annotated[Context, Parameter(show=False)] = None,
|
||||
):
|
||||
"""Rewind the recorder playback."""
|
||||
ctx.client.recorder.rew()
|
||||
console.out.print('Recorder playback rewound.')
|
||||
|
||||
|
||||
@app.command(name='load')
|
||||
def load(
|
||||
file_path: Annotated[
|
||||
Path,
|
||||
Parameter(
|
||||
help='The path to the recording file to load.',
|
||||
validator=validators.Path(exists=True),
|
||||
),
|
||||
],
|
||||
/,
|
||||
*,
|
||||
ctx: Annotated[Context, Parameter(show=False)] = None,
|
||||
):
|
||||
"""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."""
|
||||
ctx.client.recorder.load(file_path)
|
||||
console.out.print(f'Loaded file: {file_path}')
|
||||
|
||||
|
||||
@app.command(name='goto')
|
||||
def goto(
|
||||
time_string: Annotated[
|
||||
str,
|
||||
Parameter(
|
||||
help='The timestamp to go to in the recorder playback (format: HH:MM:SS).',
|
||||
validator=validation.is_valid_time_string,
|
||||
),
|
||||
],
|
||||
/,
|
||||
*,
|
||||
ctx: Annotated[Context, Parameter(show=False)] = None,
|
||||
):
|
||||
"""Go to a specific timestamp in the recorder playback."""
|
||||
ctx.client.recorder.goto(time_string)
|
||||
console.out.print(f'Went to timestamp {time_string} in recorder playback.')
|
||||
@@ -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
|
||||
|
||||
9
src/vban_cli/validation.py
Normal file
9
src/vban_cli/validation.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import re
|
||||
|
||||
|
||||
def is_valid_time_string(type_, value: str) -> str:
|
||||
"""Validate if the given string is a valid time format (HH:MM:SS)."""
|
||||
pattern = r'^(?:[01]\d|2[0123]):(?:[012345]\d):(?:[012345]\d)$'
|
||||
if not re.match(pattern, value):
|
||||
raise ValueError('Invalid time format. Expected HH:MM:SS.')
|
||||
return value
|
||||
4
uv.lock
generated
4
uv.lock
generated
@@ -124,7 +124,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "vban-cli"
|
||||
version = "0.6.0"
|
||||
version = "0.9.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "cyclopts" },
|
||||
@@ -141,7 +141,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "vban-cmd"
|
||||
version = "2.7.1"
|
||||
version = "2.8.0"
|
||||
source = { editable = "../vban-cmd-python" }
|
||||
|
||||
[package.metadata]
|
||||
|
||||
Reference in New Issue
Block a user