// editor_paint_waveform.cpp — the WAVEFORM band's painter: the channel lane(s), the loop // span + start/loop markers, and the amp-envelope overlay. Windows-only. // // Overlay contract: see waveform_view.h's WaveformSurface. #include "shell/instrument/reasampler_editor.h" #ifdef _WIN32 #include #include #include #include #include "core/audio/peaks.h" // computeEnvelope (waveform binning) #include "core/instrument/ui/waveform_view.h" // waveformSurface / laneEnvelope / frameToX #include "shell/instrument/editor_internal.h" // kit adapters #include "shell/instrument/reasampler_processor.h" namespace reasampler::vst { using namespace reasampler::ui; // kit vocabulary using namespace reasampler::instrument::ui; // lanes + waveform geometry using audio::computeEnvelope; namespace { // Marker roles — semantic, drawn through the kit's palette: start = teal (secondary), loop // start/end = purple (tertiary). The loop-span fill is a faint purple. constexpr Role kRoleStartMarker = Role::AccentSecondary; constexpr Role kRoleLoopMarker = Role::AccentTertiary; } // namespace void ReaSamplerEditor::paintWaveform(LICE_IBitmap* bmp, const Rect& band) { fillSurface(bmp, toKitBox(band), Role::BgBase, InteractionState::Rest); if (band.empty()) return; const std::vector& mono = monoPcmFor(selectedId_); const std::int64_t frames = static_cast(mono.size()); if (frames <= 0) { kitTextCentered(bmp, band, "(decoding...)", Font::Label, Role::TextDim); return; } const ChannelPcm& src = channelPcmFor(selectedId_); const WaveformSurface surface = waveformSurface( band, channelMode_ == ChannelMode::Stereo, src.channelCount); if (!surface.upper.empty()) { // Gap-free: one bin per drawn pixel column (kWaveformOversample == 1, so this // multiplies by 1). The gap-free draw comes from peaks::columnMinMax's exact // partition — extra bins produce no visible change. Clamped to frame count below. // Both lanes share a width, so one bin count serves both. const std::int64_t wantBins = static_cast( (std::max)(1, waveformColumnCount(toKitBox(surface.upper)))) * kWaveformOversample; const std::size_t bins = static_cast(wantBins < frames ? wantBins : frames); if (surface.laneCount == 2) { // ONE pass over the interleaved source: computeEnvelope already envelopes each // channel independently, so the second lane costs no second scan of the PCM. const Envelope env = computeEnvelope(src.interleaved, static_cast(src.channelCount), static_cast(src.frameCount()), bins); drawEnvelope(bmp, surface.upper, laneEnvelope(env, 0)); drawEnvelope(bmp, surface.lower, laneEnvelope(env, 1)); } else { // One lane draws what one lane plays: the downmix, not channel 0 of a stereo // source. drawEnvelope(bmp, surface.upper, computeEnvelope(mono, 1, mono.size(), bins)); } } // Markers and the loop span are overlays: ONE draw across the full stacked height, so a // stereo view reads one loop region rather than two. const OverlayArea& overlay = surface.overlay; const Rect& overlayRect = overlay.rect; const SetupMarkers m = pickedMarkers(frames); if (m.hasLoop && m.loopEnd > m.loopStart) { const int lx = frameToX(overlay, frames, m.loopStart); const int rx = frameToX(overlay, frames, m.loopEnd); if (rx > lx) { LICE_FillRect(bmp, lx, overlayRect.y, rx - lx, overlayRect.height, toLice(roleColor(kRoleLoopMarker)), 0.20f, 0); } } const std::int64_t markerFrames[3] = {m.start, m.loopStart, m.loopEnd}; const Role markerRoles[3] = {kRoleStartMarker, kRoleLoopMarker, kRoleLoopMarker}; for (int i = 0; i < 3; ++i) { const int mx = frameToX(overlay, frames, markerFrames[i]); const bool loopMarker = (i != 0); const float alpha = (loopMarker && !m.hasLoop) ? 0.4f : 1.0f; LICE_FillRect(bmp, mx - 1, overlayRect.y, 2, overlayRect.height, toLice(roleColor(markerRoles[i])), alpha, 0); } paintEnvelopeOverlay(bmp, overlay, frames); } void ReaSamplerEditor::paintEnvelopeOverlay(LICE_IBitmap* bmp, const OverlayArea& waveArea, std::int64_t frames) { const Rect& area = waveArea.rect; if (frames <= 0 || area.width <= 0 || area.height <= 0) return; const double rate = liveSampleRate(); if (rate <= 0.0) return; const double totalSeconds = static_cast(frames) / rate; const std::int64_t startFrame = params_.startPoint.value_or(0); const AmpEnvelope env = packEnvelope(params_.play, frames, startFrame); const std::vector poly = buildEnvelopePolyline(env, waveArea, totalSeconds); // Trace the polyline in the categorical secondary accent (teal) so it reads as a distinct // curve over the waveform. Clip x to the wave rect (a Gate release tail maps past the right). const LICE_pixel line = toLice(roleColor(Role::AccentSecondary)); for (std::size_t i = 1; i < poly.size(); ++i) { const int x0 = (std::max)(area.x, (std::min)(area.right() - 1, poly[i - 1].x)); const int x1 = (std::max)(area.x, (std::min)(area.right() - 1, poly[i].x)); LICE_Line(bmp, x0, poly[i - 1].y, x1, poly[i].y, line, 1.0f, 0, true); } // Draggable node handles: a small square per draggable node (Origin + ReleaseStart are // draw-only). Lit accent-hot when this node is the grabbed one. Every vertex is // guaranteed in-bounds (edge nodes like ReleaseEnd at area.right()-1 must get handles); // the handle square is additionally clamped inside the band so a 6px box on an edge // node never overhangs into the neighbouring bands. const LICE_pixel handle = toLice(roleColor(Role::AccentPrimary)); const LICE_pixel handleHot = toLice(roleColor(Role::AccentHot)); for (const EnvVertex& v : poly) { if (v.node == EnvNode::Origin || v.node == EnvNode::ReleaseStart) continue; const bool grabbed = (drag_ == DragKind::kEnvNode && envNode_ == v.node); const int r = 3; const int hx = (std::max)(area.x + r, (std::min)(area.right() - 1 - r, v.x)); const int hy = (std::max)(area.y + r, (std::min)(area.bottom() - 1 - r, v.y)); LICE_FillRect(bmp, hx - r, hy - r, 2 * r, 2 * r, grabbed ? handleHot : handle, 1.0f, 0); } } } // namespace reasampler::vst #endif // _WIN32