diff --git a/CHANGELOG.md b/CHANGELOG.md index aa69915e5..dd8f4929e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Pedalboard/snapshot titles now auto-scroll only while selected with the NAV encoder (at most one thing scrolls at a time, and they sit at their leftmost position otherwise) — LCD updates over SPI are audible on the DAC at high gain, so the screen stays quiet while you play ### Fixed - Long titles scrolled slightly past their last pixel before bouncing back; the scroll window now matches the drawn text area exactly +- 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 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..9825366f2 100644 --- a/tests/v3/test_audio_midi_panel.py +++ b/tests/v3/test_audio_midi_panel.py @@ -231,6 +231,58 @@ 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): + 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 #