Skip to content

Commit 444e837

Browse files
committed
more pitch shift groundwork
1 parent de4e804 commit 444e837

20 files changed

Lines changed: 528 additions & 16 deletions

kitdsp/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ add_library(KitDSP STATIC
1313
src/snesBitcrush.cpp
1414
src/dsfOscillator.cpp
1515
src/chorus.cpp
16-
src/harmonizer.cpp
1716
src/frequencyShifter.cpp
17+
src/pitch/h910PitchShifter.cpp
1818
src/pitch/zeroCrossingPitchDetector.cpp
1919
)
2020

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
#include "kitdsp/math/vector.h"
88

99
namespace kitdsp {
10-
class Harmonizer {
10+
class H910PitchShifter {
1111
public:
12-
Harmonizer(etl::span<float> buffer, float sampleRate);
12+
H910PitchShifter(etl::span<float> buffer, float sampleRate);
1313
void Reset();
1414

1515
void SetParams(float pitchRatio, float grainSizeMs = 30.0f, float baseDelayMs = 0.0f, float feedback = 0.0f);

kitdsp/include/kitdsp/chunkifier.h

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#pragma once
2+
3+
#include <etl/span.h>
4+
#include <cassert>
5+
#include "kitdsp/delayLine.h"
6+
#include "kitdsp/macros.h"
7+
8+
namespace kitdsp {
9+
/**
10+
* Turns a continuous signal into equally sized chunks (for STFT or other chunk based algorithms).
11+
*/
12+
template <typename TSample>
13+
class Chunkifier {
14+
public:
15+
static constexpr size_t GetDesiredBufferSize(size_t chunkLength) { return chunkLength * 2; }
16+
/**
17+
* @param buffer should be sized to fit GetDesiredBufferSize()
18+
* @param chunkLength should be the number of samples to include in a chunk
19+
* @param overlapSamples should be the number of samples that should be included in the last chunk and the current
20+
* chunk. 0 means no overlap.
21+
*/
22+
Chunkifier(etl::span<TSample> buffer, size_t chunkLength, size_t overlapSamples)
23+
: mBuf({buffer.data(), chunkLength}),
24+
mChunk(buffer.data() + chunkLength, chunkLength),
25+
mHopSize(chunkLength - overlapSamples) {
26+
assert(buffer.size() == GetDesiredBufferSize(chunkLength));
27+
assert(overlapSamples <= chunkLength);
28+
}
29+
void Reset() {
30+
mBuf.Reset();
31+
mHopIdx = 0;
32+
}
33+
template <typename TFn>
34+
void Process(TSample input, TFn fn) {
35+
mBuf.Write(input);
36+
mHopIdx++;
37+
if (mHopIdx > mHopSize) {
38+
// mChunk is necessary here because the delay line might not be continuous
39+
mBuf.ReadChunk(mChunk.size() - 1, mChunk);
40+
fn(mChunk);
41+
mHopIdx -= mHopSize;
42+
}
43+
}
44+
45+
private:
46+
DelayLine<TSample> mBuf;
47+
etl::span<TSample> mChunk;
48+
size_t mHopIdx = 0;
49+
size_t mHopSize;
50+
};
51+
} // namespace kitdsp

kitdsp/include/kitdsp/control/adsr.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ class ApproachAdsr {
3737
// 0 -> 1
3838
mAttackH = Approach::CalculateHalfLifeFromSettleTime(attackMs, sampleRate, cSettlePrecision, 1.0f);
3939
// 1 -> sustain
40-
mDecayH = Approach::CalculateHalfLifeFromSettleTime(decayMs, sampleRate, cSettlePrecision, 1.0f - sustainValue);
40+
mDecayH = Approach::CalculateHalfLifeFromSettleTime(decayMs, sampleRate, cSettlePrecision,
41+
kitdsp::max(0.001f, 1.0f - sustainValue));
4142
mSustain = sustainValue;
4243
// sustain -> 0
4344
// sustain needs to at least be a little bit above 0 to avoid infinities

kitdsp/include/kitdsp/delayLine.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <etl/span.h>
44
#include <cstdint>
55
#include <cstring>
6+
#include "kitdsp/macros.h"
67
#include "kitdsp/math/interpolate.h"
78

89
namespace kitdsp {
@@ -28,6 +29,7 @@ class DelayLine {
2829
}
2930

3031
inline const TSample Read(int32_t delayIndex) const {
32+
assert(delayIndex < narrow_cast<int32_t>(Size()));
3133
// non-interpolating read
3234
size_t size = mBuffer.size();
3335
// TODO: if size is a power-of-two this could be a cheap & instead
@@ -46,20 +48,31 @@ class DelayLine {
4648
using namespace interpolate;
4749
switch (strategy) {
4850
case InterpolationStrategy::None: {
51+
assert(idx >= 0 && idx < Size());
4952
return Read(idx);
5053
};
5154
case InterpolationStrategy::Linear: {
55+
assert(idx >= 0 && idx < narrow_cast<int32_t>(Size()) - 1);
5256
return linear(Read(idx), Read(idx + 1), frac);
5357
};
5458
case InterpolationStrategy::Hermite: {
59+
assert(idx >= 1 && idx < narrow_cast<int32_t>(Size()) - 2);
5560
return hermite4pt3oX(Read(idx - 1), Read(idx), Read(idx + 1), Read(idx + 2), frac);
5661
};
5762
case InterpolationStrategy::Cubic: {
63+
assert(idx >= 1 && idx < narrow_cast<int32_t>(Size()) - 2);
5864
return cubic(Read(idx - 1), Read(idx), Read(idx + 1), Read(idx + 2), frac);
5965
};
6066
}
6167
}
6268

69+
void ReadChunk(size_t startSample, etl::span<TSample>& out) {
70+
assert(startSample - static_cast<int32_t>(out.size()) + 1 >= 0);
71+
for (size_t i = 0; i < out.size(); ++i) {
72+
out[i] = Read(startSample - i);
73+
}
74+
}
75+
6376
/**
6477
* This is a shorcut if you're planning to use your delay as part of an allpass filter.
6578
*/

kitdsp/include/kitdsp/lookupTables/sineLut.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ namespace kitdsp {
1010
inline float sin2pif_lut(float x) {
1111
return lut[static_cast<int32_t>(x * 1024) % 1024];
1212
}
13+
inline float cos2pif_lut(float x) {
14+
return sin2pif_lut(x + 0.25f);
15+
}
1316
} // namespace kitdsp
1417

1518
namespace {

kitdsp/include/kitdsp/math/shy_fft.h

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
//
2727

2828
// ported from stmlib: https://github.com/pichenettes/stmlib/blob/master/fft/shy_fft.h
29+
// plus some comments to document the input/output formats
30+
// extra notes here:
31+
// https://forum.electro-smith.com/t/my-battle-with-shy-fft-h-and-what-it-taught-me-shyfft-quick-guide/8455/5
2932
// @KitDSP
3033
// -----------------------------------------------------------------------------
3134
//
@@ -678,6 +681,12 @@ struct InverseTransform<T, 2, Phasor> {
678681
}
679682
};
680683

684+
/**
685+
* ShyFFT public class.
686+
* @param T the sample format
687+
* @param size the max number of samples in the input/output buffers
688+
* @param Phasor pick between LutPhasor (more memory, faster) and RotationPhaser
689+
*/
681690
template <typename T = float, size_t size = 16, template <typename, size_t> class Phasor = LutPhasor>
682691
class ShyFFT {
683692
public:
@@ -708,21 +717,44 @@ class ShyFFT {
708717
phasor_.Init();
709718
}
710719

720+
/**
721+
* perform direct FFT.
722+
* @param input the input buffer. should be `size` samples long. this is used destructively.
723+
* @param output the output buffer. should be `size` samples long, returns the real components (from [0, size*0.5),
724+
* then the imaginary components (from [size*0.5, size), non-interleaved.
725+
*/
711726
void Direct(T* input, T* output) {
712727
DirectTransform<T, num_passes, Phasor<T, num_passes> > d;
713728
d(input, output, num_passes <= 8 ? &bit_rev_[0] : bit_rev_256_lut_, &phasor_);
714729
}
715730

731+
/**
732+
* perform inverse FFT.
733+
* @param input the input buffer. should be the real values, then the imaginary. this is used destructively.
734+
* @param output the output buffer. should be `size` samples long, this is an audio signal.
735+
*/
716736
void Inverse(T* input, T* output) {
717737
InverseTransform<T, num_passes, Phasor<T, num_passes> > i;
718738
i(input, output, num_passes <= 8 ? &bit_rev_[0] : bit_rev_256_lut_, &phasor_);
719739
}
720740

741+
/**
742+
* perform direct FFT.
743+
* @param input the input buffer. should be `n` samples long. this is used destructively.
744+
* @param output the output buffer. should be `n` samples long, non-interleaved.
745+
* @param n the size of the buffer.
746+
*/
721747
void Direct(T* input, T* output, size_t n) {
722748
DirectTransform<T, num_passes, Phasor<T, num_passes> > d;
723749
d(input, output, bit_rev_256_lut_, &phasor_, n);
724750
}
725751

752+
/**
753+
* perform inverse FFT.
754+
* @param input the input buffer. should be `n` samples long, non-interleaved. this is used destructively.
755+
* @param output the output buffer. should be `n` samples long.
756+
* @param n the size of the buffer.
757+
*/
726758
void Inverse(T* input, T* output, size_t n) {
727759
InverseTransform<T, num_passes, Phasor<T, num_passes> > i;
728760
i(input, output, bit_rev_256_lut_, &phasor_, n);

kitdsp/include/kitdsp/math/stft.h

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#pragma once
2+
3+
#include "kitdsp/chunkifier.h"
4+
#include "kitdsp/lookupTables/sineLut.h"
5+
#include "kitdsp/math/shy_fft.h"
6+
7+
namespace {
8+
float hammingWindow(float t) {
9+
return 0.54f - (0.46f * kitdsp::cos2pif_lut(t));
10+
}
11+
12+
float hannWindow(float t) {
13+
return 0.5f * (1.0f - kitdsp::cos2pif_lut(t));
14+
}
15+
16+
float rectangleWindow(float t) {
17+
return 1;
18+
}
19+
} // namespace
20+
21+
namespace kitdsp {
22+
template <size_t CHUNK_SIZE>
23+
class STFT {
24+
public:
25+
static constexpr size_t GetDesiredBufferSize() { return CHUNK_SIZE * 3; }
26+
static constexpr size_t GetDesiredOverlap() { return CHUNK_SIZE / 2; }
27+
28+
explicit STFT(etl::span<float> buffer)
29+
: mChunkifier({buffer.data(), CHUNK_SIZE * 2}, CHUNK_SIZE, GetDesiredOverlap()),
30+
mOut(buffer.data() + CHUNK_SIZE * 2, CHUNK_SIZE),
31+
mFFT() {
32+
mFFT.Init();
33+
}
34+
35+
void Process(float in) {
36+
mChunkifier.Process(in, [this](etl::span<float> chunk) {
37+
for (size_t i = 0; i < chunk.size(); ++i) {
38+
chunk[i] = chunk[i] * hammingWindow(static_cast<float>(i) / static_cast<float>(chunk.size()));
39+
}
40+
mFFT.Direct(chunk.data(), mOut.data());
41+
});
42+
}
43+
44+
etl::span<float> mOut;
45+
46+
private:
47+
Chunkifier<float> mChunkifier;
48+
ShyFFT<float, CHUNK_SIZE> mFFT;
49+
};
50+
} // namespace kitdsp
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#pragma once
2+
3+
#include "kitdsp/control/lfo.h"
4+
#include "kitdsp/delayLine.h"
5+
#include "kitdsp/filters/biquad.h"
6+
7+
namespace kitdsp {
8+
class H910PitchShifter {
9+
public:
10+
H910PitchShifter(etl::span<float> buffer, float sampleRate);
11+
void Reset();
12+
13+
void SetParams(float pitchRatio, float grainSizeMs = 30.0f, float baseDelayMs = 0.0f, float feedback = 0.0f);
14+
15+
float Process(float in);
16+
17+
size_t GetMaxGrainSize() const;
18+
19+
size_t GetEffectiveDelay() const;
20+
21+
private:
22+
DelayLine<float> mDelayLine;
23+
lfo::Phasor mGrainPhasor;
24+
float mSampleRate;
25+
float mGrainSizeSamples;
26+
float mBaseDelaySamples;
27+
bool mSlowing;
28+
float mFeedback;
29+
rbj::BiquadFilter<rbj::BiquadFilterMode::LowPass> mFilterOut;
30+
};
31+
} // namespace kitdsp

kitdsp/include/kitdsp/pitch/psolaPitchShifter.h

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ namespace pitch {
3434
*/
3535
class PsolaPitchShifter {
3636
struct Grain {
37-
float sizeSamples;
38-
float samplesPlayed;
39-
float speed;
37+
float sizeSamples = 0.0f;
38+
float samplesPlayed = 0.0f;
39+
float speed = 0.0f;
4040
explicit Grain(DelayLine<float>& buf) : mBuf(buf) {}
4141
void Set(float start, float size, float speed) {
4242
this->pos = start;
@@ -49,6 +49,9 @@ class PsolaPitchShifter {
4949
samplesPlayed += speed;
5050
}
5151
float Read() {
52+
if (sizeSamples == 0.0f) {
53+
return 0.0f;
54+
}
5255
float progress = kitdsp::clamp(samplesPlayed / sizeSamples, 0.0f, 1.0f);
5356
using namespace kitdsp::interpolate;
5457
return mBuf.Read<InterpolationStrategy::Linear>(pos) * hanningWindow(progress);
@@ -66,6 +69,7 @@ class PsolaPitchShifter {
6669
mSpeed = pitchMultiplier;
6770
mPeriod = 0.05f * sampleRate;
6871
float overlapRatio = 1.0f - 0.2f; // TODO not this
72+
assert(pitchMultiplier != 0);
6973
mGrainPicker.SetPeriodSamples(overlapRatio * (mPeriod / pitchMultiplier));
7074
}
7175
float Process(float input) {

0 commit comments

Comments
 (0)