diff --git a/main.py b/main.py index c672f16..af3de29 100755 --- a/main.py +++ b/main.py @@ -3937,6 +3937,39 @@ def main(): sys.exit(ret) +class AudioWaveform(QWidget): + """Read-only waveform strip: paints normalized peak bars for the current + audio region. Populated on demand via manual refresh (no implicit UI-thread + decode).""" + + def __init__(self, parent=None): + super().__init__(parent) + self._peaks: list[float] = [] + self.setFixedHeight(48) + self.setToolTip("Waveform of the current audio area (↻ to refresh)") + + def set_peaks(self, peaks: list[float]) -> None: + self._peaks = list(peaks or []) + self.update() + + def clear(self) -> None: + self._peaks = [] + self.update() + + def paintEvent(self, _ev): + p = QPainter(self) + p.fillRect(self.rect(), QColor(30, 30, 30)) + if not self._peaks: + return + w = self.width(); h = self.height(); mid = h / 2 + n = len(self._peaks) + p.setPen(QPen(QColor(0, 220, 190))) + for i, v in enumerate(self._peaks): + x = int(i * w / n) + bar_h = v * (h - 4) / 2 + p.drawLine(x, int(mid - bar_h), x, int(mid + bar_h)) + + class MainWindow(QMainWindow): def __init__(self): super().__init__() @@ -4495,6 +4528,11 @@ class MainWindow(QMainWindow): self._spn_gain.setSingleStep(0.5) self._spn_gain.setSuffix(" dB") self._spn_gain.setToolTip("Gain applied to the extracted audio (0 = unchanged)") + self._wave = AudioWaveform() + self._btn_wave_refresh = QPushButton("↻ Waveform") + self._btn_wave_refresh.setToolTip( + "Decode the current audio area and draw its waveform") + self._btn_wave_refresh.clicked.connect(self._on_wave_refresh) self._transport_row = transport_row # Row 1b — subcategory (subprofile) export buttons live on their own @@ -4797,10 +4835,12 @@ class MainWindow(QMainWindow): g.addWidget(self._chk_normalize, 3, 0, 1, 2) g.addWidget(QLabel("Gain:"), 3, 2) g.addWidget(self._spn_gain, 3, 3) - # Row 4 reserved for the waveform strip (added in a later task). - g.addWidget(self._btn_extract_audio, 5, 0, 1, 4) + # Row 4 — read-only waveform strip for the current audio area. + g.addWidget(self._wave, 4, 0, 1, 4) + g.addWidget(self._btn_wave_refresh, 5, 0, 1, 2) + g.addWidget(self._btn_extract_audio, 6, 0, 1, 4) g.setColumnStretch(4, 1) # spare gutter column clusters label/field pairs to the left - g.setRowStretch(6, 1) # anchor content to the top (row 4 stays reserved for the waveform) + g.setRowStretch(7, 1) # anchor content to the top tb.addItem(extract_page, "Extract & Edit") # ── Page 2: Scan / Classify (moved verbatim) ───────────── @@ -6463,6 +6503,21 @@ class MainWindow(QMainWindow): start = self._cursor self._timeline.set_audio_region(start, start + self._spn_audio_len.value()) + def _on_wave_refresh(self) -> None: + """Decode the current audio area and repaint the waveform strip.""" + if not self._file_path: + self._wave.clear() + return + from core.waveform import load_region_samples, peaks + start = self._cursor + dur = self._spn_audio_len.value() + QApplication.setOverrideCursor(Qt.CursorShape.WaitCursor) + try: + samples = load_region_samples(self._file_path, start, dur) + finally: + QApplication.restoreOverrideCursor() + self._wave.set_peaks(peaks(samples)) + def _on_extract_audio(self) -> None: """Extract an exact-length audio slice starting at the playhead and prompt for where to save it (format follows the chosen extension).""" diff --git a/tests/test_ui_structure.py b/tests/test_ui_structure.py index 242b134..8e1e6ec 100644 --- a/tests/test_ui_structure.py +++ b/tests/test_ui_structure.py @@ -372,3 +372,17 @@ def test_extract_no_edits_passes_no_filters(win, monkeypatch, tmp_path): with pytest.raises(_Stop): win._on_extract_audio() assert seen["filters"] is None + + +def test_waveform_strip_present_and_safe(win): + from PyQt6.QtWidgets import QPushButton + assert win._wave is not None + assert isinstance(win._btn_wave_refresh, QPushButton) + # set_peaks stores + doesn't crash + win._wave.set_peaks([0.1, 0.5, 0.9]) + assert list(win._wave._peaks) == [0.1, 0.5, 0.9] + win._wave.clear() + assert win._wave._peaks == [] + # refresh with no file loaded is a safe no-op + win._file_path = "" + win._on_wave_refresh()