Files
mitus/cht/ui/waveform.py
2026-04-03 08:39:24 -03:00

103 lines
3.1 KiB
Python

"""
WaveformWidget: GTK4 DrawingArea that renders waveform peaks with a playhead.
Driven by Timeline "changed" signal — redraws when cursor or duration changes.
Peak data is set externally via set_peaks() from GLib.idle_add.
"""
import logging
import math
import numpy as np
import gi
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk, GLib
log = logging.getLogger(__name__)
class WaveformWidget(Gtk.Box):
"""Waveform display synced to Timeline state."""
def __init__(self, timeline, **kwargs):
super().__init__(orientation=Gtk.Orientation.VERTICAL, **kwargs)
self._timeline = timeline
self._peaks = None
self._bucket_duration = 0.05
label = Gtk.Label(label="Waveform")
label.add_css_class("heading")
label.set_margin_top(4)
label.set_margin_bottom(4)
self.append(label)
self._area = Gtk.DrawingArea()
self._area.set_content_height(250)
self._area.set_hexpand(True)
self._area.set_vexpand(True)
self._area.set_draw_func(self._draw)
self.append(self._area)
timeline.connect("changed", self._on_timeline_changed)
def set_peaks(self, peaks, bucket_duration):
"""Update peak data. Call from GLib.idle_add."""
self._peaks = peaks
self._bucket_duration = bucket_duration
self._area.queue_draw()
def _on_timeline_changed(self, timeline):
self._area.queue_draw()
def _draw(self, area, cr, width, height):
# Background
cr.set_source_rgb(0.1, 0.1, 0.12)
cr.rectangle(0, 0, width, height)
cr.fill()
state = self._timeline.state
duration = state.duration
mid_y = height / 2
# Center line
cr.set_source_rgba(0.3, 0.3, 0.35, 1.0)
cr.set_line_width(1)
cr.move_to(0, mid_y)
cr.line_to(width, mid_y)
cr.stroke()
# Draw peaks — always stretch to fill full width
if self._peaks is not None and len(self._peaks) > 0:
n_peaks = len(self._peaks)
max_peak = np.max(self._peaks) if np.max(self._peaks) > 0 else 1.0
for x in range(width):
idx = int((x / width) * n_peaks)
if 0 <= idx < n_peaks:
val = self._peaks[idx] / max_peak
bar_h = val * (height * 0.45)
cr.set_source_rgba(0.2, 0.6 + val * 0.4, 0.3, 0.85)
cr.rectangle(x, mid_y - bar_h, 1, bar_h * 2)
cr.fill()
# Scene markers and playhead use timeline duration
x_duration = max(duration, 0.1)
cr.set_source_rgba(1.0, 1.0, 0.3, 0.3)
cr.set_line_width(1)
for marker in state.scene_markers:
mx = (marker / x_duration) * width
if 0 <= mx <= width:
cr.move_to(mx, 0)
cr.line_to(mx, height)
cr.stroke()
# Playhead
px = (state.cursor / x_duration) * width
cr.set_source_rgba(1.0, 0.3, 0.3, 0.9)
cr.set_line_width(2)
cr.move_to(px, 0)
cr.line_to(px, height)
cr.stroke()