diff --git a/CHANGELOG.md b/CHANGELOG.md index ae3f5c6..0845b97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,13 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass - [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 ### Added diff --git a/README.md b/README.md index 0c4f8da..fa01963 100644 --- a/README.md +++ b/README.md @@ -292,6 +292,23 @@ example: 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 The following properties are available. diff --git a/examples/eq_edit/README.md b/examples/eq_edit/README.md new file mode 100644 index 0000000..04af6a9 --- /dev/null +++ b/examples/eq_edit/README.md @@ -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. diff --git a/examples/eq_edit/__main__.py b/examples/eq_edit/__main__.py new file mode 100644 index 0000000..51e5ce9 --- /dev/null +++ b/examples/eq_edit/__main__.py @@ -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() diff --git a/pyproject.toml b/pyproject.toml index 9e716c3..1ec628f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "voicemeeter-api" -version = "2.6.1" +version = "2.7.0" description = "A Python wrapper for the Voiceemeter API" authors = [ {name = "Onyx and Iris",email = "code@onyxandiris.online"} @@ -16,7 +16,7 @@ dependencies = [ packages = [{ include = "voicemeeterlib" }] [tool.poetry.requires-plugins] -poethepoet = "^0.32.1" +poethepoet = "^0.35.0" [tool.poetry.group.dev.dependencies] pytest = "^8.3.4" @@ -37,6 +37,7 @@ levels.script = "scripts:ex_levels" midi.script = "scripts:ex_midi" obs.script = "scripts:ex_obs" observer.script = "scripts:ex_observer" +eqedit.script = "scripts:ex_eqedit" test-basic.script = "scripts:test_basic" test-banana.script = "scripts:test_banana" test-potato.script = "scripts:test_potato" diff --git a/scripts.py b/scripts.py index 821bcff..76e9aa6 100644 --- a/scripts.py +++ b/scripts.py @@ -37,6 +37,11 @@ def ex_observer(): subprocess.run([sys.executable, str(scriptpath)]) +def ex_eqedit(): + scriptpath = Path.cwd() / 'examples' / 'eq_edit' / '.' + subprocess.run([sys.executable, str(scriptpath)]) + + def test_basic(): subprocess.run(['tox'], env=os.environ.copy() | {'KIND': 'basic'}) diff --git a/voicemeeterlib/bus.py b/voicemeeterlib/bus.py index 17654ea..fb702c0 100644 --- a/voicemeeterlib/bus.py +++ b/voicemeeterlib/bus.py @@ -88,6 +88,25 @@ class Bus(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 def identifier(self) -> str: return f'Bus[{self.index}].eq' @@ -109,6 +128,86 @@ class BusEQ(IRemote): 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): @classmethod 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), 'mode': BUSMODEMIXIN_cls(remote, i), - 'eq': BusEQ(remote, i), + 'eq': BusEQ.make(remote, i), }, )(remote, i) diff --git a/voicemeeterlib/kinds.py b/voicemeeterlib/kinds.py index 93f3cfe..ab73065 100644 --- a/voicemeeterlib/kinds.py +++ b/voicemeeterlib/kinds.py @@ -31,6 +31,8 @@ class KindMapClass(metaclass=SingletonType): asio: tuple insert: int composite: int + channels: int + cells: int @property def phys_in(self) -> int: @@ -76,6 +78,8 @@ class BasicMap(KindMapClass): asio: tuple = (0, 0) insert: int = 0 composite: int = 0 + channels: int = 0 + cells: int = 0 @dataclass(frozen=True) @@ -86,6 +90,8 @@ class BananaMap(KindMapClass): asio: tuple = (6, 8) insert: int = 22 composite: int = 8 + channels: int = 8 + cells: int = 6 @dataclass(frozen=True) @@ -96,6 +102,8 @@ class PotatoMap(KindMapClass): asio: tuple = (10, 8) insert: int = 34 composite: int = 8 + channels: int = 8 + cells: int = 6 def kind_factory(kind_id):