Merge pull request #16 from wcyoung08/add-to-bus-class

Extends BusEQclass with BusEQChCell, giving access to all bus eq channel cell parameters.
This commit is contained in:
Onyx and Iris 2025-06-15 23:47:15 +01:00 committed by GitHub
commit 738688a8a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 199 additions and 3 deletions

View File

@ -11,6 +11,13 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
- [x] - [x]
## [2.7.0] - 2025-06-15
### Added
- Bus.EQ Channel Cell commands added, see [Bus.EQ.Channel.Cell]().
- Added by [PR #16](https://github.com/onyx-and-iris/voicemeeter-api-python/pull/16)
## [2.6.0] - 2024-06-29 ## [2.6.0] - 2024-06-29
### Added ### Added

View File

@ -292,6 +292,23 @@ example:
vm.bus[3].eq.on = True vm.bus[3].eq.on = True
``` ```
##### Bus.EQ.Channel.Cell
The following properties are available.
- `on`: boolean
- `type`: int
- `f`: float
- `gain`: float
- `q`: quality
example:
```python
vm.bus[3].eq.channel[0].cell[2].on = True
vm.bus[3].eq.channel[0].cell[2].f = 5000
```
##### Bus.Modes ##### Bus.Modes
The following properties are available. The following properties are available.

View File

@ -0,0 +1,9 @@
## About
The purpose of this script is to demonstratehow to utilize the channels and cells that are available as part of the EQ. It should take audio playing in the second virtual strip and then apply a LGF on the first physical at 500 Hz.
## Use
Configured for banana version.
Make sure you are playing audio into the second virtual strip and out of the first physical bus, both channels are unmuted and that you aren't monitoring another mixbus. Then run the script.

View File

@ -0,0 +1,50 @@
import time
import voicemeeterlib
def main():
KIND_ID = 'banana'
BUS_INDEX = 0 # Index of the bus to edit, can be changed as needed
CHANNEL_INDEX = 0 # Index of the channel to edit, can be changed as needed
with voicemeeterlib.api(KIND_ID) as vm:
print(f'Bus[{BUS_INDEX}].EQ.on: {vm.bus[BUS_INDEX].eq.on}')
print(
f'Bus[{BUS_INDEX}].EQ.channel[{CHANNEL_INDEX}].cell[0].on: {vm.bus[BUS_INDEX].eq.channel[CHANNEL_INDEX].cell[0].on}'
)
print('Check sending commands (should affect your VM Banana window)')
vm.bus[BUS_INDEX].eq.on = True
vm.bus[BUS_INDEX].eq.ab = 0 # corresponds to A EQ memory slot
vm.bus[BUS_INDEX].mute = False
for j, cell in enumerate(vm.bus[BUS_INDEX].eq.channel[CHANNEL_INDEX].cell):
cell.on = True
cell.f = 500
cell.gain = -10
cell.type = 3 # Should correspond to LPF
cell.q = 10
print(
f'Channel {CHANNEL_INDEX}, Cell {j}: on={cell.on}, f={cell.f}, type={cell.type}, gain={cell.gain}, q={cell.q}'
)
time.sleep(1) # Sleep to simulate processing time
cell.on = False
cell.f = 50
cell.gain = 0
cell.type = 0
cell.q = 3
print(
f'Channel {CHANNEL_INDEX}, Cell {j}: on={cell.on}, f={cell.f}, type={cell.type} , gain={cell.gain}, q={cell.q}'
)
vm.bus[BUS_INDEX].eq.on = False
if __name__ == '__main__':
main()

View File

@ -1,6 +1,6 @@
[project] [project]
name = "voicemeeter-api" name = "voicemeeter-api"
version = "2.6.1" version = "2.7.0"
description = "A Python wrapper for the Voiceemeter API" description = "A Python wrapper for the Voiceemeter API"
authors = [ authors = [
{name = "Onyx and Iris",email = "code@onyxandiris.online"} {name = "Onyx and Iris",email = "code@onyxandiris.online"}
@ -16,7 +16,7 @@ dependencies = [
packages = [{ include = "voicemeeterlib" }] packages = [{ include = "voicemeeterlib" }]
[tool.poetry.requires-plugins] [tool.poetry.requires-plugins]
poethepoet = "^0.32.1" poethepoet = "^0.35.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pytest = "^8.3.4" pytest = "^8.3.4"
@ -37,6 +37,7 @@ levels.script = "scripts:ex_levels"
midi.script = "scripts:ex_midi" midi.script = "scripts:ex_midi"
obs.script = "scripts:ex_obs" obs.script = "scripts:ex_obs"
observer.script = "scripts:ex_observer" observer.script = "scripts:ex_observer"
eqedit.script = "scripts:ex_eqedit"
test-basic.script = "scripts:test_basic" test-basic.script = "scripts:test_basic"
test-banana.script = "scripts:test_banana" test-banana.script = "scripts:test_banana"
test-potato.script = "scripts:test_potato" test-potato.script = "scripts:test_potato"

View File

@ -37,6 +37,11 @@ def ex_observer():
subprocess.run([sys.executable, str(scriptpath)]) subprocess.run([sys.executable, str(scriptpath)])
def ex_eqedit():
scriptpath = Path.cwd() / 'examples' / 'eq_edit' / '.'
subprocess.run([sys.executable, str(scriptpath)])
def test_basic(): def test_basic():
subprocess.run(['tox'], env=os.environ.copy() | {'KIND': 'basic'}) subprocess.run(['tox'], env=os.environ.copy() | {'KIND': 'basic'})

View File

@ -88,6 +88,25 @@ class Bus(IRemote):
class BusEQ(IRemote): class BusEQ(IRemote):
@classmethod
def make(cls, remote, i):
"""
Factory method for BusEQ.
Returns a BusEQ class.
"""
kls = (cls,)
BusEQ_cls = type(
'BusEQ',
kls,
{
'channel': tuple(
BusEQCh.make(remote, i, j) for j in range(remote.kind.channels)
)
},
)
return BusEQ_cls(remote, i)
@property @property
def identifier(self) -> str: def identifier(self) -> str:
return f'Bus[{self.index}].eq' return f'Bus[{self.index}].eq'
@ -109,6 +128,86 @@ class BusEQ(IRemote):
self.setter('ab', 1 if val else 0) self.setter('ab', 1 if val else 0)
class BusEQCh(IRemote):
@classmethod
def make(cls, remote, i, j):
"""
Factory method for Bus EQ channel.
Returns a BusEQCh class.
"""
kls = (cls,)
BusEQCh_cls = type(
'BusEQCh',
kls,
{
'cell': tuple(
BusEQChCell(remote, i, j, k) for k in range(remote.kind.cells)
)
},
)
return BusEQCh_cls(remote, i, j)
def __init__(self, remote, i, j):
super().__init__(remote, i)
self.channel_index = j
@property
def identifier(self) -> str:
return f'Bus[{self.index}].eq.channel[{self.channel_index}]'
class BusEQChCell(IRemote):
def __init__(self, remote, i, j, k):
super().__init__(remote, i)
self.channel_index = j
self.cell_index = k
@property
def identifier(self) -> str:
return f'Bus[{self.index}].eq.channel[{self.channel_index}].cell[{self.cell_index}]'
@property
def on(self) -> bool:
return self.getter('on') == 1
@on.setter
def on(self, val: bool):
self.setter('on', 1 if val else 0)
@property
def type(self) -> int:
return int(self.getter('type'))
@type.setter
def type(self, val: int):
self.setter('type', val)
@property
def f(self) -> float:
return round(self.getter('f'), 1)
@f.setter
def f(self, val: float):
self.setter('f', val)
@property
def gain(self) -> float:
return round(self.getter('gain'), 1)
@gain.setter
def gain(self, val: float):
self.setter('gain', val)
@property
def q(self) -> float:
return round(self.getter('q'), 1)
@q.setter
def q(self, val: float):
self.setter('q', val)
class PhysicalBus(Bus): class PhysicalBus(Bus):
@classmethod @classmethod
def make(cls, remote, i, kind): def make(cls, remote, i, kind):
@ -321,7 +420,7 @@ def bus_factory(is_phys_bus, remote, i) -> Union[PhysicalBus, VirtualBus]:
{ {
'levels': BusLevel(remote, i), 'levels': BusLevel(remote, i),
'mode': BUSMODEMIXIN_cls(remote, i), 'mode': BUSMODEMIXIN_cls(remote, i),
'eq': BusEQ(remote, i), 'eq': BusEQ.make(remote, i),
}, },
)(remote, i) )(remote, i)

View File

@ -31,6 +31,8 @@ class KindMapClass(metaclass=SingletonType):
asio: tuple asio: tuple
insert: int insert: int
composite: int composite: int
channels: int
cells: int
@property @property
def phys_in(self) -> int: def phys_in(self) -> int:
@ -76,6 +78,8 @@ class BasicMap(KindMapClass):
asio: tuple = (0, 0) asio: tuple = (0, 0)
insert: int = 0 insert: int = 0
composite: int = 0 composite: int = 0
channels: int = 0
cells: int = 0
@dataclass(frozen=True) @dataclass(frozen=True)
@ -86,6 +90,8 @@ class BananaMap(KindMapClass):
asio: tuple = (6, 8) asio: tuple = (6, 8)
insert: int = 22 insert: int = 22
composite: int = 8 composite: int = 8
channels: int = 8
cells: int = 6
@dataclass(frozen=True) @dataclass(frozen=True)
@ -96,6 +102,8 @@ class PotatoMap(KindMapClass):
asio: tuple = (10, 8) asio: tuple = (10, 8)
insert: int = 34 insert: int = 34
composite: int = 8 composite: int = 8
channels: int = 8
cells: int = 6
def kind_factory(kind_id): def kind_factory(kind_id):