fix(waveform): 4x-oversampled min/max envelope, columnMinMax homed in peaks, panel routed through shared drawWaveform — one gap-free algorithm on all surfaces

This commit is contained in:
2026-07-27 19:09:50 -04:00
parent 20308c842e
commit b3c9fad9ba
11 changed files with 240 additions and 203 deletions
+20 -44
View File
@@ -13,7 +13,8 @@
// LICE-drawn named-banks tab-page region below (one tab per named bank, an
// overflow/scroll strip), and two full-height toggles that collapse the split.
// Each region reuses the M5 grid render loop (waveform thumbnails / empty state).
// * per-sample PCM read via PCM_source fed to peaks::computeEnvelope at cell width.
// * per-sample PCM read via PCM_source fed to peaks::computeEnvelope, oversampled
// (kWaveformOversample bins per drawn pixel column) for the FA3 gap-free draw.
// * an in-memory thumbnail cache keyed by (sample id, draw width, bank generation).
// * id-keyed bank management (create / rename / delete / evacuate / activate) and
// sample move/copy — driven from a tab context menu and a drag — against the B1
@@ -416,7 +417,7 @@ std::vector<const Bank*> namedBanks() {
return out;
}
// --- Thumbnail computation (unchanged from M5) --------------------------------
// --- Thumbnail computation (M5; `width` is a BIN count since FA3 oversampling) --
Envelope computeThumbnail(const std::string& absPath, int width) {
if (width <= 0 || absPath.empty()) return {};
@@ -458,9 +459,13 @@ Envelope computeThumbnail(const std::string& absPath, int width) {
for (std::size_t i = 0; i < sampleCount; ++i)
pcm[i] = static_cast<float>(buf[i]);
// Clamp bins to the frame count: computeEnvelope pads binCount > frameCount with
// trailing empty {0,0} bins, which would render a very short sample as a comb of
// spikes over flat gaps.
const int binCount = width < got ? width : got;
return computeEnvelope(pcm, static_cast<std::size_t>(nch),
static_cast<std::size_t>(got),
static_cast<std::size_t>(width));
static_cast<std::size_t>(binCount));
}
const Envelope& thumbnailFor(const Sample& sample, int width,
@@ -479,7 +484,7 @@ const Envelope& thumbnailFor(const Sample& sample, int width,
return ins.first->second.envelope;
}
// --- Drawing: thumbnails (unchanged from M5) ----------------------------------
// --- Drawing: thumbnails (via the kit's shared drawWaveform since FA3) ---------
// Draws the L7 decorative metadata overlay on a card: bars.beats.subdivisions bottom-LEFT
// (musical, from the capture-time tempo + meter stamp) and seconds.milliseconds bottom-RIGHT
@@ -529,45 +534,11 @@ void drawThumbnail(LICE_IBitmap* bmp, const CellRect& rect, const Envelope& env,
LICE_DrawRect(bmp, rect.x + 1, rect.y + 1, rect.width - 2, rect.height - 2, ring, 1.0f, 0);
}
// Waveform plot (peaks invariant: min<=max). The wave keeps its NORMAL accent color in
// every state (L7 dropped the inverted bg/base wave on the selected cell — the cell fill
// is no longer inverted, so no contrast swap is needed).
const LICE_pixel midCol = toLice(roleColor(Role::LineHairline));
const LICE_pixel waveCol = toLice(roleColor(Role::AccentPrimary));
if (env.empty()) {
const int midY = rect.y + rect.height / 2;
LICE_Line(bmp, rect.x + 2, midY, rect.x + rect.width - 2, midY, midCol, 1.0f, 0, false);
if (sample) drawCardMeta(bmp, rect, *sample); // L7 overlay even on an empty envelope
return;
}
const int channels = static_cast<int>(env.size());
const int bandH = rect.height / channels;
for (int ch = 0; ch < channels; ++ch) {
const ChannelEnvelope& bins = env[ch];
const int bandTop = rect.y + ch * bandH;
const int midY = bandTop + bandH / 2;
const double halfSpan = (bandH / 2) - 2;
LICE_Line(bmp, rect.x + 2, midY, rect.x + rect.width - 2, midY, midCol, 1.0f, 0, false);
const int nbins = static_cast<int>(bins.size());
if (nbins <= 0) continue;
const int innerW = rect.width - 4;
for (int i = 0; i < nbins; ++i) {
const int x = rect.x + 2 + (nbins > 1 ? (i * (innerW - 1)) / (nbins - 1) : 0);
// min<=max always (peaks invariant). Draw a vertical line from the
// min sample to the max sample, clamped to the band.
int yMax = midY - static_cast<int>(compressAmplitudeForDisplay(bins[i].max) * halfSpan); // max -> up
int yMin = midY - static_cast<int>(compressAmplitudeForDisplay(bins[i].min) * halfSpan); // min -> down
if (yMax < bandTop) yMax = bandTop;
if (yMin > bandTop + bandH - 1) yMin = bandTop + bandH - 1;
LICE_Line(bmp, x, yMin, x, yMax, waveCol, 1.0f, 0, false);
}
}
// Waveform plot through the kit's shared primitive (FA3): the SAME per-pixel-column
// min/max envelope draw the VST editor hero + browser cards use — one algorithm, one
// look, everywhere. The oversampled env (see drawRegionGrid's binWidth) collapses per
// column via peaks::columnMinMax inside the kit; an empty env draws just the midline.
drawWaveform(bmp, cell, env);
// L7 decorative metadata overlay, drawn last so it sits over the waveform.
if (sample) drawCardMeta(bmp, rect, *sample);
@@ -1379,7 +1350,12 @@ void drawRegionGrid(LICE_IBitmap* bmp, const RECT& region, bool isBanks,
// BankIndex insertion order. Selection/focus are keyed by the occupied-ordinal (selection
// space); a slot maps back to its ordinal via selectionForSlot.
const RegionDisplay disp = regionDisplay(region, isBanks, reg);
const int binWidth = kGrid.cellWidth - 4;
// FA3 anti-alias: thumbnails are computed OVERSAMPLED — kWaveformOversample bins per
// drawn pixel column — and drawWaveform collapses them per column (peaks::columnMinMax)
// so steep transients render as true full-height spans. computeThumbnail clamps the
// request to the frame count.
const int binWidth = kWaveformOversample *
waveformColumnCount(KitBox{0, 0, kGrid.cellWidth, kGrid.cellHeight});
for (const SlotCellRect& r : disp.slotRects) {
if (r.y >= grid.bottom) continue; // below the viewport: skip (no scroll)
const CellRect rect{r.x, r.y, r.width, r.height};