Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
phase_vocoder.hpp
Go to the documentation of this file.
1#pragma once
2
18
19#include <algorithm>
20#include <cmath>
21#include <complex>
22#include <vector>
23
24#include <pulp/signal/fft.hpp>
26
27namespace pulp::signal {
28
34
35template <typename SampleType = float>
37public:
40
42 config_ = config;
43 // Fft requires a power-of-two size; round the requested size to the
44 // nearest power of two (down on a tie) so a non-pow2 request can't
45 // silently transform only part of each frame.
46 fft_size_ = round_to_pow2(config.fft_size > 1 ? config.fft_size : 2048);
47 synthesis_hop_ =
48 config.synthesis_hop > 0 ? config.synthesis_hop : fft_size_ / 4;
49 if (synthesis_hop_ < 1) synthesis_hop_ = 1;
51 fft_size_, config.window, SampleType{0});
52 }
53
54 const PhaseVocoderConfig& config() const noexcept { return config_; }
55
60 std::vector<SampleType> time_stretch(const std::vector<SampleType>& input,
61 double factor) const {
62 const int N = fft_size_;
63 if (input.empty() || factor <= 0.0 || N <= 1) return input;
64 // Guard against an absurd factor allocating gigabytes — a 100x stretch is
65 // already far past any musical use. Clamp rather than throw bad_alloc.
66 factor = std::min(factor, kMaxStretch());
67
68 const std::size_t n_in = input.size();
69 // Exact length contract: every path returns round(n_in * factor) frames.
70 const std::size_t target = static_cast<std::size_t>(
71 std::lround(static_cast<double>(n_in) * factor));
72
73 const int Hs = synthesis_hop_;
74 int Ha = static_cast<int>(std::lround(static_cast<double>(Hs) / factor));
75 Ha = std::max(1, Ha);
76
77 if (n_in < static_cast<std::size_t>(N)) {
78 // Too short to form even one analysis frame — no phase-vocoder pitch
79 // preservation is possible below one FFT frame. Honor the length
80 // contract by linear-resampling to the target (resample_linear
81 // returns an empty buffer when target == 0).
82 return resample_linear(input, target);
83 }
84
85 const int num_frames =
86 static_cast<int>((n_in - static_cast<std::size_t>(N)) / Ha) + 1;
87 const std::size_t out_len =
88 static_cast<std::size_t>(num_frames - 1) * Hs + N;
89
90 std::vector<SampleType> out(out_len, SampleType{0});
91 std::vector<SampleType> norm(out_len, SampleType{0});
92
93 FftT<SampleType> fft(N);
94 std::vector<std::complex<SampleType>> buf(N);
95 std::vector<double> prev_phase(N, 0.0);
96 std::vector<double> sum_phase(N, 0.0);
97
98 for (int frame = 0; frame < num_frames; ++frame) {
99 const std::size_t in_pos = static_cast<std::size_t>(frame) * Ha;
100 const std::size_t out_pos = static_cast<std::size_t>(frame) * Hs;
101
102 for (int k = 0; k < N; ++k)
103 buf[k] = std::complex<SampleType>(input[in_pos + k] * window_[k],
104 SampleType{0});
105
106 fft.forward(buf.data());
107
108 for (int k = 0; k < N; ++k) {
109 const double mag = std::abs(buf[k]);
110 const double phase = std::arg(buf[k]);
111 const double omega = kTwoPi() * static_cast<double>(k) / N;
112
113 if (frame == 0) {
114 sum_phase[k] = phase;
115 } else {
116 const double expected = static_cast<double>(Ha) * omega;
117 const double dev = princarg((phase - prev_phase[k]) - expected);
118 const double true_freq = omega + dev / static_cast<double>(Ha);
119 sum_phase[k] += static_cast<double>(Hs) * true_freq;
120 }
121 prev_phase[k] = phase;
122 buf[k] = std::polar(static_cast<SampleType>(mag),
123 static_cast<SampleType>(sum_phase[k]));
124 }
125
126 fft.inverse(buf.data());
127
128 for (int k = 0; k < N; ++k) {
129 out[out_pos + k] += buf[k].real() * window_[k];
130 norm[out_pos + k] += window_[k] * window_[k];
131 }
132 }
133
134 for (std::size_t i = 0; i < out_len; ++i)
135 if (norm[i] > static_cast<SampleType>(1e-8)) out[i] /= norm[i];
136
137 // Deliver exactly round(n_in * factor) frames: trim the overlap-add
138 // tail, or zero-pad when the framed output falls a hop short (so callers
139 // can size a destination buffer off round(n_in * factor)). resize(0) is
140 // well-defined and yields the correct empty buffer for a tiny factor.
141 out.resize(target, SampleType{0});
142 return out;
143 }
144
149 std::vector<SampleType> pitch_shift(const std::vector<SampleType>& input,
150 double semitones) const {
151 if (input.empty() || semitones == 0.0) return input;
152 const double ratio = std::pow(2.0, semitones / 12.0);
153 // Stretch by ratio (same pitch, longer), then resample back to the
154 // original length (speeds up by ratio), shifting pitch by ratio.
155 const std::vector<SampleType> stretched = time_stretch(input, ratio);
156 return resample_linear(stretched, input.size());
157 }
158
159private:
160 static constexpr double kPi() { return 3.14159265358979323846; }
161 static constexpr double kTwoPi() { return 2.0 * kPi(); }
162 // Upper bound on the stretch factor — caps output allocation at 100x the
163 // input so a pathological factor can't request a multi-gigabyte buffer.
164 static constexpr double kMaxStretch() { return 100.0; }
165
166 // Nearest power of two >= 2 (rounds down on a tie).
167 static int round_to_pow2(int n) {
168 if (n < 2) return 2;
169 int lower = 1;
170 while (lower * 2 <= n) lower *= 2;
171 const int upper = lower * 2;
172 return (n - lower <= upper - n) ? lower : upper;
173 }
174
175 static double princarg(double phase) {
176 return phase - kTwoPi() * std::round(phase / kTwoPi());
177 }
178
179 static std::vector<SampleType> resample_linear(const std::vector<SampleType>& in,
180 std::size_t out_len) {
181 std::vector<SampleType> out(out_len, SampleType{0});
182 if (in.empty() || out_len == 0) return out;
183 if (in.size() == 1) {
184 std::fill(out.begin(), out.end(), in[0]);
185 return out;
186 }
187 const double step = static_cast<double>(in.size() - 1) /
188 static_cast<double>(out_len > 1 ? out_len - 1 : 1);
189 for (std::size_t i = 0; i < out_len; ++i) {
190 const double pos = static_cast<double>(i) * step;
191 const std::size_t i0 = static_cast<std::size_t>(pos);
192 const std::size_t i1 = std::min(i0 + 1, in.size() - 1);
193 const SampleType frac = static_cast<SampleType>(pos - static_cast<double>(i0));
194 out[i] = in[i0] * (SampleType{1} - frac) + in[i1] * frac;
195 }
196 return out;
197 }
198
199 PhaseVocoderConfig config_{};
200 int fft_size_ = 2048;
201 int synthesis_hop_ = 512;
202 std::vector<SampleType> window_;
203};
204
207
208} // namespace pulp::signal
Definition fft.hpp:176
Definition phase_vocoder.hpp:36
PhaseVocoderT(const PhaseVocoderConfig &config)
Definition phase_vocoder.hpp:39
void configure(const PhaseVocoderConfig &config)
Definition phase_vocoder.hpp:41
const PhaseVocoderConfig & config() const noexcept
Definition phase_vocoder.hpp:54
std::vector< SampleType > time_stretch(const std::vector< SampleType > &input, double factor) const
Time-stretch input by factor (> 0).
Definition phase_vocoder.hpp:60
std::vector< SampleType > pitch_shift(const std::vector< SampleType > &input, double semitones) const
Pitch-shift input by semitones (positive = up), preserving duration.
Definition phase_vocoder.hpp:149
PhaseVocoderT()
Definition phase_vocoder.hpp:38
static std::vector< SampleType > generate(int size, Type type, SampleType param=SampleType{0.0f})
Definition windowing.hpp:56
Type
Definition windowing.hpp:43
@ hann
Definition windowing.hpp:45
Definition additive_bank.hpp:218
PhaseVocoderT< float > PhaseVocoder
Definition phase_vocoder.hpp:205
@ ratio
base_frequency_hz * ratio.
Definition fm_operator_engine.hpp:20
@ i
Definition formant_filter_bank.hpp:20
@ e
Definition formant_filter_bank.hpp:20
PhaseVocoderT< double > PhaseVocoder64
Definition phase_vocoder.hpp:206
Definition phase_vocoder.hpp:29
int fft_size
Analysis/synthesis frame size (power of two).
Definition phase_vocoder.hpp:30
int synthesis_hop
Output hop; 0 selects fft_size/4 (75% overlap).
Definition phase_vocoder.hpp:31
WindowFunction::Type window
Definition phase_vocoder.hpp:32