Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
buffered_content_source.hpp
Go to the documentation of this file.
1#pragma once
2
41
42#include <algorithm>
43#include <atomic>
44#include <cstdint>
45#include <functional>
46#include <thread>
47#include <utility>
48
49#include <pulp/audio/buffer.hpp>
52
53namespace pulp::playback {
54
58using BufferedContentEpoch = std::uint64_t;
59
71 std::function<std::uint64_t(BufferedContentEpoch epoch, std::uint64_t start_frame,
72 audio::BufferView<float> destination,
73 std::uint64_t frames, audio::FrameReaderStopToken stop_token)>;
74
76 std::uint32_t channels = 0;
80 std::uint64_t declared_frames = 0;
81 std::uint32_t sample_rate = 0;
83 std::uint64_t max_pull_frames = 0;
87 std::uint64_t ring_capacity_frames = 0;
89 std::uint64_t produce_chunk_frames = 4096;
95 std::uint64_t preroll_frames = 0;
100 std::uint64_t loop_start_frame = 0;
101 std::uint64_t loop_end_frame = 0;
108};
109
111 public:
114
117
118 struct Stats {
120 std::uint64_t produced_frames = 0;
123 std::uint64_t starved_frames = 0;
125 std::uint64_t starvation_events = 0;
128 std::uint64_t ring_underrun_frames = 0;
130 std::uint64_t ring_available_frames = 0;
133 std::uint64_t producer_errors = 0;
134 };
135
140 release();
142 return false;
145 return false;
146 if (config.channels == 0 || config.declared_frames == 0 || config.sample_rate == 0 ||
147 config.max_pull_frames == 0 || !producer)
148 return false;
149 if (config.initial_epoch == 0)
150 return false;
151 if (config.preroll_frames > config.declared_frames)
152 return false;
153 const bool declares_loop = config.loop_start_frame != 0 || config.loop_end_frame != 0;
154 if (declares_loop && (config.loop_start_frame >= config.loop_end_frame ||
155 config.loop_end_frame > config.declared_frames))
156 return false;
157
158 const std::uint64_t declared_lookahead =
159 std::max<std::uint64_t>(
160 lookahead_frames(declaration, config.sample_rate), 1);
161 if (config.ring_capacity_frames != 0 &&
162 (config.ring_capacity_frames < config.max_pull_frames ||
163 config.ring_capacity_frames < config.preroll_frames))
164 return false;
165
166 declaration_ = declaration;
167 declared_frames_ = config.declared_frames;
168 preroll_frames_ = config.preroll_frames;
169 loop_start_frame_ = config.loop_start_frame;
170 loop_end_frame_ = config.loop_end_frame;
171 declared_lookahead_ = declared_lookahead;
172 config_ = config;
173 producer_ = std::move(producer);
174 epoch_.store(config.initial_epoch, std::memory_order_relaxed);
175 base_frame_ = 0;
176 if (!start_stream()) {
177 release();
178 return false;
179 }
180 prepared_ = true;
181 return true;
182 }
183
185 void release() noexcept {
186 // Ask an in-flight producer call to cut short before the stream joins
187 // its thread, so the wait stays bounded by the remainder of one chunk.
188 producer_stop_requested_.store(true, std::memory_order_release);
189 stream_.release();
190 producer_stop_requested_.store(false, std::memory_order_release);
191 declaration_ = {};
192 declared_frames_ = 0;
193 preroll_frames_ = 0;
194 loop_start_frame_ = 0;
195 loop_end_frame_ = 0;
196 declared_lookahead_ = 0;
197 config_ = {};
198 producer_ = nullptr;
199 epoch_.store(0, std::memory_order_relaxed);
200 base_frame_ = 0;
201 prepared_ = false;
202 produced_frames_.store(0, std::memory_order_relaxed);
203 starved_frames_.store(0, std::memory_order_relaxed);
204 starvation_events_.store(0, std::memory_order_relaxed);
205 }
206
217 bool seek(std::uint64_t frame, BufferedContentEpoch epoch) {
218 if (!prepared_)
219 return false;
220 if (epoch == 0 || epoch <= current_epoch())
221 return false;
222 if (frame >= declared_frames_)
223 return false;
224 if (preroll_frames_ > declared_frames_ - frame)
225 return false;
226
227 // Publish the epoch before teardown: any producer call still running
228 // against the old ring becomes recognizable as superseded, and the
229 // stop request keeps its remaining run inside the current chunk.
230 epoch_.store(epoch, std::memory_order_release);
231 producer_stop_requested_.store(true, std::memory_order_release);
232 stream_.release();
233 producer_stop_requested_.store(false, std::memory_order_release);
234 base_frame_ = frame;
235 if (!start_stream()) {
236 release();
237 return false;
238 }
239 return true;
240 }
241
247 if (!prepared_ || (loop_start_frame_ == 0 && loop_end_frame_ == 0))
248 return false;
249 return seek(loop_start_frame_, epoch);
250 }
251
259 void cancel_production() noexcept {
260 if (!prepared_)
261 return;
262 producer_stop_requested_.store(true, std::memory_order_release);
263 while (producer_calls_in_flight_.load(std::memory_order_acquire) != 0)
264 std::this_thread::yield();
265 producer_stop_requested_.store(false, std::memory_order_release);
266 }
267
271 std::uint64_t pull(audio::BufferView<float> destination, std::uint64_t frames) noexcept {
272 frames = std::min<std::uint64_t>(frames, destination.num_samples());
273 const std::uint64_t position = base_frame_ + stream_.position();
274 const std::uint64_t remaining =
275 declared_frames_ > position ? declared_frames_ - position : 0;
276 const std::uint64_t expected = std::min(frames, remaining);
277 const std::uint64_t produced = stream_.pull(destination, frames);
278 produced_frames_.fetch_add(produced, std::memory_order_relaxed);
279 if (produced < expected) {
280 starved_frames_.fetch_add(expected - produced, std::memory_order_relaxed);
281 starvation_events_.fetch_add(1, std::memory_order_relaxed);
282 }
283 return produced;
284 }
285
289 std::uint64_t pump_background() noexcept {
290 return stream_.pump_background();
291 }
292
293 bool prepared() const noexcept {
294 return prepared_;
295 }
297 return declaration_;
298 }
299 std::uint64_t declared_frames() const noexcept {
300 return declared_frames_;
301 }
302
304 return epoch_.load(std::memory_order_acquire);
305 }
306
307 std::uint64_t loop_start_frame() const noexcept {
308 return loop_start_frame_;
309 }
310 std::uint64_t loop_end_frame() const noexcept {
311 return loop_end_frame_;
312 }
313
314 std::uint64_t position() const noexcept {
315 return base_frame_ + stream_.position();
316 }
317
318 bool exhausted() const noexcept {
319 return position() >= declared_frames_;
320 }
321
322 Stats stats() const noexcept {
323 const auto stream_stats = stream_.stats();
324 Stats result;
325 result.produced_frames = produced_frames_.load(std::memory_order_relaxed);
326 result.starved_frames = starved_frames_.load(std::memory_order_relaxed);
327 result.starvation_events = starvation_events_.load(std::memory_order_relaxed);
328 result.ring_underrun_frames = stream_stats.underrun_frames;
329 result.ring_available_frames = stream_stats.ring_available_frames;
330 result.producer_errors = stream_stats.read_errors;
331 return result;
332 }
333
336 std::uint32_t sample_rate) noexcept {
337 return declaration.lookahead_ms * static_cast<std::uint64_t>(sample_rate) / 1000u;
338 }
339
340 private:
345 bool start_stream() {
347 stream_config.channels = config_.channels;
348 stream_config.total_frames = declared_frames_ - base_frame_;
349 stream_config.sample_rate = config_.sample_rate;
350 // No resident head: buffered content is produced on demand, so every
351 // frame must travel through the ring where it can be counted.
352 stream_config.preload_frames = 0;
353 stream_config.ring_capacity_frames =
354 config_.ring_capacity_frames != 0
355 ? config_.ring_capacity_frames
356 : std::max({declared_lookahead_, config_.max_pull_frames, preroll_frames_});
357 stream_config.read_chunk_frames = config_.produce_chunk_frames;
358 stream_config.start_background_thread = config_.start_background_thread;
359 // Generated content is tied to playhead deadlines. Once a frame has
360 // been emitted as starvation silence, producing it later must not move
361 // subsequent content out of time.
362 stream_config.underrun_policy =
364 stream_config.max_read_ahead_frames =
365 std::max(declared_lookahead_, preroll_frames_);
366
368 binding.read = [this](std::uint64_t stream_frame, audio::BufferView<float> destination,
369 std::uint64_t frames, audio::FrameReaderStopToken stop_token) {
370 return produce_for_stream(stream_frame, destination, frames, stop_token);
371 };
372 if (!stream_.prepare(stream_config, std::move(binding)))
373 return false;
374 // The stream primes its ring synchronously during prepare; the
375 // declared preroll is the part of that fill the source guarantees.
376 if (preroll_frames_ > 0 &&
377 stream_.stats().ring_available_frames < preroll_frames_) {
378 stream_.release();
379 return false;
380 }
381 return true;
382 }
383
391 std::uint64_t produce_for_stream(std::uint64_t stream_frame,
392 audio::BufferView<float> destination,
393 std::uint64_t frames,
394 audio::FrameReaderStopToken stream_stop_token) {
395 if (stream_stop_token.stop_requested())
396 return 0;
397 const BufferedContentEpoch call_epoch = epoch_.load(std::memory_order_acquire);
398 producer_calls_in_flight_.fetch_add(1, std::memory_order_acq_rel);
399 struct InFlight {
400 std::atomic<std::uint32_t>& count;
401 ~InFlight() {
402 count.fetch_sub(1, std::memory_order_acq_rel);
403 }
404 } in_flight{producer_calls_in_flight_};
405 const std::uint64_t produced =
406 producer_(call_epoch, base_frame_ + stream_frame, destination, frames,
407 audio::FrameReaderStopToken{producer_stop_requested_});
408 // The producer may ignore the stop token and return frames anyway; an
409 // interrupted chunk publishes nothing either way.
410 if (producer_stop_requested_.load(std::memory_order_acquire))
411 return 0;
412 return produced;
413 }
414
415 audio::StreamingSampleSource stream_;
416 timeline::ProductionDeclaration declaration_;
417 std::uint64_t declared_frames_ = 0;
418 std::uint64_t preroll_frames_ = 0;
419 std::uint64_t loop_start_frame_ = 0;
420 std::uint64_t loop_end_frame_ = 0;
421 std::uint64_t declared_lookahead_ = 0;
422 BufferedContentSourceConfig config_{};
423 BufferedContentProducer producer_;
427 std::uint64_t base_frame_ = 0;
428 bool prepared_ = false;
429
430 std::atomic<BufferedContentEpoch> epoch_{0};
431 std::atomic<bool> producer_stop_requested_{false};
432 std::atomic<std::uint32_t> producer_calls_in_flight_{0};
433
434 std::atomic<std::uint64_t> produced_frames_{0};
435 std::atomic<std::uint64_t> starved_frames_{0};
436 std::atomic<std::uint64_t> starvation_events_{0};
437};
438
439} // namespace pulp::playback
Non-owning view over multi-channel audio sample data.
Definition buffer.hpp:30
Cooperative cancellation signal passed to a streaming frame reader.
Definition streaming_sample_source.hpp:45
Stats stats() const noexcept
void release() noexcept
Stop the reader thread and free all storage. Control thread only.
bool prepare(const StreamingSampleSourceConfig &config, FrameReader reader)
Allocate buffers, synchronously fill the preload window via reader, and (optionally) start the backgr...
bool exhausted() const noexcept
True once every declared frame has been accounted for, produced or not.
Definition buffered_content_source.hpp:318
static std::uint64_t lookahead_frames(const timeline::ProductionDeclaration &declaration, std::uint32_t sample_rate) noexcept
Frames the declaration's wall-clock lookahead covers at sample_rate.
Definition buffered_content_source.hpp:335
bool prepare(const timeline::ProductionDeclaration &declaration, const BufferedContentSourceConfig &config, BufferedContentProducer producer)
Control thread.
Definition buffered_content_source.hpp:138
~BufferedContentSource()
Definition buffered_content_source.hpp:113
std::uint64_t declared_frames() const noexcept
Definition buffered_content_source.hpp:299
std::uint64_t pump_background() noexcept
Background or host thread.
Definition buffered_content_source.hpp:289
BufferedContentSource & operator=(const BufferedContentSource &)=delete
bool seek(std::uint64_t frame, BufferedContentEpoch epoch)
Control thread; no pull() may be in flight.
Definition buffered_content_source.hpp:217
void release() noexcept
Control thread. Stops the producer and frees all storage.
Definition buffered_content_source.hpp:185
BufferedContentEpoch current_epoch() const noexcept
The epoch production currently belongs to. RT-safe.
Definition buffered_content_source.hpp:303
std::uint64_t loop_end_frame() const noexcept
Definition buffered_content_source.hpp:310
std::uint64_t loop_start_frame() const noexcept
Declared loop region; both zero when no loop is declared.
Definition buffered_content_source.hpp:307
const timeline::ProductionDeclaration & declaration() const noexcept
Definition buffered_content_source.hpp:296
bool prepared() const noexcept
Definition buffered_content_source.hpp:293
void cancel_production() noexcept
Control thread.
Definition buffered_content_source.hpp:259
BufferedContentSource(const BufferedContentSource &)=delete
Stats stats() const noexcept
Definition buffered_content_source.hpp:322
std::uint64_t position() const noexcept
Audio-thread position in content frames of the current epoch.
Definition buffered_content_source.hpp:314
bool loop_wrap(BufferedContentEpoch epoch)
Control thread.
Definition buffered_content_source.hpp:246
std::uint64_t pull(audio::BufferView< float > destination, std::uint64_t frames) noexcept
Audio thread.
Definition buffered_content_source.hpp:271
ProductionDeclarationErrorCode validate_production_declaration(const ProductionDeclaration &declaration) noexcept
Validate a declaration. Returns None when the declaration is coherent.
@ Buffered
Produced ahead of the playhead on another thread and consumed through a ring.
Definition production_mode.hpp:38
@ None
Definition production_mode.hpp:98
@ AdvancePosition
Definition streaming_sample_source.hpp:96
@ count
sentinel: number of real capabilities (keep LAST)
Definition canvas_capability.hpp:38
Definition audio_renderer.hpp:20
std::uint64_t BufferedContentEpoch
Identifies one continuous production interval.
Definition buffered_content_source.hpp:58
std::function< std::uint64_t(BufferedContentEpoch epoch, std::uint64_t start_frame, audio::BufferView< float > destination, std::uint64_t frames, audio::FrameReaderStopToken stop_token)> BufferedContentProducer
Caller-supplied producer of buffered content.
Definition buffered_content_source.hpp:70
How content is produced, and how honestly a render of it can be replayed.
Real-time-safe streaming sample playback primitive.
Definition streaming_sample_source.hpp:89
StoppableFrameReader read
Definition streaming_sample_source.hpp:90
Definition streaming_sample_source.hpp:99
std::uint32_t sample_rate
Source sample rate (carried, not resampled here).
Definition streaming_sample_source.hpp:102
std::uint64_t total_frames
Total length of the source in frames.
Definition streaming_sample_source.hpp:101
std::uint64_t preload_frames
Resident head length in frames.
Definition streaming_sample_source.hpp:107
std::uint64_t ring_capacity_frames
Streamed-tail ring capacity in frames.
Definition streaming_sample_source.hpp:111
bool start_background_thread
Spawn the owned background reader thread in prepare().
Definition streaming_sample_source.hpp:125
std::uint64_t max_read_ahead_frames
Maximum distance the reader may produce ahead of the play cursor.
Definition streaming_sample_source.hpp:137
std::uint64_t read_chunk_frames
Background read granularity in frames (how much the reader pulls per refill step).
Definition streaming_sample_source.hpp:115
std::uint32_t channels
Source channel count (1 or 2 typical).
Definition streaming_sample_source.hpp:100
StreamingUnderrunPolicy underrun_policy
Whether a streaming underrun preserves the source cursor or consumes its missing interval.
Definition streaming_sample_source.hpp:130
Definition buffered_content_source.hpp:75
BufferedContentEpoch initial_epoch
First production epoch.
Definition buffered_content_source.hpp:104
std::uint64_t max_pull_frames
Largest block the audio thread will request in one pull.
Definition buffered_content_source.hpp:83
std::uint64_t loop_start_frame
Declared loop region [loop_start_frame, loop_end_frame).
Definition buffered_content_source.hpp:100
std::uint64_t declared_frames
Frames the content declares it will produce.
Definition buffered_content_source.hpp:80
std::uint32_t sample_rate
Definition buffered_content_source.hpp:81
std::uint64_t preroll_frames
Frames synchronously produced into the ring during prepare() and again after every seek,...
Definition buffered_content_source.hpp:95
bool start_background_thread
Spawn an owned producer thread.
Definition buffered_content_source.hpp:107
std::uint32_t channels
Definition buffered_content_source.hpp:76
std::uint64_t produce_chunk_frames
How much the pump asks the producer for per refill step.
Definition buffered_content_source.hpp:89
std::uint64_t ring_capacity_frames
Ring capacity in frames.
Definition buffered_content_source.hpp:87
std::uint64_t loop_end_frame
Definition buffered_content_source.hpp:101
Definition buffered_content_source.hpp:118
std::uint64_t ring_underrun_frames
Ring-level underruns reported by the underlying stream.
Definition buffered_content_source.hpp:128
std::uint64_t starved_frames
Declared frames the audio thread had to zero-fill because they had not been produced in time.
Definition buffered_content_source.hpp:123
std::uint64_t producer_errors
Producer exceptions.
Definition buffered_content_source.hpp:133
std::uint64_t starvation_events
Pull calls that came up short of what the declaration promised.
Definition buffered_content_source.hpp:125
std::uint64_t produced_frames
Frames the producer supplied and the audio thread consumed.
Definition buffered_content_source.hpp:120
std::uint64_t ring_available_frames
Frames currently ready for the audio thread.
Definition buffered_content_source.hpp:130
What a piece of content declares about its own production.
Definition production_mode.hpp:88