mirror of
https://github.com/Ardour/ardour.git
synced 2025-12-06 14:54:56 +01:00
559 lines
19 KiB
C++
559 lines
19 KiB
C++
#include "staffpad/TimeAndPitch.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cassert>
|
|
#include <random>
|
|
#include <stdlib.h>
|
|
#include <utility>
|
|
|
|
#include "CircularSampleBuffer.h"
|
|
#include "FourierTransform_pffft.h"
|
|
#include "SamplesFloat.h"
|
|
#include "SimdTypes.h"
|
|
|
|
using namespace staffpad::audio;
|
|
|
|
namespace staffpad {
|
|
namespace {
|
|
constexpr double twoPi = 6.28318530717958647692f;
|
|
|
|
/**
|
|
* 6-point lagrange interpolator SNR ~ -35.2db
|
|
* interpolates between samples 2 and 3 using t.
|
|
*
|
|
* smp pointer to a memory region containing 6 samples
|
|
* t fractional sample to interpolate t >= 0 && t < 1
|
|
* return value the interpolated sample
|
|
*/
|
|
inline float lagrange6(const float (& smp)[6], float t)
|
|
{
|
|
auto* y = &smp[2];
|
|
|
|
auto ym1_y1 = y[-1] + y[1];
|
|
auto twentyfourth_ym2_y2 = 1.f / 24.f * (y[-2] + y[2]);
|
|
auto c0 = y[0];
|
|
auto c1 = 0.05f * y[-2] - 0.5f * y[-1] - 1.f / 3.f * y[0] + y[1] - 0.25f * y[2] + 1.f / 30.f * y[3];
|
|
auto c2 = 2.f / 3.f * ym1_y1 - 1.25f * y[0] - twentyfourth_ym2_y2;
|
|
auto c3 = 5.f / 12.f * y[0] - 7.f / 12.f * y[1] + 7.f / 24.f * y[2] - 1.f / 24.f * (y[-2] + y[-1] + y[3]);
|
|
auto c4 = 0.25f * y[0] - 1.f / 6.f * ym1_y1 + twentyfourth_ym2_y2;
|
|
auto c5 = 1.f / 120.f * (y[3] - y[-2]) + 1.f / 24.f * (y[-1] - y[2]) + 1.f / 12.f * (y[1] - y[0]);
|
|
|
|
// Estrin's scheme
|
|
auto t2 = t * t;
|
|
return (c0 + c1 * t) + (c2 + c3 * t) * t2 + (c4 + c5 * t) * t2 * t2;
|
|
}
|
|
|
|
void generateRandomPhaseVector(float* dst, size_t size, std::mt19937& gen)
|
|
{
|
|
std::vector<std::complex<float> > v(size);
|
|
std::uniform_real_distribution<float> dis(
|
|
-3.14159265358979323846f, 3.14159265358979323846f);
|
|
std::for_each(dst, dst + size, [&dis, &gen](auto& a) { a = dis(gen); });
|
|
}
|
|
} // namespace
|
|
|
|
struct TimeAndPitch::impl
|
|
{
|
|
impl(int fft_size)
|
|
: fft(fft_size)
|
|
{
|
|
randomGenerator.seed(0);
|
|
}
|
|
|
|
FourierTransform fft;
|
|
std::mt19937 randomGenerator;
|
|
CircularSampleBuffer<float> inResampleInputBuffer[2];
|
|
CircularSampleBuffer<float> inCircularBuffer[2];
|
|
CircularSampleBuffer<float> outCircularBuffer[2];
|
|
CircularSampleBuffer<float> normalizationBuffer;
|
|
|
|
SamplesReal fft_timeseries;
|
|
SamplesComplex spectrum;
|
|
SamplesReal norm;
|
|
SamplesReal phase;
|
|
SamplesReal last_phase;
|
|
SamplesReal phase_accum;
|
|
SamplesReal cosWindow;
|
|
SamplesReal sqWindow;
|
|
SamplesReal last_norm;
|
|
SamplesReal random_phases;
|
|
|
|
double exact_hop_a = 512.0, hop_a_err = 0.0;
|
|
double exact_hop_s = 0.0;
|
|
double next_exact_hop_s = 512.0;
|
|
double hop_s_err = 0.0;
|
|
|
|
std::vector<int> peak_index, trough_index;
|
|
};
|
|
|
|
TimeAndPitch::TimeAndPitch(
|
|
int fftSize, bool reduceImaging, ShiftTimbreCb shiftTimbreCb)
|
|
: fftSize(fftSize)
|
|
, _reduceImaging(reduceImaging)
|
|
, _shiftTimbreCb(std::move(shiftTimbreCb))
|
|
{
|
|
}
|
|
|
|
TimeAndPitch::~TimeAndPitch()
|
|
{
|
|
// Here for ~std::shared_ptr<impl>() to know ~impl()
|
|
}
|
|
|
|
void TimeAndPitch::setup(int numChannels, int maxBlockSize)
|
|
{
|
|
assert(numChannels == 1 || numChannels == 2);
|
|
_numChannels = numChannels;
|
|
|
|
d = std::make_unique<impl>(fftSize);
|
|
_maxBlockSize = maxBlockSize;
|
|
_numBins = fftSize / 2 + 1;
|
|
|
|
// audio sample buffers
|
|
d->fft_timeseries.setSize(_numChannels, fftSize);
|
|
int outBufferSize = fftSize + 2 * _maxBlockSize; // 1 block extra for safety
|
|
for (int ch = 0; ch < _numChannels; ++ch) {
|
|
d->inResampleInputBuffer[ch].setSize(_maxBlockSize
|
|
+ 6); // needs additional history for resampling. more in the future
|
|
d->inCircularBuffer[ch].setSize(fftSize);
|
|
d->outCircularBuffer[ch].setSize(outBufferSize);
|
|
}
|
|
d->normalizationBuffer.setSize(outBufferSize);
|
|
|
|
// fft coefficient buffers
|
|
d->spectrum.setSize(_numChannels, _numBins);
|
|
d->norm.setSize(1, _numBins);
|
|
d->last_norm.setSize(1, _numBins);
|
|
d->phase.setSize(_numChannels, _numBins);
|
|
d->last_phase.setSize(_numChannels, _numBins);
|
|
d->phase_accum.setSize(_numChannels, _numBins);
|
|
d->random_phases.setSize(1, _numBins);
|
|
generateRandomPhaseVector(
|
|
d->random_phases.getPtr(0), _numBins, d->randomGenerator);
|
|
|
|
_expectedPhaseChangePerBinPerSample = twoPi / double(fftSize);
|
|
|
|
// Cos window
|
|
// For perfect int-factor overlaps the first sample needs to be 0 and the last non-zero.
|
|
d->cosWindow.setSize(1, fftSize);
|
|
d->sqWindow.setSize(1, fftSize);
|
|
auto* w = d->cosWindow.getPtr(0);
|
|
auto* sqw = d->sqWindow.getPtr(0);
|
|
for (int i = 0; i < fftSize; ++i) {
|
|
w[i] = -0.5f * std::cos(float(twoPi * (float)i / (float)fftSize)) + 0.5f;
|
|
sqw[i] = w[i] * w[i];
|
|
}
|
|
|
|
d->peak_index.reserve(_numBins);
|
|
d->trough_index.reserve(_numBins);
|
|
|
|
// force fft to allocate all buffers now. Currently there is no other way in the fft class
|
|
d->fft.forwardReal(d->fft_timeseries, d->spectrum);
|
|
|
|
reset();
|
|
}
|
|
|
|
int TimeAndPitch::getSamplesToNextHop() const
|
|
{
|
|
return std::max(0, int(std::ceil(d->exact_hop_a)) - _analysis_hop_counter
|
|
+ 1); // 1 extra to counter pitch factor error accumulation
|
|
}
|
|
|
|
int TimeAndPitch::getNumAvailableOutputSamples() const
|
|
{
|
|
return _availableOutputSamples;
|
|
}
|
|
|
|
void TimeAndPitch::reset()
|
|
{
|
|
_analysis_hop_counter = 0;
|
|
_availableOutputSamples = 0;
|
|
for (int ch = 0; ch < _numChannels; ++ch) {
|
|
d->inResampleInputBuffer[ch].reset();
|
|
d->inCircularBuffer[ch].reset();
|
|
d->outCircularBuffer[ch].reset();
|
|
}
|
|
d->normalizationBuffer.reset();
|
|
d->last_norm.zeroOut();
|
|
d->last_phase.zeroOut();
|
|
d->phase_accum.zeroOut();
|
|
_outBufferWriteOffset = 0;
|
|
d->hop_a_err = 0.0;
|
|
d->hop_s_err = 0.0;
|
|
d->exact_hop_s = 0.0;
|
|
_resampleReadPos = 0.0;
|
|
}
|
|
|
|
namespace {
|
|
// wrap a phase value into -PI..PI
|
|
inline float _unwrapPhase(float arg)
|
|
{
|
|
using namespace audio::simd;
|
|
return arg - rint(arg * 0.15915494309f) * 6.283185307f;
|
|
}
|
|
|
|
void _unwrapPhaseVec(float* v, int n)
|
|
{
|
|
audio::simd::perform_parallel_simd_aligned(v, n, [](auto& a) { a = a - rint(a * 0.15915494309f) * 6.283185307f; });
|
|
}
|
|
|
|
/// rotate even-sized array by half its size to align fft phase at the center
|
|
void _fft_shift(float* v, int n)
|
|
{
|
|
assert((n & 1) == 0);
|
|
int n2 = n >> 1;
|
|
audio::simd::perform_parallel_simd_aligned(v, v + n2, n2, [](auto& a, auto& b) {
|
|
auto tmp = a;
|
|
a = b;
|
|
b = tmp;
|
|
});
|
|
}
|
|
|
|
void _lr_to_ms(float* ch1, float* ch2, int n)
|
|
{
|
|
audio::simd::perform_parallel_simd_aligned(ch1, ch2, n, [](auto& a, auto& b) {
|
|
auto l = a, r = b;
|
|
a = 0.5f * (l + r);
|
|
b = 0.5f * (l - r);
|
|
});
|
|
}
|
|
|
|
void _ms_to_lr(float* ch1, float* ch2, int n)
|
|
{
|
|
audio::simd::perform_parallel_simd_aligned(ch1, ch2, n, [](auto& a, auto& b) {
|
|
auto m = a, s = b;
|
|
a = m + s;
|
|
b = m - s;
|
|
});
|
|
}
|
|
} // namespace
|
|
|
|
// ----------------------------------------------------------------------------
|
|
|
|
template<int num_channels>
|
|
void TimeAndPitch::_time_stretch(float a_a, float a_s)
|
|
{
|
|
auto alpha = a_s / a_a; // this is the real stretch factor based on integer hop sizes
|
|
|
|
// Create a norm array
|
|
const auto* norms = d->norm.getPtr(0); // for stereo, just use the mid-channel
|
|
const auto* norms_last = d->last_norm.getPtr(0);
|
|
|
|
d->peak_index.clear();
|
|
d->trough_index.clear();
|
|
float lowest = norms[0];
|
|
int trough = 0;
|
|
if (norms_last[0] >= norms[1]) {
|
|
d->peak_index.emplace_back(0);
|
|
d->trough_index.emplace_back(0);
|
|
}
|
|
for (int i = 1; i < _numBins - 1; ++i) {
|
|
if (norms_last[i] >= norms[i - 1] && norms_last[i] >= norms[i + 1]) {
|
|
d->peak_index.emplace_back(i);
|
|
d->trough_index.emplace_back(trough);
|
|
trough = i;
|
|
lowest = norms[i];
|
|
} else if (norms[i] < lowest) {
|
|
lowest = norms[i];
|
|
trough = i;
|
|
}
|
|
}
|
|
if (norms_last[_numBins - 1] > norms[_numBins - 2]) {
|
|
d->peak_index.emplace_back(_numBins - 1);
|
|
d->trough_index.emplace_back(trough);
|
|
}
|
|
|
|
if (d->peak_index.size() == 0) {
|
|
// use max norm of last frame
|
|
int max_idx = 0;
|
|
float max_norm = 0.f;
|
|
vo::findMaxElement(norms_last, _numBins, max_idx, max_norm);
|
|
d->peak_index.emplace_back(max_idx);
|
|
}
|
|
|
|
const float** p = const_cast<const float**>(d->phase.getPtrs());
|
|
const float** p_l = const_cast<const float**>(d->last_phase.getPtrs());
|
|
float** acc = d->phase_accum.getPtrs();
|
|
|
|
float expChange_a = a_a * float(_expectedPhaseChangePerBinPerSample);
|
|
float expChange_s = a_s * float(_expectedPhaseChangePerBinPerSample);
|
|
|
|
int num_peaks = (int)d->peak_index.size();
|
|
// time integration for all peaks
|
|
for (int i = 0; i < num_peaks; ++i) {
|
|
const int n = d->peak_index[i];
|
|
auto fn = float(n);
|
|
|
|
// determine phase from last frame
|
|
float fn_expChange_a = fn * expChange_a;
|
|
float fn_expChange_s = fn * expChange_s;
|
|
|
|
for (int ch = 0; ch < num_channels; ++ch) {
|
|
acc[ch][n] = acc[ch][n] + alpha * _unwrapPhase(p[ch][n] - p_l[ch][n] - fn_expChange_a) + fn_expChange_s;
|
|
}
|
|
}
|
|
|
|
// go from first peak to 0
|
|
for (int n = d->peak_index[0]; n > 0; --n) {
|
|
for (int ch = 0; ch < num_channels; ++ch) {
|
|
acc[ch][n - 1] = acc[ch][n] - alpha * _unwrapPhase(p[ch][n] - p[ch][n - 1]);
|
|
}
|
|
}
|
|
|
|
// 'grow' from pairs of peaks to the lowest norm in between
|
|
for (int i = 0; i < num_peaks - 1; ++i) {
|
|
const int mid = d->trough_index[i + 1];
|
|
for (int n = d->peak_index[i]; n < mid; ++n) {
|
|
for (int ch = 0; ch < num_channels; ++ch) {
|
|
acc[ch][n + 1] = acc[ch][n] + alpha * _unwrapPhase(p[ch][n + 1] - p[ch][n]);
|
|
}
|
|
}
|
|
for (int n = d->peak_index[i + 1]; n > mid + 1; --n) {
|
|
for (int ch = 0; ch < num_channels; ++ch) {
|
|
acc[ch][n - 1] = acc[ch][n] - alpha * _unwrapPhase(p[ch][n] - p[ch][n - 1]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// last peak to the end
|
|
for (int n = d->peak_index[num_peaks - 1]; n < _numBins - 1; ++n) {
|
|
for (int ch = 0; ch < num_channels; ++ch) {
|
|
acc[ch][n + 1] = acc[ch][n] + alpha * _unwrapPhase(p[ch][n + 1] - p[ch][n]);
|
|
}
|
|
}
|
|
|
|
d->last_norm.assignSamples(d->norm);
|
|
d->last_phase.assignSamples(d->phase);
|
|
}
|
|
|
|
void TimeAndPitch::_applyImagingReduction()
|
|
{
|
|
// Upsampling brings spectral components down, including those that were
|
|
// beyond the Nyquist. From `imagingBeginBin`, we have a mirroring of the
|
|
// spectrum, which, if not lowpass-filtered by the resampler, may yield
|
|
// an aliasing-like quality if what is mirrored is a harmonic series. A
|
|
// simple thing to do against that is to scramble the phase values there.
|
|
// This way we preserve natural energy fluctuations, but the artifact
|
|
// isn't noticeable as much anymore, because much more noise-like, which
|
|
// is what one typically gets at those frequencies.
|
|
constexpr auto alignment = 64 / sizeof(float);
|
|
const int imagingBeginBin
|
|
=std::ceil((fftSize / 2 * _pitchFactor + 1) / alignment) * alignment;
|
|
|
|
if (imagingBeginBin >= _numBins) {
|
|
return;
|
|
}
|
|
|
|
const auto n = _numBins - imagingBeginBin;
|
|
auto pSpec = d->spectrum.getPtr(0);
|
|
auto pRand = d->random_phases.getPtr(0);
|
|
vo::rotate(nullptr, pRand, pSpec + imagingBeginBin, n);
|
|
// Just rotating the random phase vector to produce pseudo-random
|
|
// sequences of phases.
|
|
std::uniform_int_distribution<size_t> dis(0, n - 1);
|
|
const auto middle = dis(d->randomGenerator);
|
|
std::rotate(pRand, pRand + middle, pRand + n);
|
|
}
|
|
|
|
/// process one hop/chunk in _fft_timeSeries and add the result to output circular buffer
|
|
void TimeAndPitch::_process_hop(int hop_a, int hop_s)
|
|
{
|
|
if (d->exact_hop_a != d->exact_hop_s) {
|
|
if (_numChannels == 2) {
|
|
_lr_to_ms(d->fft_timeseries.getPtr(0), d->fft_timeseries.getPtr(1), fftSize);
|
|
}
|
|
|
|
for (int ch = 0; ch < _numChannels; ++ch) {
|
|
vo::multiply(d->fft_timeseries.getPtr(ch), d->cosWindow.getPtr(0), d->fft_timeseries.getPtr(ch), fftSize);
|
|
_fft_shift(d->fft_timeseries.getPtr(ch), fftSize);
|
|
}
|
|
|
|
// determine norm/phase
|
|
d->fft.forwardReal(d->fft_timeseries, d->spectrum);
|
|
// norms of the mid channel only (or sole channel) are needed in
|
|
// _time_stretch
|
|
vo::calcNorms(d->spectrum.getPtr(0), d->norm.getPtr(0), d->spectrum.getNumSamples());
|
|
for (int ch = 0; ch < _numChannels; ++ch) {
|
|
vo::calcPhases(d->spectrum.getPtr(ch), d->phase.getPtr(ch), d->spectrum.getNumSamples());
|
|
}
|
|
|
|
if (_shiftTimbreCb) {
|
|
_shiftTimbreCb(
|
|
1 / _pitchFactor, d->spectrum.getPtr(0), d->norm.getPtr(0));
|
|
}
|
|
|
|
if (_reduceImaging && _pitchFactor < 1.) {
|
|
_applyImagingReduction();
|
|
}
|
|
|
|
if (_numChannels == 1) {
|
|
_time_stretch<1>((float)hop_a, (float)hop_s);
|
|
} else if (_numChannels == 2) {
|
|
_time_stretch<2>((float)hop_a, (float)hop_s);
|
|
}
|
|
|
|
for (int ch = 0; ch < _numChannels; ++ch) {
|
|
_unwrapPhaseVec(d->phase_accum.getPtr(ch), _numBins);
|
|
}
|
|
|
|
for (int ch = 0; ch < _numChannels; ++ch) {
|
|
vo::rotate(d->phase.getPtr(ch), d->phase_accum.getPtr(ch), d->spectrum.getPtr(ch),
|
|
d->spectrum.getNumSamples());
|
|
}
|
|
d->fft.inverseReal(d->spectrum, d->fft_timeseries);
|
|
|
|
for (int ch = 0; ch < _numChannels; ++ch) {
|
|
vo::constantMultiply(d->fft_timeseries.getPtr(ch), 1.f / fftSize, d->fft_timeseries.getPtr(ch),
|
|
d->fft_timeseries.getNumSamples());
|
|
}
|
|
|
|
if (_numChannels == 2) {
|
|
_ms_to_lr(d->fft_timeseries.getPtr(0), d->fft_timeseries.getPtr(1), fftSize);
|
|
}
|
|
|
|
for (int ch = 0; ch < _numChannels; ++ch) {
|
|
_fft_shift(d->fft_timeseries.getPtr(ch), fftSize);
|
|
vo::multiply(d->fft_timeseries.getPtr(ch), d->cosWindow.getPtr(0), d->fft_timeseries.getPtr(ch), fftSize);
|
|
}
|
|
} else { // stretch factor == 1.0 => just apply window
|
|
for (int ch = 0; ch < _numChannels; ++ch) {
|
|
vo::multiply(d->fft_timeseries.getPtr(ch), d->sqWindow.getPtr(0), d->fft_timeseries.getPtr(ch), fftSize);
|
|
}
|
|
}
|
|
|
|
{
|
|
float gainFact = float(_timeStretch * ((8.f / 3.f) / _overlap_a)); // overlap add normalization factor
|
|
for (int ch = 0; ch < _numChannels; ++ch) {
|
|
d->outCircularBuffer[ch].writeAddBlockWithGain(_outBufferWriteOffset, fftSize, d->fft_timeseries.getPtr(ch),
|
|
gainFact);
|
|
}
|
|
|
|
if (normalize_window) {
|
|
d->normalizationBuffer.writeAddBlockWithGain(_outBufferWriteOffset, fftSize, d->sqWindow.getPtr(0), gainFact);
|
|
}
|
|
}
|
|
_outBufferWriteOffset += hop_s;
|
|
_availableOutputSamples += hop_s;
|
|
}
|
|
|
|
void TimeAndPitch::setTimeStretchAndPitchFactor(double timeScale, double pitchFactor)
|
|
{
|
|
assert(timeScale > 0.0);
|
|
assert(pitchFactor > 0.0);
|
|
_pitchFactor = pitchFactor;
|
|
_timeStretch = timeScale * pitchFactor;
|
|
|
|
_overlap_a = overlap;
|
|
double overlap_s = overlap;
|
|
if (_timeStretch > 1.0 || !modulate_synthesis_hop) {
|
|
_overlap_a *= _timeStretch;
|
|
} else {
|
|
overlap_s /= _timeStretch;
|
|
}
|
|
|
|
d->exact_hop_a = double(fftSize) / _overlap_a;
|
|
if (!modulate_synthesis_hop) {
|
|
d->exact_hop_s = double(fftSize) / overlap_s;
|
|
} else {
|
|
// switch after processing next block, unless it's the first one
|
|
d->next_exact_hop_s = double(fftSize) / overlap_s;
|
|
if (d->exact_hop_s == 0.0) { // on first chunk set it immediately
|
|
d->exact_hop_s = d->next_exact_hop_s;
|
|
}
|
|
}
|
|
}
|
|
|
|
void TimeAndPitch::feedAudio(const float* const* input_smp, int numSamples)
|
|
{
|
|
assert(numSamples <= _maxBlockSize);
|
|
for (int ch = 0; ch < _numChannels; ++ch) {
|
|
d->inResampleInputBuffer[ch].writeBlock(0, numSamples, input_smp[ch]);
|
|
d->inResampleInputBuffer[ch].advance(numSamples);
|
|
}
|
|
_resampleReadPos -= numSamples;
|
|
|
|
// determine integer hop size for the next hop.
|
|
// The error accumulators will make the value fluctuate by one to reach arbitrary scale
|
|
// ratios
|
|
if (d->exact_hop_s == 0.0) { // this happens if feedAudio is called without setting up stretch factors
|
|
d->exact_hop_s = d->next_exact_hop_s;
|
|
}
|
|
|
|
const int hop_s = int(d->exact_hop_s + d->hop_s_err);
|
|
const int hop_a = int(d->exact_hop_a + d->hop_a_err);
|
|
|
|
double step = 0.0;
|
|
double read_pos = _resampleReadPos;
|
|
while (read_pos < 0.0)
|
|
{
|
|
auto int_pos = int(std::floor(read_pos));
|
|
float frac_pos = float(read_pos - int_pos);
|
|
for (int ch = 0; ch < _numChannels; ++ch) {
|
|
float smp[6];
|
|
d->inResampleInputBuffer[ch].readBlock(int_pos - 6, 6, smp);
|
|
float s = (frac_pos == 0) ? smp[2] : lagrange6(smp, frac_pos);
|
|
d->inCircularBuffer[ch].writeOffset0(s);
|
|
d->inCircularBuffer[ch].advance(1);
|
|
}
|
|
_analysis_hop_counter++;
|
|
step++;
|
|
|
|
if (_analysis_hop_counter >= hop_a) {
|
|
_analysis_hop_counter -= hop_a;
|
|
d->hop_s_err += d->exact_hop_s - hop_s;
|
|
d->hop_a_err += d->exact_hop_a - hop_a;
|
|
for (int ch = 0; ch < _numChannels; ++ch) {
|
|
d->inCircularBuffer[ch].readBlock(-fftSize, fftSize, d->fft_timeseries.getPtr(ch));
|
|
}
|
|
_process_hop(hop_a, hop_s);
|
|
}
|
|
read_pos = _resampleReadPos + step * _pitchFactor;
|
|
}
|
|
_resampleReadPos = read_pos;
|
|
}
|
|
|
|
void TimeAndPitch::retrieveAudio(float* const* out_smp, int numSamples)
|
|
{
|
|
assert(numSamples <= _maxBlockSize);
|
|
for (int ch = 0; ch < _numChannels; ++ch) {
|
|
d->outCircularBuffer[ch].readAndClearBlock(0, numSamples, out_smp[ch]);
|
|
if (normalize_window) {
|
|
constexpr float curve
|
|
=4.f * 4.f; // the curve approximates 1/x over 1 but fades to 0 near 0 to avoid fade-in clicks
|
|
for (int i = 0; i < numSamples; ++i) {
|
|
float x = d->normalizationBuffer.read(i);
|
|
out_smp[ch][i] *= x / (x * x + 1 / curve);
|
|
}
|
|
}
|
|
d->outCircularBuffer[ch].advance(numSamples);
|
|
}
|
|
|
|
d->normalizationBuffer.clearBlock(0, numSamples);
|
|
d->normalizationBuffer.advance(numSamples);
|
|
|
|
_outBufferWriteOffset -= numSamples;
|
|
_availableOutputSamples -= numSamples;
|
|
|
|
if (modulate_synthesis_hop) {
|
|
d->exact_hop_s = d->next_exact_hop_s;
|
|
}
|
|
}
|
|
|
|
void TimeAndPitch::processPitchShift(float* const* smp, int numSamples, double pitchFactor)
|
|
{
|
|
setTimeStretchAndPitchFactor(1.0, pitchFactor);
|
|
feedAudio(smp, numSamples);
|
|
retrieveAudio(smp, numSamples);
|
|
}
|
|
|
|
int TimeAndPitch::getLatencySamples() const
|
|
{
|
|
return fftSize - fftSize / overlap + 3; // 3 for resampling
|
|
}
|
|
|
|
int TimeAndPitch::getLatencySamplesForStretchRatio(float timeStretch) const
|
|
{
|
|
const float coeff = (timeStretch < 1.f) ? (1.f / 3.f) : (2.f / 3.f);
|
|
return int(getLatencySamples() * (timeStretch * coeff + (1 - coeff)));
|
|
}
|
|
} // namespace staffpad
|