├── .gitignore ├── LICENSE ├── README.md ├── __main__.py ├── setup.py └── voicemeeter ├── __init__.py ├── driver.py ├── errors.py ├── input.py ├── kinds.py ├── output.py ├── profiles.py ├── remote.py ├── strip.py └── util.py /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__ 2 | 3 | build 4 | dist 5 | *.egg-info 6 | 7 | .vscode 8 | venv 9 | 10 | old 11 | vendor 12 | profiles -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Christian Volkmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Voicemeeter Remote 2 | A Python API to [Voicemeeter](https://www.vb-audio.com/Voicemeeter/potato.htm), a virtual audio mixer for Windows. 3 | 4 | This work-in-progress package wraps the [Voicemeeter Remote C API](https://forum.vb-audio.com/viewtopic.php?f=8&t=346) and provides a higher-level interface for developers. 5 | 6 | Tested against Voicemeeter Potato, release from September 2019. 7 | 8 | 9 | ## Prerequisites 10 | - Voicemeeter 1 (Basic), 2 (Banana) or 3 (Potato) 11 | - Python 3.6+ 12 | 13 | ## Installation 14 | ``` 15 | git clone https://github.com/Freemanium/voicemeeter-remote-python 16 | cd voicemeeter-remote-python 17 | pip install . 18 | ``` 19 | 20 | ## Usage 21 | ```python 22 | import voicemeeter 23 | 24 | # Can be 'basic', 'banana' or 'potato' 25 | kind = 'potato' 26 | 27 | # Ensure that Voicemeeter is launched 28 | voicemeeter.launch(kind) 29 | 30 | with voicemeeter.remote(kind) as vmr: 31 | # Set the mapping of the second input strip 32 | vmr.inputs[1].A4 = True 33 | print(f'Output A4 of Strip {vmr.inputs[1].label}: {vmr.inputs[1].A4}') 34 | 35 | # Set the gain slider of the leftmost output bus 36 | vmr.outputs[0].gain = -6.0 37 | 38 | # Also supports assignment through a dict 39 | vmr.apply({ 40 | 'in-5': dict(A1=True, B1=True, gain=-6.0), 41 | 'out-2': dict(mute=True) 42 | }) 43 | 44 | # Resets all UI elements to a base profile 45 | vmr.reset() 46 | ``` 47 | Or if you want to use the API outside of a with statment 48 | ```python 49 | import voicemeeter 50 | 51 | # Can be 'basic', 'banana' or 'potato' 52 | kind = 'potato' 53 | 54 | # Ensure that Voicemeeter is launched 55 | voicemeeter.launch(kind) 56 | 57 | vmr = voicemeeter.remote(kind) 58 | vmr.login() 59 | # Set the mapping of the second input strip 60 | vmr.inputs[1].A4 = True 61 | print(f'Output A4 of Strip {vmr.inputs[1].label}: {vmr.inputs[1].A4}') 62 | 63 | # Set the gain slider of the leftmost output bus 64 | vmr.outputs[0].gain = -6.0 65 | 66 | # Also supports assignment through a dict 67 | vmr.apply({ 68 | 'in-5': dict(A1=True, B1=True, gain=-6.0), 69 | 'out-2': dict(mute=True) 70 | }) 71 | 72 | # Resets all UI elements to a base profile 73 | vmr.reset() 74 | vmr.logout() 75 | ``` 76 | 77 | ## Profiles 78 | Profiles through config files are supported. 79 | ``` 80 | mkdir profiles 81 | mkdir profiles/potato 82 | touch profiles/potato/mySetup.toml 83 | ``` 84 | 85 | A config can contain any key that `remote.apply()` would accept. Additionally, `extends` can be provided to inherit from another profile. Two profiles are available by default: 86 | - `blank`, all inputs off and all sliders to `0.0` 87 | - `base`, all physical inputs to `A1`, all virtual inputs to `B1`, all sliders to `0.0` 88 | 89 | Sample `mySetup.toml` 90 | ```toml 91 | extends = 'base' 92 | [in-0] 93 | mute = 1 94 | 95 | [in-5] 96 | A1 = 0 97 | A2 = 1 98 | A4 = 1 99 | gain = 0.0 100 | 101 | [in-6] 102 | A1 = 0 103 | A2 = 1 104 | A4 = 1 105 | gain = 0.0 106 | ``` 107 | 108 | ## API 109 | ### Kinds 110 | A *kind* specifies a major Voicemeeter version. Currently this encompasses 111 | - `basic`: [Voicemeeter](https://www.vb-audio.com/Voicemeeter/index.htm) 112 | - `banana`: [Voicemeeter Banana](https://www.vb-audio.com/Voicemeeter/banana.htm) 113 | - `potato`: [Voicemeeter Potato](https://www.vb-audio.com/Voicemeeter/potato.htm) 114 | 115 | #### `voicemeeter.launch(kind_id, delay=1)` 116 | Launches Voicemeeter. If Voicemeeter is already launched, it is brought to the front. Wait for `delay` seconds after a launch is dispatched. 117 | 118 | #### `voicemeeter.remote(kind_id, delay=0.015)` 119 | Factory function for remotes. `delay` specifies a cooldown time after every command in seconds. Returns a `VMRemote` based on the `kind_id`. 120 | Use it with a context manager 121 | ```python 122 | with voicemeeter.remote('potato') as vmr: 123 | vmr.inputs[0].mute = True 124 | ``` 125 | 126 | ### `VMRemote` (higher level) 127 | #### `vmr.type` 128 | The kind of the Voicemeeter instance. 129 | 130 | #### `vmr.version` 131 | A tuple of the form `(v1, v2, v3, v4)`. 132 | 133 | #### `vmr.inputs` 134 | An `InputStrip` tuple, containing both physical and virtual. 135 | #### `vmr.outputs` 136 | An `OutputBus` tuple, containing both physical and virtual. 137 | 138 | #### `vmr.show()` 139 | Shows Voicemeeter if it's minimized. No effect otherwise. 140 | #### `vmr.shutdown()` 141 | Closes Voicemeeter. 142 | #### `vmr.restart()` 143 | Restarts Voicemeeter's audio engine. 144 | 145 | #### `vmr.apply(mapping)` 146 | Updates values through a dict. 147 | Example: 148 | ```python 149 | vmr.apply({ 150 | 'in-5': dict(A1=True, B1=True, gain=-6.0), 151 | 'out-2': dict(mute=True) 152 | }) 153 | ``` 154 | #### `vmr.apply_profile(profile_name)` 155 | Loads a profile. 156 | #### `vmr.reset()` 157 | Resets everything to the `base` profile. 158 | 159 | ### `InputStrip` 160 | Any property is gettable and settable. 161 | - `label`: string 162 | - `solo`: boolean 163 | - `mute`: boolean 164 | - `gain`: float, from -60.0 to 12.0 165 | - `eqgain1`: float, from -12.0 to 12.0 166 | - `eqgain2`: float, from -12.0 to 12.0 167 | - `eqgain3`: float, from -12.0 to 12.0 168 | - `comp`: float, from 0.0 to 10.0 169 | - `gate`: float, from 0.0 to 10.0 170 | - Output mapping (e.g. `A1`, `B3`, etc.): boolean, depends on the Voicemeeter kind 171 | - `apply()`: Works similar to `vmr.apply()` 172 | ### `OutputBus` 173 | Any property is gettable and settable. 174 | - `mute`: boolean 175 | - `gain`: float, from -60.0 to 12.0 176 | - `apply()`: Works similar to `vmr.apply()` 177 | 178 | ### `VMRemote` (lower level) 179 | #### `vmr.dirty` 180 | `True` iff UI parameters have been updated. Use this if to poll for UI updates. 181 | 182 | #### `vmr.get(param_name, string=False)` 183 | Calls the C API's parameter getters, `GetParameterFloat` or `GetParameterStringW` respectively. Tries to cache the value on the first call and updates the cached value if `vmr.dirty` is `True`. 184 | 185 | #### `vmr.set(param_name, value)` 186 | Calls the C API's parameter getters, `SetParameterFloat` or `SetParameterStringW` respectively. 187 | 188 | ### Errors 189 | - `errors.VMRError`: Base Voicemeeter Remote error class. 190 | - `errors.VMRDriverError`: Raised when a C API function returns an unexpected value. 191 | 192 | ## Resources 193 | - [Voicemeeter Remote C API](https://forum.vb-audio.com/viewtopic.php?f=8&t=346) 194 | -------------------------------------------------------------------------------- /__main__.py: -------------------------------------------------------------------------------- 1 | import voicemeeter 2 | 3 | # Can be 'basic', 'banana' or 'potato' 4 | kind = 'potato' 5 | 6 | # Ensure that Voicemeeter is launched 7 | voicemeeter.launch(kind) 8 | 9 | with voicemeeter.remote(kind) as vmr: 10 | # Set the mapping of the second input strip 11 | vmr.inputs[1].A4 = True 12 | print(f'Output A4 of Strip {vmr.inputs[1].label}: {vmr.inputs[1].A4}') 13 | 14 | # Set the gain slider of the leftmost output bus 15 | vmr.outputs[0].gain = -6.0 16 | 17 | # Also supports assignment through a dict 18 | vmr.apply({ 19 | 'in-5': dict(A1=True, B1=True, gain=-6.0), 20 | 'out-2': dict(mute=True) 21 | }) 22 | 23 | # Resets all UI elements to a base profile 24 | vmr.reset() -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='voicemeeter', 5 | version='0.1', 6 | description='Voicemeeter Remote Python API', 7 | packages=['voicemeeter'], 8 | install_requires=[ 9 | 'toml' 10 | ] 11 | ) -------------------------------------------------------------------------------- /voicemeeter/__init__.py: -------------------------------------------------------------------------------- 1 | import subprocess as sp 2 | import time 3 | 4 | from .remote import connect as remote 5 | from .driver import vm_subpath 6 | from . import kinds 7 | 8 | def launch(kind_id, delay=1): 9 | """ Starts Voicemeeter. """ 10 | kind = kinds.get(kind_id) 11 | sp.Popen([vm_subpath(kind.executable)]) 12 | time.sleep(delay) 13 | 14 | __ALL__ = ['launch', 'remote'] -------------------------------------------------------------------------------- /voicemeeter/driver.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | import sys 3 | import platform 4 | import ctypes 5 | 6 | from .errors import VMRError 7 | 8 | bits = 64 if sys.maxsize > 2**32 else 32 9 | os = platform.system() 10 | 11 | if os != 'Windows' or bits != 64: 12 | raise VMRError('The vmr package only supports Windows 64-bit') 13 | 14 | 15 | DLL_NAME = 'VoicemeeterRemote64.dll' 16 | 17 | vm_base = path.join(path.expandvars('%ProgramFiles(x86)%'), 'VB', 'Voicemeeter') 18 | 19 | def vm_subpath(*fragments): 20 | """ Returns a path based from Voicemeeter's install directory. """ 21 | return path.join(vm_base, *fragments) 22 | 23 | dll_path = vm_subpath(DLL_NAME) 24 | 25 | if not path.exists(dll_path): 26 | raise VMRError(f'Could not find {DLL_NAME}') 27 | 28 | dll = ctypes.cdll.LoadLibrary(dll_path) -------------------------------------------------------------------------------- /voicemeeter/errors.py: -------------------------------------------------------------------------------- 1 | class VMRError(Exception): 2 | """ Base error class for Voicemeeter Remote. """ 3 | pass 4 | 5 | class VMRDriverError(VMRError): 6 | """ Raised when a low-level C API function returns an unexpected value. """ 7 | def __init__(self, fn_name, retval): 8 | self.function = fn_name 9 | self.retval = retval 10 | 11 | super().__init__(f'VMR#{fn_name}() returned {retval}') -------------------------------------------------------------------------------- /voicemeeter/input.py: -------------------------------------------------------------------------------- 1 | from .errors import VMRError 2 | from .strip import VMElement, bool_prop, str_prop, float_prop 3 | from . import kinds 4 | 5 | class InputStrip(VMElement): 6 | """ Base class for input strips. """ 7 | @classmethod 8 | def make(cls, is_physical, remote, index, **kwargs): 9 | """ 10 | Factory function for input strips. 11 | 12 | Returns a physical/virtual strip for the remote's kind. 13 | """ 14 | PhysStrip, VirtStrip = _strip_pairs[remote.kind.id] 15 | IS_cls = PhysStrip if is_physical else VirtStrip 16 | return IS_cls(remote, index, **kwargs) 17 | 18 | @property 19 | def identifier(self): 20 | return f'Strip[{self.index}]' 21 | 22 | solo = bool_prop('Solo') 23 | mute = bool_prop('Mute') 24 | 25 | gain = float_prop('Gain', range=(-60,12)) 26 | comp = float_prop('Comp', range=(0,10)) 27 | gate = float_prop('Gate', range=(0,10)) 28 | limit = float_prop('Limit', range=(-40,12)) 29 | 30 | eqgain1 = float_prop('EQGain1', range=(-12,12)) 31 | eqgain2 = float_prop('EQGain2', range=(-12,12)) 32 | eqgain3 = float_prop('EQGain3', range=(-12,12)) 33 | 34 | label = str_prop('Label') 35 | device = str_prop('device.name') 36 | sr = str_prop('device.sr') 37 | 38 | class PhysicalInputStrip(InputStrip): 39 | mono = bool_prop('Mono') 40 | 41 | class VirtualInputStrip(InputStrip): 42 | mono = bool_prop('MC') 43 | 44 | 45 | def _make_strip_mixin(kind): 46 | """ Creates a mixin with the kind's strip layout set as class variables. """ 47 | num_A, num_B = kind.layout 48 | return type(f'StripMixin{kind.name}', (), { 49 | **{f'A{i}': bool_prop(f'A{i}') for i in range(1, num_A+1)}, 50 | **{f'B{i}': bool_prop(f'B{i}') for i in range(1, num_B+1)} 51 | }) 52 | 53 | _strip_mixins = {kind.id: _make_strip_mixin(kind) for kind in kinds.all} 54 | 55 | def _make_strip_pair(kind): 56 | """ Creates a PhysicalInputStrip and a VirtualInputStrip of a kind. """ 57 | StripMixin = _strip_mixins[kind.id] 58 | PhysStrip = type(f'PhysicalInputStrip{kind.name}', (PhysicalInputStrip, StripMixin), {}) 59 | VirtStrip = type(f'VirtualInputStrip{kind.name}', (VirtualInputStrip, StripMixin), {}) 60 | return (PhysStrip, VirtStrip) 61 | 62 | _strip_pairs = {kind.id: _make_strip_pair(kind) for kind in kinds.all} 63 | -------------------------------------------------------------------------------- /voicemeeter/kinds.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from .errors import VMRError 3 | 4 | """ 5 | Represents a major version of Voicemeeter and describes 6 | its strip layout. 7 | """ 8 | VMKind = namedtuple('VMKind', ['id', 'name', 'layout', 'executable']) 9 | 10 | _kind_map = { 11 | 'basic': VMKind('basic', 'Basic', (2,1), 'voicemeeter.exe'), 12 | 'banana': VMKind('banana', 'Banana', (3,2), 'voicemeeterpro.exe'), 13 | 'potato': VMKind('potato', 'Potato', (5,3), 'voicemeeter8.exe') 14 | } 15 | 16 | def get(kind_id): 17 | try: 18 | return _kind_map[kind_id] 19 | except KeyError: 20 | raise VMRError(f'Invalid Voicemeeter kind: {kind_id}') 21 | 22 | all = list(_kind_map.values()) -------------------------------------------------------------------------------- /voicemeeter/output.py: -------------------------------------------------------------------------------- 1 | # param_name => is_numeric 2 | from .errors import VMRError 3 | from .strip import VMElement, bool_prop, str_prop, float_prop 4 | 5 | class OutputBus(VMElement): 6 | """ Base class for output busses. """ 7 | @classmethod 8 | def make(cls, is_physical, *args, **kwargs): 9 | """ 10 | Factory function for output busses. 11 | 12 | Returns a physical/virtual strip for the remote's kind 13 | """ 14 | OB_cls = PhysicalOutputBus if is_physical else VirtualOutputBus 15 | return OB_cls(*args, **kwargs) 16 | 17 | @property 18 | def identifier(self): 19 | return f'Bus[{self.index}]' 20 | 21 | mute = bool_prop('Mute') 22 | gain = float_prop('Gain', range=(-60,12)) 23 | 24 | class PhysicalOutputBus(OutputBus): 25 | pass 26 | 27 | class VirtualOutputBus(OutputBus): 28 | pass -------------------------------------------------------------------------------- /voicemeeter/profiles.py: -------------------------------------------------------------------------------- 1 | import os 2 | import toml 3 | from . import kinds 4 | from .errors import VMRError 5 | from .util import project_path, merge_dicts 6 | 7 | profiles = {} 8 | 9 | def _make_blank_profile(kind): 10 | num_A, num_B = kind.layout 11 | input_strip_config = { 12 | 'gain': 0.0, 13 | 'solo': False, 14 | 'mute': False, 15 | 'mono': False, 16 | **{f'A{i}': False for i in range(1, num_A+1)}, 17 | **{f'B{i}': False for i in range(1, num_B+1)} 18 | } 19 | output_strip_config = { 20 | 'gain': 0.0, 21 | 'mute': False 22 | } 23 | return { 24 | **{f'in-{i}': input_strip_config for i in range(num_A+num_B)}, 25 | **{f'out-{i}': output_strip_config for i in range(num_A+num_B)} 26 | } 27 | 28 | def _make_base_profile(kind): 29 | num_A, num_B = kind.layout 30 | blank = _make_blank_profile(kind) 31 | overrides = { 32 | **{f'in-{i}': dict(B1=True) for i in range(num_A)}, 33 | **{f'in-{i}': dict(A1=True) for i in range(num_A, num_A+num_B)} 34 | } 35 | abc = merge_dicts(blank, overrides) 36 | return abc 37 | 38 | for kind in kinds.all: 39 | profiles[kind.id] = { 40 | 'blank': _make_blank_profile(kind), 41 | 'base': _make_base_profile(kind) 42 | } 43 | 44 | # Load profiles from config files in profiles//.toml 45 | if os.path.exists(project_path('profiles')): 46 | for kind in kinds.all: 47 | profile_folder = project_path('profiles', kind.id) 48 | if os.path.exists(profile_folder): 49 | filenames = [f for f in os.listdir(profile_folder) if f.endswith('.toml')] 50 | configs = {} 51 | for filename in filenames: 52 | name = filename[:-5] # strip of .toml extension 53 | try: 54 | configs[name] = toml.load(project_path('profiles', kind.id, filename)) 55 | except toml.TomlDecodeError: 56 | print(f'Invalid TOML profile: {kind.id}/{filename}') 57 | 58 | for name, cfg in configs.items(): 59 | print(f'Loaded profile {kind.id}/{name}') 60 | profiles[kind.id][name] = cfg -------------------------------------------------------------------------------- /voicemeeter/remote.py: -------------------------------------------------------------------------------- 1 | import ctypes as ct 2 | import time 3 | import abc 4 | 5 | from .driver import dll 6 | from .errors import VMRError, VMRDriverError 7 | from .input import InputStrip 8 | from .output import OutputBus 9 | from . import kinds 10 | from . import profiles 11 | from .util import merge_dicts 12 | 13 | loggedIn = False 14 | 15 | 16 | class VMRemote(abc.ABC): 17 | """ Wrapper around Voicemeeter Remote's C API. """ 18 | 19 | def __init__(self, delay=.015): 20 | self.delay = delay 21 | self.cache = {} 22 | 23 | def _call(self, fn, *args, check=True, expected=(0,)): 24 | """ 25 | Runs a C API function. 26 | 27 | Raises an exception when check is True and the 28 | function's return value is not 0 (OK). 29 | """ 30 | fn_name = 'VBVMR_' + fn 31 | retval = getattr(dll, fn_name)(*args) 32 | if check and retval not in expected: 33 | raise VMRDriverError(fn_name, retval) 34 | time.sleep(self.delay) 35 | return retval 36 | 37 | def _login(self): 38 | global loggedIn 39 | if (loggedIn == False): 40 | self._call('Login') 41 | loggedIn = True 42 | 43 | def _logout(self): 44 | global loggedIn 45 | if (loggedIn == True): 46 | self._call('Logout') 47 | loggedIn = False 48 | 49 | def login(self): 50 | global loggedIn 51 | if (loggedIn == False): 52 | self._call('Login') 53 | loggedIn = True 54 | 55 | def logout(self): 56 | self._logout() 57 | 58 | @property 59 | def type(self): 60 | """ Returns the type of Voicemeeter installation (basic, banana, potato). """ 61 | buf = ct.c_long() 62 | self._call('GetVoicemeeterType', ct.byref(buf)) 63 | val = buf.value 64 | if val == 1: 65 | return 'basic' 66 | elif val == 2: 67 | return 'banana' 68 | elif val == 3: 69 | return 'potato' 70 | else: 71 | raise VMRError(f'Unexpected Voicemeeter type: {val}') 72 | 73 | @property 74 | def version(self): 75 | """ Returns Voicemeeter's version as a tuple (v1, v2, v3, v4) """ 76 | buf = ct.c_long() 77 | self._call('GetVoicemeeterVersion', ct.byref(buf)) 78 | v1 = (buf.value & 0xFF000000) >> 24 79 | v2 = (buf.value & 0x00FF0000) >> 16 80 | v3 = (buf.value & 0x0000FF00) >> 8 81 | v4 = (buf.value & 0x000000FF) 82 | return (v1, v2, v3, v4) 83 | 84 | @property 85 | def dirty(self): 86 | """ True iff UI parameters have been updated. """ 87 | val = self._call('IsParametersDirty', expected=(0, 1)) 88 | return (val == 1) 89 | 90 | def get(self, param, string=False): 91 | """ Retrieves a parameter. """ 92 | param = param.encode('ascii') 93 | if not self.dirty: 94 | if param in self.cache: 95 | pass 96 | # return self.cache[param] 97 | 98 | if string: 99 | buf = (ct.c_wchar * 512)() 100 | self._call('GetParameterStringW', param, ct.byref(buf)) 101 | else: 102 | buf = ct.c_float() 103 | self._call('GetParameterFloat', param, ct.byref(buf)) 104 | val = buf.value 105 | self.cache[param] = val 106 | return val 107 | 108 | def set(self, param, val): 109 | """ Updates a parameter. """ 110 | param = param.encode('ascii') 111 | if isinstance(val, str): 112 | if len(val) >= 512: 113 | raise VMRError('String is too long') 114 | self._call('SetParameterStringW', param, ct.c_wchar_p(val)) 115 | else: 116 | self._call('SetParameterFloat', param, ct.c_float(float(val))) 117 | 118 | def show(self): 119 | """ Shows Voicemeeter if it's hidden. """ 120 | self.set('Command.Show', 1) 121 | 122 | def shutdown(self): 123 | """ Closes Voicemeeter. """ 124 | self.set('Command.Shutdown', 1) 125 | 126 | def restart(self): 127 | """ Restarts Voicemeeter's audio engine. """ 128 | self.set('Command.Restart', 1) 129 | 130 | def apply(self, mapping): 131 | """ Sets all parameters of a dict. """ 132 | for key, submapping in mapping.items(): 133 | strip, index = key.split('-') 134 | index = int(index) 135 | if strip in ('in', 'input'): 136 | target = self.inputs[index] 137 | elif strip in ('out', 'output'): 138 | target = self.outputs[index] 139 | else: 140 | raise ValueError(strip) 141 | target.apply(submapping) 142 | 143 | def apply_profile(self, name): 144 | try: 145 | profile = self.profiles[name] 146 | if 'extends' in profile: 147 | base = self.profiles[profile['extends']] 148 | profile = merge_dicts(base, profile) 149 | del profile['extends'] 150 | self.apply(profile) 151 | except KeyError: 152 | raise VMRError(f'Unknown profile: {self.kind.id}/{name}') 153 | 154 | def reset(self): 155 | self.apply_profile('base') 156 | 157 | def __enter__(self): 158 | self._login() 159 | return self 160 | 161 | def __exit__(self, type, value, traceback): 162 | self.logout() 163 | 164 | 165 | def _make_remote(kind): 166 | """ 167 | Creates a new remote class and sets its number of inputs 168 | and outputs for a VM kind. 169 | 170 | The returned class will subclass VMRemote. 171 | """ 172 | 173 | def init(self, *args, **kwargs): 174 | VMRemote.__init__(self, *args, **kwargs) 175 | self.kind = kind 176 | self.num_A, self.num_B = kind.layout 177 | self.inputs = tuple(InputStrip.make((i < self.num_A), self, i) for i in range(self.num_A + self.num_B)) 178 | self.outputs = tuple(OutputBus.make((i < self.num_B), self, i) for i in range(self.num_A + self.num_B)) 179 | 180 | def get_profiles(self): 181 | return profiles.profiles[kind.id] 182 | 183 | return type(f'VMRemote{kind.name}', (VMRemote,), { 184 | '__init__': init, 185 | 'profiles': property(get_profiles) 186 | }) 187 | 188 | 189 | _remotes = {kind.id: _make_remote(kind) for kind in kinds.all} 190 | 191 | 192 | def connect(kind_id, delay=None): 193 | if delay is None: 194 | delay = .015 195 | """ Connect to Voicemeeter and sets its strip layout. """ 196 | try: 197 | cls = _remotes[kind_id] 198 | return cls(delay=delay) 199 | except KeyError as err: 200 | raise VMRError(f'Invalid Voicemeeter kind: {kind_id}') -------------------------------------------------------------------------------- /voicemeeter/strip.py: -------------------------------------------------------------------------------- 1 | import abc 2 | from .errors import VMRError 3 | 4 | class VMElement(abc.ABC): 5 | """ Base class for InputStrip and OutputBus. """ 6 | def __init__(self, remote, index): 7 | self._remote = remote 8 | self.index = index 9 | 10 | def get(self, param, **kwargs): 11 | """ Returns the param value of the current strip. """ 12 | return self._remote.get(f'{self.identifier}.{param}', **kwargs) 13 | def set(self, param, val, **kwargs): 14 | """ Sets the param value of the current strip. """ 15 | self._remote.set(f'{self.identifier}.{param}', val, **kwargs) 16 | 17 | @abc.abstractmethod 18 | def identifier(self): 19 | pass 20 | 21 | def apply(self, mapping): 22 | """ Sets all parameters of a dict for the strip. """ 23 | for key, val in mapping.items(): 24 | if not hasattr(self, key): 25 | raise VMRError(f'Invalid {self.identifier} attribute: {key}') 26 | setattr(self, key, val) 27 | 28 | 29 | def bool_prop(param): 30 | """ A boolean VM parameter. """ 31 | def getter(self): 32 | return (self.get(param) == 1) 33 | def setter(self, val): 34 | return self.set(param, 1 if val else 0) 35 | return property(getter, setter) 36 | 37 | def str_prop(param): 38 | """ A string VM parameter. """ 39 | def getter(self): 40 | return self.get(param, string=True) 41 | def setter(self, val): 42 | return self.set(param, val) 43 | return property(getter, setter) 44 | 45 | def float_prop(param, range=None, normalize=False): 46 | """ A floating point VM parameter. """ 47 | def getter(self): 48 | val = self.get(param) 49 | if range: 50 | lo, hi = range 51 | if normalize: 52 | val = (val-lo)/(hi-lo) 53 | return val 54 | def setter(self, val): 55 | if range: 56 | lo, hi = range 57 | if normalize: 58 | val = val*(hi-lo)+lo 59 | return self.set(param, val) 60 | return property(getter, setter) -------------------------------------------------------------------------------- /voicemeeter/util.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | PROJECT_DIR = os.path.realpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..')) 4 | 5 | def project_path(*parts): 6 | return os.path.join(PROJECT_DIR, *parts) 7 | 8 | def merge_dicts(*srcs, dest={}): 9 | target = dest 10 | for src in srcs: 11 | for key, val in src.items(): 12 | if isinstance(val, dict): 13 | node = target.setdefault(key, {}) 14 | merge_dicts(val, dest=node) 15 | else: 16 | target[key] = val 17 | return target --------------------------------------------------------------------------------