98 [[nodiscard]]
bool isValid() const noexcept
100 return state_ !=
nullptr;
108 return state_ ? state_->report :
Report{};
112 return state_ ?
sizeof(State) + 2 * state_->count *
sizeof(
double) +
113 state_->exclusions.retainedBytes()
116 [[nodiscard]] std::span<const OfflineRegion>
exclusions() const noexcept
118 return state_ ? state_->exclusions.view() : std::span<const OfflineRegion>{};
122 [[nodiscard]]
double gainAt(std::int64_t frame)
const noexcept
124 return state_ ? control(*state_, state_->guard.get(), frame) : 1;
136 std::unique_ptr<double[]> level, guard;
137 std::size_t count = 0, window = 1;
138 std::int64_t hop = 1;
139 double peak = 0, sidePeak = 0, gate = 0, feather = 1;
140 bool changed =
false;
142 std::unique_ptr<State> state_;
167 [[nodiscard]] PlanResult
177 detail::OfflineSession job(jobOptions);
178 job.charge(
sizeof(State));
180 plan.state_ = std::make_unique<State>();
181 auto &s = *plan.state_;
189 s.options.exclusions = {};
190 s.exclusions.assign(job, options.exclusions, s.spec.frames);
191 s.feather = std::max(1., s.spec.sampleRate * .005);
192 const auto bins = analysis.
bins();
193 const auto n = bins.size();
196 s.level = job.allocate<
double>(s.count);
197 s.guard = job.allocate<
double>(s.count);
198 const double perSecond = s.spec.sampleRate /
static_cast<double>(s.hop);
200 std::max<std::size_t>(1,
static_cast<std::size_t
>(std::round(.04 * perSecond)));
201 s.report.analysisHopFrames = s.hop;
202 s.report.guardWindowFrames = s.hop *
static_cast<std::int64_t
>(s.window);
203 s.report.inputSideShare = share(analysis.
midRms(), analysis.
sideRms());
204 s.report.targetSideShare = options.targetSideShare.value_or(
205 std::isfinite(s.report.inputSideShare)
206 ? std::clamp(s.report.inputSideShare, .001, .49)
207 : std::numeric_limits<double>::quiet_NaN());
208 const double ratio = std::isfinite(s.report.targetSideShare)
209 ? s.report.targetSideShare / (1 - s.report.targetSideShare)
211 s.report.guardCeilingRatio = ratio * std::pow(10., options.guardMarginDb / 10);
212 const double scale = s.peak > 0 ? s.peak : 1;
213 const double mr = analysis.
midRms() / scale, sr = analysis.
sideRms() / scale;
214 const double floor = std::min(1., 1e-7 / scale);
215 s.gate = std::max(floor * floor, (mr * mr + sr * sr) * 1e-6);
216 auto mid = job.allocate<
double>(n), side = job.allocate<
double>(n);
217 for (std::size_t i = 0; i < n; ++i)
221 static_cast<std::int64_t
>(n));
222 const double m = bins[i].midRms / scale, v = bins[i].sideRms / scale;
226 const auto mean = [&](
const double *power, std::size_t begin, std::size_t end)
228 double energy = 0, frames = 0;
229 for (
auto i = begin; i < end; ++i)
231 energy += power[i] *
static_cast<double>(bins[i].frames);
232 frames +=
static_cast<double>(bins[i].frames);
234 return energy / frames;
237 std::max<std::size_t>(1,
static_cast<std::size_t
>(std::round(.2 * perSecond)));
238 for (std::size_t i = 0; i < n; ++i)
242 static_cast<std::int64_t
>(n));
243 const auto begin = i > radius ? i - radius : 0;
244 const auto end = std::min(n, i + radius + 1);
245 const double m = mean(mid.get(), begin, end), v = mean(side.get(), begin, end);
246 if (options.levelingAmount > 0 && m > s.gate && v > s.gate)
248 const double wanted = .5 * (std::log(ratio) + std::log(m) - std::log(v));
249 const double bounded = std::clamp(wanted, -options.maximumCutDb * logDb,
250 options.maximumBoostDb * logDb);
251 s.report.limitedPoints += wanted != bounded;
252 s.level[i] = options.levelingAmount * bounded;
255 auto scratch = job.allocate<
double>(s.count);
256 const auto smoothRadius =
257 std::max<std::size_t>(1,
static_cast<std::size_t
>(std::round(.5 * perSecond)));
258 average(s.level.get(), scratch.get(), n, smoothRadius, job);
259 average(scratch.get(), s.level.get(), n, smoothRadius, job);
260 s.level[n] = s.level[n - 1];
263 for (std::size_t i = 0; i < n; ++i)
267 static_cast<std::int64_t
>(n));
268 const auto end = std::min(n, i + s.window);
269 double m = 0, v = 0, frames = 0,
270 maximum = -std::numeric_limits<double>::infinity();
271 for (
auto j = i; j < end; ++j)
273 const double count =
static_cast<double>(bins[j].frames);
275 v += side[j] * count;
277 maximum = std::max({maximum, s.level[j], s.level[j + 1]});
279 if (v > s.gate * frames)
281 const double safe = m > 0 ? .5 * (std::log(s.report.guardCeilingRatio) +
282 std::log(m) - std::log(v)) -
284 : -std::numeric_limits<double>::infinity();
285 scratch[i] = std::clamp(safe, -options.maximumGuardCutDb * logDb, 0.);
290 for (std::size_t i = 0; i <= n; ++i)
291 for (
auto j = i > s.window ? i - s.window : 0; j <= std::min(i, n - 1); ++j)
292 s.guard[i] = std::min(s.guard[i], scratch[j]);
293 limitSlopes(s, s.guard.get());
300 if ((!options.guard && options.levelingAmount == 0) ||
301 s.exclusions.covers(s.spec.frames))
303 std::fill_n(s.level.get(), s.count, 0.);
304 std::fill_n(s.guard.get(), s.count, 0.);
310 s.report.minimumGainDb = s.report.maximumGainDb =
311 s.exclusions.view().empty() ? (s.level[0] + s.guard[0]) / logDb : 0;
312 for (std::size_t i = 0; i < s.count; ++i)
314 const double db = (s.level[i] + s.guard[i]) / logDb;
315 s.report.minimumGainDb = std::min(s.report.minimumGainDb, db);
316 s.report.maximumGainDb = std::max(s.report.maximumGainDb, db);
317 s.changed = s.changed || db != 0;
320 static_cast<std::int64_t
>(n));
322 result.memoryBytes = job.bytes();
323 result.plan = std::move(plan);
341 auto energy = OfflineEnergyAnalyzer<T>().analyzeMidSide(source, job);
342 if (!energy.succeeded())
344 result.status = energy.status;
347 result =
makePlan(energy.analysis, options,
350 std::max(energy.memoryBytes, result.memoryBytes + energy.analysis.retainedBytes());
370 const auto &s = *plan.state_;
371 result.report = s.report;
372 if (source.
getSpec() != s.spec)
375 auto guards = job.allocate<
double>(s.count);
376 auto corrections = job.allocate<
double>(s.count);
377 auto measured = job.allocate<Energy>(s.count - 1);
378 std::copy_n(s.guard.get(), s.count, guards.get());
379 SideSource side(source, s, job, options.blockFrames);
380 StereoSink output(source, s, job, options.blockFrames, measured.get());
381 bool publish = !s.options.guard;
382 for (
int pass = 0; pass < 17; ++pass)
384 output.destination = publish ? &sink :
nullptr;
385 output.guardRequired = publish && s.options.guard;
387 auto rendered = detail::offlineRenderGain<Result>(
388 side, side.getSpec(), s.sideFingerprint, s.sidePeak,
true, s.changed,
390 { return [&](std::int64_t frame) { return control(s, guards.get(), frame); }; },
391 {&s.exclusions, s.feather,
false});
393 std::max(result.memoryBytes, job.bytes() + rendered.memoryBytes);
394 result.report.renderInfo = rendered.report.renderInfo;
395 result.report.minimumGainDb =
397 result.report.maximumGainDb =
399 output.copyReport(result.report);
400 if (!rendered.succeeded())
409 result.status = rendered.status;
412 ++result.report.guardCalibrationPasses;
413 if (result.report.guardTargetMet)
418 if (pass == 15 || !refine(s, measured.get(), guards.get(), corrections.get()))
434 template <
int MaxChannels>
443 using State =
typename Plan::State;
444 static constexpr double logDb = .1151292546497022842;
447 double mid = 0, side = 0;
448 std::int64_t frames = 0;
450 static double share(
double midRms,
double sideRms)
noexcept
452 const double scale = std::max(midRms, sideRms);
454 return std::numeric_limits<double>::quiet_NaN();
455 const double m = midRms / scale, s = sideRms / scale;
456 return s * s / (m * m + s * s);
458 static void validate(
const Options &options)
460 const auto range = [](
double x,
double low,
double high)
461 {
return std::isfinite(x) && x >= low && x <= high; };
462 if (!range(options.levelingAmount, 0, 1) || !range(options.guardMarginDb, 0, 12) ||
463 !range(options.maximumBoostDb, 0, 24) || !range(options.maximumCutDb, 0, 60) ||
464 !range(options.maximumGuardCutDb, 0, 120) ||
465 (options.targetSideShare && !range(*options.targetSideShare, .001, .49)))
468 static void average(
const double *input,
double *output, std::size_t count, std::size_t radius,
469 const detail::OfflineSession &job)
471 std::size_t begin = 0, end = 0;
473 for (std::size_t i = 0; i < count; ++i)
475 const auto first = i > radius ? i - radius : 0, last = std::min(count, i + radius + 1);
478 while (begin < first)
483 static_cast<std::int64_t
>(count));
485 for (
auto j = begin; j < end; ++j)
488 output[i] = sum /
static_cast<double>(end - begin);
491 static void limitSlopes(
const State &s,
double *guard)
noexcept
493 const double step =
static_cast<double>(s.hop) / s.spec.sampleRate;
494 for (std::size_t i = 1; i < s.count; ++i)
495 guard[i] = std::min(guard[i], guard[i - 1] + 20 * logDb * step);
496 for (
auto i = s.count - 1; i > 0; --i)
497 guard[i - 1] = std::min(guard[i - 1], guard[i] + 60 * logDb * step);
499 static double control(
const State &s,
const double *guard, std::int64_t frame)
noexcept
501 if (frame < 0 || frame >= s.spec.frames)
503 const auto i =
static_cast<std::size_t
>(frame / s.hop);
504 const double t =
static_cast<double>(frame % s.hop) /
static_cast<double>(s.hop);
505 const double logGain = std::lerp(s.level[i] + guard[i], s.level[i + 1] + guard[i + 1], t);
506 return s.exclusions.apply(frame, std::exp(logGain), s.feather);
508 static Energy window(
const State &s,
const Energy *bins, std::size_t first)
noexcept
511 for (
auto j = first; j < std::min(s.count - 1, first + s.window); ++j)
513 result.mid += bins[j].mid;
514 result.side += bins[j].side;
515 result.frames += bins[j].frames;
519 static double excess(
const State &s,
const Energy &e)
noexcept
521 if (e.side <= s.gate *
static_cast<double>(e.frames))
522 return -std::numeric_limits<double>::infinity();
524 ? 10 / std::log(10.) *
525 (std::log(e.side) - std::log(e.mid) - std::log(s.report.guardCeilingRatio))
526 : std::numeric_limits<double>::infinity();
528 static bool refine(
const State &s,
const Energy *bins,
double *guards,
529 double *corrections)
noexcept
531 std::fill_n(corrections, s.count, 0.);
532 for (std::size_t i = 0; i + 1 < s.count; ++i)
534 const double over = excess(s, window(s, bins, i));
537 const double correction =
538 std::isfinite(over) ? (over + .01) * logDb : s.options.maximumGuardCutDb * logDb;
539 for (
auto j = i; j <= std::min(s.count - 1, i + s.window); ++j)
540 corrections[j] = std::max(corrections[j], correction);
542 bool changed =
false;
543 for (std::size_t j = 0; j < s.count; ++j)
546 std::max(-s.options.maximumGuardCutDb * logDb, guards[j] - corrections[j]);
547 changed = changed || next < guards[j];
550 limitSlopes(s, guards);
554 class SideSource final :
public OfflineAudioSource<double>
558 SideSource(OfflineAudioSource<T> &source,
const State &state, detail::OfflineSession &job,
560 : source_(source), s_(state), scratch_(job, state.spec, std::min(4096, block))
563 OfflineAudioSpec getSpec() const noexcept
override
565 auto spec = source_.getSpec();
569 bool read(std::int64_t first, AudioBufferView<double> output)
override
573 for (
int offset = 0; offset < output.getNumSamples();)
575 const int count = std::min(scratch_.frames(), output.getNumSamples() - offset);
576 auto input = scratch_.view(count);
577 detail::offlineRead(source_, s_.spec, first + offset, input);
578 for (
int i = 0; i < count; ++i)
580 const double l = input.getChannel(0)[i], r = input.getChannel(1)[i];
581 if (!std::isfinite(l) || !std::isfinite(r))
582 detail::offlineFail(OfflineStatus::NonFiniteInput);
583 output.getChannel(0)[offset + i] = std::midpoint(l, -r);
591 failure = detail::offlineExceptionStatus();
597 OfflineAudioSource<T> &source_;
599 detail::OfflineBlock<T> scratch_;
602 class StereoSink final :
public OfflineAudioSink<double>
605 OfflineAudioSink<T> *destination =
nullptr;
607 bool guardRequired =
false;
608 StereoSink(OfflineAudioSource<T> &source,
const State &state, detail::OfflineSession &job,
609 int block, Energy *bins)
610 : source_(source), s_(state), scratch_(job, state.spec, std::min(4096, block)),
614 bool begin(
const OfflineAudioSpec &)
override
623 peak_ = truePeak_ = 0;
624 failure = OfflineStatus::Success;
625 return destination ? destination->begin(s_.spec) :
true;
627 bool write(std::int64_t first, AudioBufferView<const double> side)
override
632 detail::offlineFail(OfflineStatus::InvalidInput);
633 for (
int offset = 0; offset < side.getNumSamples();)
635 const int count = std::min(scratch_.frames(), side.getNumSamples() - offset);
636 auto output = scratch_.view(count);
637 detail::offlineRead(source_, s_.spec, first + offset, output);
638 const double scale = s_.peak > 0 ? s_.peak : 1;
639 for (
int i = 0; i < count; ++i)
641 const double l = output.getChannel(0)[i], r = output.getChannel(1)[i];
642 if (!std::isfinite(l) || !std::isfinite(r))
643 detail::offlineFail(OfflineStatus::NonFiniteInput);
644 detail::offlineHash(fingerprint_, output.getChannel(0)[i]);
645 detail::offlineHash(fingerprint_, output.getChannel(1)[i]);
646 const double delta = side.getChannel(0)[offset + i] - std::midpoint(l, -r);
649 const double a = l + delta, b = r - delta;
650 const double maximum =
651 static_cast<double>(std::numeric_limits<T>::max());
652 if (!std::isfinite(a) || !std::isfinite(b) || std::abs(a) > maximum ||
653 std::abs(b) > maximum)
654 detail::offlineFail(OfflineStatus::NumericalFailure);
655 output.getChannel(0)[i] =
static_cast<T
>(a);
656 output.getChannel(1)[i] =
static_cast<T
>(b);
658 const double a = output.getChannel(0)[i], b = output.getChannel(1)[i];
659 const double m = std::midpoint(a, b) / scale,
660 v = std::midpoint(a, -b) / scale;
665 peak_ = std::max({peak_, std::abs(a), std::abs(b)});
666 truePeak_ = std::max({truePeak_, detector_.processSample(a / scale, 0),
667 detector_.processSample(b / scale, 1)});
670 if (inBin_ == s_.hop || next_ == s_.spec.frames)
672 const double frames =
static_cast<double>(inBin_);
673 const double mr = mid_.rms(frames), sr = side_.rms(frames);
674 bins_[
static_cast<std::size_t
>((next_ - 1) / s_.hop)] = {
675 mr * mr * frames, sr * sr * frames, inBin_};
683 std::array<const T *, 2> channels{output.getChannel(0),
684 output.getChannel(1)};
685 detail::offlineCallSink(
688 return destination->write(first + offset,
689 {channels.data(), 2, count});
698 failure = detail::offlineExceptionStatus();
702 bool commit()
override
704 if (next_ != s_.spec.frames || fingerprint_ != s_.fingerprint ||
705 source_.getSpec() != s_.spec)
707 failure = OfflineStatus::SourceMismatch;
711 for (std::size_t i = 0; i + 1 < s_.count; ++i)
712 if (excess(s_, window(s_, bins_, i)) > 1e-9)
714 failure = OfflineStatus::TargetUnreachable;
717 return destination ? destination->commit() :
true;
719 void abort() noexcept
override
722 destination->abort();
724 void copyReport(Report &report)
726 report.peaksMeasured = next_ == s_.spec.frames;
727 if (!report.peaksMeasured)
729 const double silence = -std::numeric_limits<double>::infinity();
730 for (
int c = 0; c < 2; ++c)
731 truePeak_ = std::max(truePeak_, detector_.getTailPeak(c));
733 report.outputTruePeakDb = truePeak_ > 0
737 report.outputSideShare = share(totalMid_.rms(
static_cast<double>(next_)),
738 totalSide_.rms(
static_cast<double>(next_)));
739 report.guardMeasured = s_.options.guard;
740 report.guardWindows = report.violatedWindows = 0;
741 report.maximumWindowExcessDb = silence;
742 if (s_.options.guard)
743 for (std::size_t i = 0; i + 1 < s_.count; ++i)
745 const auto e = window(s_, bins_, i);
746 if (e.side <= s_.gate *
static_cast<double>(e.frames))
748 ++report.guardWindows;
749 const double db = excess(s_, e);
750 report.maximumWindowExcessDb = std::max(report.maximumWindowExcessDb, db);
751 report.violatedWindows += db > 1e-9;
753 report.guardTargetMet = s_.options.guard && report.violatedWindows == 0;
757 OfflineAudioSource<T> &source_;
759 detail::OfflineBlock<T> scratch_;
761 OfflineFingerprint fingerprint_;
762 std::int64_t next_ = 0, inBin_ = 0;
763 detail::ScaledSumSquares mid_, side_, totalMid_, totalSide_;
764 TruePeakDetector<double, 2> detector_;
765 double peak_ = 0, truePeak_ = 0;