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
12 changes: 8 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

env:
PWG_VERSION: "0.3.6"
PWG_REQUIREMENT: "pipewire-gobject>=0.3.6,<0.4"

jobs:
changes:
runs-on: ubuntu-24.04
Expand Down Expand Up @@ -291,11 +295,11 @@ jobs:
run: |
python3 -m venv /tmp/mini-eq-pwg-build
/tmp/mini-eq-pwg-build/bin/python -m pip install --upgrade pip
/tmp/mini-eq-pwg-build/bin/python -m pip wheel 'pipewire-gobject>=0.3.5,<0.4' -w /tmp/mini-eq-wheelhouse
/tmp/mini-eq-pwg-build/bin/python -m pip wheel "$PWG_REQUIREMENT" -w /tmp/mini-eq-wheelhouse

python3 -m venv --system-site-packages .venv
.venv/bin/python -m pip install --upgrade pip
.venv/bin/python -m pip install --no-index --find-links /tmp/mini-eq-wheelhouse 'pipewire-gobject>=0.3.5,<0.4'
.venv/bin/python -m pip install --no-index --find-links /tmp/mini-eq-wheelhouse "$PWG_REQUIREMENT"
.venv/bin/python -m pip install -e '.[dev]'

- name: Lint
Expand Down Expand Up @@ -323,7 +327,7 @@ jobs:
run: |
python3 -m venv --system-site-packages /tmp/mini-eq-wheel-test
/tmp/mini-eq-wheel-test/bin/python -m pip install --upgrade pip
/tmp/mini-eq-wheel-test/bin/python -m pip install --no-index --find-links /tmp/mini-eq-wheelhouse 'pipewire-gobject>=0.3.5,<0.4'
/tmp/mini-eq-wheel-test/bin/python -m pip install --no-index --find-links /tmp/mini-eq-wheelhouse "$PWG_REQUIREMENT"
/tmp/mini-eq-wheel-test/bin/python -m pip install dist/mini_eq-*.whl
/tmp/mini-eq-wheel-test/bin/mini-eq --help

Expand Down Expand Up @@ -369,7 +373,7 @@ jobs:
- name: Check pipewire-gobject GI compatibility
run: |
flatpak run --filesystem="$PWD":ro --command=python3 io.github.bhack.mini-eq \
"$PWD/tools/check_pipewire_gobject.py" --expect-version 0.3.5
"$PWD/tools/check_pipewire_gobject.py" --expect-version "$PWG_VERSION"

- name: Smoke-test Flatpak runtime modules
run: |
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 0.7.3 - 2026-05-11

- Require pipewire-gobject 0.3.6 and use its synchronous PipeWire registry,
metadata, node, and device APIs for startup and routing state.
- Replace Mini EQ's PipeWire startup and routing sleep loops with bounded sync
or registry-event waits for route params, virtual sink creation, and stream
routing.
- Avoid a headless startup hang when system-wide EQ fails synchronously before
the GLib main loop starts.

## 0.7.2 - 2026-05-10

- Finish GNOME Shell startup notification after Mini EQ's delayed first-window
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.7.2/docs/screenshots/mini-eq.png</image>
<image>https://raw.githubusercontent.com/bhack/mini-eq/v0.7.3/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.7.2/docs/screenshots/mini-eq-dark.png</image>
<image>https://raw.githubusercontent.com/bhack/mini-eq/v0.7.3/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.7.3" date="2026-05-11">
<description>
<ul>
<li>Require pipewire-gobject 0.3.6 and use its synchronous PipeWire registry, metadata, node, and device APIs for startup and routing state.</li>
<li>Replace Mini EQ's PipeWire startup and routing sleep loops with bounded sync or registry-event waits for route params, virtual sink creation, and stream routing.</li>
<li>Avoid a headless startup hang when system-wide EQ fails synchronously before the GLib main loop starts.</li>
</ul>
</description>
</release>
<release version="0.7.2" date="2026-05-10">
<description>
<ul>
Expand Down
4 changes: 2 additions & 2 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,11 @@ Install the Python package after the system packages are present:
```bash
python3 -m venv /tmp/mini-eq-pwg-build
/tmp/mini-eq-pwg-build/bin/python -m pip install --upgrade pip
/tmp/mini-eq-pwg-build/bin/python -m pip wheel 'pipewire-gobject>=0.3.5,<0.4' -w /tmp/mini-eq-wheelhouse
/tmp/mini-eq-pwg-build/bin/python -m pip wheel 'pipewire-gobject>=0.3.6,<0.4' -w /tmp/mini-eq-wheelhouse

python3 -m venv --system-site-packages ~/.local/share/mini-eq/venv
~/.local/share/mini-eq/venv/bin/python -m pip install --upgrade pip
~/.local/share/mini-eq/venv/bin/python -m pip install --no-index --find-links /tmp/mini-eq-wheelhouse 'pipewire-gobject>=0.3.5,<0.4'
~/.local/share/mini-eq/venv/bin/python -m pip install --no-index --find-links /tmp/mini-eq-wheelhouse 'pipewire-gobject>=0.3.6,<0.4'
~/.local/share/mini-eq/venv/bin/python -m pip install mini-eq
~/.local/share/mini-eq/venv/bin/mini-eq --check-deps
~/.local/share/mini-eq/venv/bin/mini-eq
Expand Down
4 changes: 2 additions & 2 deletions io.github.bhack.mini-eq.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ modules:
sources:
- type: git
url: https://github.com/bhack/pipewire-gobject.git
tag: 0.3.5
commit: b570bc4ff0a53223416fff9a4fc05d367273789d
tag: 0.3.6
commit: 7e20b29865c0a869695bc730bacb68e2f0853077

- python3-dependencies.yaml

Expand Down
4 changes: 2 additions & 2 deletions 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.7.2"
version = "0.7.3"
description = "Compact PipeWire system-wide parametric equalizer for Linux desktops."
readme = "README.md"
requires-python = ">=3.11"
Expand Down Expand Up @@ -40,7 +40,7 @@ classifiers = [
"Topic :: Multimedia :: Sound/Audio :: Analysis",
"Topic :: Multimedia :: Sound/Audio :: Mixers",
]
dependencies = ["numpy>=1.26", "pipewire-gobject>=0.3.5,<0.4"]
dependencies = ["numpy>=1.26", "pipewire-gobject>=0.3.6,<0.4"]

[project.urls]
Homepage = "https://github.com/bhack/mini-eq"
Expand Down
101 changes: 80 additions & 21 deletions src/mini_eq/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ def __init__(self, args: Namespace) -> None:
self.dbus_control: MiniEqDbusControl | None = None
self.signal_source_ids: list[int] = []
self.window_present_source_id = 0
self.window_starting = False
self.window_start_hold = False
self.pending_present_when_ready = False
self.background_mode = load_background_mode() or bool(getattr(args, "background", False))
self.start_at_login = load_start_at_login()
self.start_active_at_login = load_start_active_at_login() and self.start_at_login
Expand Down Expand Up @@ -94,13 +97,15 @@ def ensure_window(self, *, present: bool) -> None:
self.window.schedule_startup_ready()
return

if self.window_starting:
self.pending_present_when_ready = self.pending_present_when_ready or present
return

controller: SystemWideEqController | None = None
initial_curve_label: str | None = None

try:
controller = SystemWideEqController(self.args.output_sink)
controller.start()

if self.args.import_apo:
controller.import_apo_preset(self.args.import_apo)
initial_curve_label = imported_apo_curve_label(self.args.import_apo)
Expand All @@ -110,14 +115,52 @@ def ensure_window(self, *, present: bool) -> None:
raise SystemExit(str(exc)) from exc

self.controller = controller
self.window = MiniEqWindow(self, self.controller, self.args.auto_route, initial_curve_label=initial_curve_label)
self.window.set_icon_name(APP_ICON_NAME)
self.window.present_when_ready = present
self.window.set_visible(False)
self.window.schedule_startup_ready()
if not present:
self.update_background_status()
self.emit_control_state_changed()
self.window_starting = True
self.window_start_hold = True
self.pending_present_when_ready = present
self.hold()

def release_start_hold() -> None:
if not self.window_start_hold:
return

self.window_start_hold = False
self.release()

def on_ready() -> None:
if self.controller is not controller:
return

try:
self.window_starting = False
present_when_ready = self.pending_present_when_ready
self.pending_present_when_ready = False
self.window = MiniEqWindow(
self, self.controller, self.args.auto_route, initial_curve_label=initial_curve_label
)
self.window.set_icon_name(APP_ICON_NAME)
self.window.present_when_ready = present_when_ready
self.window.set_visible(False)
self.window.schedule_startup_ready()
if not present_when_ready:
self.update_background_status()
self.emit_control_state_changed()
finally:
release_start_hold()

def on_error(exc: Exception) -> None:
self.window_starting = False
self.pending_present_when_ready = False
try:
controller.shutdown()
finally:
if self.controller is controller:
self.controller = None
print(str(exc), file=sys.stderr)
release_start_hold()
self.quit()

controller.start(on_ready=on_ready, on_error=on_error)

def present_main_window(self) -> None:
self.ensure_window(present=True)
Expand Down Expand Up @@ -220,34 +263,50 @@ def run_headless(args: Namespace) -> int:
duration_ms = 0

controller: SystemWideEqController | None = None
exit_code = 0
loop = GLib.MainLoop()
signal_source_ids = install_unix_signal_handlers(loop.quit)

try:
controller = SystemWideEqController(args.output_sink)
controller.start()

if args.import_apo:
controller.import_apo_preset(args.import_apo)

if args.auto_route:
controller.route_system_audio(True)

loop = GLib.MainLoop()
signal_source_ids = install_unix_signal_handlers(loop.quit)

if duration_ms > 0:
GLib.timeout_add(duration_ms, lambda: (loop.quit(), False)[1])

def on_ready() -> None:
nonlocal exit_code

try:
if args.auto_route:
controller.route_system_audio(True)
except Exception as exc:
exit_code = 1
print(str(exc), file=sys.stderr)
loop.quit()

def on_error(exc: Exception) -> None:
nonlocal exit_code

exit_code = 1
print(str(exc), file=sys.stderr)
loop.quit()

controller.start(on_ready=on_ready, on_error=on_error)

try:
loop.run()
if exit_code == 0:
loop.run()
except KeyboardInterrupt:
pass
finally:
signal_source_ids.clear()
finally:
signal_source_ids.clear()
if controller is not None:
controller.shutdown()

return 0
return exit_code


def install_unix_signal_handlers(callback) -> list[int]:
Expand Down
10 changes: 8 additions & 2 deletions src/mini_eq/deps.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,21 @@

Status = Literal["ok", "missing", "warning"]

PWG_REQUIRED_VERSION = "0.3.5"
PWG_REQUIRED_VERSION_PARTS = (0, 3, 5)
PWG_REQUIRED_VERSION = "0.3.6"
PWG_REQUIRED_VERSION_PARTS = (0, 3, 6)
PWG_REQUIRED_SYMBOLS = (
"Core.set_pipewire_property",
"Core.sync",
"Device.enum_all_params",
"Device.enum_params",
"Device.enum_params_sync",
"Device.new",
"Device.subscribe_params",
"Device.sync",
"Metadata.sync",
"Node.sync",
"Param.new_props_controls",
"Registry.sync",
"RouteInfo.new_from_param",
"Stream.set_pipewire_property",
)
Expand Down
Loading
Loading