Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
scope_capture.hpp
Go to the documentation of this file.
1#pragma once
2
16
17#include <algorithm>
18#include <array>
19#include <cmath>
20#include <cstddef>
21#include <cstdint>
22#include <limits>
23#include <span>
24#include <type_traits>
25
26namespace pulp::signal {
27
34
36 std::size_t capture_samples = 1024;
37 std::size_t pretrigger_samples = 256;
39 double trigger_level = 0.0;
40 double hysteresis = 0.0;
41 std::uint64_t holdoff_samples = 0;
42 bool continuous = false;
43};
44
45template <typename SampleType, std::size_t MaxSamples> struct ScopeCaptureFrameT {
46 static_assert(std::is_floating_point_v<SampleType>);
47
48 std::array<SampleType, MaxSamples> samples{};
49 std::size_t sample_count = 0;
50 std::size_t trigger_offset = 0;
51 std::uint64_t trigger_sample_index = 0;
52 std::uint64_t generation = 0;
53 std::uint64_t nonfinite_samples_seen = 0;
54};
55
56template <typename SampleType = float, std::size_t MaxSamples = 8192> class ScopeCaptureT {
57 static_assert(std::is_floating_point_v<SampleType>);
58 static_assert(MaxSamples > 0);
59
60 public:
62
63 ScopeCaptureT() noexcept {
65 config.capture_samples = std::min<std::size_t>(1024, MaxSamples);
66 config.pretrigger_samples = std::min<std::size_t>(256, config.capture_samples - 1);
67 (void)configure(config);
68 }
69
71 [[nodiscard]] bool configure(const ScopeCaptureConfig& config) noexcept {
72 const double low = config.trigger_level - config.hysteresis;
73 const double high = config.trigger_level + config.hysteresis;
74 const auto mode_valid = config.trigger_mode == ScopeTriggerMode::immediate ||
75 config.trigger_mode == ScopeTriggerMode::rising_edge ||
76 config.trigger_mode == ScopeTriggerMode::falling_edge ||
78 if (config.capture_samples == 0 || config.capture_samples > MaxSamples ||
79 config.pretrigger_samples >= config.capture_samples ||
80 !std::isfinite(config.trigger_level) || !std::isfinite(config.hysteresis) ||
81 config.hysteresis < 0.0 || !std::isfinite(low) || !std::isfinite(high) ||
82 !representable_(low) || !representable_(high) || !mode_valid) {
83 return false;
84 }
85 config_ = config;
86 reset();
87 return true;
88 }
89
90 void reset() noexcept {
91 workspace_.fill(SampleType{0});
92 frame_ = {};
93 state_ = State::armed;
94 history_write_ = 0;
95 history_count_ = 0;
96 capture_write_ = 0;
97 holdoff_remaining_ = 0;
98 samples_seen_ = 0;
99 generation_ = 0;
100 nonfinite_samples_seen_ = 0;
101 capture_trigger_sample_index_ = 0;
102 rising_primed_ = false;
103 falling_primed_ = false;
104 ready_ = false;
105 }
106
108 void arm() noexcept {
109 if (state_ == State::capturing) {
110 history_count_ = capture_write_;
111 history_write_ = capture_write_ % MaxSamples;
112 }
113 state_ = State::armed;
114 capture_write_ = 0;
115 holdoff_remaining_ = 0;
116 rising_primed_ = false;
117 falling_primed_ = false;
118 ready_ = false;
119 }
120
121 void process(std::span<const SampleType> input) noexcept {
122 for (const auto sample : input)
123 process_sample_(sample);
124 }
125
126 [[nodiscard]] bool capture_ready() const noexcept {
127 return ready_;
128 }
129 [[nodiscard]] bool armed() const noexcept {
130 return state_ == State::armed;
131 }
132 [[nodiscard]] bool capturing() const noexcept {
133 return state_ == State::capturing;
134 }
135
136 [[nodiscard]] const Frame& capture() const noexcept {
137 return frame_;
138 }
139 [[nodiscard]] const ScopeCaptureConfig& config() const noexcept {
140 return config_;
141 }
142 [[nodiscard]] std::uint64_t samples_seen() const noexcept {
143 return samples_seen_;
144 }
145 [[nodiscard]] std::uint64_t holdoff_remaining() const noexcept {
146 return holdoff_remaining_;
147 }
148 [[nodiscard]] std::size_t capture_completion_latency_samples() const noexcept {
149 return config_.capture_samples - config_.pretrigger_samples - 1;
150 }
151
152 private:
153 enum class State : std::uint8_t { armed, capturing, holdoff, stopped };
154
155 static bool representable_(double value) noexcept {
156 return value >= static_cast<double>(std::numeric_limits<SampleType>::lowest()) &&
157 value <= static_cast<double>(std::numeric_limits<SampleType>::max());
158 }
159
160 void process_sample_(SampleType input) noexcept {
161 const bool finite = std::isfinite(input);
162 const SampleType sample = finite ? input : SampleType{0};
163 if (!finite)
164 ++nonfinite_samples_seen_;
165
166 if (state_ == State::capturing) {
167 workspace_[capture_write_++] = sample;
168 if (capture_write_ == config_.capture_samples)
169 complete_capture_();
170 ++samples_seen_;
171 return;
172 }
173
174 if (state_ == State::holdoff) {
175 if (holdoff_remaining_ > 0)
176 --holdoff_remaining_;
177 if (holdoff_remaining_ == 0) {
178 state_ = State::armed;
179 rising_primed_ = false;
180 falling_primed_ = false;
181 }
182 push_history_(sample);
183 ++samples_seen_;
184 return;
185 }
186
187 if (state_ == State::armed) {
188 const bool immediate = config_.trigger_mode == ScopeTriggerMode::immediate;
189 const bool edge_triggered = finite && !immediate && triggered_(sample);
190 if (history_count_ >= config_.pretrigger_samples &&
191 (immediate || edge_triggered)) {
192 begin_capture_(sample);
193 ++samples_seen_;
194 return;
195 } else if (finite) {
196 update_priming_(sample);
197 }
198 }
199
200 push_history_(sample);
201 ++samples_seen_;
202 }
203
204 void update_priming_(SampleType sample) noexcept {
205 const auto low = static_cast<SampleType>(config_.trigger_level - config_.hysteresis);
206 const auto high = static_cast<SampleType>(config_.trigger_level + config_.hysteresis);
207 if (sample < low)
208 rising_primed_ = true;
209 if (sample > high)
210 falling_primed_ = true;
211 }
212
213 bool triggered_(SampleType sample) noexcept {
214 const auto low = static_cast<SampleType>(config_.trigger_level - config_.hysteresis);
215 const auto high = static_cast<SampleType>(config_.trigger_level + config_.hysteresis);
216 const bool rising = rising_primed_ && sample >= high;
217 const bool falling = falling_primed_ && sample <= low;
218 if (rising)
219 rising_primed_ = false;
220 if (falling)
221 falling_primed_ = false;
222 switch (config_.trigger_mode) {
224 return true;
226 return rising;
228 return falling;
230 return rising || falling;
231 }
232 return false;
233 }
234
235 void begin_capture_(SampleType trigger_sample) noexcept {
236 if (config_.pretrigger_samples > 0) {
237 const auto history_end = history_count_ == MaxSamples ? MaxSamples : history_count_;
238 const auto history_start =
239 (history_write_ + MaxSamples - config_.pretrigger_samples) % MaxSamples;
240 std::rotate(workspace_.begin(), workspace_.begin() + history_start,
241 workspace_.begin() + history_end);
242 }
243 workspace_[config_.pretrigger_samples] = trigger_sample;
244 capture_trigger_sample_index_ = samples_seen_;
245 capture_write_ = config_.pretrigger_samples + 1;
246 state_ = State::capturing;
247 if (capture_write_ == config_.capture_samples)
248 complete_capture_();
249 }
250
251 void complete_capture_() noexcept {
252 std::copy_n(workspace_.begin(), config_.capture_samples, frame_.samples.begin());
253 frame_.sample_count = config_.capture_samples;
254 frame_.trigger_offset = config_.pretrigger_samples;
255 frame_.trigger_sample_index = capture_trigger_sample_index_;
256 frame_.generation = ++generation_;
257 frame_.nonfinite_samples_seen = nonfinite_samples_seen_;
258 ready_ = true;
259 history_count_ = config_.capture_samples;
260 history_write_ = config_.capture_samples % MaxSamples;
261 capture_write_ = 0;
262 if (!config_.continuous) {
263 state_ = State::stopped;
264 return;
265 }
266 holdoff_remaining_ = config_.holdoff_samples;
267 state_ = holdoff_remaining_ == 0 ? State::armed : State::holdoff;
268 rising_primed_ = false;
269 falling_primed_ = false;
270 }
271
272 void push_history_(SampleType sample) noexcept {
273 workspace_[history_write_] = sample;
274 history_write_ = (history_write_ + 1) % MaxSamples;
275 history_count_ = std::min(history_count_ + 1, MaxSamples);
276 }
277
278 ScopeCaptureConfig config_{};
279 std::array<SampleType, MaxSamples> workspace_{};
280 Frame frame_{};
281 State state_ = State::armed;
282 std::size_t history_write_ = 0;
283 std::size_t history_count_ = 0;
284 std::size_t capture_write_ = 0;
285 std::uint64_t holdoff_remaining_ = 0;
286 std::uint64_t samples_seen_ = 0;
287 std::uint64_t generation_ = 0;
288 std::uint64_t nonfinite_samples_seen_ = 0;
289 std::uint64_t capture_trigger_sample_index_ = 0;
290 bool rising_primed_ = false;
291 bool falling_primed_ = false;
292 bool ready_ = false;
293};
294
297
298} // namespace pulp::signal
Definition scope_capture.hpp:56
std::size_t capture_completion_latency_samples() const noexcept
Definition scope_capture.hpp:148
bool armed() const noexcept
Definition scope_capture.hpp:129
const Frame & capture() const noexcept
Same-thread view. This reference is not a concurrent publication API.
Definition scope_capture.hpp:136
ScopeCaptureT() noexcept
Definition scope_capture.hpp:63
void reset() noexcept
Definition scope_capture.hpp:90
bool configure(const ScopeCaptureConfig &config) noexcept
Control-thread operation. Invalid configurations leave all state intact.
Definition scope_capture.hpp:71
bool capture_ready() const noexcept
Definition scope_capture.hpp:126
void arm() noexcept
Arms a one-shot capture without discarding accumulated pretrigger history.
Definition scope_capture.hpp:108
const ScopeCaptureConfig & config() const noexcept
Definition scope_capture.hpp:139
std::uint64_t holdoff_remaining() const noexcept
Definition scope_capture.hpp:145
void process(std::span< const SampleType > input) noexcept
Definition scope_capture.hpp:121
std::uint64_t samples_seen() const noexcept
Definition scope_capture.hpp:142
bool capturing() const noexcept
Definition scope_capture.hpp:132
ScopeCaptureFrameT< SampleType, MaxSamples > Frame
Definition scope_capture.hpp:61
@ high
Definition gpu_capabilities.hpp:157
@ low
Definition background_job.hpp:22
Definition additive_bank.hpp:218
@ falling
Definition rise_fall_generator.hpp:19
@ rising
Definition rise_fall_generator.hpp:19
ScopeTriggerMode
Definition scope_capture.hpp:28
@ falling_edge
Definition scope_capture.hpp:31
@ either_edge
Definition scope_capture.hpp:32
@ immediate
Definition scope_capture.hpp:29
@ rising_edge
Definition scope_capture.hpp:30
ScopeCaptureT< double > ScopeCapture64
Definition scope_capture.hpp:296
ScopeCaptureT< float > ScopeCapture
Definition scope_capture.hpp:295
@ immediate
Replace the output with the hold from the adopting frame onward.
Definition freeze_hold.hpp:155
Definition scope_capture.hpp:35
std::size_t capture_samples
Definition scope_capture.hpp:36
ScopeTriggerMode trigger_mode
Definition scope_capture.hpp:38
double trigger_level
Definition scope_capture.hpp:39
std::uint64_t holdoff_samples
Definition scope_capture.hpp:41
std::size_t pretrigger_samples
Definition scope_capture.hpp:37
bool continuous
Definition scope_capture.hpp:42
double hysteresis
Definition scope_capture.hpp:40
Definition scope_capture.hpp:45
std::uint64_t trigger_sample_index
Definition scope_capture.hpp:51
std::uint64_t generation
Definition scope_capture.hpp:52
std::size_t sample_count
Definition scope_capture.hpp:49
std::size_t trigger_offset
Definition scope_capture.hpp:50
std::uint64_t nonfinite_samples_seen
Definition scope_capture.hpp:53
std::array< SampleType, MaxSamples > samples
Definition scope_capture.hpp:48