Compare commits

...

4 Commits
v2.7.1 ... dev

Author SHA1 Message Date
23b99cb66b perform some path magic so Voicemeeter receives the entire path
patch bump
2026-03-01 21:29:09 +00:00
2fd7b8ad8b minor bump 2026-03-01 21:13:38 +00:00
c851cb5abe add Recorder section to README 2026-03-01 21:11:48 +00:00
dc681f50d0 add Recorder
add it to banana+potato
2026-03-01 21:10:10 +00:00
4 changed files with 183 additions and 5 deletions

View File

@ -349,6 +349,40 @@ vban.strip[0].fadeto(-10.3, 1000)
vban.bus[3].fadeby(-5.6, 500)
```
### Recorder
The following methods are available
- `play()`
- `stop()`
- `pause()`
- `record()`
- `ff()`
- `rew()`
- `load(filepath)`: raw string
- `goto(time_string)`: time string in format `hh:mm:ss`
The following properties are available
- `samplerate`: int, (22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000)
- `bitresolution`: int, (8, 16, 24, 32)
- `channel`: int, from 1 to 8
- `kbps`: int, (32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320)
- `gain`: float, from -60.0 to 12.0
example:
```python
vban.recorder.play()
vban.recorder.stop()
# filepath as raw string
vban.recorder.load(r'C:\music\mytune.mp3')
# set the goto time to 1m 30s
vban.recorder.goto('00:01:30')
```
### Command
Certain 'special' commands are defined by the API as performing actions rather than setting values. The following methods are available:

View File

@ -1,6 +1,6 @@
[project]
name = "vban-cmd"
version = "2.7.1"
version = "2.8.1"
description = "Python interface for the VBAN RT Packet Service (Sendtext)"
authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }]
license = { text = "MIT" }
@ -9,7 +9,7 @@ requires-python = ">=3.10"
dependencies = ["tomli (>=2.0.1,<3.0) ; python_version < '3.11'"]
[tool.poetry.requires-plugins]
poethepoet = "^0.35.0"
poethepoet = ">=0.42.0"
[tool.poetry.group.dev.dependencies]
pytest = "^8.3.4"

View File

@ -11,6 +11,7 @@ from .error import VBANCMDError
from .kinds import KindMapClass
from .kinds import request_kind_map as kindmap
from .macrobutton import MacroButton
from .recorder import Recorder
from .strip import request_strip_obj as strip
from .vban import request_vban_obj as vban
from .vbancmd import VbanCmd
@ -26,7 +27,7 @@ class FactoryBuilder:
"""
BuilderProgress = IntEnum(
'BuilderProgress', 'strip bus command macrobutton vban', start=0
'BuilderProgress', 'strip bus command macrobutton vban recorder', start=0
)
def __init__(self, factory, kind: KindMapClass):
@ -38,6 +39,7 @@ class FactoryBuilder:
f'Finished building commands for {self._factory}',
f'Finished building macrobuttons for {self._factory}',
f'Finished building vban in/out streams for {self._factory}',
f'Finished building recorder for {self._factory}',
)
self.logger = logger.getChild(self.__class__.__name__)
@ -72,6 +74,10 @@ class FactoryBuilder:
self._factory.vban = vban(self._factory)
return self
def make_recorder(self):
self._factory.recorder = Recorder.make(self._factory)
return self
class FactoryBase(VbanCmd):
"""Base class for factories, subclasses VbanCmd."""
@ -166,7 +172,7 @@ class BananaFactory(FactoryBase):
@property
def steps(self) -> Iterable:
"""steps required to build the interface for a kind"""
return self._steps
return self._steps + (self.builder.make_recorder,)
class PotatoFactory(FactoryBase):
@ -188,7 +194,7 @@ class PotatoFactory(FactoryBase):
@property
def steps(self) -> Iterable:
"""steps required to build the interface for a kind"""
return self._steps
return self._steps + (self.builder.make_recorder,)
def vbancmd_factory(kind_id: str, **kwargs) -> VbanCmd:

138
vban_cmd/recorder.py Normal file
View File

@ -0,0 +1,138 @@
import os
import re
from .error import VBANCMDError
from .iremote import IRemote
from .meta import action_fn
class Recorder(IRemote):
"""
Implements the common interface
Defines concrete implementation for recorder
"""
@classmethod
def make(cls, remote):
"""
Factory function for recorder class.
Returns a Recorder class of a kind.
"""
Recorder_cls = type(
f'Recorder{remote.kind}',
(cls,),
{
**{
param: action_fn(param)
for param in [
'play',
'stop',
'pause',
'replay',
'record',
'ff',
'rew',
]
},
},
)
return Recorder_cls(remote)
def __str__(self):
return f'{type(self).__name__}'
@property
def identifier(self) -> str:
return 'recorder'
@property
def samplerate(self) -> int:
return
@samplerate.setter
def samplerate(self, val: int):
opts = (22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000)
if val not in opts:
self.logger.warning(f'samplerate got: {val} but expected a value in {opts}')
self.setter('samplerate', val)
@property
def bitresolution(self) -> int:
return
@bitresolution.setter
def bitresolution(self, val: int):
opts = (8, 16, 24, 32)
if val not in opts:
self.logger.warning(
f'bitresolution got: {val} but expected a value in {opts}'
)
self.setter('bitresolution', val)
@property
def channel(self) -> int:
return
@channel.setter
def channel(self, val: int):
if not 1 <= val <= 8:
self.logger.warning(f'channel got: {val} but expected a value from 1 to 8')
self.setter('channel', val)
@property
def kbps(self):
return
@kbps.setter
def kbps(self, val: int):
opts = (32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320)
if val not in opts:
self.logger.warning(f'kbps got: {val} but expected a value in {opts}')
self.setter('kbps', val)
@property
def gain(self) -> float:
return
@gain.setter
def gain(self, val: float):
self.setter('gain', val)
def load(self, file: os.PathLike):
try:
# Convert to string, use forward slashes, and wrap in quotes for spaces
file_path = f'"{os.fspath(file).replace(chr(92), "/")}"'
self.setter('load', file_path)
except UnicodeError:
raise VBANCMDError('File full directory must be a raw string')
def goto(self, time_str):
def get_sec():
"""Get seconds from time string"""
h, m, s = time_str.split(':')
return int(h) * 3600 + int(m) * 60 + int(s)
time_str = str(time_str) # coerce the type
if (
re.match(
r'^(?:[01]\d|2[0123]):(?:[012345]\d):(?:[012345]\d)$',
time_str,
)
is not None
):
self.setter('goto', get_sec())
else:
self.logger.warning(
"goto expects a string that matches the format 'hh:mm:ss'"
)
def filetype(self, val: str):
opts = {'wav': 1, 'aiff': 2, 'bwf': 3, 'mp3': 100}
try:
self.setter('filetype', opts[val.lower()])
except KeyError:
self.logger.warning(
f'filetype got: {val} but expected a value in {list(opts.keys())}'
)