Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
unison_voice_stack.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <array>
4#include <cstddef>
5#include <cstdint>
6#include <limits>
7#include <span>
8
11
12namespace pulp::audio {
13
15 // Nonzero logical-note identity owned by the caller for this trigger.
16 std::uint64_t source_id = 0;
17 // Complete child count requested for the logical note.
18 std::size_t voice_count = 1;
19 int note = 60;
20 std::uint32_t sample_id = kInvalidSampleId;
21 std::uint32_t voice_group = 0;
22 std::uint32_t choke_group = 0;
23};
24
26 std::uint32_t voice_index = 0;
27 std::uint64_t voice_id = 0;
28 std::uint64_t generation = 0;
29};
30
35
37 bool allocated = false;
38 std::uint64_t source_id = 0;
39 std::size_t voice_count = 0;
40 std::uint32_t termination_count = 0;
41};
42
44 bool terminated = false;
45 std::uint32_t termination_count = 0;
46};
47
48// Exclusive RT owner of an InstrumentVoiceAllocator. Mixing direct allocator
49// calls with this manager is unsupported: ownership is what lets it preflight
50// capacity and steal complete logical stacks without partial allocation or a
51// newly-created child stealing its sibling.
52template <std::size_t MaximumLogicalNotes = 32, std::size_t MaximumVoicesPerNote = 16>
54public:
55 static_assert(MaximumLogicalNotes > 0 && MaximumVoicesPerNote > 0);
56 static_assert(MaximumLogicalNotes <=
57 std::numeric_limits<std::size_t>::max() / MaximumVoicesPerNote);
58
61 bool prepare(InstrumentVoiceAllocator& allocator) noexcept {
62 if (prepared_ && allocator_ != &allocator && !rebind_allowed_) return false;
63 if (prepared_ && (!owned_empty() ||
64 (allocator_ && allocator_->allocated_voice_count() != 0)))
65 return false;
66 if (allocator.max_voices() == 0 ||
67 allocator.max_voices() > MaximumLogicalNotes * MaximumVoicesPerNote ||
68 allocator.allocated_voice_count() != 0) return false;
69 allocator_ = &allocator;
70 reset_records();
71 prepared_ = true;
72 rebind_allowed_ = false;
73 return true;
74 }
75
76 bool prepared() const noexcept { return prepared_; }
77
81 std::span<UnisonVoiceTermination> terminations,
82 std::span<VoiceModulationBuffer> modulation = {}) noexcept {
83 UnisonVoiceStackResult result{false, request.source_id, 0, 0};
84 if (!valid(request, modulation) || find_source(request.source_id) != npos ||
85 !consistent()) return result;
86
87 std::array<bool, MaximumLogicalNotes> victims{};
88 std::size_t victim_voice_count = 0;
89 for (std::size_t i = 0; i < MaximumLogicalNotes; ++i) {
90 if (stacks_[i].used && request.choke_group != 0 &&
91 stacks_[i].choke_group == request.choke_group) {
92 victims[i] = true;
93 victim_voice_count += stacks_[i].count;
94 }
95 }
96 std::size_t free_after = allocator_->max_voices() -
97 allocator_->allocated_voice_count() + victim_voice_count;
98 while (free_after < request.voice_count) {
99 const auto oldest = oldest_unmarked(victims);
100 if (oldest == npos) return result;
101 victims[oldest] = true;
102 victim_voice_count += stacks_[oldest].count;
103 free_after += stacks_[oldest].count;
104 }
105 auto slot = first_unused_or_victim(victims);
106 if (slot == npos) {
107 slot = oldest_unmarked(victims);
108 if (slot == npos) return result;
109 victims[slot] = true;
110 victim_voice_count += stacks_[slot].count;
111 }
112 if (terminations.size() < victim_voice_count) return result;
113
114 // Any new identity in this generation consumes the one-shot graph-reset
115 // permission to switch allocator domains.
116 rebind_allowed_ = false;
117
118 for (std::size_t i = 0; i < MaximumLogicalNotes; ++i) {
119 if (!victims[i]) continue;
120 const auto reason = request.choke_group != 0 &&
121 stacks_[i].choke_group == request.choke_group
124 terminate_stack(i, reason, terminations, result.termination_count, modulation);
125 }
126
127 Stack next{};
128 next.used = true;
129 next.source_id = request.source_id;
130 next.choke_group = request.choke_group;
131 next.serial = next_serial_++;
132 for (std::size_t i = 0; i < request.voice_count; ++i) {
133 auto allocation = allocator_->trigger(InstrumentVoiceTrigger{
134 .note = request.note,
135 .sample_id = request.sample_id,
136 .voice_group = request.voice_group,
137 .choke_group = 0,
138 });
139 if (!allocation.allocated || allocation.stolen || allocation.termination_count != 0) {
140 for (std::size_t j = 0; j < next.count; ++j)
141 allocator_->finish_voice(next.children[j].voice_index);
142 return result;
143 }
144 next.children[next.count++] = {allocation.voice_index, allocation.voice_id,
145 generation_};
146 if (!modulation.empty()) modulation[allocation.voice_index].reset();
147 }
148 stacks_[slot] = next;
149 result.allocated = true;
150 result.voice_count = next.count;
151 return result;
152 }
153
155 std::span<const UnisonVoiceChild> children(std::uint64_t source_id) const noexcept {
156 const auto index = find_source(source_id);
157 if (index == npos) return {};
158 return {stacks_[index].children.data(), stacks_[index].count};
159 }
160
162 bool release(std::uint64_t source_id) noexcept {
163 const auto index = find_source(source_id);
164 if (index == npos || !consistent()) return false;
165 bool changed = false;
166 for (std::size_t i = 0; i < stacks_[index].count; ++i) {
167 const auto child = stacks_[index].children[i];
168 changed = allocator_->release_voice(child.voice_index) || changed;
169 }
170 return changed;
171 }
172
173 // Exact voice_id matching rejects stale renderer tails after a slot has
174 // been recycled. A stack disappears only after its final child finishes.
175 bool finish(std::uint32_t voice_index, std::uint64_t voice_id,
176 std::uint64_t generation,
177 std::span<VoiceModulationBuffer> modulation = {}) noexcept {
178 if (!prepared_ || !modulation_valid(modulation) || voice_index >= allocator_->max_voices())
179 return false;
180 for (auto& stack : stacks_) {
181 if (!stack.used) continue;
182 for (std::size_t i = 0; i < stack.count; ++i) {
183 if (stack.children[i].voice_index != voice_index ||
184 stack.children[i].voice_id != voice_id ||
185 stack.children[i].generation != generation) continue;
186 const auto& voice = allocator_->voices()[voice_index];
187 if (voice.voice_id != voice_id || voice.state != VoiceState::Released)
188 return false;
189 if (!allocator_->finish_voice(voice_index)) return false;
190 if (!modulation.empty()) modulation[voice_index].reset();
191 stack.children[i] = stack.children[stack.count - 1];
192 --stack.count;
193 if (stack.count == 0) stack = {};
194 return true;
195 }
196 }
197 return false;
198 }
199
200 // Panic/graph teardown path that preserves every child identity and fade
201 // instruction. Failure to fit all records is atomic and changes nothing.
203 std::span<UnisonVoiceTermination> terminations,
204 std::span<VoiceModulationBuffer> modulation = {}) noexcept {
206 if (!prepared_ || !modulation_valid(modulation) || !consistent()) return result;
207 std::size_t required = 0;
208 for (const auto& stack : stacks_) if (stack.used) required += stack.count;
209 if (terminations.size() < required) return result;
210 for (std::size_t i = 0; i < MaximumLogicalNotes; ++i)
211 if (stacks_[i].used)
212 terminate_stack(i, VoiceTerminationReason::Stolen, terminations,
213 result.termination_count, modulation);
214 result.terminated = true;
215 return result;
216 }
217
218 // Hard graph-wide reset: intentionally emits no tails and invalidates all
219 // prior tail callbacks. This is the only operation that permits rebinding
220 // to another allocator, whose local voice IDs may restart at one. Use
221 // terminate_all() when renderers must receive bounded fade records; that
222 // path deliberately keeps this manager in the original identity domain.
223 bool reset(std::span<VoiceModulationBuffer> modulation = {}) noexcept {
224 if (!prepared_ || !modulation_valid(modulation)) return false;
225 allocator_->reset();
226 if (!modulation.empty()) for (auto& buffer : modulation) buffer.reset();
227 reset_records();
228 if (++generation_ == 0) ++generation_;
229 rebind_allowed_ = true;
230 return true;
231 }
232
233private:
234 struct Stack {
235 std::array<UnisonVoiceChild, MaximumVoicesPerNote> children{};
236 std::uint64_t source_id = 0;
237 std::uint64_t serial = 0;
238 std::uint32_t choke_group = 0;
239 std::size_t count = 0;
240 bool used = false;
241 };
242 static constexpr std::size_t npos = std::numeric_limits<std::size_t>::max();
243
244 bool valid(const UnisonVoiceStackTrigger& request,
245 std::span<VoiceModulationBuffer> modulation) const noexcept {
246 return prepared_ && request.source_id != 0 && request.voice_count > 0 &&
247 request.voice_count <= MaximumVoicesPerNote &&
248 request.voice_count <= allocator_->max_voices() && request.note >= 0 &&
249 request.note <= 127 && request.sample_id != kInvalidSampleId &&
250 modulation_valid(modulation);
251 }
252 bool modulation_valid(std::span<VoiceModulationBuffer> modulation) const noexcept {
253 return modulation.empty() ||
254 (allocator_ && modulation.size() == allocator_->max_voices());
255 }
256 std::size_t find_source(std::uint64_t id) const noexcept {
257 for (std::size_t i = 0; i < MaximumLogicalNotes; ++i)
258 if (stacks_[i].used && stacks_[i].source_id == id) return i;
259 return npos;
260 }
261 bool consistent() const noexcept {
262 std::size_t tracked = 0;
263 for (const auto& stack : stacks_) {
264 if (!stack.used) continue;
265 tracked += stack.count;
266 for (std::size_t i = 0; i < stack.count; ++i) {
267 const auto child = stack.children[i];
268 if (child.voice_index >= allocator_->max_voices()) return false;
269 const auto& voice = allocator_->voices()[child.voice_index];
270 if (voice.state == VoiceState::Free || voice.voice_id != child.voice_id) return false;
271 }
272 }
273 return tracked == allocator_->allocated_voice_count();
274 }
275 bool owned_empty() const noexcept {
276 for (const auto& stack : stacks_) if (stack.used) return false;
277 return true;
278 }
279 std::size_t oldest_unmarked(const std::array<bool, MaximumLogicalNotes>& marked) const noexcept {
280 std::size_t result = npos;
281 for (std::size_t i = 0; i < MaximumLogicalNotes; ++i)
282 if (stacks_[i].used && !marked[i] &&
283 (result == npos || stacks_[i].serial < stacks_[result].serial)) result = i;
284 return result;
285 }
286 std::size_t first_unused_or_victim(
287 const std::array<bool, MaximumLogicalNotes>& victims) const noexcept {
288 for (std::size_t i = 0; i < MaximumLogicalNotes; ++i)
289 if (!stacks_[i].used || victims[i]) return i;
290 return npos;
291 }
292 void terminate_stack(std::size_t index, VoiceTerminationReason reason,
293 std::span<UnisonVoiceTermination> out, std::uint32_t& written,
294 std::span<VoiceModulationBuffer> modulation) noexcept {
295 auto& stack = stacks_[index];
296 for (std::size_t i = 0; i < stack.count; ++i) {
297 const auto child = stack.children[i];
298 out[written++] = {{child.voice_index, child.voice_id, reason,
299 allocator_->termination_fade_frames()},
300 child.generation};
301 allocator_->finish_voice(child.voice_index);
302 if (!modulation.empty()) modulation[child.voice_index].reset();
303 }
304 stack = {};
305 }
306 void reset_records() noexcept {
307 for (auto& stack : stacks_) stack = {};
308 next_serial_ = 1;
309 }
310
311 std::array<Stack, MaximumLogicalNotes> stacks_{};
312 InstrumentVoiceAllocator* allocator_ = nullptr;
313 std::uint64_t next_serial_ = 1;
314 bool prepared_ = false;
315 bool rebind_allowed_ = true;
316 std::uint64_t generation_ = 1;
317};
318
319} // namespace pulp::audio
Definition instrument_voice_allocator.hpp:68
std::span< const InstrumentVoice > voices() const noexcept
Definition instrument_voice_allocator.hpp:83
std::uint32_t max_voices() const noexcept
Definition instrument_voice_allocator.hpp:76
bool finish_voice(std::uint32_t voice_index) noexcept
Definition unison_voice_stack.hpp:53
bool prepared() const noexcept
Definition unison_voice_stack.hpp:76
bool release(std::uint64_t source_id) noexcept
Audio-thread-safe note-off for every still-active child in a stack.
Definition unison_voice_stack.hpp:162
UnisonVoiceStackResult trigger(const UnisonVoiceStackTrigger &request, std::span< UnisonVoiceTermination > terminations, std::span< VoiceModulationBuffer > modulation={}) noexcept
Audio-thread-safe, bounded complete-stack allocation.
Definition unison_voice_stack.hpp:80
bool reset(std::span< VoiceModulationBuffer > modulation={}) noexcept
Definition unison_voice_stack.hpp:223
bool prepare(InstrumentVoiceAllocator &allocator) noexcept
Control-side binding to an already prepared, empty allocator.
Definition unison_voice_stack.hpp:61
std::span< const UnisonVoiceChild > children(std::uint64_t source_id) const noexcept
Non-owning child view, invalidated by the next mutating manager call.
Definition unison_voice_stack.hpp:155
bool finish(std::uint32_t voice_index, std::uint64_t voice_id, std::uint64_t generation, std::span< VoiceModulationBuffer > modulation={}) noexcept
Definition unison_voice_stack.hpp:175
UnisonVoiceStackTerminationResult terminate_all(std::span< UnisonVoiceTermination > terminations, std::span< VoiceModulationBuffer > modulation={}) noexcept
Definition unison_voice_stack.hpp:202
Definition analyzer_provider.hpp:12
VoiceTerminationReason
Definition instrument_voice_allocator.hpp:23
@ Stolen
Definition instrument_voice_allocator.hpp:25
@ Choked
Definition instrument_voice_allocator.hpp:24
@ Free
Definition instrument_voice_allocator.hpp:18
@ Released
Definition instrument_voice_allocator.hpp:20
constexpr std::uint32_t kInvalidSampleId
Definition sample_identity.hpp:8
void request(Permission p, RequestCallback cb)
Async request.
@ index
Definition gpu_pass.hpp:140
@ oldest
Recycle the grain furthest through its window — the quietest one when durations match,...
Definition granular.hpp:123
@ i
Definition formant_filter_bank.hpp:20
@ j
Definition input_events.hpp:40
Definition unison_voice_stack.hpp:25
std::uint64_t generation
Definition unison_voice_stack.hpp:28
std::uint64_t voice_id
Definition unison_voice_stack.hpp:27
std::uint32_t voice_index
Definition unison_voice_stack.hpp:26
Definition unison_voice_stack.hpp:36
std::uint64_t source_id
Definition unison_voice_stack.hpp:38
std::uint32_t termination_count
Definition unison_voice_stack.hpp:40
bool allocated
Definition unison_voice_stack.hpp:37
std::size_t voice_count
Definition unison_voice_stack.hpp:39
Definition unison_voice_stack.hpp:43
bool terminated
Definition unison_voice_stack.hpp:44
std::uint32_t termination_count
Definition unison_voice_stack.hpp:45
Definition unison_voice_stack.hpp:14
std::uint32_t voice_group
Definition unison_voice_stack.hpp:21
std::uint32_t sample_id
Definition unison_voice_stack.hpp:20
std::uint64_t source_id
Definition unison_voice_stack.hpp:16
std::size_t voice_count
Definition unison_voice_stack.hpp:18
std::uint32_t choke_group
Definition unison_voice_stack.hpp:22
int note
Definition unison_voice_stack.hpp:19
Definition unison_voice_stack.hpp:31
VoiceTermination voice
Definition unison_voice_stack.hpp:32
std::uint64_t generation
Definition unison_voice_stack.hpp:33
Definition instrument_voice_allocator.hpp:40