DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
OfflineAttenuation.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
10#include "../OfflineProcessing.h"
11#if DSPARK_HAS_OFFLINE
12#include "OfflineBoxSmoother.h"
13#include <algorithm>
14#include <cmath>
15#include <limits>
16#include <memory>
17
18namespace dspark::detail
19{
20
21// Extend each ordered required hold by L frames, then take the minimum gain
22// and exponential release. At source frame p, four box averages read that raw
23// envelope over [p,p+L]. Every term is <= required[p], so the average retains
24// the peak bound. The four-box cascade smooths both attacks and changes of winner.
25// Amplitude deficits make release prefixes independent of absolute gain.
26// Optional local releases change the common decay clock at each extended hold
27// end. All surviving tails share that clock; these are not independent tails
28// with different slopes, for which a single maximum prefix would be invalid.
30{
31 public:
32 struct Hold
33 {
34 std::int64_t begin = 0, end = 0;
35 double deficit = 0, releasePrefix = 0;
36 };
37 void prepare(OfflineSession &job, std::size_t count, std::int64_t attackFrames,
38 double releaseFrames, bool localRelease = false)
39 {
40 if (attackFrames < 1 || attackFrames > 1000000 || !std::isfinite(releaseFrames) ||
41 releaseFrames <= 0)
43 capacity_ = std::max(std::size_t(1), count);
44 holds_ = job.allocate<Hold>(capacity_);
45 releases_ = localRelease ? job.allocate<double>(capacity_) : nullptr;
46 count_ = count;
47 attack_ = attackFrames;
48 release_ = releaseFrames;
49 coefficient_ = std::exp(-1 / release_);
50 smoother_.prepare(job, attack_, count_ != 0);
51 maximumDeficit_ = 0;
52 }
53 void set(std::size_t index, std::int64_t begin, std::int64_t end, double gain,
54 double localReleaseFrames = 0)
55 {
56 if (index >= count_ || begin < 0 || end <= begin ||
57 end > std::numeric_limits<std::int64_t>::max() - attack_ || !std::isfinite(gain) ||
58 gain <= 0 || gain > 1 ||
59 (index && (begin < holds_[index - 1].begin || end < holds_[index - 1].end)) ||
60 (releases_ && (!std::isfinite(localReleaseFrames) || localReleaseFrames <= 0)))
62 if (releases_)
63 releases_[index] = localReleaseFrames;
64 auto &hold = holds_[index];
65 hold = {begin, end, 1 - gain, 1 - gain};
66 maximumDeficit_ = std::max(maximumDeficit_, hold.deficit);
67 if (index)
68 {
69 const auto &previous = holds_[index - 1];
70 hold.releasePrefix =
71 std::max(hold.deficit, previous.releasePrefix * decay(end - previous.end, index - 1));
72 }
73 }
74 [[nodiscard]] bool isValid() const noexcept
75 {
76 return holds_ != nullptr;
77 }
78 [[nodiscard]] std::size_t retainedBytes() const noexcept
79 {
80 return isValid() ? capacity_ * (sizeof(Hold) + (releases_ ? sizeof(double) : 0)) +
81 smoother_.retainedBytes() : 0;
82 }
83 // Keep event holds and release constants; move the smoothed attack earlier.
84 // Offline peak calibration can require more anticipation to avoid ringing
85 // exceeding the target after baseband projection.
87 std::int64_t attackFrames) const
88 {
89 if (!isValid())
91 OfflineAttenuation result;
92 result.prepare(job, count_, attackFrames, release_, releases_ != nullptr);
93 for (std::size_t i = 0; i < count_; ++i)
94 result.set(i, holds_[i].begin, holds_[i].end, 1 - holds_[i].deficit,
95 releases_ ? releases_[i] : 0);
96 return result;
97 }
98 // Random access uses the finite box support; streaming rendering uses the
99 // cursor below, with constant amortized work and O(lookahead) scratch.
100 [[nodiscard]] double gainAt(std::int64_t frame) const noexcept
101 {
102 if (!isValid() || count_ == 0)
103 return 1;
104 double sum = 0;
105 for (std::int64_t k = 0; k <= attack_; ++k)
106 sum += smoother_.weights()[static_cast<std::size_t>(k)] * rawAt(frame + k);
107 return gain(sum);
108 }
109 class Cursor final
110 {
111 public:
113 : curve_(curve), smoother_(curve.smoother_, job)
114 {
115 }
116 [[nodiscard]] double operator()(std::int64_t frame) noexcept
117 {
118 if (!curve_.isValid() || curve_.count_ == 0)
119 return 1;
120 // Fixed source-clock anchors make a cached block identical whether
121 // reached sequentially or after a rewind for another channel/map.
122 if (frame != next_ || (frame & 4095) == 0)
123 {
124 smoother_.reset();
125 tail_ = 0;
126 ended_ = 0;
127 tailEnd_ = std::numeric_limits<std::size_t>::max();
128 for (std::int64_t k = 0; k < curve_.attack_; ++k)
129 advance(frame + k);
130 }
131 const double deficit = advance(frame + curve_.attack_);
132 next_ = frame + 1;
133 return curve_.gain(deficit);
134 }
135
136 private:
137 double advance(std::int64_t frame) noexcept
138 {
139 while (ended_ < curve_.count_ && curve_.holds_[ended_].end + curve_.attack_ <= frame)
140 ++ended_;
141 if (ended_ != tailEnd_ || (frame & 4095) == 0)
142 {
143 tail_ =
144 ended_
145 ? curve_.holds_[ended_ - 1].releasePrefix *
146 curve_.decay(frame - curve_.holds_[ended_ - 1].end - curve_.attack_,
147 ended_ - 1)
148 : 0;
149 coefficient_ = curve_.releases_ && ended_
150 ? std::exp(-1 / curve_.releases_[ended_ - 1])
151 : curve_.coefficient_;
152 }
153 else
154 tail_ *= coefficient_;
155 tailEnd_ = ended_;
156 const double raw = curve_.active(frame, ended_, tail_);
157 return smoother_.advance(raw, frame);
158 }
159 const OfflineAttenuation &curve_;
160 OfflineBoxSmoother::Cursor smoother_;
161 std::int64_t next_ = -1;
162 std::size_t ended_ = 0, tailEnd_ = std::numeric_limits<std::size_t>::max();
163 double tail_ = 0, coefficient_ = 0;
164 };
165
166 private:
167 [[nodiscard]] double decay(std::int64_t frames, std::size_t hold) const noexcept
168 {
169 const double time = static_cast<double>(frames) / (releases_ ? releases_[hold] : release_);
170 return time >= 36 ? 0 : std::exp(-time);
171 }
172 [[nodiscard]] double gain(double deficit) const noexcept
173 {
174 return deficit <= 2.32e-16 ? 1 : 1 - std::min(maximumDeficit_, deficit);
175 }
176 [[nodiscard]] double active(std::int64_t frame, std::size_t ended, double tail) const noexcept
177 {
178 double deficit = tail;
179 for (auto i = ended; i < count_ && holds_[i].begin <= frame; ++i)
180 deficit = std::max(deficit, holds_[i].deficit);
181 return deficit <= 2.32e-16 ? 0 : deficit;
182 }
183 [[nodiscard]] double rawAt(std::int64_t frame) const noexcept
184 {
185 std::size_t lo = 0, hi = count_;
186 while (lo < hi)
187 {
188 const auto mid = lo + (hi - lo) / 2;
189 if (holds_[mid].end + attack_ <= frame)
190 lo = mid + 1;
191 else
192 hi = mid;
193 }
194 return active(
195 frame, lo,
196 lo ? holds_[lo - 1].releasePrefix * decay(frame - holds_[lo - 1].end - attack_, lo - 1)
197 : 0);
198 }
199 std::unique_ptr<Hold[]> holds_;
200 std::unique_ptr<double[]> releases_;
201 std::size_t count_ = 0, capacity_ = 0;
202 std::int64_t attack_ = 1;
203 OfflineBoxSmoother smoother_;
204 double release_ = 1, coefficient_ = 0, maximumDeficit_ = 0;
205};
206} // namespace dspark::detail
207#endif // DSPARK_HAS_OFFLINE
Cursor(const OfflineAttenuation &curve, OfflineSession &job)
double operator()(std::int64_t frame) noexcept
void set(std::size_t index, std::int64_t begin, std::int64_t end, double gain, double localReleaseFrames=0)
void prepare(OfflineSession &job, std::size_t count, std::int64_t attackFrames, double releaseFrames, bool localRelease=false)
std::size_t retainedBytes() const noexcept
OfflineAttenuation withAttack(OfflineSession &job, std::int64_t attackFrames) const
double gainAt(std::int64_t frame) const noexcept
double advance(double raw, std::int64_t frame) noexcept
void prepare(OfflineSession &job, std::int64_t span, bool active)
const double * weights() const noexcept
std::size_t retainedBytes() const noexcept
std::unique_ptr< U[]> allocate(std::uint64_t count)
void offlineFail(OfflineStatus status)