71a3b26a47
Docked SWELL window toggled by a new action, drawing the current bank as per-sample min/max thumbnails (PCM_source + peaks) with an in-memory cache. Pure grid-layout/cache-key math in bank_grid (tested). Renames peaks::Sample -> AudioSample to avoid colliding with bank_model::Sample.
70 lines
3.3 KiB
C++
70 lines
3.3 KiB
C++
#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 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<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<AudioSample>& interleaved,
|
|
std::size_t channelCount,
|
|
std::size_t frameCount,
|
|
std::size_t binCount);
|
|
|
|
} // namespace reasampler
|