8 Commits

Author SHA1 Message Date
c2daba1a62 when out of bounds values are passed, log warnings
bump to version 2.1.1

closes #6
2023-07-01 19:50:54 +01:00
3036cdff2f upd banana badge 2023-07-01 19:26:56 +01:00
b02f3af665 add recorder, recorder.mode tests 2023-07-01 19:26:32 +01:00
145f85b7cd rename ARMSTRIPMIXIN_cls to ARMCHANNELMIXIN_cls 2023-07-01 18:09:31 +01:00
71f77b7830 md fix 2023-07-01 00:05:01 +01:00
4415851816 Recorder.Mode section added
new recorder properties and methods added
2023-06-30 23:53:08 +01:00
8b63cbfe8d add 2.1.0 section to readme 2023-06-30 23:51:43 +01:00
de4ce850eb add recorder.loop forwarder methods
add RecorderArmChannel class.

add logger warning if channel value not from 1 to 8
2023-06-30 23:51:20 +01:00
9 changed files with 198 additions and 61 deletions

View File

@@ -11,6 +11,23 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
- [x]
## [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 +380,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

View File

@@ -398,13 +398,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
- `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 +418,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 +629,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 +639,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
@@ -664,7 +697,7 @@ 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

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "voicemeeter-api"
version = "2.0.2"
version = "2.1.1"
description = "A Python wrapper for the Voiceemeter API"
authors = ["onyx-and-iris <code@onyxandiris.online>"]
license = "MIT"

View File

@@ -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():

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -21,10 +21,10 @@ class Recorder(IRemote):
Returns a Recorder class of a kind.
"""
CHANNELOUTMIXIN_cls = _make_channelout_mixins[remote.kind.name]
ARMSTRIPMIXIN_cls = _make_armchannel_mixins(remote)[remote.kind.name]
ARMCHANNELMIXIN_cls = _make_armchannel_mixins(remote)[remote.kind.name]
REC_cls = type(
f"Recorder{remote.kind}",
(cls, CHANNELOUTMIXIN_cls, ARMSTRIPMIXIN_cls),
(cls, CHANNELOUTMIXIN_cls, ARMCHANNELMIXIN_cls),
{
**{
param: action_fn(param)
@@ -80,7 +80,9 @@ class Recorder(IRemote):
@channel.setter
def channel(self, val: int):
self.getter("channel", val)
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):
@@ -107,10 +109,14 @@ class Recorder(IRemote):
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():
@@ -135,7 +141,7 @@ class Recorder(IRemote):
def filetype(self, val: str):
opts = {"wav": 1, "aiff": 2, "bwf": 3, "mp3": 100}
try:
self.setter("filetype", opts[val])
self.setter("filetype", opts[val.lower()])
except KeyError:
self.logger.warning(
f"filetype got: {val} but expected a value in {list(opts.keys())}"
@@ -143,6 +149,7 @@ class Recorder(IRemote):
class RecorderMode(IRemote):
@property
def identifier(self):
return "recorder.mode"
@@ -179,31 +186,26 @@ class RecorderMode(IRemote):
self.setter("multitrack", 1 if val else 0)
class RecorderArmStrip(IRemote):
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}]"
def set(self, val: bool):
self.setter("", 1 if val else 0)
class RecorderArmBus(IRemote):
def __init__(self, remote, i):
super().__init__(remote)
self._i = i
class RecorderArmBus(RecorderArmChannel):
@property
def identifier(self):
return f"recorder.armbus[{self._i}]"
def set(self, val: bool):
self.setter("", 1 if val else 0)
def _make_armchannel_mixin(remote, kind):
"""Creates an armchannel out mixin"""

View File

@@ -1,6 +1,5 @@
from abc import abstractmethod
from .error import VMError
from .iremote import IRemote
@@ -50,7 +49,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 +62,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 +72,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 +82,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 +92,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 +102,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)