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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ concurrency:
cancel-in-progress: true

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

jobs:
changes:
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## 0.8.8 - 2026-09-17

- Harden single-instance lock handling against unsafe files and concurrent
startup races.
- Keep EQ frequency controls consistent across processing sample rates using
native rate-aware PipeWire biquads.
- Follow the processing clock for response curves and AutoEq requests, keeping
downloaded AutoEq profiles cached separately for each sample rate.
- Make large preset libraries searchable and scrollable in the GNOME Shell
extension.
- Improve background permission error messages.
- Update bundled PipeWire filter modules to 1.6.9 and NumPy to 2.4.6.
- Require pipewire-gobject 0.3.10 for runtime graph-clock observation.

## 0.8.7 - 2026-06-12

- Fix saved preset loading when many presets are stored.
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ routing, metadata, and monitor streams, and PipeWire filter-chain with builtin
biquad filters for the equalizer. When libebur128 is available, the monitor can
also show live LUFS loudness.

The native PipeWire biquad filters calculate coefficients at their processing
sample rate; EQ processing is not pinned to 48 kHz.

![Mini EQ screenshot](https://raw.githubusercontent.com/bhack/mini-eq/main/docs/screenshots/mini-eq.png)

## Features
Expand Down
16 changes: 14 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,30 @@
</description>
<screenshots>
<screenshot type="default">
<image>https://raw.githubusercontent.com/bhack/mini-eq/v0.8.7/docs/screenshots/mini-eq.png</image>
<image>https://raw.githubusercontent.com/bhack/mini-eq/v0.8.8/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.7/docs/screenshots/mini-eq-dark.png</image>
<image>https://raw.githubusercontent.com/bhack/mini-eq/v0.8.8/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.8" date="2026-09-17">
<description>
<ul>
<li>Harden single-instance lock handling against unsafe files and concurrent startup races.</li>
<li>Keep EQ frequency controls consistent across processing sample rates using native rate-aware PipeWire biquads.</li>
<li>Follow the processing clock for response curves and AutoEq requests, with separate profile caches for each sample rate.</li>
<li>Make large preset libraries searchable and scrollable in the GNOME Shell extension.</li>
<li>Improve background permission error messages.</li>
<li>Update bundled PipeWire filter modules to 1.6.9 and NumPy to 2.4.6.</li>
</ul>
</description>
</release>
<release version="0.8.7" date="2026-06-12">
<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 @@ -87,11 +87,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.9,<0.4' -w /tmp/mini-eq-wheelhouse
/tmp/mini-eq-pwg-build/bin/python -m pip wheel 'pipewire-gobject>=0.3.10,<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.9,<0.4'
~/.local/share/mini-eq/venv/bin/python -m pip install --no-index --find-links /tmp/mini-eq-wheelhouse 'pipewire-gobject>=0.3.10,<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
174 changes: 165 additions & 9 deletions extensions/gnome-shell/mini-eq@bhack.github.io/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Clutter from 'gi://Clutter';
import Gio from 'gi://Gio';
import GObject from 'gi://GObject';
import GLib from 'gi://GLib';
import Pango from 'gi://Pango';
import Shell from 'gi://Shell';
import St from 'gi://St';

Expand All @@ -27,6 +28,10 @@ const PANEL_ANALYZER_MIN_ACTIVE_HEIGHT = 3;
const PANEL_ANALYZER_ACTIVE_COLOR = 'rgba(127, 213, 232, 0.96)';
const PANEL_ANALYZER_DIM_COLOR = 'rgba(255, 255, 255, 0.24)';
const PANEL_ANALYZER_STANDBY_COLOR = 'rgba(255, 255, 255, 0.16)';
const PRESET_SEARCH_THRESHOLD = 12;
const PRESET_PICKER_WIDTH = 280;
const PRESET_PICKER_MAX_HEIGHT = 296;
const PRESET_SEARCH_ENTRY_WIDTH = 232;
const SHELL_ICON_FILE = 'mini-eq-symbolic.svg';

function unpackValue(value) {
Expand Down Expand Up @@ -62,6 +67,12 @@ class MiniEqIndicator extends PanelMenu.Button {
this._presetsSignalId = 0;
this.connect('destroy', () => this._beginDispose());
this._presetItems = [];
this._allPresets = [];
this._currentPresetName = '';
this._presetSearchEntry = null;
this._presetResultsBox = null;
this._presetFilterText = '';
this._presetFocusSourceId = 0;
this._analyzerBars = [];
this._analyzerBarHeights = [];
this._analyzerBarStyles = [];
Expand Down Expand Up @@ -111,6 +122,25 @@ class MiniEqIndicator extends PanelMenu.Button {
this.menu.addMenuItem(this._eqItem);

this._presetsItem = new PopupMenu.PopupSubMenuMenuItem(_('Presets'));
this._presetsItem.menu.connect('open-state-changed', () => {
if (this._presetFocusSourceId) {
GLib.source_remove(this._presetFocusSourceId);
this._presetFocusSourceId = 0;
}
if (!this._presetsItem.menu.isOpen) {
this._resetPresetSearch();
return;
}

if (this._presetSearchEntry !== null) {
this._presetFocusSourceId = GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
this._presetFocusSourceId = 0;
if (!this._disposed && this._presetsItem.menu.isOpen)
this._presetSearchEntry?.grab_key_focus();
return GLib.SOURCE_REMOVE;
});
}
});
this.menu.addMenuItem(this._presetsItem);

this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
Expand Down Expand Up @@ -184,6 +214,10 @@ class MiniEqIndicator extends PanelMenu.Button {
return;

this._disposed = true;
if (this._presetFocusSourceId) {
GLib.source_remove(this._presetFocusSourceId);
this._presetFocusSourceId = 0;
}

if (this._refreshSourceId) {
GLib.source_remove(this._refreshSourceId);
Expand Down Expand Up @@ -341,8 +375,7 @@ class MiniEqIndicator extends PanelMenu.Button {
const analyzerEnabled = 'analyzer_enabled' in state
? Boolean(unpackValue(state.analyzer_enabled))
: true;
const presetName = unpackValue(state.preset_name) || _('Current State');
const curveLabel = unpackValue(state.curve_label) || presetName;
const rawPresetName = unpackValue(state.preset_name) || '';
const outputPresetName = unpackValue(state.output_preset_name) || '';
const outputPresetLabel = unpackValue(state.output_preset_label) || outputPresetName;
const capabilities = unpackValue(state.capabilities) || [];
Expand All @@ -353,6 +386,8 @@ class MiniEqIndicator extends PanelMenu.Button {
this._routed = routed;
this._eqEnabled = eqEnabled;
this._analyzerEnabled = analyzerEnabled;
this._currentPresetName = rawPresetName;
this._syncPresetOrnaments();
this.visible = running;
this._syncPanelStateStyle(running, routed, eqEnabled, analyzerEnabled);
this._updating = true;
Expand All @@ -369,7 +404,7 @@ class MiniEqIndicator extends PanelMenu.Button {
this._routingItem.setSensitive(running);
this._eqItem.setSensitive(running && routed);
this._presetsItem.setSensitive(running);
this._presetsItem.label.text = running ? _('Curve: %s').format(curveLabel) : _('Presets');
this._presetsItem.label.text = running ? _('Load Preset') : _('Presets');
this._statusItem.label.text = this._statusText(running, routed, eqEnabled);
this._outputPresetItem.label.text = this._outputPresetText(running, outputPresetLabel);
this._quitItem.visible = running && canQuit;
Expand Down Expand Up @@ -500,22 +535,143 @@ class MiniEqIndicator extends PanelMenu.Button {
}

_setPresets(presets) {
this._allPresets = Array.isArray(presets) ? presets : [];
this._presetFilterText = '';
this._rebuildPresetsMenu();
}

_resetPresetSearch() {
if (this._presetFilterText === '')
return;

this._presetFilterText = '';
if (this._presetSearchEntry !== null)
this._presetSearchEntry.text = '';
this._refreshFilteredPresetRows();
}

_rebuildPresetsMenu() {
this._presetsItem.menu.removeAll();
this._presetItems = [];
this._presetSearchEntry = null;
this._presetResultsBox = null;

if (!presets.length) {
if (!this._allPresets.length) {
const item = new PopupMenu.PopupMenuItem(_('No saved presets'));
item.setSensitive(false);
this._presetsItem.menu.addMenuItem(item);
return;
}

for (const preset of presets) {
const item = new PopupMenu.PopupMenuItem(preset);
item.connect('activate', () => this._setPreset(preset));
this._presetsItem.menu.addMenuItem(item);
this._presetItems.push(item);
if (this._allPresets.length <= PRESET_SEARCH_THRESHOLD) {
for (const preset of this._allPresets)
this._presetsItem.menu.addMenuItem(this._makePresetItem(preset));
return;
}

this._buildPresetSearch();
this._buildPresetResults();
}

_buildPresetSearch() {
const searchItem = new PopupMenu.PopupMenuSection();
const searchBox = new St.BoxLayout({
x_align: Clutter.ActorAlign.START,
x_expand: false,
});
this._presetSearchEntry = new St.Entry({
can_focus: true,
hint_text: _('Search presets'),
style_class: 'search-entry',
style: `width: ${PRESET_SEARCH_ENTRY_WIDTH}px;`,
x_expand: false,
track_hover: true,
});
this._presetSearchEntry.clutter_text.connect('text-changed', () => {
this._presetFilterText = this._presetSearchEntry?.text ?? '';
this._refreshFilteredPresetRows();
});
searchBox.add_child(this._presetSearchEntry);
searchItem.actor.add_child(searchBox);
this._presetsItem.menu.addMenuItem(searchItem);
}

_buildPresetResults() {
const resultsItem = new PopupMenu.PopupMenuSection();
const scrollView = new St.ScrollView({
hscrollbar_policy: St.PolicyType.NEVER,
style: `width: ${PRESET_PICKER_WIDTH}px; max-height: ${PRESET_PICKER_MAX_HEIGHT}px; padding-bottom: 6px;`,
x_expand: true,
});
this._presetResultsBox = new St.BoxLayout({
orientation: Clutter.Orientation.VERTICAL,
style: 'padding-top: 4px; padding-bottom: 4px;',
x_expand: true,
});
scrollView.set_child(this._presetResultsBox);
resultsItem.actor.add_child(scrollView);
this._presetsItem.menu.addMenuItem(resultsItem);
this._refreshFilteredPresetRows();
}

_refreshFilteredPresetRows() {
if (this._presetResultsBox === null)
return;

this._presetResultsBox.destroy_all_children();
this._presetItems = [];

const filteredPresets = this._filteredPresets();
if (!filteredPresets.length) {
const item = new PopupMenu.PopupMenuItem(_('No matching presets'));
item.setSensitive(false);
this._presetResultsBox.add_child(item);
return;
}

for (const preset of filteredPresets)
this._presetResultsBox.add_child(this._makePresetItem(preset));
}

_filteredPresets() {
const query = this._presetFilterText.trim().toLocaleLowerCase();
if (!query)
return this._allPresets;

const tokens = query.split(/\s+/).filter(Boolean);
return this._allPresets.filter(preset => {
const normalizedPreset = preset.toLocaleLowerCase();
return tokens.every(term => normalizedPreset.includes(term));
});
}

_makePresetItem(preset) {
const item = new PopupMenu.PopupMenuItem(preset);
item._miniEqPresetName = preset;
item.label.clutter_text.set({
ellipsize: Pango.EllipsizeMode.END,
line_wrap: false,
});
item.connect('activate', () => this._activatePreset(preset));
this._presetItems.push(item);
this._syncPresetItemOrnament(item);
return item;
}

_activatePreset(preset) {
this.menu.close();
this._setPreset(preset);
}

_syncPresetItemOrnament(item) {
item.setOrnament(item._miniEqPresetName === this._currentPresetName
? PopupMenu.Ornament.CHECK
: PopupMenu.Ornament.NONE);
}

_syncPresetOrnaments() {
for (const item of this._presetItems)
this._syncPresetItemOrnament(item);
}
});

Expand Down
8 changes: 4 additions & 4 deletions io.github.bhack.mini-eq.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ modules:
sources:
- type: git
url: https://gitlab.freedesktop.org/pipewire/pipewire.git
tag: 1.6.1
commit: b7341d068947225fcdf62d39277606e8516d7f52
tag: 1.6.9
commit: 8fa27cabdc6c0c1350c69c026af5850ef0af1e26

- name: pipewire-gobject
buildsystem: meson
Expand All @@ -126,8 +126,8 @@ modules:
sources:
- type: git
url: https://github.com/bhack/pipewire-gobject.git
tag: 0.3.9
commit: d132598a7e03800fb169fd39d2c71afacdb47195
tag: 0.3.10
commit: a9d652f0f2abdba0ab7dcba12e52835f679d24a4

- 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.8.7"
version = "0.8.8"
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.9,<0.4"]
dependencies = ["numpy>=1.26", "pipewire-gobject>=0.3.10,<0.4"]

[project.urls]
Homepage = "https://github.com/bhack/mini-eq"
Expand Down
Loading