From 773f1aa8b7e07cfa7e92c28eb03a2347e8e7cc95 Mon Sep 17 00:00:00 2001 From: Cam Gorrie Date: Mon, 31 Aug 2026 21:06:35 -0400 Subject: [PATCH 1/3] Don't show audio midi menu for non-iqaudio cards --- CHANGELOG.md | 4 +++ plugins/audio_midi/panel.py | 49 ++++++++++++++-------------- tests/v3/test_audio_midi_panel.py | 54 ++++++++++++++++++++++++++++++- 3 files changed, 82 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7a6b2bab..b63582582 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Notable user visible changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Fixed +- Global EQ in the Audio & MIDI menu no longer disappears on sound cards without the DAC EQ (and at unsupported sample rates): the Equalizer row and bars stay visible, perma-disabled with an [N/A] badge + ## [v3.3.1] - 2026-08-14 ### Fixed - Pedalboards list no longer shows deleted pedalboards after they are removed from MOD-UI diff --git a/plugins/audio_midi/panel.py b/plugins/audio_midi/panel.py index 0f6fb8903..2a38e4419 100644 --- a/plugins/audio_midi/panel.py +++ b/plugins/audio_midi/panel.py @@ -40,6 +40,10 @@ opens a radio submenu (Internal / Ableton Link / MIDI Clock Slave); VU Cal opens the existing VU calibration dialog. Switching the Equalizer off drops the bands out of the NAV cycle and dims them. + +Cards without the DAC EQ hardware (and rates where the DA7213 EQ is N/A) +keep the row and bars visible — perma-disabled with an [N/A] badge, bands +greyed at their stored/0 dB values and out of the NAV cycle. """ from __future__ import annotations @@ -139,7 +143,7 @@ def _eq_badge(on: bool, supported: bool = True) -> PillGlyph: if not supported: # Never toggles, so it can size to its own label. - return PillGlyph("DISABLED", height=_BADGE_GLYPH_H, color=DEFAULT_COLOR, outline=True) + return PillGlyph("N/A", height=_BADGE_GLYPH_H, color=DEFAULT_COLOR, outline=True) # Both states share the wider of the two labels so the row doesn't reflow. w = max(PillGlyph(t, height=_BADGE_GLYPH_H).width for t in ("ON", "OFF")) return PillGlyph( @@ -362,7 +366,7 @@ def __init__(self, *, handler: "Modhandler", on_dismiss: Callable[[], None]) -> except RuntimeError: logging.warning("no sample rate; disabling the DAC EQ") specs = None - self._eq_supported = specs is not None + self._eq_supported = specs is not None and self._has_eq self._bands = specs if specs is not None else BAND_SPECS super().__init__( plugin=source, # type: ignore[arg-type] # ParamSource, not Plugin @@ -382,11 +386,10 @@ def eq_enabled(self) -> bool: def snapshot_state(self) -> AudioMidiState: params = self.plugin.parameters bands: dict[str, GraphicBandParams] = {} - if self._has_eq: - for band in self._bands: - p = params.get(band.gain_sym) - gain = float(p.value) if p is not None else 0.0 - bands[band.name] = GraphicBandParams(enabled=True, gain_db=gain) + for band in self._bands: + p = params.get(band.gain_sym) + gain = float(p.value) if p is not None else 0.0 + bands[band.name] = GraphicBandParams(enabled=True, gain_db=gain) ac = self._handler.audiocard in_sym = Symbol(ac.CAPTURE_VOLUME) if ac.CAPTURE_VOLUME is not None else None out_sym = Symbol(ac.MASTER) if ac.MASTER is not None else None @@ -450,28 +453,26 @@ def build_widgets(self) -> None: self._out_arc.set_badge(_BADGE_TWEAK3) # Right column: EQ bars (4px top margin). - if self._has_eq: - self._bar_widget = _CompactEqWidget( - box=Box.xywh(cb.x0 + _EQ_BAR_X, cb.y0 + _EQ_Y, _EQ_BAR_W, _EQ_H), - bands=self._bands, - font=self._tiny_font, - parent=self, - ) - self._bar_widget.set_enabled(self.eq_enabled) - self._bar_widget.set_state(self.snapshot_state().eq) + self._bar_widget = _CompactEqWidget( + box=Box.xywh(cb.x0 + _EQ_BAR_X, cb.y0 + _EQ_Y, _EQ_BAR_W, _EQ_H), + bands=self._bands, + font=self._tiny_font, + parent=self, + ) + self._bar_widget.set_enabled(self.eq_enabled) + self._bar_widget.set_state(self.snapshot_state().eq) self.apply_state(self.snapshot_state()) def _build_rows(self) -> None: cb = self.content_box - if self._has_eq: - self._eq_row = _DiscreteRow( - box=Box.xywh(cb.x0 + _ROWS_X, cb.y0 + _EQ_SW_Y, _ROWS_W, _ROW_H), - segments=self._eq_row_segments(), - action=self._on_eq_row, - font=self._row_font, - parent=self, - ) + self._eq_row = _DiscreteRow( + box=Box.xywh(cb.x0 + _ROWS_X, cb.y0 + _EQ_SW_Y, _ROWS_W, _ROW_H), + segments=self._eq_row_segments(), + action=self._on_eq_row, + font=self._row_font, + parent=self, + ) y = cb.y0 + _ROWS_Y sync_segs = self._sync_row_segments() self._sync_row = _DiscreteRow( diff --git a/tests/v3/test_audio_midi_panel.py b/tests/v3/test_audio_midi_panel.py index 32195b0da..f6a2353e5 100644 --- a/tests/v3/test_audio_midi_panel.py +++ b/tests/v3/test_audio_midi_panel.py @@ -139,7 +139,6 @@ def test_eq_switched_off(self, audio_midi_system: SystemFixture, snapshot): # --------------------------------------------------------------------------- -class TestAudioMidiPanelBehaviour: @pytest.mark.parametrize( "rate", [ @@ -231,6 +230,59 @@ def test_no_bypass_button(self, audio_midi_system: SystemFixture): assert panel._btn_back is not None +# --------------------------------------------------------------------------- +# Non-IQaudIO cards (no DAC EQ): the EQ is perma-disabled, not a black hole +# --------------------------------------------------------------------------- + + +class TestAudioMidiPanelNoEqHardware: + """Cards without ``DAC_EQ`` keep the Equalizer row and the bars visible + but inert — [N/A] badge, bars greyed, nothing EQ in the NAV cycle, and + no writes to the audiocard.""" + + @pytest.fixture + def no_eq_system(self, audio_midi_system: SystemFixture) -> SystemFixture: + handler = audio_midi_system.handler + ac = cast(MagicMock, handler.audiocard) + ac.DAC_EQ = None + ac.EQ_1 = ac.EQ_2 = ac.EQ_3 = ac.EQ_4 = ac.EQ_5 = None + handler.eq_status = True # stale bit; the panel must still grey the EQ + return audio_midi_system + + def test_eq_visible_inert_with_na_badge(self, no_eq_system: SystemFixture): + from plugins.audio_midi.panel import _eq_badge + from uilib.misc import InputEvent + + handler = no_eq_system.handler + panel = _open_panel(no_eq_system) + + assert panel._eq_supported is False + assert panel.eq_enabled is False + # Shown, not hidden: row and bars exist. + assert panel._eq_row is not None + assert panel._bar_widget is not None + # [N/A] badge, not the toggleable [OFF]. + assert _eq_badge(True, supported=False)._label == "N/A" + # Nothing EQ participates in the nav cycle. + assert panel._eq_row not in panel.sel_list + assert not any(isinstance(w, _BandSelectable) for w in panel.sel_children()) + # Bars render greyed-out at their fallback 0 dB. + st = panel.snapshot_state().eq + assert [b.name for b in BAND_SPECS] == list(st.bands) + assert all(p.gain_db == 0.0 for p in st.bands.values()) + assert panel._bar_widget._enabled is False + # Row click is a no-op — no toggle, no hardware write. + assert panel._on_eq_row(InputEvent.CLICK) is False + ac = cast(MagicMock, handler.audiocard) + ac.set_switch_parameter.assert_not_called() + + def test_eq_row_skipped_in_nav_cycle(self, no_eq_system: SystemFixture): + handler = no_eq_system.handler + panel = _open_panel(no_eq_system) + # Initial selection falls through the EQ column to the Input arc. + assert panel.sel_ref is panel._in_arc + + # --------------------------------------------------------------------------- # Sagas — AudioCard writes flow through the synthetic source # From 4140de2447f2bd7d25704397c0aa7c0a1f3aad33 Mon Sep 17 00:00:00 2001 From: Cam Gorrie Date: Mon, 31 Aug 2026 21:08:47 -0400 Subject: [PATCH 2/3] ruff ruff --- tests/v3/test_audio_midi_panel.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tests/v3/test_audio_midi_panel.py b/tests/v3/test_audio_midi_panel.py index f6a2353e5..63c3d8a73 100644 --- a/tests/v3/test_audio_midi_panel.py +++ b/tests/v3/test_audio_midi_panel.py @@ -133,11 +133,9 @@ def test_eq_switched_off(self, audio_midi_system: SystemFixture, snapshot): handler.poll_lcd_updates() assert panel.sel_ref is panel._sync_row - -# --------------------------------------------------------------------------- -# Behaviour: tweak bindings, sync-mode echo -# --------------------------------------------------------------------------- - + # --------------------------------------------------------------------------- + # Behaviour: tweak bindings, sync-mode echo + # --------------------------------------------------------------------------- @pytest.mark.parametrize( "rate", @@ -277,7 +275,6 @@ def test_eq_visible_inert_with_na_badge(self, no_eq_system: SystemFixture): ac.set_switch_parameter.assert_not_called() def test_eq_row_skipped_in_nav_cycle(self, no_eq_system: SystemFixture): - handler = no_eq_system.handler panel = _open_panel(no_eq_system) # Initial selection falls through the EQ column to the Input arc. assert panel.sel_ref is panel._in_arc From 696b5a66c0e961739f5d528055bc03fb64bf6363 Mon Sep 17 00:00:00 2001 From: Cam Gorrie Date: Mon, 31 Aug 2026 22:09:31 -0400 Subject: [PATCH 3/3] Fix --- tests/v3/test_audio_midi_panel.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/v3/test_audio_midi_panel.py b/tests/v3/test_audio_midi_panel.py index 63c3d8a73..9825366f2 100644 --- a/tests/v3/test_audio_midi_panel.py +++ b/tests/v3/test_audio_midi_panel.py @@ -133,10 +133,13 @@ def test_eq_switched_off(self, audio_midi_system: SystemFixture, snapshot): handler.poll_lcd_updates() assert panel.sel_ref is panel._sync_row - # --------------------------------------------------------------------------- - # Behaviour: tweak bindings, sync-mode echo - # --------------------------------------------------------------------------- +# --------------------------------------------------------------------------- +# Behaviour: tweak bindings, sync-mode echo +# --------------------------------------------------------------------------- + + +class TestAudioMidiPanelBehaviour: @pytest.mark.parametrize( "rate", [