I’m doing this at the end of loop, to save a cpu resources.
void process(...) {
for (loop) {
...
}
state.phase = state.phase - PhaseLengthDouble * int(state.phase * PhaseLengthDoubleMult); // same as std::fmod
}
I’m guessing that state.sampleA and state.sampleB are two wavetables but I’m not sure what the scheme is here, for example, why you’ve got two fmPhases or why state.transitionFrac could be lower than 0.0.
Two fmAmount and it’s because I need smooth crossfade between FM modulation. For example if LFO/or user set FM Amount to 1.0 and it was before 0.0 or 0.5, then Sine at the low notes may crackle due to abrupt transition of amplitude.
Same actually for stateA and stateB it’s for morphing between two different waves in the wavetable. If user or LFO changing sample, then to avoid crackles I’m doing crossfade for about 0.005 or 0.007 seconds (it’s transition about 220.5 sample at 44100 sample rate). These crackles are especially audible if it is, for example, a transition from a wave where the phase significantly changes its amplitude: From Sine to Square or Square to Triangle, etc.
the state.transitionFrac < 0 in my code mean’s NoTransitionRequired, sorry for confusion. state.transitionFrac can be -2.0, -1.0 or 0.0-1.0.
-2.0: Sample need to be initialized. although I’ll move it to noteOn or can use simply sampleA == nullptr.
-1.0: No transition. i.e. I’m using just sampleA from state.
That’s just to not to create extra bool values like bool transitionRequired, sampleInitialized, since I’m also copying osc State from Heap memory to the Stack before main loop. That is, I squeeze it to the maximum and save processor resources.
Here is code. It looks a little ugly for now, but I’m still testing, in the future of course it will be the norm:
struct OscState {
DoubleType increment{};
DoubleType phase{};
DoubleType morphRatioA{};
DoubleType fmAmountA{};
DoubleType morphRatioB{};
DoubleType fmAmountB{};
DoubleType transitionFrac{-2.0}; // -2 sample init -1 no transition
DoubleType *sampleA{};
DoubleType *sampleB{};
} JUCE_PACKED;
OscState state = state_; // copy on stack
for (loop){
...
}
state.phase = state.phase - PhaseLengthDouble * int(state.phase * PhaseLengthDoubleMult); // same as std::fmod
state_ = state; // copy back to the HEAP memory
Then, over the next 64 samples or so I interpolate between ‘src’ and ‘dst’ samples. At the end of 64 samples I swap the ‘src’ and ‘dst’ buffer pointers and re-trigger the band-limited wavetable generation into ‘dst’
When you say you are interpolating between src and dst. Are you doing the same as me with transitionFrac ? or do you also do an additional hermite interpolation somehow between these two waves? Or just linear transition between two samples like me? soundA * a + soundB * b
I’m also do swap between samples at the end of transition. Simply changing pointers. But I also check that the values in B state did not receive new values until the transition ended.
So I have something like stateA, stateB, stateReal.
auto newOctave = ...
auto fmAmount = ...
auto morphRatio = ...
// Init sample TODO: Move this to the noteOn()
if (state.transitionFrac < -1.0) {
state.transitionFrac = -1.0;
state.morphRatioA = morphRatio;
state.fmAmountA = fmAmount;
state.sampleA = oscConfiguration_.waveTable.getSample(newOctave, morphRatio);
state.sampleB = oscConfiguration_.waveTable.getSample(newOctave, morphRatio);
currentOctave = newOctave;
} else {
// Has changes?
if (state.transitionFrac < 0.0) {
const bool hasMorphChanges = morphRatio != state.morphRatioA;
const bool hasFMChanges = fmAmount != state.fmAmountA;
if (hasMorphChanges || hasFMChanges) {
state.morphRatioB = morphRatio;
state.fmAmountB = fmAmount;
state.transitionFrac = 0.0; // initiate transition process
currentOctave = newOctave;
if (hasMorphChanges) {
state.sampleB = oscConfiguration_.waveTable.getSample(newOctave, morphRatio);
}
}
}
}
// Transition in progress
if (state.transitionFrac > -1.0) {
state.transitionFrac += transitionIncrement;
if (state.transitionFrac >= 1.0) {
state.transitionFrac = -1.0; // end of transition
state.morphRatioA = state.morphRatioB;
state.fmAmountA = state.fmAmountB;
std::swap(state.sampleA, state.sampleB);
}
}
if (currentOctave != newOctave) {
currentOctave = newOctave;
state.sampleA = oscConfiguration_.waveTable.getSample(newOctave, state.morphRatioA);
state.sampleB = oscConfiguration_.waveTable.getSample(newOctave, state.morphRatioB);
}