mirror of
https://github.com/onyx-and-iris/voicemeeter-api-python.git
synced 2026-04-06 23:43:30 +00:00
Compare commits
41 Commits
5b99f8aae3
...
add-button
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d5c611ed8 | |||
| 46e7ffe478 | |||
| bf77ded007 | |||
| 236125d095 | |||
| 7841dfe10f | |||
| bdf8dc489a | |||
| 160a6f89f9 | |||
| 4fcb2f93ca | |||
| 8acd0b1385 | |||
| 89866bb87b | |||
| f996fc0d9c | |||
| 68177c3c6c | |||
| 54a1938694 | |||
| 9a4205ce64 | |||
| 9b2e38aab3 | |||
| 278566c2e0 | |||
| b0acde6a52 | |||
| 07b04d16d8 | |||
| f854ec7875 | |||
| 5640f54e65 | |||
| 4569e8c760 | |||
| 5e39461966 | |||
| 6de78a4037 | |||
| bafaa58507 | |||
| af368b4b0a | |||
| 32527e37bd | |||
| c21b04e1a8 | |||
| 76960f36d0 | |||
| 2849b37670 | |||
| 7732a26c40 | |||
| c1e23ab250 | |||
| c2daba1a62 | |||
| 3036cdff2f | |||
| b02f3af665 | |||
| 145f85b7cd | |||
| 71f77b7830 | |||
| 4415851816 | |||
| 8b63cbfe8d | |||
| de4ce850eb | |||
| ee3fa0a372 | |||
| f92bb1e457 |
48
CHANGELOG.md
48
CHANGELOG.md
@@ -11,6 +11,53 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
|
||||
|
||||
- [x]
|
||||
|
||||
## [2.3.2] - 2023-07-12
|
||||
|
||||
### Added
|
||||
|
||||
- vban.{instream,outstream} tuples now contain classes that represent MIDI and TEXT streams.
|
||||
|
||||
### Fixed
|
||||
|
||||
- apply_config() now performs a deep merge when extending a config with another.
|
||||
|
||||
## [2.3.0] - 2023-07-11
|
||||
|
||||
### Added
|
||||
|
||||
- user configs may now extend other user configs. check `config extends` section in README.
|
||||
|
||||
## [2.2.0] - 2023-07-10
|
||||
|
||||
### Added
|
||||
|
||||
- CAPIError class now stores fn_name, error code and message as class attributes.
|
||||
|
||||
### Changed
|
||||
|
||||
- macrobutton capi calls now use error code -9 on AttributeError (using an old version of the API).
|
||||
|
||||
### Fixed
|
||||
|
||||
- call to `self.vm_get_midi_message` now wrapped by {CBindings}.call.
|
||||
|
||||
## [2.1.1] - 2023-07-01
|
||||
|
||||
### Added
|
||||
|
||||
- RecorderMode added to Recorder class. See Recorder section in README for new properties and methods.
|
||||
- recorder.loop is now a forwarder method for recorder.mode.loop for backwards compatibility
|
||||
|
||||
- RecorderArmStrip, RecorderArmBus mixed into Recorder class.
|
||||
|
||||
### Removed
|
||||
|
||||
- Recorder.loop removed from documentation
|
||||
|
||||
### Changed
|
||||
|
||||
- When out of bounds values are passed, log warnings instead of raising Errors. See [Issue #6][Issue 6].
|
||||
|
||||
## [2.0.0] - 2023-06-25
|
||||
|
||||
Where possible I've attempted to make the changes backwards compatible. The breaking changes affect two higher classes, Strip and Bus, as well as the behaviour of events. All other changes are additive or QOL aimed at giving more options to the developer. For example, every low-level CAPI call is now logged and error raised on Exception, you can now register callback functions as well as observer classes, extra examples to demonstrate different use cases etc.
|
||||
@@ -363,3 +410,4 @@ I will move this commit to a separate branch in preparation for version 2.0.
|
||||
- project packaged with poetry and added to pypi.
|
||||
|
||||
[issue 4]: https://github.com/onyx-and-iris/voicemeeter-api-python/issues/4
|
||||
[Issue 6]: https://github.com/onyx-and-iris/voicemeeter-api-python/issues/6
|
||||
|
||||
123
README.md
123
README.md
@@ -380,12 +380,13 @@ The following properties are available.
|
||||
- `state`: boolean
|
||||
- `stateonly`: boolean
|
||||
- `trigger`: boolean
|
||||
- `color`: int, from 0 to 8
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vm.button[37].state = True
|
||||
vm.button[55].trigger = False
|
||||
vm.button[4].color = 1
|
||||
```
|
||||
|
||||
### Recorder
|
||||
@@ -398,13 +399,19 @@ The following methods are available
|
||||
- `record()`
|
||||
- `ff()`
|
||||
- `rew()`
|
||||
- `load(<filepath>)`: string
|
||||
- `load(filepath)`: raw string
|
||||
- `goto(time_string)`: time string in format `hh:mm:ss`
|
||||
- `filetype(filetype)`: string, ("wav", "aiff", "bwf", "mp3")
|
||||
|
||||
The following properties are available
|
||||
|
||||
- `loop`: boolean
|
||||
- `A1 - A5`: boolean
|
||||
- `B1 - A3`: boolean
|
||||
- `B1 - B3`: boolean
|
||||
- `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:
|
||||
|
||||
@@ -412,17 +419,46 @@ example:
|
||||
vm.recorder.play()
|
||||
vm.recorder.stop()
|
||||
|
||||
# Enable loop play
|
||||
vm.recorder.loop = True
|
||||
|
||||
# Disable recorder out channel B2
|
||||
vm.recorder.B2 = False
|
||||
|
||||
# filepath as raw string
|
||||
vm.recorder.load(r'C:\music\mytune.mp3')
|
||||
|
||||
# set the goto time to 1m 30s
|
||||
vm.recorder.goto("00:01:30")
|
||||
```
|
||||
|
||||
Recorder properties are defined as write only.
|
||||
#### Recorder.Mode
|
||||
|
||||
The following properties are available
|
||||
|
||||
- `recbus`: boolean
|
||||
- `playonload`: boolean
|
||||
- `loop`: boolean
|
||||
- `multitrack`: boolean
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
# Enable loop play
|
||||
vm.recorder.mode.loop = True
|
||||
```
|
||||
|
||||
#### Recorder.ArmStrip[i]|ArmBus[i]
|
||||
|
||||
The following method is available
|
||||
|
||||
- `set(val)`: boolean
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
# Arm strip 3
|
||||
vm.recorder.armstrip[3].set(True)
|
||||
# Arm bus 0
|
||||
vm.recorder.armbus[0].set(True)
|
||||
```
|
||||
|
||||
### VBAN
|
||||
|
||||
@@ -594,7 +630,9 @@ vm.option.sr = 48000
|
||||
|
||||
The following methods are available:
|
||||
|
||||
- `buffer(driver, buffer)` : Set buffer size for particular audio driver.
|
||||
- `buffer(driver, buf)` : Set buffer size for particular audio driver.
|
||||
- buf: int, from 128 to 2048
|
||||
- driver:str, ("mme", "wdm", "ks", "asio")
|
||||
|
||||
example:
|
||||
|
||||
@@ -602,10 +640,6 @@ example:
|
||||
vm.option.buffer("wdm", 512)
|
||||
```
|
||||
|
||||
driver defined as one of ("mme", "wdm", "ks", "asio")
|
||||
|
||||
buffer, from 128 to 2048
|
||||
|
||||
##### delay[i]
|
||||
|
||||
- `get()`: int
|
||||
@@ -658,17 +692,17 @@ vm.apply(
|
||||
Or for each class you may do:
|
||||
|
||||
```python
|
||||
vm.strip[0].apply(mute: True, gain: 3.2, A1: True)
|
||||
vm.vban.outstream[0].apply(on: True, name: 'streamname', bit: 24)
|
||||
vm.strip[0].apply({"mute": True, "gain": 3.2, "A1": True})
|
||||
vm.vban.outstream[0].apply({"on": True, "name": "streamname", "bit": 24})
|
||||
```
|
||||
|
||||
## Config Files
|
||||
|
||||
`vm.apply_config(<configname>)`
|
||||
`vm.apply_config(configname)`
|
||||
|
||||
You may load config files in TOML format.
|
||||
Three example configs have been included with the package. Remember to save
|
||||
current settings before loading a user config. To set one you may do:
|
||||
current settings before loading a user config. To load one you may do:
|
||||
|
||||
```python
|
||||
import voicemeeterlib
|
||||
@@ -676,7 +710,26 @@ with voicemeeterlib.api('banana') as vm:
|
||||
vm.apply_config('example')
|
||||
```
|
||||
|
||||
will load a user config file at configs/banana/example.toml for Voicemeeter Banana.
|
||||
Your configs may be located in one of the following paths:
|
||||
- \<current working directory\> / "configs" / kind_id
|
||||
- \<user home directory\> / ".config" / "voicemeeter" / kind_id
|
||||
- \<user home directory\> / "Documents" / "Voicemeeter" / "configs" / kind_id
|
||||
|
||||
If a config with the same name is located in multiple locations, only the first one found is loaded into memory, in the above order.
|
||||
|
||||
#### `config extends`
|
||||
|
||||
You may also load a config that extends another config with overrides or additional parameters.
|
||||
|
||||
You just need to define a key `extends` in the config TOML, that names the config to be extended.
|
||||
|
||||
Three example 'extender' configs are included with the repo. You may load them with:
|
||||
|
||||
```python
|
||||
import voicemeeterlib
|
||||
with voicemeeterlib.api('banana') as vm:
|
||||
vm.apply_config('extender')
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
@@ -688,7 +741,7 @@ example:
|
||||
import voicemeeterlib
|
||||
# Set event updates to occur every 50ms
|
||||
# Listen for level updates only
|
||||
with voicemeeterlib.api('banana', ratelimit=0.05, ldirty=True}) as vm:
|
||||
with voicemeeterlib.api('banana', ratelimit=0.05, ldirty=True) as vm:
|
||||
...
|
||||
```
|
||||
|
||||
@@ -775,19 +828,42 @@ vm.set('Strip[0].Gain', -3.6)
|
||||
|
||||
Access to lower level polling functions are provided with the following property objects:
|
||||
|
||||
#### `vm.pdirty`
|
||||
##### `vm.pdirty`
|
||||
|
||||
True iff a parameter has been updated.
|
||||
|
||||
#### `vm.mdirty`
|
||||
##### `vm.mdirty`
|
||||
|
||||
True iff a macrobutton has been updated.
|
||||
|
||||
#### `vm.ldirty`
|
||||
##### `vm.ldirty`
|
||||
|
||||
True iff a level has been updated.
|
||||
|
||||
|
||||
### Errors
|
||||
|
||||
- `errors.VMError`: Exception raised when general errors occur.
|
||||
- `errors.InstallError`: Exception raised when installation errors occur.
|
||||
- `errors.CAPIError`: Exception raised when the C-API returns error values.
|
||||
- Error codes are stored in {Exception Class}.code. For a full list of error codes [check the VoicemeeterRemote header file][Voicemeeter Remote Header].
|
||||
|
||||
|
||||
### Logging
|
||||
|
||||
It's possible to see the messages sent by the interface's setters and getters, may be useful for debugging.
|
||||
|
||||
example:
|
||||
```python
|
||||
import voicemeeterlib
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
with voicemeeterlib.api("banana") as vm:
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
### Run tests
|
||||
|
||||
To run all tests:
|
||||
@@ -799,3 +875,6 @@ pytest -v
|
||||
### Official Documentation
|
||||
|
||||
- [Voicemeeter Remote C API](https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/update-docs/VoicemeeterRemoteAPI.pdf)
|
||||
|
||||
|
||||
[Voicemeeter Remote Header]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/update-docs/VoicemeeterRemote.h
|
||||
12
configs/banana/extender.toml
Normal file
12
configs/banana/extender.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
extends = "example"
|
||||
[strip-0]
|
||||
label = "strip0_extended"
|
||||
A1 = false
|
||||
gain = 0.0
|
||||
|
||||
[bus-0]
|
||||
label = "bus0_extended"
|
||||
mute = false
|
||||
|
||||
[vban-in-3]
|
||||
name = "vban_extended"
|
||||
12
configs/basic/extender.toml
Normal file
12
configs/basic/extender.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
extends = "example"
|
||||
[strip-0]
|
||||
label = "strip0_extended"
|
||||
A1 = false
|
||||
gain = 0.0
|
||||
|
||||
[bus-0]
|
||||
label = "bus0_extended"
|
||||
mute = false
|
||||
|
||||
[vban-in-3]
|
||||
name = "vban_extended"
|
||||
12
configs/potato/extender.toml
Normal file
12
configs/potato/extender.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
extends = "example"
|
||||
[strip-0]
|
||||
label = "strip0_extended"
|
||||
A1 = false
|
||||
gain = 0.0
|
||||
|
||||
[bus-0]
|
||||
label = "bus0_extended"
|
||||
mute = false
|
||||
|
||||
[vban-in-3]
|
||||
name = "vban_extended"
|
||||
@@ -63,8 +63,6 @@ class Parser:
|
||||
|
||||
def interactive_mode(parser):
|
||||
while cmd := input("Please enter command (Press <Enter> to exit)\n"):
|
||||
if not cmd:
|
||||
break
|
||||
if res := parser.parse((cmd,)):
|
||||
print(res)
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ class App:
|
||||
|
||||
def __enter__(self):
|
||||
self.vm.init_thread()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.vm.end_thread()
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
[tool.poetry]
|
||||
name = "voicemeeter-api"
|
||||
version = "2.0.2"
|
||||
version = "2.3.6"
|
||||
description = "A Python wrapper for the Voiceemeter API"
|
||||
authors = ["onyx-and-iris <code@onyxandiris.online>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/onyx-and-iris/voicemeeter-api-python"
|
||||
|
||||
packages = [
|
||||
{ include = "voicemeeterlib" },
|
||||
]
|
||||
packages = [{ include = "voicemeeterlib" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
|
||||
29
scripts.py
29
scripts.py
@@ -1,40 +1,41 @@
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def ex_dsl():
|
||||
path = Path.cwd() / "examples" / "dsl" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
scriptpath = Path.cwd() / "examples" / "dsl" / "."
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def ex_events():
|
||||
path = Path.cwd() / "examples" / "events" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
scriptpath = Path.cwd() / "examples" / "events" / "."
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def ex_gui():
|
||||
path = Path.cwd() / "examples" / "gui" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
scriptpath = Path.cwd() / "examples" / "gui" / "."
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def ex_levels():
|
||||
path = Path.cwd() / "examples" / "levels" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
scriptpath = Path.cwd() / "examples" / "levels" / "."
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def ex_midi():
|
||||
path = Path.cwd() / "examples" / "midi" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
scriptpath = Path.cwd() / "examples" / "midi" / "."
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def ex_obs():
|
||||
path = Path.cwd() / "examples" / "obs" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
scriptpath = Path.cwd() / "examples" / "obs" / "."
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def ex_observer():
|
||||
path = Path.cwd() / "examples" / "observer" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
scriptpath = Path.cwd() / "examples" / "observer" / "."
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def test():
|
||||
|
||||
@@ -6,36 +6,50 @@ import voicemeeterlib
|
||||
from voicemeeterlib.kinds import KindId
|
||||
from voicemeeterlib.kinds import request_kind_map as kindmap
|
||||
|
||||
# let's keep things random
|
||||
KIND_ID = random.choice(tuple(kind_id.name.lower() for kind_id in KindId))
|
||||
vm = voicemeeterlib.api(KIND_ID)
|
||||
kind = kindmap(KIND_ID)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Data:
|
||||
"""bounds data to map tests to a kind"""
|
||||
|
||||
name: str = kind.name
|
||||
phys_in: int = kind.ins[0] - 1
|
||||
virt_in: int = kind.ins[0] + kind.ins[-1] - 1
|
||||
phys_out: int = kind.outs[0] - 1
|
||||
virt_out: int = kind.outs[0] + kind.outs[-1] - 1
|
||||
vban_in: int = kind.vban[0] - 1
|
||||
vban_out: int = kind.vban[-1] - 1
|
||||
button_lower: int = 0
|
||||
button_upper: int = 79
|
||||
asio_in: int = kind.asio[0] - 1
|
||||
asio_out: int = kind.asio[-1] - 1
|
||||
insert_lower: int = 0
|
||||
insert_higher: int = kind.insert - 1
|
||||
name: str
|
||||
phys_in: int
|
||||
virt_in: int
|
||||
phys_out: int
|
||||
virt_out: int
|
||||
vban_in: int
|
||||
vban_out: int
|
||||
button_lower: int
|
||||
button_upper: int
|
||||
asio_in: int
|
||||
asio_out: int
|
||||
insert_lower: int
|
||||
insert_higher: int
|
||||
|
||||
@property
|
||||
def channels(self):
|
||||
return (2 * self.phys_in) + (8 * self.virt_in)
|
||||
|
||||
|
||||
data = Data()
|
||||
# let's keep things random
|
||||
KIND_ID = random.choice(tuple(kind_id.name.lower() for kind_id in KindId))
|
||||
vm = voicemeeterlib.api(KIND_ID)
|
||||
kind = kindmap(KIND_ID)
|
||||
|
||||
data = Data(
|
||||
name=kind.name,
|
||||
phys_in=kind.ins[0] - 1,
|
||||
virt_in=kind.ins[0] + kind.ins[-1] - 1,
|
||||
phys_out=kind.outs[0] - 1,
|
||||
virt_out=kind.outs[0] + kind.outs[-1] - 1,
|
||||
vban_in=kind.vban[0] - 1,
|
||||
vban_out=kind.vban[-1] - 1,
|
||||
button_lower=0,
|
||||
button_upper=79,
|
||||
asio_in=kind.asio[0] - 1,
|
||||
asio_out=kind.asio[-1] - 1,
|
||||
insert_lower=0,
|
||||
insert_higher=kind.insert - 1,
|
||||
)
|
||||
|
||||
|
||||
def setup_module():
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 139"><title>tests: 139</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">139</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">139</text></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 155"><title>tests: 155</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">155</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">155</text></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -22,7 +22,7 @@ class TestRemoteFactories:
|
||||
assert len(vm.strip) == 3
|
||||
assert len(vm.bus) == 2
|
||||
assert len(vm.button) == 80
|
||||
assert len(vm.vban.instream) == 4 and len(vm.vban.outstream) == 4
|
||||
assert len(vm.vban.instream) == 6 and len(vm.vban.outstream) == 5
|
||||
|
||||
@pytest.mark.skipif(
|
||||
data.name != "banana",
|
||||
@@ -42,7 +42,7 @@ class TestRemoteFactories:
|
||||
assert len(vm.strip) == 5
|
||||
assert len(vm.bus) == 5
|
||||
assert len(vm.button) == 80
|
||||
assert len(vm.vban.instream) == 8 and len(vm.vban.outstream) == 8
|
||||
assert len(vm.vban.instream) == 10 and len(vm.vban.outstream) == 9
|
||||
|
||||
@pytest.mark.skipif(
|
||||
data.name != "potato",
|
||||
@@ -63,4 +63,4 @@ class TestRemoteFactories:
|
||||
assert len(vm.strip) == 8
|
||||
assert len(vm.bus) == 8
|
||||
assert len(vm.button) == 80
|
||||
assert len(vm.vban.instream) == 8 and len(vm.vban.outstream) == 8
|
||||
assert len(vm.vban.instream) == 10 and len(vm.vban.outstream) == 9
|
||||
|
||||
@@ -156,6 +156,7 @@ class TestSetAndGetBoolHigher:
|
||||
[("A1"), ("B2")],
|
||||
)
|
||||
def test_it_sets_and_gets_recorder_bool_params(self, param, value):
|
||||
assert hasattr(vm.recorder, param)
|
||||
setattr(vm.recorder, param, value)
|
||||
assert getattr(vm.recorder, param) == value
|
||||
|
||||
@@ -168,7 +169,56 @@ class TestSetAndGetBoolHigher:
|
||||
[("loop")],
|
||||
)
|
||||
def test_it_sets_recorder_bool_params(self, param, value):
|
||||
assert hasattr(vm.recorder, param)
|
||||
setattr(vm.recorder, param, value)
|
||||
assert getattr(vm.recorder, param) == value
|
||||
|
||||
""" recoder.mode tests """
|
||||
|
||||
@pytest.mark.skipif(
|
||||
data.name == "basic",
|
||||
reason="Skip test if kind is basic",
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"param",
|
||||
[("loop"), ("recbus")],
|
||||
)
|
||||
def test_it_sets_recorder_mode_bool_params(self, param, value):
|
||||
assert hasattr(vm.recorder.mode, param)
|
||||
setattr(vm.recorder.mode, param, value)
|
||||
assert getattr(vm.recorder.mode, param) == value
|
||||
|
||||
""" recorder.armstrip """
|
||||
|
||||
@pytest.mark.skipif(
|
||||
data.name == "basic",
|
||||
reason="Skip test if kind is basic",
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"index",
|
||||
[
|
||||
(data.phys_out),
|
||||
(data.virt_out),
|
||||
],
|
||||
)
|
||||
def test_it_sets_recorder_armstrip_bool_params(self, index, value):
|
||||
vm.recorder.armstrip[index].set(value)
|
||||
|
||||
""" recorder.armbus """
|
||||
|
||||
@pytest.mark.skipif(
|
||||
data.name == "basic",
|
||||
reason="Skip test if kind is basic",
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"index",
|
||||
[
|
||||
(data.phys_out),
|
||||
(data.virt_out),
|
||||
],
|
||||
)
|
||||
def test_it_sets_recorder_armbus_bool_params(self, index, value):
|
||||
vm.recorder.armbus[index].set(True)
|
||||
|
||||
""" fx tests """
|
||||
|
||||
@@ -323,6 +373,26 @@ class TestSetAndGetIntHigher:
|
||||
vm.option.delay[index].set(value)
|
||||
assert vm.option.delay[index].get() == value
|
||||
|
||||
""" recorder tests """
|
||||
|
||||
@pytest.mark.skipif(
|
||||
data.name == "basic",
|
||||
reason="Skip test if kind is basic",
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"param,value",
|
||||
[
|
||||
("samplerate", 32000),
|
||||
("samplerate", 96000),
|
||||
("bitresolution", 16),
|
||||
("bitresolution", 32),
|
||||
],
|
||||
)
|
||||
def test_it_sets_and_gets_recorder_int_params(self, param, value):
|
||||
assert hasattr(vm.recorder, param)
|
||||
setattr(vm.recorder, param, value)
|
||||
assert getattr(vm.recorder, param) == value
|
||||
|
||||
|
||||
class TestSetAndGetFloatHigher:
|
||||
__test__ = True
|
||||
|
||||
@@ -16,110 +16,110 @@ class CBindings(metaclass=ABCMeta):
|
||||
Maps expected ctype argument and res types for each binding.
|
||||
"""
|
||||
|
||||
logger_cbindings = logger.getChild("Cbindings")
|
||||
logger_cbindings = logger.getChild("CBindings")
|
||||
|
||||
vm_login = libc.VBVMR_Login
|
||||
vm_login.restype = LONG
|
||||
vm_login.argtypes = None
|
||||
bind_login = libc.VBVMR_Login
|
||||
bind_login.restype = LONG
|
||||
bind_login.argtypes = None
|
||||
|
||||
vm_logout = libc.VBVMR_Logout
|
||||
vm_logout.restype = LONG
|
||||
vm_logout.argtypes = None
|
||||
bind_logout = libc.VBVMR_Logout
|
||||
bind_logout.restype = LONG
|
||||
bind_logout.argtypes = None
|
||||
|
||||
vm_runvm = libc.VBVMR_RunVoicemeeter
|
||||
vm_runvm.restype = LONG
|
||||
vm_runvm.argtypes = [LONG]
|
||||
bind_run_voicemeeter = libc.VBVMR_RunVoicemeeter
|
||||
bind_run_voicemeeter.restype = LONG
|
||||
bind_run_voicemeeter.argtypes = [LONG]
|
||||
|
||||
vm_get_type = libc.VBVMR_GetVoicemeeterType
|
||||
vm_get_type.restype = LONG
|
||||
vm_get_type.argtypes = [ct.POINTER(LONG)]
|
||||
bind_get_voicemeeter_type = libc.VBVMR_GetVoicemeeterType
|
||||
bind_get_voicemeeter_type.restype = LONG
|
||||
bind_get_voicemeeter_type.argtypes = [ct.POINTER(LONG)]
|
||||
|
||||
vm_get_version = libc.VBVMR_GetVoicemeeterVersion
|
||||
vm_get_version.restype = LONG
|
||||
vm_get_version.argtypes = [ct.POINTER(LONG)]
|
||||
bind_get_voicemeeter_version = libc.VBVMR_GetVoicemeeterVersion
|
||||
bind_get_voicemeeter_version.restype = LONG
|
||||
bind_get_voicemeeter_version.argtypes = [ct.POINTER(LONG)]
|
||||
|
||||
if hasattr(libc, "VBVMR_MacroButton_IsDirty"):
|
||||
vm_mdirty = libc.VBVMR_MacroButton_IsDirty
|
||||
vm_mdirty.restype = LONG
|
||||
vm_mdirty.argtypes = None
|
||||
bind_macro_button_is_dirty = libc.VBVMR_MacroButton_IsDirty
|
||||
bind_macro_button_is_dirty.restype = LONG
|
||||
bind_macro_button_is_dirty.argtypes = None
|
||||
|
||||
if hasattr(libc, "VBVMR_MacroButton_GetStatus"):
|
||||
vm_get_buttonstatus = libc.VBVMR_MacroButton_GetStatus
|
||||
vm_get_buttonstatus.restype = LONG
|
||||
vm_get_buttonstatus.argtypes = [LONG, ct.POINTER(FLOAT), LONG]
|
||||
bind_macro_button_get_status = libc.VBVMR_MacroButton_GetStatus
|
||||
bind_macro_button_get_status.restype = LONG
|
||||
bind_macro_button_get_status.argtypes = [LONG, ct.POINTER(FLOAT), LONG]
|
||||
|
||||
if hasattr(libc, "VBVMR_MacroButton_SetStatus"):
|
||||
vm_set_buttonstatus = libc.VBVMR_MacroButton_SetStatus
|
||||
vm_set_buttonstatus.restype = LONG
|
||||
vm_set_buttonstatus.argtypes = [LONG, FLOAT, LONG]
|
||||
bind_macro_button_set_status = libc.VBVMR_MacroButton_SetStatus
|
||||
bind_macro_button_set_status.restype = LONG
|
||||
bind_macro_button_set_status.argtypes = [LONG, FLOAT, LONG]
|
||||
|
||||
vm_pdirty = libc.VBVMR_IsParametersDirty
|
||||
vm_pdirty.restype = LONG
|
||||
vm_pdirty.argtypes = None
|
||||
bind_is_parameters_dirty = libc.VBVMR_IsParametersDirty
|
||||
bind_is_parameters_dirty.restype = LONG
|
||||
bind_is_parameters_dirty.argtypes = None
|
||||
|
||||
vm_get_parameter_float = libc.VBVMR_GetParameterFloat
|
||||
vm_get_parameter_float.restype = LONG
|
||||
vm_get_parameter_float.argtypes = [ct.POINTER(CHAR), ct.POINTER(FLOAT)]
|
||||
bind_get_parameter_float = libc.VBVMR_GetParameterFloat
|
||||
bind_get_parameter_float.restype = LONG
|
||||
bind_get_parameter_float.argtypes = [ct.POINTER(CHAR), ct.POINTER(FLOAT)]
|
||||
|
||||
vm_set_parameter_float = libc.VBVMR_SetParameterFloat
|
||||
vm_set_parameter_float.restype = LONG
|
||||
vm_set_parameter_float.argtypes = [ct.POINTER(CHAR), FLOAT]
|
||||
bind_set_parameter_float = libc.VBVMR_SetParameterFloat
|
||||
bind_set_parameter_float.restype = LONG
|
||||
bind_set_parameter_float.argtypes = [ct.POINTER(CHAR), FLOAT]
|
||||
|
||||
vm_get_parameter_string = libc.VBVMR_GetParameterStringW
|
||||
vm_get_parameter_string.restype = LONG
|
||||
vm_get_parameter_string.argtypes = [ct.POINTER(CHAR), ct.POINTER(WCHAR * 512)]
|
||||
bind_get_parameter_string_w = libc.VBVMR_GetParameterStringW
|
||||
bind_get_parameter_string_w.restype = LONG
|
||||
bind_get_parameter_string_w.argtypes = [ct.POINTER(CHAR), ct.POINTER(WCHAR * 512)]
|
||||
|
||||
vm_set_parameter_string = libc.VBVMR_SetParameterStringW
|
||||
vm_set_parameter_string.restype = LONG
|
||||
vm_set_parameter_string.argtypes = [ct.POINTER(CHAR), ct.POINTER(WCHAR)]
|
||||
bind_set_parameter_string_w = libc.VBVMR_SetParameterStringW
|
||||
bind_set_parameter_string_w.restype = LONG
|
||||
bind_set_parameter_string_w.argtypes = [ct.POINTER(CHAR), ct.POINTER(WCHAR)]
|
||||
|
||||
vm_set_parameter_multi = libc.VBVMR_SetParameters
|
||||
vm_set_parameter_multi.restype = LONG
|
||||
vm_set_parameter_multi.argtypes = [ct.POINTER(CHAR)]
|
||||
bind_set_parameters = libc.VBVMR_SetParameters
|
||||
bind_set_parameters.restype = LONG
|
||||
bind_set_parameters.argtypes = [ct.POINTER(CHAR)]
|
||||
|
||||
vm_get_level = libc.VBVMR_GetLevel
|
||||
vm_get_level.restype = LONG
|
||||
vm_get_level.argtypes = [LONG, LONG, ct.POINTER(FLOAT)]
|
||||
bind_get_level = libc.VBVMR_GetLevel
|
||||
bind_get_level.restype = LONG
|
||||
bind_get_level.argtypes = [LONG, LONG, ct.POINTER(FLOAT)]
|
||||
|
||||
vm_get_num_indevices = libc.VBVMR_Input_GetDeviceNumber
|
||||
vm_get_num_indevices.restype = LONG
|
||||
vm_get_num_indevices.argtypes = None
|
||||
bind_input_get_device_number = libc.VBVMR_Input_GetDeviceNumber
|
||||
bind_input_get_device_number.restype = LONG
|
||||
bind_input_get_device_number.argtypes = None
|
||||
|
||||
vm_get_desc_indevices = libc.VBVMR_Input_GetDeviceDescW
|
||||
vm_get_desc_indevices.restype = LONG
|
||||
vm_get_desc_indevices.argtypes = [
|
||||
bind_input_get_device_desc_w = libc.VBVMR_Input_GetDeviceDescW
|
||||
bind_input_get_device_desc_w.restype = LONG
|
||||
bind_input_get_device_desc_w.argtypes = [
|
||||
LONG,
|
||||
ct.POINTER(LONG),
|
||||
ct.POINTER(WCHAR * 256),
|
||||
ct.POINTER(WCHAR * 256),
|
||||
]
|
||||
|
||||
vm_get_num_outdevices = libc.VBVMR_Output_GetDeviceNumber
|
||||
vm_get_num_outdevices.restype = LONG
|
||||
vm_get_num_outdevices.argtypes = None
|
||||
bind_output_get_device_number = libc.VBVMR_Output_GetDeviceNumber
|
||||
bind_output_get_device_number.restype = LONG
|
||||
bind_output_get_device_number.argtypes = None
|
||||
|
||||
vm_get_desc_outdevices = libc.VBVMR_Output_GetDeviceDescW
|
||||
vm_get_desc_outdevices.restype = LONG
|
||||
vm_get_desc_outdevices.argtypes = [
|
||||
bind_output_get_device_desc_w = libc.VBVMR_Output_GetDeviceDescW
|
||||
bind_output_get_device_desc_w.restype = LONG
|
||||
bind_output_get_device_desc_w.argtypes = [
|
||||
LONG,
|
||||
ct.POINTER(LONG),
|
||||
ct.POINTER(WCHAR * 256),
|
||||
ct.POINTER(WCHAR * 256),
|
||||
]
|
||||
|
||||
vm_get_midi_message = libc.VBVMR_GetMidiMessage
|
||||
vm_get_midi_message.restype = LONG
|
||||
vm_get_midi_message.argtypes = [ct.POINTER(CHAR * 1024), LONG]
|
||||
bind_get_midi_message = libc.VBVMR_GetMidiMessage
|
||||
bind_get_midi_message.restype = LONG
|
||||
bind_get_midi_message.argtypes = [ct.POINTER(CHAR * 1024), LONG]
|
||||
|
||||
def call(self, func, *args, ok=(0,), ok_exp=None):
|
||||
try:
|
||||
res = func(*args)
|
||||
if ok_exp is None:
|
||||
if res not in ok:
|
||||
raise CAPIError(f"{func.__name__} returned {res}")
|
||||
elif not ok_exp(res):
|
||||
raise CAPIError(f"{func.__name__} returned {res}")
|
||||
raise CAPIError(func.__name__, res)
|
||||
elif not ok_exp(res) and res not in ok:
|
||||
raise CAPIError(func.__name__, res)
|
||||
return res
|
||||
except CAPIError as e:
|
||||
self.logger_cbindings.exception(f"{type(e).__name__}: {e}")
|
||||
self.logger_cbindings.exception(str(e))
|
||||
raise
|
||||
|
||||
@@ -23,6 +23,7 @@ class Adapter(IRemote):
|
||||
def output(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
pass
|
||||
|
||||
|
||||
@@ -5,6 +5,15 @@ class InstallError(Exception):
|
||||
class CAPIError(Exception):
|
||||
"""Exception raised when the C-API returns error values"""
|
||||
|
||||
def __init__(self, fn_name, code, msg=None):
|
||||
self.fn_name = fn_name
|
||||
self.code = code
|
||||
self.message = msg if msg else f"{fn_name} returned {code}"
|
||||
super().__init__(self.message)
|
||||
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}: {self.message}"
|
||||
|
||||
|
||||
class VMError(Exception):
|
||||
"""Exception raised when general errors occur"""
|
||||
|
||||
@@ -33,6 +33,7 @@ class IRemote(metaclass=ABCMeta):
|
||||
cmd += (f".{param}",)
|
||||
return "".join(cmd)
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def identifier(self):
|
||||
pass
|
||||
|
||||
@@ -32,29 +32,37 @@ class KindMapClass(metaclass=SingletonType):
|
||||
insert: int
|
||||
|
||||
@property
|
||||
def phys_in(self):
|
||||
def phys_in(self) -> int:
|
||||
return self.ins[0]
|
||||
|
||||
@property
|
||||
def virt_in(self):
|
||||
def virt_in(self) -> int:
|
||||
return self.ins[-1]
|
||||
|
||||
@property
|
||||
def phys_out(self):
|
||||
def phys_out(self) -> int:
|
||||
return self.outs[0]
|
||||
|
||||
@property
|
||||
def virt_out(self):
|
||||
def virt_out(self) -> int:
|
||||
return self.outs[-1]
|
||||
|
||||
@property
|
||||
def num_strip(self):
|
||||
def num_strip(self) -> int:
|
||||
return sum(self.ins)
|
||||
|
||||
@property
|
||||
def num_bus(self):
|
||||
def num_bus(self) -> int:
|
||||
return sum(self.outs)
|
||||
|
||||
@property
|
||||
def num_strip_levels(self) -> int:
|
||||
return 2 * self.phys_in + 8 * self.virt_in
|
||||
|
||||
@property
|
||||
def num_bus_levels(self) -> int:
|
||||
return 8 * (self.phys_out + self.virt_out)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name.capitalize()
|
||||
|
||||
@@ -64,7 +72,7 @@ class BasicMap(KindMapClass):
|
||||
name: str
|
||||
ins: tuple = (2, 1)
|
||||
outs: tuple = (1, 1)
|
||||
vban: tuple = (4, 4)
|
||||
vban: tuple = (4, 4, 1, 1)
|
||||
asio: tuple = (0, 0)
|
||||
insert: int = 0
|
||||
|
||||
@@ -74,7 +82,7 @@ class BananaMap(KindMapClass):
|
||||
name: str
|
||||
ins: tuple = (3, 2)
|
||||
outs: tuple = (3, 2)
|
||||
vban: tuple = (8, 8)
|
||||
vban: tuple = (8, 8, 1, 1)
|
||||
asio: tuple = (6, 8)
|
||||
insert: int = 22
|
||||
|
||||
@@ -84,7 +92,7 @@ class PotatoMap(KindMapClass):
|
||||
name: str
|
||||
ins: tuple = (5, 3)
|
||||
outs: tuple = (5, 3)
|
||||
vban: tuple = (8, 8)
|
||||
vban: tuple = (8, 8, 1, 1)
|
||||
asio: tuple = (10, 8)
|
||||
insert: int = 34
|
||||
|
||||
|
||||
@@ -1,20 +1,43 @@
|
||||
from enum import IntEnum
|
||||
|
||||
from .iremote import IRemote
|
||||
|
||||
ButtonModes = IntEnum(
|
||||
"ButtonModes",
|
||||
"state stateonly trigger",
|
||||
start=1,
|
||||
)
|
||||
|
||||
|
||||
class Adapter(IRemote):
|
||||
"""Adapter to the common interface."""
|
||||
|
||||
def identifier(self):
|
||||
pass
|
||||
|
||||
def getter(self, mode):
|
||||
self.logger.debug(f"getter: button[{self.index}].{ButtonModes(mode).name}")
|
||||
return self._remote.get_buttonstatus(self.index, mode)
|
||||
|
||||
def setter(self, val, mode):
|
||||
def setter(self, mode, val):
|
||||
self.logger.debug(
|
||||
f"setter: button[{self.index}].{ButtonModes(mode).name}={val}"
|
||||
)
|
||||
self._remote.set_buttonstatus(self.index, val, mode)
|
||||
|
||||
|
||||
class MacroButton(Adapter):
|
||||
class MacroButtonColorMixin(IRemote):
|
||||
@property
|
||||
def identifier(self):
|
||||
return f"command.button[{self.index}]"
|
||||
|
||||
@property
|
||||
def color(self) -> int:
|
||||
return int(IRemote.getter(self, "color"))
|
||||
|
||||
@color.setter
|
||||
def color(self, val: int):
|
||||
IRemote.setter(self, "color", val)
|
||||
|
||||
|
||||
class MacroButton(Adapter, MacroButtonColorMixin):
|
||||
"""Defines concrete implementation for macrobutton"""
|
||||
|
||||
def __str__(self):
|
||||
@@ -22,24 +45,24 @@ class MacroButton(Adapter):
|
||||
|
||||
@property
|
||||
def state(self) -> bool:
|
||||
return self.getter(1) == 1
|
||||
return self.getter(ButtonModes.state) == 1
|
||||
|
||||
@state.setter
|
||||
def state(self, val):
|
||||
self.setter(1 if val else 0, 1)
|
||||
def state(self, val: bool):
|
||||
self.setter(ButtonModes.state, 1 if val else 0)
|
||||
|
||||
@property
|
||||
def stateonly(self) -> bool:
|
||||
return self.getter(2) == 1
|
||||
return self.getter(ButtonModes.stateonly) == 1
|
||||
|
||||
@stateonly.setter
|
||||
def stateonly(self, val):
|
||||
self.setter(1 if val else 0, 2)
|
||||
def stateonly(self, val: bool):
|
||||
self.setter(ButtonModes.stateonly, 1 if val else 0)
|
||||
|
||||
@property
|
||||
def trigger(self) -> bool:
|
||||
return self.getter(3) == 1
|
||||
return self.getter(ButtonModes.trigger) == 1
|
||||
|
||||
@trigger.setter
|
||||
def trigger(self, val):
|
||||
self.setter(1 if val else 0, 3)
|
||||
def trigger(self, val: bool):
|
||||
self.setter(ButtonModes.trigger, 1 if val else 0)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from typing import Optional
|
||||
|
||||
from .error import VMError
|
||||
from .iremote import IRemote
|
||||
from .kinds import kinds_all
|
||||
|
||||
@@ -196,7 +195,7 @@ class Option(IRemote):
|
||||
def sr(self, val: int):
|
||||
opts = (44100, 48000, 88200, 96000, 176400, 192000)
|
||||
if val not in opts:
|
||||
raise VMError(f"Expected one of: {opts}")
|
||||
self.logger.warning(f"sr got: {val} but expected a value in {opts}")
|
||||
self.setter("sr", val)
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import re
|
||||
|
||||
from .error import VMError
|
||||
from .iremote import IRemote
|
||||
from .kinds import kinds_all
|
||||
@@ -19,9 +21,10 @@ class Recorder(IRemote):
|
||||
Returns a Recorder class of a kind.
|
||||
"""
|
||||
CHANNELOUTMIXIN_cls = _make_channelout_mixins[remote.kind.name]
|
||||
ARMCHANNELMIXIN_cls = _make_armchannel_mixins(remote)[remote.kind.name]
|
||||
REC_cls = type(
|
||||
f"Recorder{remote.kind}",
|
||||
(cls, CHANNELOUTMIXIN_cls),
|
||||
(cls, CHANNELOUTMIXIN_cls, ARMCHANNELMIXIN_cls),
|
||||
{
|
||||
**{
|
||||
param: action_fn(param)
|
||||
@@ -35,6 +38,7 @@ class Recorder(IRemote):
|
||||
"rew",
|
||||
]
|
||||
},
|
||||
"mode": RecorderMode(remote),
|
||||
},
|
||||
)
|
||||
return REC_cls(remote)
|
||||
@@ -46,20 +50,183 @@ class Recorder(IRemote):
|
||||
def identifier(self) -> str:
|
||||
return "recorder"
|
||||
|
||||
@property
|
||||
def samplerate(self) -> int:
|
||||
return int(self.getter("samplerate"))
|
||||
|
||||
@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 int(self.getter("bitresolution"))
|
||||
|
||||
@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 int(self.getter("channel"))
|
||||
|
||||
@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 int(self.getter("kbps"))
|
||||
|
||||
@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 round(self.getter("gain"), 1)
|
||||
|
||||
@gain.setter
|
||||
def gain(self, val: float):
|
||||
self.setter("gain", val)
|
||||
|
||||
def load(self, file: str):
|
||||
try:
|
||||
self.setter("load", file)
|
||||
except UnicodeError:
|
||||
raise VMError("File full directory must be a raw string")
|
||||
|
||||
def set_loop(self, val: bool):
|
||||
self.setter("mode.loop", 1 if val else 0)
|
||||
# loop forwarder methods, for backwards compatibility
|
||||
@property
|
||||
def loop(self):
|
||||
return self.mode.loop
|
||||
|
||||
loop = property(fset=set_loop)
|
||||
@loop.setter
|
||||
def loop(self, val: bool):
|
||||
self.mode.loop = val
|
||||
|
||||
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 (
|
||||
match := 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(
|
||||
f"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())}"
|
||||
)
|
||||
|
||||
|
||||
class RecorderMode(IRemote):
|
||||
@property
|
||||
def identifier(self):
|
||||
return "recorder.mode"
|
||||
|
||||
@property
|
||||
def recbus(self) -> bool:
|
||||
return self.getter("recbus") == 1
|
||||
|
||||
@recbus.setter
|
||||
def recbus(self, val: bool):
|
||||
self.setter("recbus", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def playonload(self) -> bool:
|
||||
return self.getter("playonload") == 1
|
||||
|
||||
@playonload.setter
|
||||
def playonload(self, val: bool):
|
||||
self.setter("playonload", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def loop(self) -> bool:
|
||||
return self.getter("loop") == 1
|
||||
|
||||
@loop.setter
|
||||
def loop(self, val: bool):
|
||||
self.setter("loop", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def multitrack(self) -> bool:
|
||||
return self.getter("multitrack") == 1
|
||||
|
||||
@multitrack.setter
|
||||
def multitrack(self, val: bool):
|
||||
self.setter("multitrack", 1 if val else 0)
|
||||
|
||||
|
||||
class RecorderArmChannel(IRemote):
|
||||
def __init__(self, remote, i):
|
||||
super().__init__(remote)
|
||||
self._i = i
|
||||
|
||||
def set(self, val: bool):
|
||||
self.setter("", 1 if val else 0)
|
||||
|
||||
|
||||
class RecorderArmStrip(RecorderArmChannel):
|
||||
@property
|
||||
def identifier(self):
|
||||
return f"recorder.armstrip[{self._i}]"
|
||||
|
||||
|
||||
class RecorderArmBus(RecorderArmChannel):
|
||||
@property
|
||||
def identifier(self):
|
||||
return f"recorder.armbus[{self._i}]"
|
||||
|
||||
|
||||
def _make_armchannel_mixin(remote, kind):
|
||||
"""Creates an armchannel out mixin"""
|
||||
return type(
|
||||
f"ArmChannelMixin{kind}",
|
||||
(),
|
||||
{
|
||||
"armstrip": tuple(
|
||||
RecorderArmStrip(remote, i) for i in range(kind.num_strip)
|
||||
),
|
||||
"armbus": tuple(RecorderArmBus(remote, i) for i in range(kind.num_bus)),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _make_armchannel_mixins(remote):
|
||||
return {kind.name: _make_armchannel_mixin(remote, kind) for kind in kinds_all}
|
||||
|
||||
|
||||
def _make_channelout_mixin(kind):
|
||||
"""Creates a channel out property mixin"""
|
||||
"""Creates a channel out mixin"""
|
||||
return type(
|
||||
f"ChannelOutMixin{kind}",
|
||||
(),
|
||||
|
||||
@@ -13,7 +13,7 @@ from .kinds import KindId
|
||||
from .misc import Midi, VmGui
|
||||
from .subject import Subject
|
||||
from .updater import Producer, Updater
|
||||
from .util import grouper, polling, script
|
||||
from .util import deep_merge, grouper, polling, script
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -64,13 +64,15 @@ class Remote(CBindings):
|
||||
|
||||
def login(self) -> NoReturn:
|
||||
"""Login to the API, initialize dirty parameters"""
|
||||
self.gui.launched = self.call(self.vm_login, ok=(0, 1)) == 0
|
||||
self.gui.launched = self.call(self.bind_login, ok=(0, 1)) == 0
|
||||
if not self.gui.launched:
|
||||
self.logger.info(
|
||||
"Voicemeeter engine running but GUI not launched. Launching the GUI now."
|
||||
)
|
||||
self.run_voicemeeter(self.kind.name)
|
||||
self.logger.info(f"{type(self).__name__}: Successfully logged into {self}")
|
||||
self.logger.info(
|
||||
f"{type(self).__name__}: Successfully logged into {self} version {self.version}"
|
||||
)
|
||||
self.clear_dirty()
|
||||
|
||||
def run_voicemeeter(self, kind_id: str) -> NoReturn:
|
||||
@@ -80,21 +82,21 @@ class Remote(CBindings):
|
||||
value = KindId[kind_id.upper()].value + 3
|
||||
else:
|
||||
value = KindId[kind_id.upper()].value
|
||||
self.call(self.vm_runvm, value)
|
||||
self.call(self.bind_run_voicemeeter, value)
|
||||
time.sleep(1)
|
||||
|
||||
@property
|
||||
def type(self) -> str:
|
||||
"""Returns the type of Voicemeeter installation (basic, banana, potato)."""
|
||||
type_ = ct.c_long()
|
||||
self.call(self.vm_get_type, ct.byref(type_))
|
||||
self.call(self.bind_get_voicemeeter_type, ct.byref(type_))
|
||||
return KindId(type_.value).name.lower()
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
"""Returns Voicemeeter's version as a string"""
|
||||
ver = ct.c_long()
|
||||
self.call(self.vm_get_version, ct.byref(ver))
|
||||
self.call(self.bind_get_voicemeeter_version, ct.byref(ver))
|
||||
return "{}.{}.{}.{}".format(
|
||||
(ver.value & 0xFF000000) >> 24,
|
||||
(ver.value & 0x00FF0000) >> 16,
|
||||
@@ -105,17 +107,21 @@ class Remote(CBindings):
|
||||
@property
|
||||
def pdirty(self) -> bool:
|
||||
"""True iff UI parameters have been updated."""
|
||||
return self.call(self.vm_pdirty, ok=(0, 1)) == 1
|
||||
return self.call(self.bind_is_parameters_dirty, ok=(0, 1)) == 1
|
||||
|
||||
@property
|
||||
def mdirty(self) -> bool:
|
||||
"""True iff MB parameters have been updated."""
|
||||
try:
|
||||
return self.call(self.vm_mdirty, ok=(0, 1)) == 1
|
||||
return self.call(self.bind_macro_button_is_dirty, ok=(0, 1)) == 1
|
||||
except AttributeError as e:
|
||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||
ERR_MSG = (
|
||||
"no bind for VBVMR_MacroButton_IsDirty.",
|
||||
"are you using an old version of the API?",
|
||||
)
|
||||
raise CAPIError(
|
||||
"no bind for VBVMR_MacroButton_IsDirty. are you using an old version of the API?"
|
||||
"VBVMR_MacroButton_IsDirty", -9, msg=" ".join(ERR_MSG)
|
||||
) from e
|
||||
|
||||
@property
|
||||
@@ -131,8 +137,10 @@ class Remote(CBindings):
|
||||
try:
|
||||
while self.pdirty or self.mdirty:
|
||||
pass
|
||||
except CAPIError:
|
||||
self.logger.error("no bind for mdirty, clearing pdirty only")
|
||||
except CAPIError as e:
|
||||
if not (e.fn_name == "VBVMR_MacroButton_IsDirty" and e.code == -9):
|
||||
raise
|
||||
self.logger.error(f"{e} clearing pdirty only.")
|
||||
while self.pdirty:
|
||||
pass
|
||||
|
||||
@@ -141,10 +149,10 @@ class Remote(CBindings):
|
||||
"""Gets a string or float parameter"""
|
||||
if is_string:
|
||||
buf = ct.create_unicode_buffer(512)
|
||||
self.call(self.vm_get_parameter_string, param.encode(), ct.byref(buf))
|
||||
self.call(self.bind_get_parameter_string_w, param.encode(), ct.byref(buf))
|
||||
else:
|
||||
buf = ct.c_float()
|
||||
self.call(self.vm_get_parameter_float, param.encode(), ct.byref(buf))
|
||||
self.call(self.bind_get_parameter_float, param.encode(), ct.byref(buf))
|
||||
return buf.value
|
||||
|
||||
def set(self, param: str, val: Union[str, float]) -> NoReturn:
|
||||
@@ -152,48 +160,63 @@ class Remote(CBindings):
|
||||
if isinstance(val, str):
|
||||
if len(val) >= 512:
|
||||
raise VMError("String is too long")
|
||||
self.call(self.vm_set_parameter_string, param.encode(), ct.c_wchar_p(val))
|
||||
self.call(
|
||||
self.bind_set_parameter_string_w, param.encode(), ct.c_wchar_p(val)
|
||||
)
|
||||
else:
|
||||
self.call(
|
||||
self.vm_set_parameter_float, param.encode(), ct.c_float(float(val))
|
||||
self.bind_set_parameter_float, param.encode(), ct.c_float(float(val))
|
||||
)
|
||||
self.cache[param] = val
|
||||
|
||||
@polling
|
||||
def get_buttonstatus(self, id: int, mode: int) -> int:
|
||||
def get_buttonstatus(self, id_: int, mode: int) -> int:
|
||||
"""Gets a macrobutton parameter"""
|
||||
state = ct.c_float()
|
||||
c_state = ct.c_float()
|
||||
try:
|
||||
self.call(
|
||||
self.vm_get_buttonstatus,
|
||||
ct.c_long(id),
|
||||
ct.byref(state),
|
||||
self.bind_macro_button_get_status,
|
||||
ct.c_long(id_),
|
||||
ct.byref(c_state),
|
||||
ct.c_long(mode),
|
||||
)
|
||||
except AttributeError as e:
|
||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||
ERR_MSG = (
|
||||
"no bind for VBVMR_MacroButton_GetStatus.",
|
||||
"are you using an old version of the API?",
|
||||
)
|
||||
raise CAPIError(
|
||||
"no bind for VBVMR_MacroButton_GetStatus. are you using an old version of the API?"
|
||||
"VBVMR_MacroButton_GetStatus", -9, msg=" ".join(ERR_MSG)
|
||||
) from e
|
||||
return int(state.value)
|
||||
return int(c_state.value)
|
||||
|
||||
def set_buttonstatus(self, id: int, state: int, mode: int) -> NoReturn:
|
||||
def set_buttonstatus(self, id_: int, val: int, mode: int) -> NoReturn:
|
||||
"""Sets a macrobutton parameter. Caches value"""
|
||||
c_state = ct.c_float(float(state))
|
||||
c_state = ct.c_float(float(val))
|
||||
try:
|
||||
self.call(self.vm_set_buttonstatus, ct.c_long(id), c_state, ct.c_long(mode))
|
||||
self.call(
|
||||
self.bind_macro_button_set_status,
|
||||
ct.c_long(id_),
|
||||
c_state,
|
||||
ct.c_long(mode),
|
||||
)
|
||||
except AttributeError as e:
|
||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||
ERR_MSG = (
|
||||
"no bind for VBVMR_MacroButton_SetStatus.",
|
||||
"are you using an old version of the API?",
|
||||
)
|
||||
raise CAPIError(
|
||||
"no bind for VBVMR_MacroButton_SetStatus. are you using an old version of the API?"
|
||||
"VBVMR_MacroButton_SetStatus", -9, msg=" ".join(ERR_MSG)
|
||||
) from e
|
||||
self.cache[f"mb_{id}_{mode}"] = int(c_state.value)
|
||||
self.cache[f"mb_{id_}_{mode}"] = int(c_state.value)
|
||||
|
||||
def get_num_devices(self, direction: str = None) -> int:
|
||||
"""Retrieves number of physical devices connected"""
|
||||
if direction not in ("in", "out"):
|
||||
raise VMError("Expected a direction: in or out")
|
||||
func = getattr(self, f"vm_get_num_{direction}devices")
|
||||
func = getattr(self, f"bind_{direction}put_get_device_number")
|
||||
res = self.call(func, ok_exp=lambda r: r >= 0)
|
||||
return res
|
||||
|
||||
@@ -204,7 +227,7 @@ class Remote(CBindings):
|
||||
type_ = ct.c_long()
|
||||
name = ct.create_unicode_buffer(256)
|
||||
hwid = ct.create_unicode_buffer(256)
|
||||
func = getattr(self, f"vm_get_desc_{direction}devices")
|
||||
func = getattr(self, f"bind_{direction}put_get_device_desc_w")
|
||||
self.call(
|
||||
func,
|
||||
ct.c_long(index),
|
||||
@@ -217,7 +240,9 @@ class Remote(CBindings):
|
||||
def get_level(self, type_: int, index: int) -> float:
|
||||
"""Retrieves a single level value"""
|
||||
val = ct.c_float()
|
||||
self.call(self.vm_get_level, ct.c_long(type_), ct.c_long(index), ct.byref(val))
|
||||
self.call(
|
||||
self.bind_get_level, ct.c_long(type_), ct.c_long(index), ct.byref(val)
|
||||
)
|
||||
return val.value
|
||||
|
||||
def _get_levels(self) -> Iterable:
|
||||
@@ -227,18 +252,21 @@ class Remote(CBindings):
|
||||
return (
|
||||
tuple(
|
||||
self.get_level(self.strip_mode, i)
|
||||
for i in range(2 * self.kind.phys_in + 8 * self.kind.virt_in)
|
||||
),
|
||||
tuple(
|
||||
self.get_level(3, i)
|
||||
for i in range(8 * (self.kind.phys_out + self.kind.virt_out))
|
||||
for i in range(self.kind.num_strip_levels)
|
||||
),
|
||||
tuple(self.get_level(3, i) for i in range(self.kind.num_bus_levels)),
|
||||
)
|
||||
|
||||
def get_midi_message(self):
|
||||
n = ct.c_long(1024)
|
||||
buf = ct.create_string_buffer(1024)
|
||||
res = self.vm_get_midi_message(ct.byref(buf), n, ok_exp=lambda r: r >= 0)
|
||||
res = self.call(
|
||||
self.bind_get_midi_message,
|
||||
ct.byref(buf),
|
||||
n,
|
||||
ok=(-5, -6), # no data received from midi device
|
||||
ok_exp=lambda r: r >= 0,
|
||||
)
|
||||
if res > 0:
|
||||
vals = tuple(
|
||||
grouper(3, (int.from_bytes(buf[i], "little") for i in range(res)))
|
||||
@@ -256,7 +284,7 @@ class Remote(CBindings):
|
||||
"""Sets many parameters from a script"""
|
||||
if len(script) > 48000:
|
||||
raise ValueError("Script too large, max size 48kB")
|
||||
self.call(self.vm_set_parameter_multi, script.encode())
|
||||
self.call(self.bind_set_parameters, script.encode())
|
||||
time.sleep(self.DELAY * 5)
|
||||
|
||||
def apply(self, data: dict):
|
||||
@@ -279,27 +307,39 @@ class Remote(CBindings):
|
||||
|
||||
def apply_config(self, name):
|
||||
"""applies a config from memory"""
|
||||
error_msg = (
|
||||
ERR_MSG = (
|
||||
f"No config with name '{name}' is loaded into memory",
|
||||
f"Known configs: {list(self.configs.keys())}",
|
||||
)
|
||||
try:
|
||||
self.apply(self.configs[name])
|
||||
self.logger.info(f"Profile '{name}' applied!")
|
||||
except KeyError:
|
||||
self.logger.error(("\n").join(error_msg))
|
||||
config = self.configs[name]
|
||||
except KeyError as e:
|
||||
self.logger.error(("\n").join(ERR_MSG))
|
||||
raise VMError(("\n").join(ERR_MSG)) from e
|
||||
|
||||
def logout(self) -> NoReturn:
|
||||
"""Wait for dirty parameters to clear, then logout of the API"""
|
||||
self.clear_dirty()
|
||||
time.sleep(0.1)
|
||||
self.call(self.vm_logout)
|
||||
self.logger.info(f"{type(self).__name__}: Successfully logged out of {self}")
|
||||
if "extends" in config:
|
||||
extended = config["extends"]
|
||||
config = {
|
||||
k: v
|
||||
for k, v in deep_merge(self.configs[extended], config)
|
||||
if k not in ("extends")
|
||||
}
|
||||
self.logger.debug(
|
||||
f"profile '{name}' extends '{extended}', profiles merged.."
|
||||
)
|
||||
self.apply(config)
|
||||
self.logger.info(f"Profile '{name}' applied!")
|
||||
|
||||
def end_thread(self):
|
||||
self.logger.debug("events thread shutdown started")
|
||||
self.running = False
|
||||
|
||||
def logout(self) -> NoReturn:
|
||||
"""Logout of the API"""
|
||||
time.sleep(0.1)
|
||||
self.call(self.bind_logout)
|
||||
self.logger.info(f"{type(self).__name__}: Successfully logged out of {self}")
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_traceback) -> NoReturn:
|
||||
"""teardown procedures"""
|
||||
self.end_thread()
|
||||
|
||||
@@ -364,7 +364,7 @@ class VirtualStrip(Strip):
|
||||
self.setter("karaoke", val)
|
||||
|
||||
@property
|
||||
def bass(self):
|
||||
def bass(self) -> float:
|
||||
return round(self.getter("EQGain1"), 1)
|
||||
|
||||
@bass.setter
|
||||
@@ -372,7 +372,7 @@ class VirtualStrip(Strip):
|
||||
self.setter("EQGain1", val)
|
||||
|
||||
@property
|
||||
def mid(self):
|
||||
def mid(self) -> float:
|
||||
return round(self.getter("EQGain2"), 1)
|
||||
|
||||
@mid.setter
|
||||
@@ -382,7 +382,7 @@ class VirtualStrip(Strip):
|
||||
med = mid
|
||||
|
||||
@property
|
||||
def treble(self):
|
||||
def treble(self) -> float:
|
||||
return round(self.getter("EQGain3"), 1)
|
||||
|
||||
high = treble
|
||||
|
||||
@@ -36,10 +36,8 @@ class Updater(threading.Thread):
|
||||
super().__init__(name="updater", daemon=True)
|
||||
self._remote = remote
|
||||
self.queue = queue
|
||||
self._remote._strip_comp = [False] * (
|
||||
2 * self._remote.kind.phys_in + 8 * self._remote.kind.virt_in
|
||||
)
|
||||
self._remote._bus_comp = [False] * (self._remote.kind.num_bus * 8)
|
||||
self._remote._strip_comp = [False] * (self._remote.kind.num_strip_levels)
|
||||
self._remote._bus_comp = [False] * (self._remote.kind.num_bus_levels)
|
||||
(
|
||||
self._remote.cache["strip_level"],
|
||||
self._remote.cache["bus_level"],
|
||||
|
||||
@@ -70,3 +70,17 @@ def grouper(n, iterable, fillvalue=None):
|
||||
"""
|
||||
args = [iter(iterable)] * n
|
||||
return zip_longest(fillvalue=fillvalue, *args)
|
||||
|
||||
|
||||
def deep_merge(dict1, dict2):
|
||||
"""Generator function for deep merging two dicts"""
|
||||
for k in set(dict1) | set(dict2):
|
||||
if k in dict1 and k in dict2:
|
||||
if isinstance(dict1[k], dict) and isinstance(dict2[k], dict):
|
||||
yield k, dict(deep_merge(dict1[k], dict2[k]))
|
||||
else:
|
||||
yield k, dict2[k]
|
||||
elif k in dict1:
|
||||
yield k, dict1[k]
|
||||
else:
|
||||
yield k, dict2[k]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from abc import abstractmethod
|
||||
|
||||
from .error import VMError
|
||||
from .iremote import IRemote
|
||||
from .kinds import kinds_all
|
||||
|
||||
|
||||
class VbanStream(IRemote):
|
||||
@@ -50,7 +50,9 @@ class VbanStream(IRemote):
|
||||
@port.setter
|
||||
def port(self, val: int):
|
||||
if not 1024 <= val <= 65535:
|
||||
raise VMError("Expected value from 1024 to 65535")
|
||||
self.logger.warning(
|
||||
f"port got: {val} but expected a value from 1024 to 65535"
|
||||
)
|
||||
self.setter("port", val)
|
||||
|
||||
@property
|
||||
@@ -61,7 +63,7 @@ class VbanStream(IRemote):
|
||||
def sr(self, val: int):
|
||||
opts = (11025, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000)
|
||||
if val not in opts:
|
||||
raise VMError(f"Expected one of: {opts}")
|
||||
self.logger.warning(f"sr got: {val} but expected a value in {opts}")
|
||||
self.setter("sr", val)
|
||||
|
||||
@property
|
||||
@@ -71,7 +73,7 @@ class VbanStream(IRemote):
|
||||
@channel.setter
|
||||
def channel(self, val: int):
|
||||
if not 1 <= val <= 8:
|
||||
raise VMError("Expected value from 1 to 8")
|
||||
self.logger.warning(f"channel got: {val} but expected a value from 1 to 8")
|
||||
self.setter("channel", val)
|
||||
|
||||
@property
|
||||
@@ -81,7 +83,7 @@ class VbanStream(IRemote):
|
||||
@bit.setter
|
||||
def bit(self, val: int):
|
||||
if val not in (16, 24):
|
||||
raise VMError("Expected value 16 or 24")
|
||||
self.logger.warning(f"bit got: {val} but expected value 16 or 24")
|
||||
self.setter("bit", 1 if (val == 16) else 2)
|
||||
|
||||
@property
|
||||
@@ -91,7 +93,7 @@ class VbanStream(IRemote):
|
||||
@quality.setter
|
||||
def quality(self, val: int):
|
||||
if not 0 <= val <= 4:
|
||||
raise VMError("Expected value from 0 to 4")
|
||||
self.logger.warning(f"quality got: {val} but expected a value from 0 to 4")
|
||||
self.setter("quality", val)
|
||||
|
||||
@property
|
||||
@@ -101,7 +103,7 @@ class VbanStream(IRemote):
|
||||
@route.setter
|
||||
def route(self, val: int):
|
||||
if not 0 <= val <= 8:
|
||||
raise VMError("Expected value from 0 to 8")
|
||||
self.logger.warning(f"route got: {val} but expected a value from 0 to 8")
|
||||
self.setter("route", val)
|
||||
|
||||
|
||||
@@ -132,6 +134,18 @@ class VbanInstream(VbanStream):
|
||||
return super(VbanInstream, self).bit
|
||||
|
||||
|
||||
class VbanAudioInstream(VbanInstream):
|
||||
"""Represents a VBAN Audio Instream"""
|
||||
|
||||
|
||||
class VbanMidiInstream(VbanInstream):
|
||||
"""Represents a VBAN Midi Instream"""
|
||||
|
||||
|
||||
class VbanTextInstream(VbanInstream):
|
||||
"""Represents a VBAN Text Instream"""
|
||||
|
||||
|
||||
class VbanOutstream(VbanStream):
|
||||
"""
|
||||
class representing a vban outstream
|
||||
@@ -147,6 +161,50 @@ class VbanOutstream(VbanStream):
|
||||
return "out"
|
||||
|
||||
|
||||
class VbanAudioOutstream(VbanOutstream):
|
||||
"""Represents a VBAN Audio Outstream"""
|
||||
|
||||
|
||||
class VbanMidiOutstream(VbanOutstream):
|
||||
"""Represents a VBAN Midi Outstream"""
|
||||
|
||||
|
||||
def _make_stream_pair(remote, kind):
|
||||
num_instream, num_outstream, num_midi, num_text = kind.vban
|
||||
|
||||
def _generate_streams(i, dir):
|
||||
"""generator function for instream/outstream types"""
|
||||
if dir == "in":
|
||||
if i < num_instream:
|
||||
yield VbanAudioInstream
|
||||
elif i < num_instream + num_midi:
|
||||
yield VbanMidiInstream
|
||||
else:
|
||||
yield VbanTextInstream
|
||||
else:
|
||||
if i < num_outstream:
|
||||
yield VbanAudioOutstream
|
||||
else:
|
||||
yield VbanMidiOutstream
|
||||
|
||||
return (
|
||||
tuple(
|
||||
cls(remote, i)
|
||||
for i in range(num_instream + num_midi + num_text)
|
||||
for cls in _generate_streams(i, "in")
|
||||
),
|
||||
tuple(
|
||||
cls(remote, i)
|
||||
for i in range(num_outstream + num_midi)
|
||||
for cls in _generate_streams(i, "out")
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _make_stream_pairs(remote):
|
||||
return {kind.name: _make_stream_pair(remote, kind) for kind in kinds_all}
|
||||
|
||||
|
||||
class Vban:
|
||||
"""
|
||||
class representing the vban module
|
||||
@@ -156,9 +214,7 @@ class Vban:
|
||||
|
||||
def __init__(self, remote):
|
||||
self.remote = remote
|
||||
num_instream, num_outstream = remote.kind.vban
|
||||
self.instream = tuple(VbanInstream(remote, i) for i in range(num_instream))
|
||||
self.outstream = tuple(VbanOutstream(remote, i) for i in range(num_outstream))
|
||||
self.instream, self.outstream = _make_stream_pairs(remote)[remote.kind.name]
|
||||
|
||||
def enable(self):
|
||||
self.remote.set("vban.Enable", 1)
|
||||
|
||||
Reference in New Issue
Block a user