Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## 0.8.10 - 2026-10-29

- Remember the A/B Compare setting across restarts, including at login.
- Update the Flatpak runtime to GNOME 51 and the bundled PipeWire modules to
1.6.10.
- Support GNOME Shell 51 in the Mini EQ Controls extension.

## 0.8.9 - 2026-10-13

- Keep the audio device stable when changing presets or filter types, avoiding
Expand Down
13 changes: 11 additions & 2 deletions data/io.github.bhack.mini-eq.metainfo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,27 @@
</description>
<screenshots>
<screenshot type="default">
<image>https://raw.githubusercontent.com/bhack/mini-eq/v0.8.9/docs/screenshots/mini-eq.png</image>
<image>https://raw.githubusercontent.com/bhack/mini-eq/v0.8.10/docs/screenshots/mini-eq.png</image>
<caption>Adjust sound output with equalizer controls</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/bhack/mini-eq/v0.8.9/docs/screenshots/mini-eq-dark.png</image>
<image>https://raw.githubusercontent.com/bhack/mini-eq/v0.8.10/docs/screenshots/mini-eq-dark.png</image>
<caption>Use the equalizer with dark style</caption>
</screenshot>
</screenshots>
<url type="homepage">https://github.com/bhack/mini-eq</url>
<url type="bugtracker">https://github.com/bhack/mini-eq/issues</url>
<url type="vcs-browser">https://github.com/bhack/mini-eq</url>
<releases>
<release version="0.8.10" date="2026-10-29">
<description>
<ul>
<li>Remember the A/B Compare setting across restarts, including at login.</li>
<li>Update the Flatpak runtime to GNOME 51 and the bundled PipeWire modules to 1.6.10.</li>
<li>Support GNOME Shell 51 in the Mini EQ Controls extension.</li>
</ul>
</description>
</release>
<release version="0.8.9" date="2026-10-13">
<description>
<ul>
Expand Down
2 changes: 1 addition & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ bundles NumPy and libebur128 for analyzer and live LUFS support.
Install the local build tools:

```bash
flatpak --user install flathub org.flatpak.Builder org.gnome.Sdk//50
flatpak --user install flathub org.flatpak.Builder org.gnome.Sdk//51
```

Build and install the local Flatpak:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"uuid": "mini-eq@bhack.github.io",
"name": "Mini EQ Controls",
"description": "Control Mini EQ from the panel: open the app, toggle system-wide routing and EQ, view analyzer activity, switch saved presets, and see auto preset links.",
"shell-version": ["50"],
"shell-version": ["50", "51"],
"url": "https://github.com/bhack/mini-eq"
}
6 changes: 3 additions & 3 deletions io.github.bhack.mini-eq.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
app-id: io.github.bhack.mini-eq
runtime: org.gnome.Platform
runtime-version: "50"
runtime-version: "51"
sdk: org.gnome.Sdk
command: mini-eq
finish-args:
Expand Down Expand Up @@ -117,8 +117,8 @@ modules:
sources:
- type: git
url: https://gitlab.freedesktop.org/pipewire/pipewire.git
tag: 1.6.9
commit: 8fa27cabdc6c0c1350c69c026af5850ef0af1e26
tag: 1.6.10
commit: f9d6cbc99c354b9b0ad1d37245dc3ab0fb9f9171

- name: pipewire-gobject
buildsystem: meson
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "mini-eq"
version = "0.8.9"
version = "0.8.10"
description = "Compact PipeWire system-wide parametric equalizer for Linux desktops."
readme = "README.md"
requires-python = ">=3.11"
Expand Down
11 changes: 5 additions & 6 deletions python3-dependencies.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Generated with flatpak-pip-generator --runtime org.gnome.Sdk//50 --yaml --output python3-dependencies --prefer-wheels=numpy numpy==2.4.4
# Updated NumPy to 2.4.6 using CPython 3.13 wheel URLs and SHA-256 hashes from PyPI.
# NumPy 2.4.6 CPython 3.14 wheel URLs and SHA-256 hashes from PyPI for GNOME SDK 51.
name: python3-dependencies
buildsystem: simple
build-commands: []
Expand All @@ -11,12 +10,12 @@ modules:
--prefix=${FLATPAK_DEST} "numpy==2.4.6" --no-build-isolation
sources:
- type: file
url: https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
sha256: a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089
url: https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
sha256: a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8
only-arches:
- x86_64
- type: file
url: https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl
sha256: 72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b
url: https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl
sha256: 40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b
only-arches:
- aarch64
6 changes: 5 additions & 1 deletion src/mini_eq/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,9 @@ def start_window_controller(self) -> None:
try:
controller = SystemWideEqController(self.args.output_sink)
if self.args.import_apo:
startup_eq_enabled = controller.eq_enabled
controller.import_apo_preset(self.args.import_apo)
controller.eq_enabled = startup_eq_enabled
initial_curve_label = imported_apo_curve_label(self.args.import_apo)
except Exception as exc:
if controller is not None:
Expand Down Expand Up @@ -363,7 +365,9 @@ def run_headless(args: Namespace) -> int:
controller = SystemWideEqController(args.output_sink)

if args.import_apo:
startup_eq_enabled = controller.eq_enabled
controller.import_apo_preset(args.import_apo)
controller.eq_enabled = startup_eq_enabled

if duration_ms > 0:
GLib.timeout_add(duration_ms, lambda: (loop.quit(), False)[1])
Expand All @@ -373,7 +377,7 @@ def on_ready() -> None:

try:
if args.auto_route:
controller.route_system_audio(True)
controller.route_system_audio(True, preserve_eq_state=True)
except Exception as exc:
exit_code = 1
print(str(exc), file=sys.stderr)
Expand Down
22 changes: 14 additions & 8 deletions src/mini_eq/dbus_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from . import __version__
from .analyzer import analyzer_level_to_display_norm
from .core import list_preset_names, sanitize_preset_name
from .settings import save_eq_enabled

BUS_NAME = "io.github.bhack.mini-eq"
OBJECT_PATH = "/io/github/bhack/mini_eq/Control"
Expand Down Expand Up @@ -366,10 +367,12 @@ def set_eq_enabled(self, enabled: bool) -> None:
raise ValueError("Mini EQ is not running")

controller.set_eq_enabled(enabled)
if window is not None and not window.ui_shutting_down:
window.refresh_after_eq_state_changed(notify=False)

self.emit_state_changed()
try:
save_eq_enabled(enabled)
finally:
if window is not None and not window.ui_shutting_down:
window.refresh_after_eq_state_changed(notify=False)
self.emit_state_changed()

def set_routing_enabled(self, enabled: bool) -> None:
if self.app.controller is None:
Expand All @@ -388,10 +391,13 @@ def set_routing_enabled(self, enabled: bool) -> None:
window.sync_control_switches_from_controller()
raise

if window is not None and not window.ui_shutting_down:
window.refresh_after_route_state_changed(eq_was_enabled=eq_was_enabled, notify=False)

self.emit_state_changed()
try:
if enabled and controller.eq_enabled != eq_was_enabled:
save_eq_enabled(controller.eq_enabled)
finally:
if window is not None and not window.ui_shutting_down:
window.refresh_after_route_state_changed(eq_was_enabled=eq_was_enabled, notify=False)
self.emit_state_changed()

def set_preset(self, name: str) -> None:
preset_name = sanitize_preset_name(name)
Expand Down
9 changes: 6 additions & 3 deletions src/mini_eq/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
)
from .pipewire_routes import PipeWireOutputPresetTarget
from .pipewire_stream_router import PipeWireStreamRouter
from .settings import load_eq_enabled

FILTER_CONTROL_PARAM_ECHO_GRACE_USEC = 500_000
_OUTPUT_PRESET_TARGET_UNSET = object()
Expand Down Expand Up @@ -141,7 +142,7 @@ def __init__(self, output_sink: str | None) -> None:
self.outputs_changed_callback: Callable[[], None] | None = None
self.analyzer_levels_callback: Callable[[list[float]], None] | None = None
self.analyzer_loudness_callback: Callable[[AnalyzerLoudnessSnapshot | None], None] | None = None
self.eq_enabled = True
self.eq_enabled = load_eq_enabled()
self.eq_mode = next(iter(EQ_MODES.values()))
self.preamp_db = 0.0
self.default_bands: list[EqBand] = self.build_default_bands()
Expand Down Expand Up @@ -644,7 +645,9 @@ def pick_virtual_sink_name(self) -> str:

return f"{VIRTUAL_SINK_BASE}_{index}"

def route_system_audio(self, enabled: bool, announce: bool = True, *, refresh_output: bool = True) -> None:
def route_system_audio(
self, enabled: bool, announce: bool = True, *, refresh_output: bool = True, preserve_eq_state: bool = False
) -> None:
if enabled and getattr(self, "shutting_down", False):
return

Expand All @@ -656,7 +659,7 @@ def route_system_audio(self, enabled: bool, announce: bool = True, *, refresh_ou
if enabled:
if not self.running or self.filter_node_id is None:
raise RuntimeError("filter-chain PipeWire EQ is not ready")
if not self.eq_enabled:
if not self.eq_enabled and not preserve_eq_state:
eq_enabled_for_route = True
self.set_eq_enabled(True)

Expand Down
12 changes: 11 additions & 1 deletion src/mini_eq/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
SETTINGS_VERSION_KEY: Final = "version"
SETTINGS_VERSION: Final = 1
MONITOR_ENABLED_KEY: Final = "monitor_enabled"
EQ_ENABLED_KEY: Final = "eq_enabled"
APPEARANCE_KEY: Final = "appearance"
BACKGROUND_MODE_KEY: Final = "background_mode"
START_AT_LOGIN_KEY: Final = "start_at_login"
START_ACTIVE_AT_LOGIN_KEY: Final = "start_active_at_login"
BOOL_SETTINGS_KEYS: Final = frozenset(
(
MONITOR_ENABLED_KEY,
EQ_ENABLED_KEY,
BACKGROUND_MODE_KEY,
START_AT_LOGIN_KEY,
START_ACTIVE_AT_LOGIN_KEY,
Expand Down Expand Up @@ -72,7 +74,7 @@ def load_settings() -> dict[str, object]:

try:
payload = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
return {}

if not isinstance(payload, dict):
Expand Down Expand Up @@ -113,3 +115,11 @@ def load_monitor_enabled() -> bool:

def save_monitor_enabled(enabled: bool) -> None:
save_bool_setting(MONITOR_ENABLED_KEY, enabled)


def load_eq_enabled() -> bool:
return load_bool_setting(EQ_ENABLED_KEY, default=True)


def save_eq_enabled(enabled: bool) -> None:
save_bool_setting(EQ_ENABLED_KEY, enabled)
15 changes: 12 additions & 3 deletions src/mini_eq/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
from .gtk_utils import create_dropdown_from_strings
from .pipewire_backend import PipeWireBackendError, PipeWireNode, node_sample_rate, parse_positive_int
from .routing import SystemWideEqController
from .settings import load_monitor_enabled
from .settings import load_monitor_enabled, save_eq_enabled
from .window_analyzer import MiniEqWindowAnalyzerMixin
from .window_autoeq import MiniEqWindowAutoEqMixin, initialize_autoeq_window_state
from .window_graph import MiniEqWindowGraphMixin
Expand Down Expand Up @@ -420,7 +420,7 @@ def apply_startup_auto_route(self) -> None:
output_sink=getattr(self.controller, "output_sink", None),
)
try:
self.controller.route_system_audio(True)
self.controller.route_system_audio(True, preserve_eq_state=True)
except Exception as exc:
retry_scheduled = self.schedule_startup_auto_route_retry_after_error(exc)
trace_startup_event(
Expand Down Expand Up @@ -1043,6 +1043,7 @@ def on_import_apo_done(self, dialog: Gtk.FileDialog, result: Gio.AsyncResult) ->
self.set_status(str(exc))

def import_apo_preset_path(self, path: str, *, imported_name: str | None = None) -> int:
eq_was_enabled = self.controller.eq_enabled
imported_count = self.controller.import_apo_preset(path)
if imported_name is not None:
curve_label = imported_apo_curve_label_for_name(imported_name)
Expand All @@ -1061,6 +1062,8 @@ def import_apo_preset_path(self, path: str, *, imported_name: str | None = None)
utility_pane_button.set_active(True)
self.sync_ui_from_state()
self.notify_control_state_changed()
if self.controller.eq_enabled != eq_was_enabled:
save_eq_enabled(self.controller.eq_enabled)
return imported_count

def on_output_changed(self, combo: Gtk.DropDown, _param: object) -> None:
Expand Down Expand Up @@ -1108,7 +1111,10 @@ def on_route_changed(self, switch: Gtk.Switch, state: object | None) -> bool:
try:
self.controller.route_system_audio(enabled)
route_changed = True
if enabled and self.controller.eq_enabled != eq_was_enabled:
save_eq_enabled(self.controller.eq_enabled)
except Exception as exc:
route_changed = False
self.set_status(str(exc))
finally:
self.refresh_after_route_state_changed(
Expand All @@ -1126,7 +1132,10 @@ def on_bypass_changed(self, switch: Gtk.Switch, state: object | None) -> bool:

try:
self.controller.set_eq_enabled(enabled)
self.refresh_after_eq_state_changed(announce_enabled=enabled)
try:
save_eq_enabled(enabled)
finally:
self.refresh_after_eq_state_changed(announce_enabled=enabled)
except Exception as exc:
self.sync_control_switches_from_controller(route=False)
self.update_eq_power_indicator()
Expand Down
13 changes: 13 additions & 0 deletions tests/test_demo_runtime.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
from tools.demo_runtime import DemoController


def test_demo_routing_preserves_bypass_on_startup() -> None:
controller = DemoController()
controller.eq_enabled = False

controller.route_system_audio(True, preserve_eq_state=True)

assert controller.routed is True
assert controller.eq_enabled is False

controller.route_system_audio(True)
assert controller.eq_enabled is True


def test_demo_output_transition_consumption() -> None:
controller = DemoController()
first = controller.output_preset_target_transition()
Expand Down
1 change: 1 addition & 0 deletions tests/test_mini_eq_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,7 @@ def test_auto_route_import_error_does_not_retry(monkeypatch) -> None:
class FakeController:
def __init__(self, output_sink: str | None) -> None:
calls.append(("controller", output_sink))
self.eq_enabled = True

def import_apo_preset(self, path: str) -> int:
calls.append(("import", path))
Expand Down
Loading
Loading