Q-W1 pt2: core/shell/app relocation + sub-namespaces; one concrete ui::Rect (LTRB fork retired); slot_map split from bank_book; BankIndex→BankModel; 59/59 green
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
#include "core/audio/peaks.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
// 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::audio {
|
||||
|
||||
Envelope computeEnvelope(const std::vector<AudioSample>& 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 AudioSample first = interleaved[begin * channelCount + ch];
|
||||
AudioSample lo = first;
|
||||
AudioSample hi = first;
|
||||
for (std::size_t f = begin + 1; f < end; ++f) {
|
||||
const AudioSample s = interleaved[f * channelCount + ch];
|
||||
lo = std::min(lo, s);
|
||||
hi = std::max(hi, s);
|
||||
}
|
||||
bins[b] = MinMax{lo, hi};
|
||||
}
|
||||
}
|
||||
|
||||
return envelope;
|
||||
}
|
||||
|
||||
MinMax columnMinMax(const ChannelEnvelope& bins, int columnCount, int col) {
|
||||
const int nbins = static_cast<int>(bins.size());
|
||||
if (columnCount <= 0 || nbins == 0) return MinMax{};
|
||||
|
||||
// Clamp col to [0, columnCount-1].
|
||||
if (col < 0) col = 0;
|
||||
if (col >= columnCount) col = columnCount - 1;
|
||||
|
||||
// Half-open bin range for this column, mirroring computeEnvelope's exact partition.
|
||||
// 64-bit products: col*nbins can exceed int range for a large oversampled envelope
|
||||
// (same overflow discipline as computeEnvelope's frame-span arithmetic above).
|
||||
const std::int64_t begin64 = (static_cast<std::int64_t>(col) * nbins) / columnCount;
|
||||
const std::int64_t end64 =
|
||||
(static_cast<std::int64_t>(col) + 1) * nbins / columnCount;
|
||||
// col <= columnCount-1 guarantees begin64 <= (columnCount-1)*nbins/columnCount < nbins.
|
||||
const int colBinBegin = static_cast<int>(begin64);
|
||||
|
||||
// When the column spans no full bin (more columns than bins), use the enclosing bin
|
||||
// so no column is left empty.
|
||||
const int scanEnd = (end64 > begin64) ? static_cast<int>(end64) : colBinBegin + 1;
|
||||
const int clampedEnd = (scanEnd <= nbins) ? scanEnd : nbins;
|
||||
|
||||
MinMax result = bins[static_cast<std::size_t>(colBinBegin)];
|
||||
for (int b = colBinBegin + 1; b < clampedEnd; ++b) {
|
||||
const MinMax& mm = bins[static_cast<std::size_t>(b)];
|
||||
if (mm.min < result.min) result.min = mm.min;
|
||||
if (mm.max > result.max) result.max = mm.max;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::size_t lastFrameAboveThreshold(const std::vector<AudioSample>& interleaved,
|
||||
std::size_t channelCount,
|
||||
std::size_t frameCount,
|
||||
AudioSample linearThreshold) {
|
||||
if (channelCount == 0) return kNoFrameAboveThreshold;
|
||||
|
||||
// Clamp to what the buffer actually holds — a caller frameCount that overstates
|
||||
// the buffer must never read past the end (mirror of computeEnvelope's guard).
|
||||
const std::size_t availableFrames = interleaved.size() / channelCount;
|
||||
const std::size_t frames = std::min(frameCount, availableFrames);
|
||||
if (frames == 0) return kNoFrameAboveThreshold;
|
||||
|
||||
// Scan backward: the first frame (from the end) whose loudest channel exceeds the
|
||||
// threshold is the last audible frame. `f` runs frames..1 so `f-1` never wraps.
|
||||
for (std::size_t f = frames; f > 0; --f) {
|
||||
const std::size_t frame = f - 1;
|
||||
const std::size_t base = frame * channelCount;
|
||||
AudioSample peak = 0.0f;
|
||||
for (std::size_t c = 0; c < channelCount; ++c) {
|
||||
const AudioSample a = std::fabs(interleaved[base + c]);
|
||||
peak = std::max(peak, a);
|
||||
}
|
||||
if (peak > linearThreshold) return frame;
|
||||
}
|
||||
return kNoFrameAboveThreshold;
|
||||
}
|
||||
|
||||
} // namespace reasampler::audio
|
||||
Reference in New Issue
Block a user