#include "core/audio/peaks.h" #include #include #include #include // 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& 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(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(col) * nbins) / columnCount; const std::int64_t end64 = (static_cast(col) + 1) * nbins / columnCount; // col <= columnCount-1 guarantees begin64 <= (columnCount-1)*nbins/columnCount < nbins. const int colBinBegin = static_cast(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(end64) : colBinBegin + 1; const int clampedEnd = (scanEnd <= nbins) ? scanEnd : nbins; MinMax result = bins[static_cast(colBinBegin)]; for (int b = colBinBegin + 1; b < clampedEnd; ++b) { const MinMax& mm = bins[static_cast(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& 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