peaks: pure per-channel min/max envelope from interleaved PCM
New STATIC lib + peaks_tests (CTest), mirroring bank_model. Float samples, per-channel (no fold); exact integer bin spans handle remainder, short buffers, and degenerate inputs with no OOB. Bin-span math guarded against size_t overflow for pathological binCount.
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
#include "peaks.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
|
||||
// peaks implementation.
|
||||
//
|
||||
// One linear pass per channel. The frame->bin partition is computed with integer
|
||||
// arithmetic so it is exact for any frameCount / binCount pairing: bin b owns the
|
||||
// half-open frame span [b*frameCount/binCount, (b+1)*frameCount/binCount). That
|
||||
// span formula distributes the remainder deterministically (earlier bins get the
|
||||
// extra frames) with no rounding drift and no dropped tail — the last bin's end is
|
||||
// always exactly frameCount.
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
Envelope computeEnvelope(const std::vector<Sample>& interleaved,
|
||||
std::size_t channelCount,
|
||||
std::size_t frameCount,
|
||||
std::size_t binCount) {
|
||||
Envelope envelope(channelCount);
|
||||
if (channelCount == 0) {
|
||||
return envelope; // no channels -> no envelopes
|
||||
}
|
||||
|
||||
// Never read past what the buffer actually holds, even if the caller's
|
||||
// frameCount overstates the buffer (defensive: no OOB on a short buffer).
|
||||
const std::size_t availableFrames = interleaved.size() / channelCount;
|
||||
const std::size_t frames = std::min(frameCount, availableFrames);
|
||||
|
||||
for (std::size_t ch = 0; ch < channelCount; ++ch) {
|
||||
ChannelEnvelope& bins = envelope[ch];
|
||||
bins.assign(binCount, MinMax{}); // empty/degenerate bins default to {0,0}
|
||||
|
||||
for (std::size_t b = 0; b < binCount; ++b) {
|
||||
// Half-open frame span for this bin: [b*frames/binCount, (b+1)*frames/binCount).
|
||||
// Guard against size_t overflow in b*frames and (b+1)*frames: binCount is
|
||||
// caller-controlled and unbounded, so when b >= SIZE_MAX/frames either
|
||||
// multiplication could wrap. Any such bin is unreachable in practice
|
||||
// (allocating that many MinMax entries would OOM first), but we guard
|
||||
// explicitly to eliminate UB.
|
||||
if (frames > 0 && b >= SIZE_MAX / frames) {
|
||||
continue; // b*frames or (b+1)*frames would overflow; span is empty
|
||||
}
|
||||
const std::size_t begin = (b * frames) / binCount;
|
||||
const std::size_t end = ((b + 1) * frames) / binCount;
|
||||
if (begin >= end) {
|
||||
continue; // empty span (binCount > frames) -> keep {0,0}
|
||||
}
|
||||
|
||||
const Sample first = interleaved[begin * channelCount + ch];
|
||||
Sample lo = first;
|
||||
Sample hi = first;
|
||||
for (std::size_t f = begin + 1; f < end; ++f) {
|
||||
const Sample s = interleaved[f * channelCount + ch];
|
||||
lo = std::min(lo, s);
|
||||
hi = std::max(hi, s);
|
||||
}
|
||||
bins[b] = MinMax{lo, hi};
|
||||
}
|
||||
}
|
||||
|
||||
return envelope;
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
#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 <cstddef>
|
||||
#include <vector>
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
// Canonical in-memory 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.
|
||||
using Sample = 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 {
|
||||
Sample min = 0.0f;
|
||||
Sample 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<MinMax>;
|
||||
|
||||
// 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<ChannelEnvelope>;
|
||||
|
||||
// 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<Sample>& interleaved,
|
||||
std::size_t channelCount,
|
||||
std::size_t frameCount,
|
||||
std::size_t binCount);
|
||||
|
||||
} // namespace reasampler
|
||||
Reference in New Issue
Block a user