Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
buffering_reader.hpp
Go to the documentation of this file.
1#pragma once
2
5
7
8#include <vector>
9#include <atomic>
10#include <chrono>
11#include <thread>
12#include <functional>
13#include <cstring>
14#include <algorithm>
15#include <limits>
16
17namespace pulp::audio {
18
35public:
41 using ReadCallback = std::function<int(float* dest, int num_frames, int num_channels)>;
42
43 BufferingReader() = default;
44
46 stop();
47 }
48
51
53 void set_read_callback(ReadCallback cb) { read_callback_ = std::move(cb); }
54
58 void start(int num_channels, int buffer_frames = 44100) {
59 stop();
60
61 if (num_channels <= 0 || buffer_frames <= 0 ||
62 num_channels > std::numeric_limits<int>::max() / buffer_frames ||
63 num_channels > std::numeric_limits<int>::max() / kChunkFrames) {
64 num_channels_ = 0;
65 buffer_size_ = 0;
66 write_pos_ = 0;
67 read_pos_ = 0;
68 available_.store(0);
69 finished_.store(false);
70 running_.store(false);
71 return;
72 }
73
74 const auto sample_capacity =
75 static_cast<int64_t>(buffer_frames) * static_cast<int64_t>(num_channels);
76 num_channels_ = num_channels;
77 buffer_size_ = static_cast<int>(sample_capacity);
78 buffer_.resize(static_cast<size_t>(buffer_size_), 0.0f);
79 write_pos_ = 0;
80 read_pos_ = 0;
81 available_.store(0);
82 finished_.store(false);
83 running_.store(true);
84
85 thread_ = std::thread([this] { background_loop(); });
86 }
87
89 void stop() {
90 running_.store(false);
91 wake_.notify();
92 if (thread_.joinable()) thread_.join();
93 buffer_.clear();
94 }
95
101 int read(float* dest, int num_frames, int num_channels) {
102 if (dest == nullptr || num_frames <= 0) return 0;
103 if (num_channels != num_channels_ || buffer_.empty()) return 0;
104 if (num_channels <= 0 ||
105 num_frames > std::numeric_limits<int>::max() / num_channels) return 0;
106
107 int samples_requested = num_frames * num_channels;
108 int avail = available_.load(std::memory_order_acquire);
109 int samples_to_read = std::min(samples_requested, avail);
110
111 if (samples_to_read <= 0) {
112 std::memset(dest, 0, static_cast<size_t>(samples_requested) * sizeof(float));
113 return 0;
114 }
115
116 // Copy from ring buffer
117 int first_chunk = std::min(samples_to_read, buffer_size_ - read_pos_);
118 std::memcpy(dest, buffer_.data() + read_pos_,
119 static_cast<size_t>(first_chunk) * sizeof(float));
120
121 int second_chunk = samples_to_read - first_chunk;
122 if (second_chunk > 0) {
123 std::memcpy(dest + first_chunk, buffer_.data(),
124 static_cast<size_t>(second_chunk) * sizeof(float));
125 }
126
127 read_pos_ = (read_pos_ + samples_to_read) % buffer_size_;
128 available_.fetch_sub(samples_to_read, std::memory_order_release);
129
130 // Wake the background thread if it is parked waiting for space. No
131 // lock, and a system call only while it is actually asleep.
132 wake_.notify();
133
134 // Zero-fill any remaining samples
135 int frames_read = samples_to_read / num_channels;
136 if (frames_read < num_frames) {
137 std::memset(dest + samples_to_read, 0,
138 static_cast<size_t>((num_frames - frames_read) * num_channels) * sizeof(float));
139 }
140
141 return frames_read;
142 }
143
145 bool is_finished() const { return finished_.load(); }
146
148 bool is_running() const { return running_.load(); }
149
151 int frames_available() const {
152 return num_channels_ > 0 ? available_.load() / num_channels_ : 0;
153 }
154
155private:
156 ReadCallback read_callback_;
157 std::vector<float> buffer_;
158 int buffer_size_ = 0;
159 int num_channels_ = 0;
160 int write_pos_ = 0;
161 int read_pos_ = 0;
162 std::atomic<int> available_{0};
163 std::atomic<bool> running_{false};
164 std::atomic<bool> finished_{false};
165 std::thread thread_;
166 // Audio thread (read) and control thread (stop) -> the background thread.
167 runtime::WakeSignal wake_;
168 static constexpr int kChunkFrames = 1024;
169 // Only bounds the cost of a missed wake; refills are driven by read().
170 static constexpr auto kIdleBackstop = std::chrono::seconds(1);
171
172 void background_loop() {
173 std::vector<float> temp(static_cast<size_t>(kChunkFrames * num_channels_));
174
175 while (running_.load()) {
176 // Sleep while the buffer is full; read() wakes this thread when it
177 // frees space, and stop() when the reader shuts down. A full,
178 // unread buffer therefore costs no wakeups at all.
179 wake_.wait_for(
180 [this] {
181 return !running_.load() ||
182 available_.load(std::memory_order_acquire) < buffer_size_;
183 },
184 kIdleBackstop);
185
186 if (!running_.load()) break;
187
188 int space = buffer_size_ - available_.load(std::memory_order_acquire);
189 int frames_to_read = std::min(kChunkFrames, space / std::max(1, num_channels_));
190
191 if (frames_to_read <= 0) continue;
192
193 int frames_got = 0;
194 if (read_callback_) {
195 frames_got = read_callback_(temp.data(), frames_to_read, num_channels_);
196 }
197
198 if (frames_got <= 0) {
199 finished_.store(true);
200 running_.store(false);
201 return;
202 }
203 if (frames_got > frames_to_read) frames_got = frames_to_read;
204
205 int samples = frames_got * num_channels_;
206 int first_chunk = std::min(samples, buffer_size_ - write_pos_);
207 std::memcpy(buffer_.data() + write_pos_, temp.data(),
208 static_cast<size_t>(first_chunk) * sizeof(float));
209
210 int second_chunk = samples - first_chunk;
211 if (second_chunk > 0) {
212 std::memcpy(buffer_.data(), temp.data() + first_chunk,
213 static_cast<size_t>(second_chunk) * sizeof(float));
214 }
215
216 write_pos_ = (write_pos_ + samples) % buffer_size_;
217 available_.fetch_add(samples, std::memory_order_release);
218 }
219 }
220};
221
222} // namespace pulp::audio
bool is_finished() const
Check if the source has been fully read.
Definition buffering_reader.hpp:145
void stop()
Stop the background thread and release resources.
Definition buffering_reader.hpp:89
void set_read_callback(ReadCallback cb)
Set the callback that provides audio data.
Definition buffering_reader.hpp:53
BufferingReader(const BufferingReader &)=delete
~BufferingReader()
Definition buffering_reader.hpp:45
int frames_available() const
Number of frames currently available in the buffer.
Definition buffering_reader.hpp:151
BufferingReader & operator=(const BufferingReader &)=delete
std::function< int(float *dest, int num_frames, int num_channels)> ReadCallback
Callback that reads audio data from a source (file, network, etc.).
Definition buffering_reader.hpp:41
bool is_running() const
Check if the reader is running.
Definition buffering_reader.hpp:148
void start(int num_channels, int buffer_frames=44100)
Start the background reader thread.
Definition buffering_reader.hpp:58
int read(float *dest, int num_frames, int num_channels)
Read frames from the buffer (audio thread, lock-free on the hot path).
Definition buffering_reader.hpp:101
Definition analyzer_provider.hpp:12
@ space
Definition editor_host.hpp:343
@ temp
Definition special_dir.hpp:66
Wakes a sleeping consumer thread from a realtime producer without a lock, and without a system call w...