diff --git a/backend/src/waveform.py b/backend/src/waveform.py
index 0e661fe..b22b29a 100644
--- a/backend/src/waveform.py
+++ b/backend/src/waveform.py
@@ -8,7 +8,10 @@ WAVEFORM_DIR.mkdir(parents=True, exist_ok=True)
def generate_waveform_svg(clip_id: str, filename: str) -> str:
- """Generate a themed FFT waveform SVG for an audio clip.
+ """Generate an amplitude-over-time waveform SVG for an audio clip.
+
+ Shows the actual dynamics of the track — quiet intros, loud drops,
+ silence gaps. Each track's waveform is unique to its amplitude envelope.
Returns the path to the generated SVG file.
"""
@@ -38,104 +41,86 @@ def generate_waveform_svg(clip_id: str, filename: str) -> str:
audio_data = audio_data.astype(np.float32) / 32768.0
# Parameters
- num_bands = 64
+ num_bars = 200 # Time resolution — 200 bars across the width
svg_width = 800
- svg_height = 200
- bar_gap = 2
- bar_width = (svg_width - (num_bands - 1) * bar_gap) / num_bands
- max_bar_height = svg_height * 0.9 # Leave some margin
+ svg_height = 160 # Slightly shorter for the detail panel
+ bar_gap = 1 # Tight spacing for dense look
+ bar_width = (svg_width - (num_bars - 1) * bar_gap) / num_bars
+ max_bar_height = svg_height * 0.95
- # Divide audio into segments and compute FFT for each
- samples_per_band = len(audio_data) // num_bands
+ # Divide audio into time slices and compute RMS amplitude for each
+ samples_per_bar = len(audio_data) // num_bars
- band_heights = []
- for i in range(num_bands):
- start = i * samples_per_band
- end = start + samples_per_band
+ bar_heights = []
+ for i in range(num_bars):
+ start = i * samples_per_bar
+ end = start + samples_per_bar
segment = audio_data[start:end]
- if len(segment) < 2:
- band_heights.append(0)
+ if len(segment) == 0:
+ bar_heights.append(0)
continue
- # Apply Hann window
- window = np.hanning(len(segment))
- windowed = segment * window
+ # RMS amplitude — the actual "loudness" of this time slice
+ rms = np.sqrt(np.mean(segment ** 2))
- # FFT
- fft = np.fft.rfft(windowed)
- magnitude = np.abs(fft)
+ # Apply slight compression so quiet parts are still visible
+ # but loud parts don't all max out
+ compressed = np.power(rms, 0.7)
- # Use the average magnitude in the lower-mid frequencies (most audible)
- # Focus on ~100Hz - 4kHz range
- freqs = np.fft.rfftfreq(len(segment), d=1.0/22050)
- mask = (freqs >= 100) & (freqs <= 4000)
- if np.any(mask):
- avg_mag = np.mean(magnitude[mask])
- else:
- avg_mag = np.mean(magnitude[:len(magnitude)//4]) # fallback: first quarter
-
- # Convert to dB-like scale and normalize
- # Add small offset to avoid log(0)
- db = 20 * np.log10(avg_mag + 1e-10)
- # Map roughly -60dB to 0dB range to 0-1
- normalized = (db + 60) / 60
- normalized = np.clip(normalized, 0, 1)
-
- band_heights.append(normalized)
+ bar_heights.append(compressed)
- # Smooth the heights slightly for visual appeal
+ # Normalize to [0, 1] based on the max in THIS track
+ # (so each track uses its full dynamic range)
+ max_height = max(bar_heights) if bar_heights else 1
+ if max_height > 0:
+ bar_heights = [h / max_height for h in bar_heights]
+
+ # Light smoothing for visual polish (very gentle, preserves dynamics)
smoothed = []
- for i, h in enumerate(band_heights):
- neighbors = band_heights[max(0, i-1):min(len(band_heights), i+2)]
- smoothed.append(np.mean(neighbors))
- band_heights = smoothed
+ for i, h in enumerate(bar_heights):
+ # 3-point moving average with center weight
+ neighbors = bar_heights[max(0, i-1):min(len(bar_heights), i+2)]
+ weights = [0.25, 0.5, 0.25] if len(neighbors) == 3 else [1.0 / len(neighbors)] * len(neighbors)
+ smoothed.append(sum(n * w for n, w in zip(neighbors, weights)))
+ bar_heights = smoothed
# Build SVG
- # Color gradient: warm amber/orange tones (lyria-accent themed)
- # Using HSL interpolation from deep amber to bright orange
- def band_color(index: int, total: int) -> str:
- """Generate color for a band — warm amber to orange gradient."""
+ # Color: warm amber-to-orange gradient that progresses with time
+ # Left side = deeper amber, right side = brighter orange
+ def bar_color(index: int, total: int) -> str:
+ """Generate color for a bar — time-progressive warm gradient."""
t = index / (total - 1) if total > 1 else 0
- # HSL: hue 25° (amber) to 35° (orange), saturation 80-90%, lightness 45-65%
- hue = 25 + t * 10
- sat = 85
- light = 45 + t * 20
- return f"hsl({hue:.1f}, {sat}%, {light:.1f}%)"
+ # HSL: hue 20° (deep amber) to 40° (bright orange)
+ # Saturation 75-90%, lightness 40-60%
+ hue = 20 + t * 20
+ sat = 75 + t * 15
+ light = 40 + t * 20
+ return f"hsl({hue:.1f}, {sat:.1f}%, {light:.1f}%)"
# SVG content
svg_parts = [
f'')
diff --git a/data/waveforms/7239af1a-2a3f-46bd-816a-2e349752cb83.svg b/data/waveforms/7239af1a-2a3f-46bd-816a-2e349752cb83.svg
index 6808582..f4aa175 100644
--- a/data/waveforms/7239af1a-2a3f-46bd-816a-2e349752cb83.svg
+++ b/data/waveforms/7239af1a-2a3f-46bd-816a-2e349752cb83.svg
@@ -1,137 +1,203 @@
-