#pragma once // peaks — waveform min/max envelope (thumbnail) computation from raw interleaved // PCM. We compute our own thumbnails from the captured file rather than depending // on REAPER's peak API: we own the file format, so this is simpler, testable, and // dependency-free. A future bank panel (M5) calls this at whatever bin resolution // the panel width dictates and draws one min/max envelope per channel. // // PURE MODULE (CLAUDE.md §load-bearing split): NO REAPER types, NO SWELL, NO // vendor/ includes. Standard library only. Builds and unit-tests without REAPER. #include #include namespace reasampler { // Canonical in-memory audio-sample type. `float` is REAPER's native audio buffer // format (its render/PCM_source callbacks hand back interleaved 32-bit float), so // peaks consumes that directly with no lossy conversion. If a capture ever lands // as a different depth, the caller converts to float at the boundary — the // thumbnail core stays single-typed. // // NAMED AudioSample, not `Sample`: `reasampler::Sample` is already bank_model's // metadata struct. A `using Sample = float` here would collide at namespace scope // wherever both headers are visible (the bank_panel module includes both). The // audio-domain name also reads more precisely — this is one PCM sample value. using AudioSample = float; // One bin of a channel's envelope: the extremes of every sample that fell in it. // min <= max always. For an empty bin (more bins than frames), both are 0. struct MinMax { AudioSample min = 0.0f; AudioSample max = 0.0f; bool operator==(const MinMax& o) const { return min == o.min && max == o.max; } }; // One channel's envelope: exactly `binCount` bins, in time order. using ChannelEnvelope = std::vector; // Per-channel envelopes: outer index is channel (channelCount entries, order // preserved — never mixed or folded), inner is that channel's bins. using Envelope = std::vector; // Computes a per-channel min/max envelope from interleaved PCM. // // interleaved frame-interleaved samples: [f0c0, f0c1, ..., f1c0, f1c1, ...]. // Size must be >= frameCount * channelCount; extra is ignored. // channelCount channels per frame (the stride). Each channel is enveloped // INDEPENDENTLY — no averaging, no stereo fold (precision // invariant: channel count preserved). // frameCount frames (samples-per-channel) to consider. // binCount requested bins per channel. Honored exactly for any frameCount. // // Frame->bin partition: frames are split into `binCount` contiguous spans as // evenly as possible; when frameCount does not divide evenly, the remainder is // spread one-frame-per-bin across the earliest bins (ceil/floor split), so the // tail is never dropped and no bin reads out of bounds. When binCount > frameCount // the trailing empty bins are {0, 0}. // // Defined behavior for degenerate input (no UB, no throw): // binCount == 0 -> per channel: an empty bin vector. // channelCount == 0 -> an empty envelope (no channels). // frameCount == 0 -> per channel: binCount bins, all {0, 0}. Envelope computeEnvelope(const std::vector& interleaved, std::size_t channelCount, std::size_t frameCount, std::size_t binCount); } // namespace reasampler