82 [[nodiscard]]
bool isValid() const noexcept
84 return state_ !=
nullptr;
96 return state_ ? state_->report :
Report{};
100 return state_ ?
sizeof(State) + state_->exclusions.retainedBytes() : 0;
103 [[nodiscard]] std::span<const OfflineRegion>
exclusions() const noexcept
105 return state_ ? state_->exclusions.view() : std::span<const OfflineRegion>{};
116 double inputPeak = 0, featherFrames = 1;
119 std::unique_ptr<State> state_;
136 return state_ !=
nullptr;
152 return state_ ?
sizeof(State) : 0;
162 double inputPeak = 0, deltaPeak = 0;
165 std::unique_ptr<State> state_;
205 const auto spec = source.
getSpec();
206 validate(spec, options);
207 detail::OfflineSession job(jobOptions);
209 detail::OfflineBlock<T> block(job, spec, std::min(4096, jobOptions.
blockFrames));
212 result.plan = createPlan(source, spec, summary.fingerprint, summary.peak, options, job);
215 result.memoryBytes = job.bytes();
235 detail::OfflineSession job(jobOptions);
240 result.memoryBytes = job.bytes();
260 const auto &old = *previous.state_;
262 createPlan(source, old.spec, old.fingerprint, old.inputPeak, options, job);
265 result.memoryBytes = job.bytes();
279 return renderImpl(source, plan, sink,
nullptr,
nullptr, jobOptions);
297 const auto &state = *plan.state_;
298 if (source.
getSpec() != state.spec)
302 job.charge(
sizeof(
typename DeltaCache::State));
303 DeltaCache candidate;
304 candidate.state_ = std::make_unique<typename DeltaCache::State>();
305 auto &cache = *candidate.state_;
306 cache.sourceSpec = state.spec;
307 cache.deltaSpec = state.spec;
308 cache.deltaSpec.channels = 1;
309 cache.sourceFingerprint = state.fingerprint;
310 cache.generation = state.report.generation;
311 cache.inputPeak = state.inputPeak;
313 state.inputPeak, cache.generation,
315 cache.latency = stream.latency();
316 result.memoryBytes = job.bytes();
317 transaction.
sink = &sink;
319 stream.run([&](std::int64_t first, AudioBufferView<T>, std::span<const double> delta) {
320 for (
const double sample : delta)
322 if (!std::isfinite(sample))
325 cache.deltaPeak = std::max(cache.deltaPeak, std::abs(sample));
327 const double *channels[]{delta.data()};
329 return sink.
write(first, {channels, 1,
static_cast<int>(delta.size())});
333 if (source.
getSpec() != state.spec)
336 transaction.
sink =
nullptr;
338 result.cache = std::move(candidate);
356 return renderImpl(source, plan, sink, &delta, &cache, jobOptions);
360 template <
int MaxChannels>
369 static void validate(OfflineAudioSpec spec,
const Options &options)
372 if (spec.channels == 1 && !options.duplicateMono)
374 if (!std::isfinite(options.width) || (options.width < 0) || (options.width > 1) ||
375 StereoGenerator<double>::getPrepareMemoryBound(
376 {spec.sampleRate, 256, 2}, {options.oversamplingFactor, options.lowCutHz}) == 0)
380 static Plan createPlan(OfflineAudioSource<T> &source, OfflineAudioSpec spec,
381 OfflineFingerprint fingerprint,
double peak,
const Options &options,
382 detail::OfflineSession &job)
384 validate(spec, options);
385 if (source.getSpec() != spec)
388 job.charge(
sizeof(
typename Plan::State));
390 plan.state_ = std::make_unique<typename Plan::State>();
391 auto &state = *plan.state_;
393 state.outputSpec = spec;
394 state.outputSpec.channels = 2;
395 state.fingerprint = fingerprint;
396 state.inputPeak = peak;
397 state.featherFrames = std::max(1., std::round(.005 * spec.sampleRate));
398 state.exclusions.assign(job, options.exclusions, spec.frames);
399 auto &report = state.report;
400 report.width = options.width;
401 report.generation = {options.oversamplingFactor, options.lowCutHz};
402 report.inputSamplePeakDb =
gainToDecibels(peak, -std::numeric_limits<double>::infinity());
403 report.duplicatedMono = spec.channels == 1;
404 const bool excluded = state.exclusions.covers(spec.frames);
405 state.active = options.width > 0 && peak > 0 && !excluded;
411 if (source.getSpec() != spec)
416 static Result renderImpl(OfflineAudioSource<T> &source,
const Plan &plan,
417 OfflineAudioSink<T> &sink, OfflineAudioSource<double> *deltaSource,
418 const DeltaCache *certificate,
const OfflineJobOptions &options)
421 detail::OfflineSinkTransaction<T> transaction;
424 if (!plan.isValid() || (certificate && !certificate->isValid()))
426 const auto &state = *plan.state_;
427 const auto &spec = state.spec;
428 if (source.getSpec() != spec)
430 const auto *cache = certificate ? certificate->state_.get() :
nullptr;
432 (cache->sourceSpec != spec || cache->sourceFingerprint != state.fingerprint ||
433 cache->inputPeak != state.inputPeak ||
434 cache->generation != state.report.generation ||
435 deltaSource->
getSpec() != cache->deltaSpec))
437 result.report = state.report;
438 auto &report = result.report;
439 report.usedDeltaCache = cache !=
nullptr;
440 detail::OfflineSession job(options);
442 const int limit = std::min(256, options.blockFrames);
443 detail::OfflineBlock<T> output(job, state.outputSpec, limit);
444 std::optional<detail::OfflineStereoStream<T>> stream;
445 std::optional<detail::OfflineBlock<T>> cacheOriginal;
446 std::optional<detail::OfflineBlock<double>> cacheDelta;
449 cacheOriginal.emplace(job, spec, limit);
450 cacheDelta.emplace(job, cache->deltaSpec, limit);
451 report.compensatedLatencyFrames = cache->latency;
455 stream.emplace(source, spec, state.fingerprint, state.inputPeak, report.generation,
456 state.active, job, limit);
457 report.compensatedLatencyFrames = stream->latency();
459 result.memoryBytes = job.bytes();
460 TruePeakDetector<double, 2> detector;
461 const double scale = state.inputPeak > 0 ? state.inputPeak : 1;
462 double peak = 0, truePeak = 0;
463 transaction.sink = &sink;
465 const auto compose = [&](std::int64_t first, AudioBufferView<T> raw,
466 std::span<const double> delta) {
467 const int count = raw.getNumSamples();
468 auto out = output.view(count);
469 for (
int i = 0; i < count; ++i)
472 state.active ? 1 - state.exclusions.apply(first + i, 0, state.featherFrames)
474 const double change = mask == 0 ? 0 : delta[i] * report.width * mask;
475 for (
int c = 0; c < 2; ++c)
477 const T original = raw.getChannel(c % spec.channels)[i];
481 const double processed =
482 static_cast<double>(original) + (c == 0 ? change : -change);
483 if (!std::isfinite(processed) ||
484 std::abs(processed) > std::numeric_limits<T>::max())
486 value =
static_cast<T
>(processed);
488 out.getChannel(c)[i] = value;
489 const double normalized =
static_cast<double>(value) / scale;
490 peak = std::max(peak, std::abs(normalized));
491 truePeak = std::max(truePeak, detector.processSample(normalized, c));
497 stream->run(compose);
500 OfflineFingerprint originalFingerprint, deltaFingerprint;
502 for (std::int64_t first = 0; first < spec.frames;)
504 const int count =
static_cast<int>(
505 std::min<std::int64_t>(cacheOriginal->frames(), spec.frames - first));
506 auto original = cacheOriginal->view(count);
507 auto delta = cacheDelta->view(count);
510 for (
int i = 0; i < count; ++i)
512 for (
int c = 0; c < spec.channels; ++c)
514 const T sample = original.getChannel(c)[i];
515 if (!std::isfinite(sample))
517 if (std::abs(
static_cast<double>(sample)) > state.inputPeak)
521 const double sample = delta.getChannel(0)[i];
522 if (!std::isfinite(sample) || std::abs(sample) > cache->deltaPeak)
526 compose(first, original,
527 {delta.getChannel(0),
static_cast<std::size_t
>(count)});
531 if (originalFingerprint != state.fingerprint ||
532 deltaFingerprint != cache->deltaFingerprint || source.
getSpec() != spec ||
533 deltaSource->getSpec() != cache->deltaSpec)
536 for (
int i = 0; i < TruePeakDetector<double, 2>::getTaps() - 1; ++i)
537 for (
int c = 0; c < 2; ++c)
538 truePeak = std::max(truePeak, detector.processSample(0, c));
539 if (!std::isfinite(peak) || !std::isfinite(truePeak))
541 constexpr double silence = -std::numeric_limits<double>::infinity();
542 report.outputSamplePeakDb =
544 report.outputTruePeakDb =
547 report.peaksMeasured =
true;
551 if (source.getSpec() != spec || (cache && deltaSource->getSpec() != cache->deltaSpec))
554 transaction.sink =
nullptr;