46 std::uint64_t{256} << 20u;
48template <
typename SampleType>
50 static_assert(std::is_floating_point_v<SampleType>);
65template <
typename SampleType =
float>
70 int channels,
int num_bins)
noexcept = 0;
78template <
typename SampleType =
float>
82 virtual void process_block(
const SampleType*
const* input, SampleType*
const* wet,
83 int channels,
int num_samples)
noexcept = 0;
86template <
typename SampleType =
float>
89 static_assert(std::is_floating_point_v<SampleType>);
112 state_->observer_context = context;
113 state_->observer = observer;
121 if (!std::isfinite(config.sample_rate)
122 || config.sample_rate <= SampleType{0}
123 || !std::isfinite(config.initial_mix)
124 || config.initial_mix < SampleType{0}
125 || config.initial_mix > SampleType{1}
126 || config.mix_ramp_samples < 0
127 || !valid_mix_curve_(config.mix_curve))
130 const auto frame_geometry =
132 if (!frame_geometry)
return false;
134 const auto retained = checked_retained_bytes_(config, *frame_geometry);
135 if (!retained || *retained > config.max_retained_bytes)
return false;
137 auto candidate = std::make_unique<PreparedState>();
138 candidate->config = config;
139 candidate->retained_bytes = *retained;
140 candidate->engine.prepare(config.frame);
143 identity.
fft_size = config.frame.fft_size;
144 identity.
num_bins = frame_geometry->num_bins;
154 candidate->frame_table = identity;
155 candidate->target_table = identity;
156 candidate->publication =
157 std::make_unique<runtime::TripleBuffer<PublishedTable>>(
158 PublishedTable{identity, 0});
159 candidate->mixer.set_mix(config.initial_mix);
160 candidate->mixer.set_curve(config.mix_curve);
161 candidate->mixer.set_ramp_samples(config.mix_ramp_samples);
162 candidate->mixer.set_wet_latency(candidate->engine.latency_samples());
163 candidate->mixer.prepare(config.frame.channels, config.frame.max_block);
164 const auto channel_count =
static_cast<std::size_t
>(config.frame.channels);
165 const auto block =
static_cast<std::size_t
>(config.frame.max_block);
166 candidate->wet_input.assign(channel_count * block, SampleType{});
167 candidate->wet_write.resize(channel_count);
168 candidate->wet_read.resize(channel_count);
169 for (std::size_t ch = 0; ch < channel_count; ++ch) {
170 candidate->wet_write[ch] = candidate->wet_input.data() + ch * block;
171 candidate->wet_read[ch] = candidate->wet_write[ch];
174 state_ = std::move(candidate);
175 next_generation_ = 1;
182 if (!state_)
return false;
185 state_->config.sample_rate, table))
194 if (!state_ || !valid_table_(table))
return false;
195 auto generation = next_generation_++;
196 if (generation == 0) generation = next_generation_++;
197 state_->publication->write_with([&](PublishedTable& payload) {
198 payload.table = table;
199 payload.generation = generation;
201 state_->published_generation.write(generation);
211 if (!state_ || !valid_layout_geometry_(layout))
return false;
212 state_->audio_layout = layout;
213 state_->audio_layout_pending =
true;
220 [[nodiscard]]
bool process(
const SampleType*
const* input,
221 SampleType*
const* output,
222 int num_samples)
noexcept {
223 if (!valid_audio_block_(input, output, num_samples)) {
224 zero_output_(output, num_samples);
228 state_->mixer.push_dry(input, state_->config.frame.channels, num_samples);
229 const SampleType*
const* analysis_input = input;
230 if (wet_source_stage_ !=
nullptr) {
231 wet_source_stage_->process_block(input, state_->wet_write.data(),
232 state_->config.frame.channels, num_samples);
233 analysis_input = state_->wet_read.data();
235 bool frame_ok =
true;
236 state_->engine.process(analysis_input, output, num_samples,
237 [
this, &frame_ok](std::complex<SampleType>*
const* frames,
239 if (pre_mask_stage_ !=
nullptr)
240 pre_mask_stage_->process_frames(
241 frames, state_->config.frame.channels, bins);
245 zero_output_(output, num_samples);
248 state_->mixer.mix_wet(output, state_->config.frame.channels, num_samples);
258 if (!state_ ||
num_bins != state_->frame_table.num_bins ||
259 state_->next_frame_ordinal == std::numeric_limits<std::uint64_t>::max()) {
264 adopt_latest_table_();
265 adopt_audio_layout_();
266 advance_transition_();
272 if (state_->observer)
273 state_->observer(state_->observer_context, state_->frame_table,
274 state_->next_frame_ordinal);
275 ++state_->next_frame_ordinal;
293 return wet_source_stage_;
298 if (state_ && std::isfinite(mix)) state_->mixer.set_mix(mix);
301 if (state_) state_->mixer.set_ramp_samples(samples);
304 if (state_ && valid_mix_curve_(curve)) state_->mixer.set_curve(curve);
311 state_->next_frame_ordinal = 0;
312 state_->engine.reset();
313 state_->mixer.reset();
314 state_->frame_table = state_->target_table;
315 state_->transition_total = 0;
316 state_->transition_position = 0;
317 state_->target_table = state_->frame_table;
320 [[nodiscard]]
bool prepared() const noexcept {
return state_ !=
nullptr; }
322 return state_ ? state_->engine.latency_samples() : 0;
329 return state_ ? state_->frame_table.num_bins : 0;
332 return state_ ? state_->config.frame.channels : 0;
335 return state_ ? state_->retained_bytes : 0;
338 return state_ ? state_->active_version.read() : 0;
342 && state_->published_generation.read()
343 != state_->active_generation.read();
347 struct PublishedTable {
349 std::uint64_t generation = 0;
352 struct PreparedState {
355 void* observer_context =
nullptr;
356 std::uint64_t next_frame_ordinal = 0;
357 SpectralFrameEngineT<SampleType> engine;
358 DryWetMixerT<SampleType> mixer;
359 std::unique_ptr<runtime::TripleBuffer<PublishedTable>> publication;
361 Table target_table{};
364 std::array<SampleType, kSpectralBandMaskMaximumBins> transition_start{};
365 runtime::SeqLock<std::uint64_t> published_generation;
366 runtime::SeqLock<std::uint64_t> active_generation;
367 runtime::SeqLock<std::uint64_t> active_version;
368 std::vector<SampleType> wet_input;
369 std::vector<SampleType*> wet_write;
370 std::vector<const SampleType*> wet_read;
371 std::uint64_t consumed_generation = 0;
372 std::uint64_t retained_bytes = 0;
373 std::uint32_t transition_total = 0;
374 std::uint32_t transition_position = 0;
375 bool audio_layout_pending =
false;
378 static std::optional<std::uint64_t> checked_retained_bytes_(
380 const SpectralFrameEngineGeometry& frame)
noexcept {
381 const auto channels =
static_cast<std::uint64_t
>(
config.frame.channels);
382 const auto latency =
static_cast<std::uint64_t
>(
config.frame.fft_size)
383 +
static_cast<std::uint64_t
>(
config.frame.analysis_hop);
384 std::uint64_t samples_per_channel = 0;
385 std::uint64_t dry_samples = 0;
386 std::uint64_t wet_samples = 0;
388 static_cast<std::uint64_t
>(
config.frame.max_block),
389 config.max_retained_bytes, samples_per_channel)
391 config.max_retained_bytes, dry_samples)
396 CheckedRetainedByteCharge charge(
config.max_retained_bytes);
397 if (!charge.add_retained_bytes(frame.retained_bytes)
398 || !charge.add_retained_bytes(
sizeof(PreparedState))
399 || !charge.add_retained_bytes(
400 sizeof(runtime::TripleBuffer<PublishedTable>))
401 || !charge.add<SampleType>(dry_samples)
402 || !charge.add<std::vector<SampleType>>(
channels)
403 || !charge.add<SampleType>(wet_samples)
404 || !charge.add<SampleType*>(
channels)
405 || !charge.add<
const SampleType*>(
channels)
406 || charge.total() >
config.max_retained_bytes)
408 return charge.total();
411 static bool valid_mix_curve_(
MixCurve curve)
noexcept {
426 bool valid_table_(
const Table& table)
const noexcept {
427 if (table.fft_size != state_->config.frame.fft_size
428 || table.num_bins != state_->frame_table.num_bins
429 || table.sample_rate != state_->config.sample_rate
430 || table.transition_frames
433 for (
int bin = 0; bin < table.num_bins; ++bin) {
434 const auto gain = table.gain_linear[
static_cast<std::size_t
>(bin)];
435 if (!std::isfinite(gain) || gain < SampleType{0})
return false;
440 bool valid_layout_geometry_(
const Layout& layout)
const noexcept {
441 if (!std::isfinite(layout.min_hz) || !std::isfinite(layout.max_hz)
442 || layout.min_hz < SampleType{0}
443 || layout.max_hz <= layout.min_hz
444 || layout.active_bands == 0
445 || layout.active_bands > layout.bands.size())
447 for (std::size_t band = 0; band < layout.active_bands; ++band) {
448 if (!std::isfinite(layout.bands[band].gain_db))
return false;
453 bool valid_audio_block_(
const SampleType*
const* input,
454 SampleType*
const* output,
455 int num_samples)
const noexcept {
456 if (!state_ || !input || !output || num_samples <= 0
457 || num_samples > state_->config.frame.max_block)
459 for (
int channel = 0; channel < state_->config.frame.channels; ++channel)
460 if (!input[channel] || !output[channel])
return false;
464 void adopt_latest_table_() noexcept {
465 const auto& publication = state_->publication->read();
466 if (publication.generation == state_->consumed_generation)
return;
468 state_->consumed_generation = publication.generation;
469 state_->active_generation.write(publication.generation);
470 state_->active_version.write(publication.table.version);
471 std::copy_n(state_->frame_table.gain_linear.begin(),
472 state_->frame_table.num_bins,
473 state_->transition_start.begin());
474 state_->target_table = publication.table;
475 state_->transition_total = publication.table.transition_frames;
476 state_->transition_position = 0;
477 if (state_->transition_total == 0) state_->frame_table = publication.table;
480 void adopt_audio_layout_() noexcept {
481 if (!state_->audio_layout_pending)
return;
482 state_->audio_layout_pending =
false;
484 state_->config.frame.fft_size,
485 state_->config.sample_rate,
487 || !valid_table_(state_->audio_table))
490 std::copy_n(state_->frame_table.gain_linear.begin(),
491 state_->frame_table.num_bins,
492 state_->transition_start.begin());
493 state_->target_table = state_->audio_table;
494 state_->transition_total = state_->audio_table.transition_frames;
495 state_->transition_position = 0;
496 if (state_->transition_total == 0)
497 state_->frame_table = state_->audio_table;
500 void advance_transition_() noexcept {
501 if (state_->transition_position >= state_->transition_total)
return;
502 ++state_->transition_position;
503 const auto mix =
static_cast<SampleType
>(state_->transition_position)
504 /
static_cast<SampleType
>(state_->transition_total);
505 state_->frame_table = state_->target_table;
506 for (
int bin = 0; bin < state_->frame_table.num_bins; ++bin) {
507 const auto index =
static_cast<std::size_t
>(bin);
508 state_->frame_table.gain_linear[
index] =
509 state_->transition_start[
index]
510 + (state_->target_table.gain_linear[
index]
511 - state_->transition_start[
index]) * mix;
515 void zero_output_(SampleType*
const* output,
int num_samples)
noexcept {
516 if (!state_ || !output || num_samples <= 0)
return;
517 const auto samples = std::min(num_samples, state_->config.frame.max_block);
518 for (
int channel = 0; channel < state_->config.frame.channels; ++channel)
519 if (output[channel]) std::fill_n(output[channel], samples, SampleType{});
522 void zero_frames_(std::complex<SampleType>*
const* frames,
524 if (!state_ || !frames ||
num_bins <= 0)
return;
525 const auto bins = std::min<std::size_t>(
527 for (
int channel = 0; channel < state_->config.frame.channels; ++channel)
528 if (frames[channel]) std::fill_n(frames[channel], bins,
529 std::complex<SampleType>{});
532 std::unique_ptr<PreparedState> state_;
535 std::uint64_t next_generation_ = 1;
Definition spectral_mask_processor.hpp:87
int num_bins() const noexcept
Definition spectral_mask_processor.hpp:328
SpectralPreMaskStageT< SampleType > PreMaskStage
Definition spectral_mask_processor.hpp:94
void set_mix(SampleType mix) noexcept
Audio-thread-safe dry/wet controls. Mix defaults to fully wet.
Definition spectral_mask_processor.hpp:297
SpectralWetSourceStageT< SampleType > WetSourceStage
Definition spectral_mask_processor.hpp:95
WetSourceStage * wet_source_stage() const noexcept
Definition spectral_mask_processor.hpp:292
void set_pre_mask_stage(PreMaskStage *stage) noexcept
Install (or clear, with nullptr) the non-owning pre-mask frame stage.
Definition spectral_mask_processor.hpp:285
bool table_publication_pending() const noexcept
Definition spectral_mask_processor.hpp:340
bool publish_table(const Table &table)
Publish a complete immutable table through the shared latest-value three-slot handoff.
Definition spectral_mask_processor.hpp:193
int channels() const noexcept
Definition spectral_mask_processor.hpp:331
PreMaskStage * pre_mask_stage() const noexcept
Definition spectral_mask_processor.hpp:286
int maximum_tail_samples() const noexcept
Conservative flush bound for the fixed-latency WOLA response.
Definition spectral_mask_processor.hpp:325
void set_mix_curve(MixCurve curve) noexcept
Definition spectral_mask_processor.hpp:303
bool prepare(const Config &config)
Prepare a complete replacement state.
Definition spectral_mask_processor.hpp:120
bool process(const SampleType *const *input, SampleType *const *output, int num_samples) noexcept
Process one prepared planar block.
Definition spectral_mask_processor.hpp:220
std::uint64_t retained_bytes() const noexcept
Definition spectral_mask_processor.hpp:334
bool set_effective_frame_observer(void *context, EffectiveFrameObserver observer) noexcept
Install/remove only while the processing owner is stopped.
Definition spectral_mask_processor.hpp:108
SpectralMaskTableT< SampleType > Table
Definition spectral_mask_processor.hpp:93
bool prepared() const noexcept
Definition spectral_mask_processor.hpp:320
bool set_layout_rt(const Layout &layout) noexcept
Stage a layout from the single audio owner.
Definition spectral_mask_processor.hpp:210
int latency_samples() const noexcept
Definition spectral_mask_processor.hpp:321
void(*)(void *, const Table &, std::uint64_t) noexcept EffectiveFrameObserver
Definition spectral_mask_processor.hpp:97
SpectralBandLayoutT< SampleType > Layout
Definition spectral_mask_processor.hpp:92
void reset() noexcept
Clear analysis/synthesis, dry-delay, and in-flight interpolation state while preserving the latest ad...
Definition spectral_mask_processor.hpp:309
bool publish_layout(const Layout &layout)
Compile and publish one layout from the prepared geometry.
Definition spectral_mask_processor.hpp:181
void set_wet_source_stage(WetSourceStage *stage) noexcept
Install (or clear, with nullptr) the non-owning time-domain wet-source stage.
Definition spectral_mask_processor.hpp:291
void set_mix_ramp_samples(int samples) noexcept
Definition spectral_mask_processor.hpp:300
SpectralMaskProcessorConfigT< SampleType > Config
Definition spectral_mask_processor.hpp:91
bool process_frame(std::complex< SampleType > *const *frames, int num_bins) noexcept
Apply the current publication/transition state to one coherent complex frame group.
Definition spectral_mask_processor.hpp:256
std::uint64_t active_table_version() const noexcept
Definition spectral_mask_processor.hpp:337
A frame-domain stage that SpectralMaskProcessorT::process() runs on each analysis frame group before ...
Definition spectral_mask_processor.hpp:66
virtual void process_frames(std::complex< SampleType > *const *frames, int channels, int num_bins) noexcept=0
virtual ~SpectralPreMaskStageT()=default
A time-domain stage that SpectralMaskProcessorT::process() runs on each block before the wet path's a...
Definition spectral_mask_processor.hpp:79
virtual ~SpectralWetSourceStageT()=default
virtual void process_block(const SampleType *const *input, SampleType *const *wet, int channels, int num_samples) noexcept=0
@ index
Definition gpu_pass.hpp:140
Definition additive_bank.hpp:218
SpectralPreMaskStageT< double > SpectralPreMaskStage64
Definition spectral_mask_processor.hpp:543
constexpr std::uint32_t kSpectralBandMaskMaximumTransitionFrames
Definition spectral_band_mask.hpp:30
std::optional< SpectralFrameEngineGeometry > checked_spectral_frame_engine_geometry(const SpectralFrameEngineConfig &config, std::uint64_t target_max_bytes=kTargetAddressMaximumBytes) noexcept
Derive every integer and typed backing-store capacity used by prepare().
Definition spectral_frame_engine.hpp:151
SpectralMaskProcessorT< float > SpectralMaskProcessor
Definition spectral_mask_processor.hpp:540
bool apply_spectral_mask(std::complex< SampleType > *const *frames, int channels, int num_bins, const SpectralMaskTableT< SampleType > &table) noexcept
Multiply one coherent positive-frequency frame group by a prepared mask.
Definition spectral_band_mask.hpp:387
SpectralMaskProcessorT< double > SpectralMaskProcessor64
Definition spectral_mask_processor.hpp:541
SpectralMaskProcessorConfigT< double > SpectralMaskProcessorConfig64
Definition spectral_mask_processor.hpp:539
constexpr std::size_t kSpectralBandMaskMaximumBins
Definition spectral_band_mask.hpp:28
SpectralPreMaskStageT< float > SpectralPreMaskStage
Definition spectral_mask_processor.hpp:542
bool checked_capacity_product(std::uint64_t lhs, std::uint64_t rhs, std::uint64_t maximum, std::uint64_t &product) noexcept
Definition checked_allocation.hpp:12
SpectralWetSourceStageT< double > SpectralWetSourceStage64
Definition spectral_mask_processor.hpp:545
bool checked_capacity_sum(std::uint64_t lhs, std::uint64_t rhs, std::uint64_t maximum, std::uint64_t &sum) noexcept
Definition checked_allocation.hpp:21
bool build_spectral_mask(const SpectralBandLayoutT< SampleType > &layout, int fft_size, SampleType sample_rate, SpectralMaskTableT< SampleType > &out_table) noexcept
Compile a complete positive-frequency gain table.
Definition spectral_band_mask.hpp:279
SpectralWetSourceStageT< float > SpectralWetSourceStage
Definition spectral_mask_processor.hpp:544
constexpr std::uint64_t kSpectralMaskProcessorDefaultMaximumRetainedBytes
Definition spectral_mask_processor.hpp:45
SpectralMaskProcessorConfigT< float > SpectralMaskProcessorConfig
Definition spectral_mask_processor.hpp:538
MixCurve
Crossfade curve between dry (mix=0) and wet (mix=1).
Definition dry_wet_mixer.hpp:29
@ Sqrt3dB
Definition dry_wet_mixer.hpp:36
@ Sqrt4_5dB
Definition dry_wet_mixer.hpp:37
@ Linear
Definition dry_wet_mixer.hpp:30
@ EqualPower
Definition dry_wet_mixer.hpp:31
@ Sin4_5dB
Definition dry_wet_mixer.hpp:34
@ Sin6dB
Definition dry_wet_mixer.hpp:35
@ Sin3dB
Definition dry_wet_mixer.hpp:33
@ Balanced
Definition dry_wet_mixer.hpp:32
Lock-free sequence-lock for coherent multi-field snapshots.
Fixed-capacity control-side compilation and frame-side application of a zoomable spectral band layout...
Streaming STFT analysis + weighted overlap-add (WOLA) synthesis for DSP-grade spectral processing — d...
Definition spectral_band_mask.hpp:55
Configuration for a SpectralFrameEngine.
Definition spectral_frame_engine.hpp:56
Definition spectral_mask_processor.hpp:49
float initial_mix
Definition spectral_mask_processor.hpp:54
int mix_ramp_samples
Definition spectral_mask_processor.hpp:55
MixCurve mix_curve
Definition spectral_mask_processor.hpp:56
SpectralFrameEngineConfig frame
Definition spectral_mask_processor.hpp:52
float sample_rate
Definition spectral_mask_processor.hpp:53
std::uint64_t max_retained_bytes
Definition spectral_mask_processor.hpp:57
Definition spectral_band_mask.hpp:104
std::array< SampleType, kSpectralBandMaskMaximumBins > gain_linear
Definition spectral_band_mask.hpp:116
SampleType sample_rate
Definition spectral_band_mask.hpp:109
std::array< SampleType, kSpectralBandMaskMaximumBands+1 > band_edges_hz
Definition spectral_band_mask.hpp:115
SampleType effective_max_hz
Definition spectral_band_mask.hpp:111
std::uint32_t active_bands
Definition spectral_band_mask.hpp:112
int num_bins
Definition spectral_band_mask.hpp:108
SampleType effective_min_hz
Definition spectral_band_mask.hpp:110
int fft_size
Definition spectral_band_mask.hpp:107
Lock-free latest-value publication between two threads.