./duplicate.py DigitalDistortion/ Synthesis YourName
ProcessDoubleReplacing
function. Instead, create an Oscillator
class, call its functions from ProcessDoubleReplacing
, and it will fill the output buffer with double
values to create a waveform. First, we use an intuitive approach. Later, faced with the shortcomings of this approach, we will find a way to achieve a better sound.#define
and #endif
in Oscillator.h : #include <math.h> enum OscillatorMode { OSCILLATOR_MODE_SINE, OSCILLATOR_MODE_SAW, OSCILLATOR_MODE_SQUARE, OSCILLATOR_MODE_TRIANGLE }; class Oscillator { private: OscillatorMode mOscillatorMode; const double mPI; double mFrequency; double mPhase; double mSampleRate; double mPhaseIncrement; void updateIncrement(); public: void setMode(OscillatorMode mode); void setFrequency(double frequency); void setSampleRate(double sampleRate); void generate(double* buffer, int nFrames); Oscillator() : mOscillatorMode(OSCILLATOR_MODE_SINE), mPI(2*acos(0.0)), mFrequency(440.0), mPhase(0.0), mSampleRate(44100.0) { updateIncrement(); }; };
enum
. Now the default sine will be generated, but this can be changed using the member function of the setMode
class.generate
. This is the very function that fills the output buffer with values. void Oscillator::setMode(OscillatorMode mode) { mOscillatorMode = mode; } void Oscillator::setFrequency(double frequency) { mFrequency = frequency; updateIncrement(); } void Oscillator::setSampleRate(double sampleRate) { mSampleRate = sampleRate; updateIncrement(); } void Oscillator::updateIncrement() { mPhaseIncrement = mFrequency * 2 * mPI / mSampleRate; }
mPhaseIncrement
depends on mFrequency
and mSampleRate
, so that it is updated each time one of these two parameters changes. We could calculate it each sample in ProcessDoubleReplacing
, but it’s much better to do it here.generate
function at the moment looks like this: void Oscillator::generate(double* buffer, int nFrames) { const double twoPI = 2 * mPI; switch (mOscillatorMode) { case OSCILLATOR_MODE_SINE: // ... break; case OSCILLATOR_MODE_SAW: // ... break; case OSCILLATOR_MODE_SQUARE: // ... break; case OSCILLATOR_MODE_TRIANGLE: // ... break; } }
ProcessDoubleReplacing
is ProcessDoubleReplacing
. Switch
used so that the appropriate code is executed depending on the desired waveform. case OSCILLATOR_MODE_SINE: for (int i = 0; i < nFrames; i++) { buffer[i] = sin(mPhase); mPhase += mPhaseIncrement; while (mPhase >= twoPI) { mPhase -= twoPI; } } break;
mFrequency
and mSampleRate
. We increment the mPhase
and limit it to between 0
and twoPI
. The only "difficult" operation here is the function call sin()
, which is performed at the hardware level on most systems. case OSCILLATOR_MODE_SAW: for (int i = 0; i < nFrames; i++) { buffer[i] = 1.0 - (2.0 * mPhase / twoPI); mPhase += mPhaseIncrement; while (mPhase >= twoPI) { mPhase -= twoPI; } } break;
mPhase
increases from 0
and jumps back to zero when the twoPI
value is twoPI
.(mPhase / twoPI)
increases from 0
to 1
and jumps back to zero.(2.0 * mPhase / twoPI)
increases from 0
, and jumps back as soon as it reaches two.mPhase
is 0
, the expression 1.0 - (2.0 * mPhase / twoPI)
is 1
. As mPhase
rises, the value of this expression drops and, as soon as it reaches -1
, jumps to 1
.while
condition would seem redundant - it will occur in each case
. But if you do otherwise, then you have to turn on the switch
in the loop. So we even get rid of the excess for
, but then the switch
will be executed more often than necessary. case OSCILLATOR_MODE_SQUARE: for (int i = 0; i < nFrames; i++) { if (mPhase <= mPI) { buffer[i] = 1.0; } else { buffer[i] = -1.0; } mPhase += mPhaseIncrement; while (mPhase >= twoPI) { mPhase -= twoPI; } } break;
twoPI
. The body of the if
sets the first half of the cycle to 1
, the second to -1
. When mPhase
becomes larger than mPI
, a sharp jump appears in the wave form. It looks like a meander. case OSCILLATOR_MODE_TRIANGLE: for (int i = 0; i < nFrames; i++) { double value = -1.0 + (2.0 * mPhase / twoPI); buffer[i] = 2.0 * (fabs(value) - 0.5); mPhase += mPhaseIncrement; while (mPhase >= twoPI) { mPhase -= twoPI; } } break;
-1.0 + (2.0 * mPhase / twoPI)
in parts, as I did before, you will see that this is the opposite of a saw. The absolute value ( fabs
) of the ascending saw means that all values below 0 will be inverted (inverted relative to the x axis).0.5
aligns the waveform with respect to zero. Multiplying by 2.0
scales the value, and it changes from -1
to 1
. Here is a triangle.Oscillator
member to the Synthesis
class: // ... #include "Oscillator.h" class Synthesis : public IPlug { // ... private: double mFrequency; void CreatePresets(); Oscillator mOscillator; };
mThreshold
to mFrequency
. GetParam(kFrequency)->InitDouble("Frequency", 440.0, 50.0, 20000.0, 0.01, "Hz"); GetParam(kFrequency)->SetShape(2.0);
createPresets
function: void Synthesis::CreatePresets() { MakePreset("clean", 440.0); }
Reset
function: void Synthesis::Reset() { TRACE; IMutexLock lock(this); mOscillator.setSampleRate(GetSampleRate()); }
GetSampleRate
inherited from the IPlugBase
class.OnParamChange
also needs to be edited so that you can change the frequency with the pen: void Synthesis::OnParamChange(int paramIdx) { IMutexLock lock(this); switch (paramIdx) { case kFrequency: mOscillator.setFrequency(GetParam(kFrequency)->Value()); break; default: break; } }
ProcessDoubleReplacing
: void Synthesis::ProcessDoubleReplacing(double** inputs, double** outputs, int nFrames) { // Mutex is already locked for us. double *leftOutput = outputs[0]; double *rightOutput = outputs[1]; mOscillator.generate(leftOutput, nFrames); // Copy left buffer into right buffer: for (int s = 0; s < nFrames; ++s) { rightOutput[s] = leftOutput[s]; } }
mOscillator
fills the left channel buffer, and we simply copy these values into the right buffer.mOscillatorMode
in Oscillator.h during the initialization phase in the constructor: Oscillator() : mOscillatorMode(OSCILLATOR_MODE_SAW), mPI(2*acos(0.0)), mFrequency(440.0), mPhase(0.0), mSampleRate(44100.0) { updateIncrement(); };
OSCILLATOR_MODE_SQUARE
and OSCILLATOR_MODE_TRIANGLE
, they have different timbres.mPhase
is subtracted from twoPI
and the value of the expression again becomes less than the mPI
. The basic idea is that abrupt signal jumps mean that it contains many high-frequency components.Source: https://habr.com/ru/post/226439/
All Articles