diff --git a/README.md b/README.md index 0d3a36b..f200343 100644 --- a/README.md +++ b/README.md @@ -13,17 +13,14 @@ For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md) ## Prerequisites - [Voicemeeter](https://voicemeeter.com/) (Basic v1.0.8.2), (Banana v2.0.6.2) or (Potato v3.0.2.2) -- [Git for Windows](https://gitforwindows.org/) -- Python 3.9+ +- Python 3.11+ ## Installation For a step-by-step guide [click here](INSTALLATION.md) ``` -git clone https://github.com/onyx-and-iris/voicemeeter-compact -cd voicemeeter-compact -pip install . +pip install voicemeeter-compact ``` ## Usage @@ -31,13 +28,13 @@ pip install . Example `__main__.py` file: ```python -import voicemeeter +import voicemeeterlib import vmcompact def main(): # pass the kind_id and the vmr object to the app - with voicemeeter.remote(kind_id) as vmr: + with voicemeeterlib.api(kind_id) as vmr: app = vmcompact.connect(kind_id, vmr) app.mainloop() @@ -46,8 +43,6 @@ if __name__ == "__main__": # choose the kind of Voicemeeter (Local connection) kind_id = "banana" - voicemeeter.launch(kind_id, hide=False) - main() ``` @@ -141,13 +136,13 @@ A valid `vban.toml` might look like this: [connection-1] kind = 'banana' ip = '192.168.1.2' -streamname = 'streampc' -port = 6990 +streamname = 'worklaptop' +port = 6980 [connection-2] kind = 'potato' ip = '192.168.1.3' -streamname = 'worklaptop' +streamname = 'streampc' port = 6990 ``` @@ -165,7 +160,4 @@ Profiles may be loaded at any time via the menu. [Vincent Burel](https://github.com/vburel2018) for creating Voicemeeter, its SDK, the C Remote API, the RT Packet service and Streamer View app! -[Christian Volkmann](https://github.com/chvolkmann) for the detailed work that went into creating the underlying Remote API Python Interface. -Unfortunately, the Remote API Python Interface has `NOT` been open source licensed. I have [raised an issue](https://github.com/chvolkmann/voicemeeter-remote-python/issues/13) and asked directly and politely but so far no response. If a license is added in future I will update this section. Without an open source license there is no guarantee that in future this package may not be pulled down, without any notice. - [Rdbende](https://github.com/rdbende) for creating the beautiful Sun Valley Tkinter theme and adding it to Pypi! diff --git a/__main__.py b/__main__.py index 9f62536..f1fb34d 100644 --- a/__main__.py +++ b/__main__.py @@ -1,9 +1,10 @@ -import voicemeeter +import voicemeeterlib + import vmcompact def main(): - with voicemeeter.remote(kind_id) as vmr: + with voicemeeterlib.api(kind_id) as vmr: app = vmcompact.connect(kind_id, vmr) app.mainloop() @@ -11,6 +12,4 @@ def main(): if __name__ == "__main__": kind_id = "banana" - voicemeeter.launch(kind_id, hide=False) - main() diff --git a/configs/banana/example.toml b/configs/banana/example.toml new file mode 100644 index 0000000..9f3d62e --- /dev/null +++ b/configs/banana/example.toml @@ -0,0 +1,45 @@ +[strip-0] +label = "PhysStrip0" +A1 = true +gain = -8.8 +comp = 3.2 + +[strip-1] +label = "PhysStrip1" +B1 = true +gate = 4.1 + +[strip-2] +label = "PhysStrip2" +gain = 1.1 +limit = -15 + +[strip-3] +label = "VirtStrip0" +bass = -3.2 +mid = 1.5 +treble = 2.1 + +[strip-4] +label = "VirtStrip1" +limit = -12 + +[bus-0] +label = "PhysBus0" +mute = true + +[bus-1] +label = "PhysBus1" +mono = true + +[bus-2] +label = "PhysBus2" +eq = true + +[bus-3] +label = "VirtBus0" +eq_ab = true + +[bus-4] +label = "VirtBus1" +gain = -22.8 diff --git a/configs/basic/example.toml b/configs/basic/example.toml new file mode 100644 index 0000000..edd2cf6 --- /dev/null +++ b/configs/basic/example.toml @@ -0,0 +1,23 @@ +[strip-0] +label = "PhysStrip0" +A1 = true +gain = -8.8 + +[strip-1] +label = "PhysStrip1" +B1 = true +audibility = 3.2 + +[strip-2] +label = "VirtStrip0" +bass = -3.2 +mid = 1.5 +treble = 2.1 + +[bus-0] +label = "PhysBus0" +mute = true + +[bus-1] +label = "PhysBus1" +mono = true diff --git a/configs/potato/example.toml b/configs/potato/example.toml new file mode 100644 index 0000000..9c43f21 --- /dev/null +++ b/configs/potato/example.toml @@ -0,0 +1,70 @@ +[strip-0] +label = "PhysStrip0" +A1 = true +gain = -8.8 +comp = 3.2 + +[strip-1] +label = "PhysStrip1" +B1 = true +gate = 4.1 + +[strip-2] +label = "PhysStrip2" +gain = 1.1 +limit = -15 + +[strip-3] +label = "PhysStrip3" +B2 = false + +[strip-4] +label = "PhysStrip4" +B3 = true +gain = -8.8 + +[strip-5] +label = "VirtStrip0" +A3 = true +A5 = true +bass = -3.2 +mid = 1.5 +treble = 2.1 + +[strip-6] +label = "VirtStrip1" +limit = -12 +k = 3 + +[strip-7] +label = "VirtStrip2" +mc = true + +[bus-0] +label = "PhysBus0" +mute = true + +[bus-1] +label = "PhysBus1" +mono = true + +[bus-2] +label = "PhysBus2" +eq = true + +[bus-3] +label = "PhysBus3" + +[bus-4] +label = "PhysBus4" + +[bus-5] +label = "VirtBus0" +eq_ab = true + +[bus-6] +label = "VirtBus1" +gain = -22.8 + +[bus-7] +label = "VirtBus2" diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..0456d77 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,42 @@ +[[package]] +name = "sv-ttk" +version = "0.1" +description = "Python module to easily use the Sun Valley ttk theme" +category = "main" +optional = false +python-versions = ">=3.4" + +[[package]] +name = "vban-cmd" +version = "1.0.5" +description = "Python interface for the VBAN RT Packet Service (Sendtext)" +category = "main" +optional = false +python-versions = ">=3.11,<4.0" + +[[package]] +name = "voicemeeter-api" +version = "0.1.6" +description = "A Python wrapper for the Voiceemeter API" +category = "main" +optional = false +python-versions = ">=3.11,<4.0" + +[metadata] +lock-version = "1.1" +python-versions = "^3.11" +content-hash = "3b50c0fa4701fbf590b50609cf3d56a8f97b274ebb9e259ba52ce6771b106d0c" + +[metadata.files] +sv-ttk = [ + {file = "sv_ttk-0.1-py3-none-any.whl", hash = "sha256:c47ab1c70aad0333bc7f5063ab55e8a16b562e700e89e7853e577a1e2fdaa091"}, + {file = "sv_ttk-0.1.tar.gz", hash = "sha256:342754618292b6d224060307eccaa35b6f6c284b34b4da1d0cf0484b652ffb0f"}, +] +vban-cmd = [ + {file = "vban-cmd-1.0.5.tar.gz", hash = "sha256:e50efd373816c01fb7e62f9ad90c33113b26cc12fbb46866b952b1946030282b"}, + {file = "vban_cmd-1.0.5-py3-none-any.whl", hash = "sha256:c51008049652fec61a94fb1b8ccd173cb0296684eee6249e9865f395340d32c5"}, +] +voicemeeter-api = [ + {file = "voicemeeter-api-0.1.6.tar.gz", hash = "sha256:354fe37aea680f2431d428c9bd489d7d9c9e143cbd84a769c567dd657e63370d"}, + {file = "voicemeeter_api-0.1.6-py3-none-any.whl", hash = "sha256:20a3072b0379862254d5ee3cf4ec96f6c839291faa4ecbd29d3c1ed66a5492cb"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b6ec6b3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[tool.poetry] +name = "voicemeeter-compact" +version = "1.0.1" +description = "A Compact Voicemeeter Remote App" +authors = ["onyx-and-iris "] +license = "MIT" +readme = "README.md" +repository = "https://github.com/onyx-and-iris/voicemeeter-compact" + +packages = [ + { include = "vmcompact" }, +] +include = ["vmcompact/img/cat.ico"] + +[tool.poetry.dependencies] +python = "^3.11" +sv-ttk = "^0.1" +voicemeeter-api = { version = "^0.1.6", python = "^3.10" } +vban-cmd = { version = "^1.0.5", python = "^3.10" } + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/vmcompact/app.py b/vmcompact/app.py index d7c78b9..94b4757 100644 --- a/vmcompact/app.py +++ b/vmcompact/app.py @@ -1,13 +1,13 @@ import tkinter as tk +from pathlib import Path from tkinter import ttk from typing import NamedTuple -from pathlib import Path -from .errors import VMCompactErrors -from .data import _kinds_all, _configuration, _base_values -from .subject import Subject from .builders import MainFrameBuilder +from .data import _base_values, _configuration, _kinds_all +from .errors import VMCompactErrors from .menu import Menus +from .subject import Subject class App(tk.Tk): @@ -89,7 +89,7 @@ class App(tk.Tk): if _configuration.extended: self.nav_frame.extend.set(True) self.nav_frame.extend_frame() - if self.kind.name == "Potato": + if self.kind.name == "potato": self.builder.create_banner() def on_update(self, subject, data): @@ -145,7 +145,7 @@ class App(tk.Tk): self.drag_id = "" -_apps = {kind.id: App.make(kind) for kind in _kinds_all} +_apps = {kind.name: App.make(kind) for kind in _kinds_all} def connect(kind_id: str, vmr) -> App: diff --git a/vmcompact/builders.py b/vmcompact/builders.py index ce3a005..e4b86c4 100644 --- a/vmcompact/builders.py +++ b/vmcompact/builders.py @@ -1,15 +1,15 @@ -import tkinter as tk -from tkinter import ttk -from functools import partial import abc +import tkinter as tk +from functools import partial +from tkinter import ttk + import sv_ttk - -from .data import _base_values, _configuration -from .channels import _make_channelframe -from .navigation import Navigation -from .config import StripConfig, BusConfig from .banner import Banner +from .channels import _make_channelframe +from .config import BusConfig, StripConfig +from .data import _base_values, _configuration +from .navigation import Navigation class AbstractBuilder(abc.ABC): @@ -144,7 +144,7 @@ class NavigationFrameBuilder(AbstractBuilder): variable=self.navframe.submix, ) self.navframe.submix_button.grid(column=0, row=0) - if self.navframe.parent.kind.name != "Potato": + if self.navframe.parent.kind.name != "potato": self.navframe.submix_button["state"] = "disabled" def create_channel_button(self): @@ -321,7 +321,7 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder): """Responsible for building channel configframe widgets""" def setup(self): - if self.configframe.parent.kind.name == "Basic": + if self.configframe.parent.kind.name == "basic": self.configframe.slider_params = ("audibility",) self.configframe.slider_vars = (tk.DoubleVar(),) else: @@ -349,12 +349,12 @@ class StripConfigFrameBuilder(ChannelConfigFrameBuilder): tk.BooleanVar() for _ in self.configframe.params ) - if self.configframe.parent.kind.name in ("Banana", "Potato"): + if self.configframe.parent.kind.name in ("banana", "potato"): if self.configframe.index == self.configframe.phys_in: self.configframe.params = list( map(lambda x: x.replace("mono", "mc"), self.configframe.params) ) - if self.configframe.parent.kind.name == "Banana": + if self.configframe.parent.kind.name == "banana": pass # karaoke modes not in RT Packet yet. May implement in future """ diff --git a/vmcompact/channels.py b/vmcompact/channels.py index 1aa3ce3..b672575 100644 --- a/vmcompact/channels.py +++ b/vmcompact/channels.py @@ -1,6 +1,6 @@ import tkinter as tk -from tkinter import ttk from math import log +from tkinter import ttk from . import builders from .data import _base_values, _configuration diff --git a/vmcompact/config.py b/vmcompact/config.py index 6437734..0deec17 100644 --- a/vmcompact/config.py +++ b/vmcompact/config.py @@ -1,9 +1,9 @@ import tkinter as tk -from tkinter import ttk from functools import partial +from tkinter import ttk from . import builders -from .data import _configuration, _base_values +from .data import _base_values, _configuration class Config(ttk.Frame): @@ -96,7 +96,7 @@ class StripConfig(Config): def make_row_0(self): if self.index < self.phys_in: - if self.parent.kind.name == "Basic": + if self.parent.kind.name == "basic": self.builder.create_audibility_slider() else: self.builder.create_comp_slider() diff --git a/vmcompact/configurations.py b/vmcompact/configurations.py index ec08f78..79f33ed 100644 --- a/vmcompact/configurations.py +++ b/vmcompact/configurations.py @@ -1,6 +1,7 @@ -import toml from pathlib import Path +import tomllib + configuration = {} config_path = [Path.cwd() / "configs"] @@ -11,8 +12,9 @@ for path in config_path: for filename in filenames: name = filename.with_suffix("").stem try: - configs[name] = toml.load(filename) - except toml.TomlDecodeError: + with open(filename, "rb") as f: + configs[name] = tomllib.load(f) + except tomllib.TOMLDecodeError: print(f"Invalid TOML profile: configs/{filename.stem}") for name, cfg in configs.items(): diff --git a/vmcompact/data.py b/vmcompact/data.py index 41b87ed..724c9dd 100644 --- a/vmcompact/data.py +++ b/vmcompact/data.py @@ -1,5 +1,6 @@ from dataclasses import dataclass -from voicemeeter import kinds + +from voicemeeterlib import kinds from .configurations import get_configuration @@ -51,7 +52,7 @@ class BaseValues(metaclass=SingletonMeta): # a vban connection established vban_connected: bool = False # pdirty delay - pdelay: int = 5 + pdelay: int = 1 # ldirty delay ldelay: int = 5 @@ -59,7 +60,7 @@ class BaseValues(metaclass=SingletonMeta): _base_values = BaseValues() _configuration = Configurations() -_kinds = {kind.id: kind for kind in kinds.all} +_kinds = {kind.name: kind for kind in kinds.kinds_all} _kinds_all = _kinds.values() diff --git a/vmcompact/gainlayer.py b/vmcompact/gainlayer.py index caf1695..6e29e39 100644 --- a/vmcompact/gainlayer.py +++ b/vmcompact/gainlayer.py @@ -1,6 +1,6 @@ import tkinter as tk -from tkinter import ttk from math import log +from tkinter import ttk from . import builders from .data import _base_values, _configuration diff --git a/vmcompact/menu.py b/vmcompact/menu.py index 8be3bea..966937f 100644 --- a/vmcompact/menu.py +++ b/vmcompact/menu.py @@ -1,16 +1,12 @@ import tkinter as tk -from tkinter import ttk, messagebox -from functools import partial import webbrowser -import sv_ttk -import vbancmd +from functools import partial +from tkinter import messagebox, ttk -from .data import ( - get_configuration, - _base_values, - _configuration, - kind_get, -) +import sv_ttk +import vban_cmd + +from .data import _base_values, _configuration, get_configuration, kind_get class Menus(tk.Menu): @@ -76,31 +72,31 @@ class Menus(tk.Menu): command=partial(self.action_set_voicemeeter, "lock", False), ) - # profiles menu - menu_profiles = tk.Menu(self, tearoff=0) - self.add_cascade(menu=menu_profiles, label="Profiles") - self.menu_profiles_load = tk.Menu(menu_profiles, tearoff=0) - menu_profiles.add_cascade(menu=self.menu_profiles_load, label="Load profile") - defaults = {"base", "blank"} - if len(self.target.profiles) > len(defaults) and all( - key in self.target.profiles for key in defaults + # configs menu + menu_configs = tk.Menu(self, tearoff=0) + self.add_cascade(menu=menu_configs, label="Configs") + self.menu_configs_load = tk.Menu(menu_configs, tearoff=0) + menu_configs.add_cascade(menu=self.menu_configs_load, label="Load profile") + defaults = {"reset"} + if len(self.target.configs) > len(defaults) and all( + key in self.target.configs for key in defaults ): [ - self.menu_profiles_load.add_command( + self.menu_configs_load.add_command( label=profile, command=partial(self.load_profile, profile) ) - for profile in self.target.profiles.keys() + for profile in self.target.configs.keys() if profile not in defaults ] else: - menu_profiles.entryconfig(0, state="disabled") - menu_profiles.add_command(label="Reset to defaults", command=self.load_defaults) + menu_configs.entryconfig(0, state="disabled") + menu_configs.add_command(label="Reset to defaults", command=self.load_defaults) # layout menu self.menu_layout = tk.Menu(self, tearoff=0) self.add_cascade(menu=self.menu_layout, label="Layout") # layout/submixes - # here we build menu regardless of kind but disable if not Potato + # here we build menu regardless of kind but disable if not potato buses = tuple(f"A{i+1}" for i in range(5)) + tuple(f"B{i+1}" for i in range(3)) self.menu_submixes = tk.Menu(self.menu_layout, tearoff=0) self.menu_layout.add_cascade(menu=self.menu_submixes, label="Submixes") @@ -116,7 +112,7 @@ class Menus(tk.Menu): for i in range(8) ] self._selected_bus[_configuration.submixes].set(True) - if self.parent.kind.name != "Potato": + if self.parent.kind.name != "potato": self.menu_layout.entryconfig(0, state="disabled") # layout/extends self.menu_extends = tk.Menu(self.menu_layout, tearoff=0) @@ -211,14 +207,14 @@ class Menus(tk.Menu): setattr(self.target.command, cmd, val) def load_profile(self, profile): - self.target.apply_profile(profile) + self.target.apply_config(profile) def load_defaults(self): resp = messagebox.askyesno( message="Are you sure you want to Reset values to defaults?\nPhysical strips B1, Virtual strips A1\nMono, Solo, Mute, EQ all OFF" ) if resp: - self.target.apply_profile("base") + self.target.apply_config("reset") def always_on_top(self): self.parent.attributes("-topmost", self._is_topmost.get()) @@ -258,7 +254,7 @@ class Menus(tk.Menu): if isinstance(menu, tk.Menu) ] self.menu_lock.config(bg=f"{'black' if theme == 'dark' else 'white'}") - self.menu_profiles_load.config(bg=f"{'black' if theme == 'dark' else 'white'}") + self.menu_configs_load.config(bg=f"{'black' if theme == 'dark' else 'white'}") [ menu.config(bg=f"{'black' if theme == 'dark' else 'white'}") for menu in self.menu_vban.winfo_children() @@ -280,7 +276,7 @@ class Menus(tk.Menu): opts = {} opts |= self.vban_config[f"connection-{i+1}"] kind_id = opts.pop("kind") - self.vban = vbancmd.connect(kind_id, **opts) + self.vban = vban_cmd.api(kind_id, **opts) # login to vban interface self.vban.login() # destroy the current App frames @@ -294,7 +290,7 @@ class Menus(tk.Menu): target_menu.entryconfig(0, state="disabled") target_menu.entryconfig(1, state="normal") self.menu_layout.entryconfig( - 0, state=f"{'normal' if kind.name == 'Potato' else 'disabled'}" + 0, state=f"{'normal' if kind.name == 'potato' else 'disabled'}" ) def vban_disconnect(self, i): @@ -311,7 +307,7 @@ class Menus(tk.Menu): target_menu.entryconfig(0, state="normal") target_menu.entryconfig(1, state="disabled") self.menu_layout.entryconfig( - 0, state=f"{'normal' if kind.name == 'Potato' else 'disabled'}" + 0, state=f"{'normal' if kind.name == 'potato' else 'disabled'}" ) self.after(15000, self.enable_vban_menus)