177 using Options = std::conditional_t<Soft, OfflineSoftClipOptions, OfflineClipOptions>;
215 return state_ !=
nullptr;
223 return state_ ? state_->report :
Report{};
227 return state_ ?
sizeof(State) + state_->exclusions.retainedBytes() : 0;
230 [[nodiscard]] std::span<const OfflineRegion>
exclusions() const noexcept
232 return state_ ? state_->exclusions.view() : std::span<const OfflineRegion>{};
243 Curve curve = Curve::Sine;
244 double inputPeak = 0, targetGain = 1, featherFrames = 1;
247 std::unique_ptr<State> state_;
279 validateOptions(options);
282 OfflineSession job(jobOptions);
286 job.charge(
sizeof(
typename Plan::State));
288 plan.state_ = std::make_unique<typename Plan::State>();
289 auto &state = *plan.state_;
290 state.spec = analysis.
getSpec();
293 state.featherFrames = std::max(1., std::round(.005 * state.spec.sampleRate));
295 state.curve = options.curve;
296 state.exclusions.assign(job, options.exclusions, state.spec.frames);
297 const bool allExcluded = state.exclusions.covers(state.spec.frames);
298 auto &report = state.report;
299 report.requestedReductionDb = options.reductionDb;
300 report.oversamplingFactor = options.oversamplingFactor;
301 report.inputSamplePeakDb =
302 gainToDecibels(state.inputPeak, -std::numeric_limits<double>::infinity());
303 if (state.inputPeak > 0)
305 state.targetGain = offlineRepresentablePeakGain<T>(
308 report.representabilityLimited =
309 (state.targetGain == 1 && options.reductionDb > 0) ||
310 options.reductionDb - report.effectiveReductionDb > 1e-6;
312 state.active = options.reductionDb > 0 && state.inputPeak > 0 && state.targetGain < 1 &&
320 if (source.
getSpec() != state.spec)
323 result.memoryBytes = job.bytes();
324 result.plan = std::move(plan);
339 validateOptions(options);
341 if (!analyzed.succeeded())
343 result.
status = analyzed.status;
346 result =
makePlan(source, analyzed.analysis, options,
348 result.memoryBytes = std::max(analyzed.memoryBytes,
349 result.memoryBytes + analyzed.analysis.retainedBytes());
362 return renderCurve<ClipperCurve::Hard>(source, plan, sink, job);
365 const auto curve = plan.
isValid() ? plan.state_->curve : Curve::Sine;
369 return renderCurve<ClipperCurve::Sine>(source, plan, sink, job);
371 return renderCurve<ClipperCurve::Tanh>(source, plan, sink, job);
372 case Curve::GoldenRatio:
373 return renderCurve<ClipperCurve::GoldenRatio>(source, plan, sink, job);
381 template <
int MaxChannels>
386 return offlineRun(*
this, input, output, sampleRate, options, job);
390 static void validateOptions(
const Options &options)
392 const int factor = options.oversamplingFactor;
393 if (!std::isfinite(options.reductionDb) || (options.reductionDb < 0) ||
394 (options.reductionDb > 12) ||
395 (factor != 1 && factor != 2 && factor != 4 && factor != 8 && factor != 16))
398 if (options.curve != Curve::Sine && options.curve != Curve::Tanh &&
399 options.curve != Curve::GoldenRatio)
402 template <ClipperCurve C>
static double initialCeiling(
double target)
404 double low = 0, high = 1;
405 while (clipperShape<C>(1., high) < target && high < 1e12)
407 for (
int i = 0; i < 64; ++i)
409 const double middle = (low + high) / 2;
410 if (clipperShape<C>(1., middle) > target)
415 return (low + high) / 2;
418 template <ClipperCurve C>
419 static Result renderCurve(OfflineAudioSource<T> &source,
const Plan &plan,
420 OfflineAudioSink<T> &sink,
const OfflineJobOptions &options)
423 OfflineSinkTransaction<T> transaction;
428 const auto &state = *plan.state_;
429 const auto spec = state.spec;
430 if (source.getSpec() != spec)
432 result.report = state.report;
433 auto &report = result.report;
434 OfflineSession job(options);
436 const int blockLimit = std::min(4096, options.blockFrames);
437 OfflineBlock<T> input(job, spec, blockLimit), output(job, spec, blockLimit);
438 const double normalizer = state.inputPeak > 0 ? state.inputPeak : 1;
439 const double maximum =
static_cast<double>(std::numeric_limits<T>::max());
444 double operator()(std::int64_t)
const noexcept
449 using Cache = OfflineGainSource<T, UnityControl>;
454 void operator()(std::int64_t first,
int count,
double *values)
456 cache->readAudio(channel, first, count, values);
459 using Projection = offline_clip::BoundedClip<C, Reader>;
460 std::optional<Cache> cached;
461 std::optional<offline_clip::TailPolicy<C>> policy;
462 std::array<offline_clip::TailCertificate, 2> sourceStats;
463 bool sourceStatsReady =
false;
466 cached.emplace(source, spec, state.fingerprint, state.inputPeak, job, options,
468 const int order = report.oversamplingFactor <= 2 ? 18 : 10;
471 static_cast<std::size_t
>(6 * (order + 1) + (order + 3) * (2 * order + 1)) *
473 policy.emplace(report.oversamplingFactor);
477 report.compensatedLatencyFrames = 0;
478 result.memoryBytes = job.bytes();
480 struct ProgressContext
482 const OfflineJobOptions *options;
484 } progress{&options, spec.frames};
485 if (options.progress)
487 passOptions.progressContext = &progress;
488 passOptions.progress = [](
void *context, OfflineProgress)
490 const auto &p = *
static_cast<ProgressContext *
>(context);
492 return p.options->progress(p.options->progressContext,
499 OfflineSession passJob(passOptions);
500 OfflineMemoryPool scratch(passJob,
false);
501 passJob.useScratchResource(scratch);
502 std::array<std::optional<Projection>, 2> projections;
503 const auto pass = [&](
double ceiling,
bool publish)
507 if (cached && ceiling > 0)
510 std::array<offline_clip::TailCertificate, 2> shapeStats;
511 for (std::size_t leaf = 0; leaf < cached->leaves(); ++leaf)
513 const auto first =
static_cast<std::int64_t
>(leaf) * cached->block();
514 const int count =
static_cast<int>(
515 std::min<std::int64_t>(cached->block(), spec.frames - first));
516 for (
int c = 0; c < spec.channels; ++c)
518 const auto *raw = cached->audio(leaf, c);
519 for (
int i = 0; i < count; ++i)
521 const double x =
static_cast<double>(raw[i]) / normalizer;
522 if (!sourceStatsReady)
523 sourceStats[c].append(x);
524 shapeStats[c].append(clipperShape<C>(x, ceiling));
529 sourceStatsReady =
true;
530 std::array<offline_clip::TailPlan, 2> tails;
531 std::int64_t padding = 64;
532 for (
int c = 0; c < spec.channels; ++c)
534 tails[c] = policy->choose(ceiling, sourceStats[c], shapeStats[c]);
535 padding = std::max(padding, tails[c].padding);
540 long double best = std::numeric_limits<long double>::infinity();
541 const long double extent =
542 static_cast<long double>(spec.frames) + 2.L * padding;
543 for (
int candidate = 128;
544 candidate <= 16384 && candidate * report.oversamplingFactor <= 65536;
547 const long double cost =
548 (1000.L * report.oversamplingFactor - 32) * candidate +
549 3456.L * extent / candidate;
556 for (
int c = 0; c < spec.channels; ++c)
559 if (projections[c] && projections[c]->hasGeometry(padding, grid))
560 projections[c]->reset(ceiling, tails[c].exterior);
562 projections[c].emplace(passJob, Reader{&*cached, c}, spec.frames,
563 padding, report.oversamplingFactor, ceiling,
564 tails[c].exterior, grid);
565 projections[c]->prepareProjection();
568 result.memoryBytes = std::max(result.memoryBytes, job.bytes() + passJob.bytes());
572 transaction.sink = &sink;
575 job.checkpoint(phase, 0, spec.frames);
578 OfflineClipMeasurement measured;
579 OfflineFingerprint fingerprint;
580 TruePeakDetector<double, 2> detector;
581 std::int64_t written = 0;
582 const auto consume = [&](std::int64_t first,
int count,
583 const std::array<const double *, 2> &filtered)
585 if (first != written)
587 for (
int offset = 0; offset < count;)
589 const auto at = first + offset;
590 int length = std::min(input.frames(), count - offset);
591 std::array<const T *, 2> raw{};
594 const auto leaf =
static_cast<std::size_t
>(at / cached->block());
595 const int local =
static_cast<int>(at % cached->block());
596 length = std::min(length, cached->block() - local);
597 for (
int c = 0; c < spec.channels; ++c)
598 raw[c] = cached->audio(leaf, c) + local;
602 auto block = input.view(length);
604 for (
int c = 0; c < spec.channels; ++c)
605 raw[c] = block.getChannel(c);
607 auto out = output.view(length);
608 for (
int f = 0; f < length; ++f)
612 ? 1 - state.exclusions.apply(at + f, 0, state.featherFrames)
614 for (
int c = 0; c < spec.channels; ++c)
616 const T rawValue = raw[c][f];
617 if (!std::isfinite(rawValue))
619 if (std::abs(
static_cast<double>(rawValue)) > state.inputPeak)
622 double normalized =
static_cast<double>(rawValue) / normalizer;
626 const double shaped =
627 ceiling == 0 ? 0 : filtered[c][offset + f];
628 normalized = std::lerp(normalized, shaped, mask);
629 if (!std::isfinite(normalized))
631 if (std::abs(normalized) > maximum / normalizer)
633 measured.representable =
false;
639 value =
static_cast<T
>(normalized * normalizer);
640 if (!std::isfinite(value))
642 normalized =
static_cast<double>(value) / normalizer;
645 measured.peak = std::max(measured.peak, std::abs(normalized));
648 out.getChannel(c)[f] = value;
649 measured.truePeak = std::max(
650 measured.truePeak, detector.processSample(normalized, c));
658 std::array<const T *, 2> pointers{};
659 for (
int c = 0; c < spec.channels; ++c)
660 pointers[c] = out.getChannel(c);
661 return sink.write(at, {pointers.data(), spec.channels, length});
665 job.checkpoint(phase, written, spec.frames);
668 if (cached && ceiling > 0)
670 for (
auto leaf = projections[0]->firstLeaf(); leaf < projections[0]->endLeaf();
673 std::array<const double *, 2> filtered{};
675 for (
int c = 0; c < spec.channels; ++c)
677 const auto data = projections[c]->get(leaf);
678 filtered[c] = data.data();
679 if (c && count !=
static_cast<int>(data.size()))
681 count =
static_cast<int>(data.size());
683 consume(projections[0]->leafOrigin(leaf), count, filtered);
687 while (written < spec.frames)
690 std::min<std::int64_t>(input.frames(), spec.frames - written)),
692 if (written != spec.frames || fingerprint != state.fingerprint ||
693 source.getSpec() != spec)
696 for (
int c = 0; c < spec.channels; ++c)
697 measured.truePeak = std::max(measured.truePeak, detector.getTailPeak(c));
700 OfflineClipCalibration calibrated;
705 [&](
double ceiling) { return pass(ceiling, false); });
706 report.calibrationPasses = calibrated.attempts;
707 report.normalizedCeiling = calibrated.ceiling;
708 report.targetErrorDb = calibrated.errorDb;
709 if (!calibrated.converged)
711 const bool constrained =
712 !calibrated.bracketed && !state.exclusions.view().empty();
719 const auto final = pass(calibrated.ceiling,
true);
720 if (state.active && (!
final.representable ||
final.peak != calibrated.peak))
722 constexpr double silence = -std::numeric_limits<double>::infinity();
723 report.outputSamplePeakDb =
final.peak > 0 ?
gainToDecibels(
final.peak, silence) +
726 report.outputTruePeakDb =
final.truePeak > 0 ?
gainToDecibels(
final.truePeak, silence) +
729 report.achievedReductionDb =
730 state.inputPeak > 0 &&
final.peak > 0 ? -
gainToDecibels(
final.peak) : 0;
732 report.targetMet = state.inputPeak > 0 &&
734 report.targetErrorDb);
735 if (state.active && !report.targetMet)
741 if (source.getSpec() != spec)
744 transaction.sink =
nullptr;