DSPark 1.8.0
Header-only C++20 DSP for real-time and offline audio
Loading...
Searching...
No Matches
OfflinePunchEnvelope.h
1// DSPark - Professional Audio DSP Framework
2// Copyright (c) 2026 Cristian Moresi - MIT License
3
4#pragma once
5
10#include "OfflineBoxSmoother.h"
11#if DSPARK_HAS_OFFLINE
12#include "../WindowFunctions.h"
13#include <algorithm>
14#include <cmath>
15#include <limits>
16#include <memory>
17
18namespace dspark::detail
19{
21{
22 public:
23 void prepare(OfflineSession &job, std::size_t count, double rate)
24 {
25 if (!std::isfinite(rate) || rate < 8000 || rate > 384000)
27 count_ = count;
28 capacity_ = std::max(std::size_t(1), count);
29 events_ = job.allocate<Event>(capacity_);
30 const auto frames = [rate](double seconds) {
31 return static_cast<int>(std::llround(seconds * rate));
32 };
33 span_ = frames(.002);
34 rise_ = frames(.005) - span_;
35 fall_ = frames(.045) - span_;
36 minimumHold_ = frames(.015);
37 maximumHold_ = frames(.050);
38 life_ = maximumHold_ + span_ + fall_;
39 smoother_.prepare(job, span_, count != 0);
40 if (count)
41 {
42 riseWindow_ = job.allocate<double>(static_cast<std::uint64_t>(2 * rise_ + 1));
43 fallWindow_ = job.allocate<double>(static_cast<std::uint64_t>(2 * fall_ + 1));
44 WindowFunctions<double>::hann(riseWindow_.get(), 2 * rise_ + 1, false);
45 WindowFunctions<double>::hann(fallWindow_.get(), 2 * fall_ + 1, false);
46 }
47 }
48 void set(std::size_t index, std::int64_t onset, std::int64_t duration)
49 {
50 if (index >= count_ || onset < 0 || duration <= 0 ||
51 onset > std::numeric_limits<std::int64_t>::max() - life_ - span_ ||
52 (index && onset <= events_[index - 1].onset))
54 events_[index] = {
55 onset, static_cast<int>(std::clamp(duration, static_cast<std::int64_t>(minimumHold_),
56 static_cast<std::int64_t>(maximumHold_)))};
57 }
58 [[nodiscard]] bool isValid() const noexcept
59 {
60 return events_ != nullptr;
61 }
62 [[nodiscard]] std::size_t retainedBytes() const noexcept
63 {
64 return isValid()
65 ? capacity_ * sizeof(Event) + smoother_.retainedBytes() +
66 (count_
67 ? static_cast<std::size_t>(2 * (rise_ + fall_) + 2) * sizeof(double)
68 : 0)
69 : 0;
70 }
71 [[nodiscard]] double weightAt(std::int64_t frame) const noexcept
72 {
73 if (!isValid() || !count_)
74 return 0;
75 double weight = 0;
76 for (int k = 0; k <= span_; ++k)
77 weight += smoother_.weights()[k] * rawAt(frame + k, firstAt(frame + k));
78 return std::clamp(weight, 0.0, 1.0);
79 }
80 class Cursor final
81 {
82 public:
84 : curve_(curve), smoother_(curve.smoother_, job)
85 {
86 }
87 [[nodiscard]] double operator()(std::int64_t frame) noexcept
88 {
89 if (!curve_.isValid() || !curve_.count_)
90 return 0;
91 // Match the renderer's fixed source-clock blocks after any rewind.
92 if (frame != next_ || (frame & 4095) == 0)
93 {
94 smoother_.reset();
95 first_ = curve_.firstAt(frame);
96 for (int k = 0; k < curve_.span_; ++k)
97 (void)advance(frame + k);
98 }
99 const double result = advance(frame + curve_.span_);
100 next_ = frame + 1;
101 return std::clamp(result, 0.0, 1.0);
102 }
103
104 private:
105 double advance(std::int64_t frame) noexcept
106 {
107 while (first_ < curve_.count_ && curve_.events_[first_].onset < frame - curve_.life_)
108 ++first_;
109 return smoother_.advance(curve_.rawAt(frame, first_), frame);
110 }
111 const OfflinePunchEnvelope &curve_;
113 std::int64_t next_ = -1;
114 std::size_t first_ = 0;
115 };
116
117 private:
118 struct Event
119 {
120 std::int64_t onset = 0;
121 int hold = 0;
122 };
123 [[nodiscard]] std::size_t firstAt(std::int64_t frame) const noexcept
124 {
125 std::size_t lo = 0, hi = count_;
126 while (lo < hi)
127 {
128 const auto mid = lo + (hi - lo) / 2;
129 if (events_[mid].onset < frame - life_)
130 lo = mid + 1;
131 else
132 hi = mid;
133 }
134 return lo;
135 }
136 [[nodiscard]] double rawAt(std::int64_t frame, std::size_t first) const noexcept
137 {
138 double weight = 0;
139 for (auto i = first; i < count_; ++i)
140 {
141 const auto age = frame - events_[i].onset;
142 if (age < -rise_)
143 break;
144 if (age < 0)
145 weight = std::max(weight, riseWindow_[static_cast<std::size_t>(age + rise_)]);
146 else if (age <= events_[i].hold + span_)
147 return 1;
148 else if (const auto elapsed = age - events_[i].hold - span_; elapsed < fall_)
149 weight = std::max(weight, fallWindow_[static_cast<std::size_t>(fall_ + elapsed)]);
150 }
151 return weight;
152 }
153 std::unique_ptr<Event[]> events_;
154 std::unique_ptr<double[]> riseWindow_, fallWindow_;
155 OfflineBoxSmoother smoother_;
156 std::size_t count_ = 0, capacity_ = 0;
157 int span_ = 1, rise_ = 1, fall_ = 1, minimumHold_ = 1, maximumHold_ = 1, life_ = 3;
158};
159} // namespace dspark::detail
160#endif // DSPARK_HAS_OFFLINE
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
double operator()(std::int64_t frame) noexcept
Cursor(const OfflinePunchEnvelope &curve, OfflineSession &job)
double weightAt(std::int64_t frame) const noexcept
void prepare(OfflineSession &job, std::size_t count, double rate)
std::size_t retainedBytes() const noexcept
void set(std::size_t index, std::int64_t onset, std::int64_t duration)
std::unique_ptr< U[]> allocate(std::uint64_t count)
void offlineFail(OfflineStatus status)
static void hann(T *output, int size, bool periodic=true) noexcept
Hann (raised cosine) window.