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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 25 additions & 24 deletions plugins/audio_midi/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down
52 changes: 52 additions & 0 deletions tests/v3/test_audio_midi_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
#
Expand Down
Loading