Time-stretch via phase vocoder analysis-synthesis.
Produces output of length approximately src.size() * stretch_factor. Spectral envelopes and pitched content are preserved without aliasing. Sharp transients exhibit mild pre-echo at high stretch ratios (> 2×), which is inherent to the standard phase vocoder algorithm.
Recommended parameters for audio at 48 kHz: window_size = 2048, analysis_hop = 512 (75% overlap).
198{
199 if (src.empty() || stretch_factor <= 0.0)
200 return {};
201
202 if (stretch_factor == 1.0)
203 return { src.begin(), src.end() };
204
205 const uint32_t
N = window_size;
206 const uint32_t Ha = analysis_hop;
207 const auto Hs = static_cast<uint32_t>(std::round(Ha * stretch_factor));
208 const uint32_t bins =
N / 2 + 1;
209
210 const std::vector<double> win = make_hann(
N);
211
212 const double omega_factor = k_tau *
static_cast<double>(Ha) /
static_cast<double>(
N);
213
214 const size_t n_frames = (src.size() >=
N)
215 ? (src.size() -
N) / Ha + 1
216 : 1;
217
218 const size_t out_len =
static_cast<size_t>(
static_cast<double>(src.size()) * stretch_factor) +
N;
219
220 std::vector<double> output(out_len, 0.0);
221 std::vector<double> norm(out_len, 0.0);
222
223 std::vector<double> phase_accum(bins, 0.0);
224 std::vector<double> prev_phase(bins, 0.0);
225
226 Eigen::FFT<double> fft;
227 Eigen::VectorXd frame(
N);
228 Eigen::VectorXcd spectrum;
229 Eigen::VectorXd ifft_out;
230
231 for (size_t f = 0; f < n_frames; ++f) {
232 const size_t src_pos = f * Ha;
233
234 for (uint32_t k = 0; k <
N; ++k) {
235 const size_t idx = src_pos + k;
236 frame(static_cast<Eigen::Index>(k)) = (idx < src.size()) ? src[idx] * win[k] : 0.0;
237 }
238
239 fft.fwd(spectrum, frame);
240
241 std::vector<std::complex<double>> synth(bins);
242 for (uint32_t
b = 0;
b < bins; ++
b) {
243 const std::complex<double> bin = spectrum(
static_cast<Eigen::Index
>(
b));
244 const double mag = std::abs(bin);
245 const double phase = std::arg(bin);
246
247 const double delta = phase - prev_phase[
b]
248 - omega_factor *
static_cast<double>(
b);
249 const double true_f = omega_factor *
static_cast<double>(
b)
250 + wrap_phase(delta);
251
252 phase_accum[
b] +=
static_cast<double>(Hs) * true_f
253 / static_cast<double>(Ha);
254 prev_phase[
b] = phase;
255
256 synth[
b] = std::polar(mag, phase_accum[
b]);
257 }
258
259 Eigen::VectorXcd full = to_full_spectrum(synth,
N);
260 fft.inv(ifft_out, full);
261
262 const size_t out_pos = f * Hs;
263 const double inv_N = 1.0 /
static_cast<double>(
N);
264 for (uint32_t k = 0; k <
N && out_pos + k < out_len; ++k) {
265 const double w = win[k];
266 output[out_pos + k] += ifft_out(static_cast<Eigen::Index>(k)) * inv_N * w;
267 norm[out_pos + k] += w * w;
268 }
269 }
270
271 for (size_t i = 0; i < out_len; ++i) {
272 if (norm[i] > 1e-10)
273 output[i] /= norm[i];
274 }
275
276 output.resize(
277 static_cast<size_t>(static_cast<double>(src.size()) * stretch_factor));
278
279 return output;
280}
#define N(method_name, full_type_name)