Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
multichannel_phase_coordinator.hpp
Go to the documentation of this file.
1#pragma once
2
22
24
25#include <algorithm>
26#include <cassert>
27#include <cmath>
28#include <complex>
29#include <vector>
30
31namespace pulp::signal {
32
36template <typename SampleType = float>
38public:
40
41 static bool checked_retained_bytes(int fft_size, std::uint64_t target_max_bytes,
42 std::uint64_t& bytes) noexcept {
43 const auto bins = static_cast<std::uint64_t>(fft_size / 2 + 1);
44 CheckedRetainedByteCharge charge(target_max_bytes);
45 if (!charge.add<double>(bins) || !charge.add<double>(bins)
46 || !charge.add<SampleType>(bins) || !charge.add<double>(bins)
47 || !charge.add<int>(bins))
48 return false;
49 bytes = charge.total();
50 return true;
51 }
52
53 void prepare(int fft_size, int channels) {
54 assert(fft_size >= 256 && (fft_size & (fft_size - 1)) == 0);
55 assert(channels >= 1);
56 fft_size_ = fft_size;
57 channels_ = channels;
58 num_bins_ = fft_size / 2 + 1;
59 prev_phase_.assign(static_cast<size_t>(num_bins_), 0.0);
60 synth_phase_.assign(static_cast<size_t>(num_bins_), 0.0);
61 ref_mag_.assign(static_cast<size_t>(num_bins_), SampleType{0});
62 ref_phase_.assign(static_cast<size_t>(num_bins_), 0.0);
63 peaks_.assign(static_cast<size_t>(num_bins_), 0);
64 reset();
65 }
66
67 void reset() {
68 std::fill(prev_phase_.begin(), prev_phase_.end(), 0.0);
69 std::fill(synth_phase_.begin(), synth_phase_.end(), 0.0);
70 first_frame_ = true;
71 }
72
75 const SampleType* reference_magnitudes() const { return ref_mag_.data(); }
76 int num_bins() const { return num_bins_; }
77
84 void process_group(std::complex<SampleType>* const* frames, int num_bins,
85 int analysis_hop, int synthesis_hop, SampleType reset_amount = SampleType{0}) {
86 assert(num_bins == num_bins_);
87 assert(analysis_hop > 0 && synthesis_hop > 0);
88
89 // Energy-weighted reference: the complex channel sum.
90 for (int k = 0; k < num_bins_; ++k) {
91 std::complex<SampleType> sum(SampleType{0}, SampleType{0});
92 for (int ch = 0; ch < channels_; ++ch) sum += frames[ch][k];
93 ref_mag_[static_cast<size_t>(k)] = std::abs(sum);
94 ref_phase_[static_cast<size_t>(k)] = static_cast<double>(std::arg(sum));
95 }
96
97 if (first_frame_ || reset_amount >= SampleType{1}) {
98 std::copy(ref_phase_.begin(), ref_phase_.end(), synth_phase_.begin());
99 std::copy(ref_phase_.begin(), ref_phase_.end(), prev_phase_.begin());
100 first_frame_ = false;
101 return; // rotation is identically zero — nothing to apply
102 }
103
104 // Peak picking on the reference magnitude (local maximum over two
105 // neighbors each side, per Laroche-Dolson region locking).
106 num_peaks_ = 0;
107 for (int k = 2; k <= num_bins_ - 3; ++k) {
108 const SampleType m = ref_mag_[static_cast<size_t>(k)];
109 if (m > ref_mag_[static_cast<size_t>(k - 1)] && m > ref_mag_[static_cast<size_t>(k - 2)]
110 && m > ref_mag_[static_cast<size_t>(k + 1)] && m > ref_mag_[static_cast<size_t>(k + 2)])
111 peaks_[static_cast<size_t>(num_peaks_++)] = k;
112 }
113 if (num_peaks_ == 0) peaks_[num_peaks_++] = 1;
114
115 // Propagate each peak's phase: instantaneous frequency from the
116 // heterodyned phase increment, advanced by the synthesis hop.
117 const double ha = static_cast<double>(analysis_hop);
118 const double hs = static_cast<double>(synthesis_hop);
119 for (int i = 0; i < num_peaks_; ++i) {
120 const int p = peaks_[static_cast<size_t>(i)];
121 const double omega = two_pi_ * p / fft_size_;
122 const double delta = princarg(ref_phase_[static_cast<size_t>(p)]
123 - prev_phase_[static_cast<size_t>(p)] - omega * ha);
124 const double inst = omega + delta / ha;
125 synth_phase_[static_cast<size_t>(p)] += inst * hs;
126 }
127
128 // Identity phase locking: every bin inherits its nearest peak's
129 // rotation, preserving the analysis phase relationships within
130 // each peak region. Then apply the rotation to all channels.
131 int pi_idx = 0;
132 for (int k = 0; k < num_bins_; ++k) {
133 while (pi_idx + 1 < num_peaks_
134 && std::abs(peaks_[static_cast<size_t>(pi_idx + 1)] - k)
135 < std::abs(peaks_[static_cast<size_t>(pi_idx)] - k))
136 ++pi_idx;
137 const int p = peaks_[static_cast<size_t>(pi_idx)];
138 if (k != p)
139 synth_phase_[static_cast<size_t>(k)] =
140 synth_phase_[static_cast<size_t>(p)]
141 + (ref_phase_[static_cast<size_t>(k)] - ref_phase_[static_cast<size_t>(p)]);
142
143 // Work with the principal rotation: scaling a raw 2*pi-multiple
144 // during a partial reset would manufacture a spurious rotation.
145 double rotation = princarg(synth_phase_[static_cast<size_t>(k)]
146 - ref_phase_[static_cast<size_t>(k)]);
147 if (reset_amount > SampleType{0})
148 rotation *= 1.0 - static_cast<double>(reset_amount);
149 synth_phase_[static_cast<size_t>(k)] =
150 ref_phase_[static_cast<size_t>(k)] + rotation;
151
152 const SampleType rot = static_cast<SampleType>(rotation);
153 if (rot != SampleType{0}) {
154 const std::complex<SampleType> r(std::cos(rot), std::sin(rot));
155 for (int ch = 0; ch < channels_; ++ch)
156 frames[ch][k] *= r;
157 }
158 }
159
160 std::copy(ref_phase_.begin(), ref_phase_.end(), prev_phase_.begin());
161 }
162
163private:
164 static double princarg(double p) {
165 return p - two_pi_ * std::round(p / two_pi_);
166 }
167
168 static constexpr double two_pi_ = 6.28318530717958647692;
169
170 int fft_size_ = 0;
171 int channels_ = 0;
172 int num_bins_ = 0;
173 bool first_frame_ = true;
174
175 std::vector<double> prev_phase_;
176 std::vector<double> synth_phase_;
177 std::vector<SampleType> ref_mag_;
178 std::vector<double> ref_phase_;
179 std::vector<int> peaks_;
180 int num_peaks_ = 0;
181};
182
185
186} // namespace pulp::signal
Definition checked_allocation.hpp:57
bool add(std::uint64_t elements) noexcept
Definition checked_allocation.hpp:63
std::uint64_t total() const noexcept
Definition checked_allocation.hpp:83
RT contract: prepare() allocates internal phase/magnitude storage and must run off the audio thread.
Definition multichannel_phase_coordinator.hpp:37
static bool checked_retained_bytes(int fft_size, std::uint64_t target_max_bytes, std::uint64_t &bytes) noexcept
Definition multichannel_phase_coordinator.hpp:41
int num_bins() const
Definition multichannel_phase_coordinator.hpp:76
void process_group(std::complex< SampleType > *const *frames, int num_bins, int analysis_hop, int synthesis_hop, SampleType reset_amount=SampleType{0})
Propagate phases for one frame group and rotate every channel's bins in place.
Definition multichannel_phase_coordinator.hpp:84
const SampleType * reference_magnitudes() const
Sum of reference magnitudes from the last processed frame — cheap energy proxy callers (transient det...
Definition multichannel_phase_coordinator.hpp:75
void reset()
Definition multichannel_phase_coordinator.hpp:67
void prepare(int fft_size, int channels)
Definition multichannel_phase_coordinator.hpp:53
Definition additive_bank.hpp:218
MultichannelPhaseCoordinatorT< double > MultichannelPhaseCoordinator64
Definition multichannel_phase_coordinator.hpp:184
MultichannelPhaseCoordinatorT< float > MultichannelPhaseCoordinator
Definition multichannel_phase_coordinator.hpp:183
@ i
Definition formant_filter_bank.hpp:20
@ r
Definition input_events.hpp:41
@ m
Definition input_events.hpp:40
@ p
Definition input_events.hpp:40
@ k
Definition input_events.hpp:40