comments
This commit is contained in:
@@ -21,10 +21,11 @@ This synthesizer isn't very good, but it's neat :3
|
|||||||
oscillators increase the sound complexity considerably
|
oscillators increase the sound complexity considerably
|
||||||
- [x] Create a UI scope to visualize the synthesized composite waveform
|
- [x] Create a UI scope to visualize the synthesized composite waveform
|
||||||
- [ ] Create wavetables for more complex tone generation. Needs to be selectable from ui
|
- [ ] Create wavetables for more complex tone generation. Needs to be selectable from ui
|
||||||
|
- [ ] Wavetable file loading
|
||||||
- [x] Create digital filters, prob biquad. Controllable from ui obv (cutoff + resonance)
|
- [x] Create digital filters, prob biquad. Controllable from ui obv (cutoff + resonance)
|
||||||
- [x] Add polyphony somewhere. Probably involves a voice class. If processing power
|
- [x] Add polyphony somewhere. Probably involves a voice class. If processing power
|
||||||
allows it, tie a voice to each midi note
|
allows it, tie a voice to each midi note
|
||||||
- [ ] polyphony is lacking sustain pedal rn
|
- [x] polyphony is lacking sustain pedal rn
|
||||||
- [ ] planning gets sparse this far out because its how far I got with the ESP32 synth
|
- [ ] planning gets sparse this far out because its how far I got with the ESP32 synth
|
||||||
- [ ] Filter upgrades including some more complex solving techniques (State Variable Filter),
|
- [ ] Filter upgrades including some more complex solving techniques (State Variable Filter),
|
||||||
better key tracking, more natural envelope curves, filter drive, etc.
|
better key tracking, more natural envelope curves, filter drive, etc.
|
||||||
@@ -34,6 +35,7 @@ This synthesizer isn't very good, but it's neat :3
|
|||||||
- [ ] Profile saving and loading, also includes loading configurations like keymaps, audio
|
- [ ] Profile saving and loading, also includes loading configurations like keymaps, audio
|
||||||
engine config, etc. from a yaml instead of being hardcoded
|
engine config, etc. from a yaml instead of being hardcoded
|
||||||
- [ ] Noise
|
- [ ] Noise
|
||||||
|
- [ ] LFO modulation
|
||||||
|
|
||||||
## setup
|
## setup
|
||||||
TODO: instructions on build setup
|
TODO: instructions on build setup
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ void KeyboardController::handleKeyPress(QKeyEvent* e) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: something like a sustain pedal will suspend note-off events. probably control that in the midi controller
|
|
||||||
void KeyboardController::handleKeyRelease(QKeyEvent* e) {
|
void KeyboardController::handleKeyRelease(QKeyEvent* e) {
|
||||||
if (e->isAutoRepeat()) return;
|
if (e->isAutoRepeat()) return;
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ MidiController::~MidiController() {
|
|||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this dont work too well but whatever
|
||||||
bool MidiController::openDefaultPort() {
|
bool MidiController::openDefaultPort() {
|
||||||
if (!midiIn_) return false;
|
if (!midiIn_) return false;
|
||||||
if (midiIn_->getPortCount() == 0) {
|
if (midiIn_->getPortCount() == 0) {
|
||||||
@@ -33,8 +34,7 @@ bool MidiController::openPort(unsigned int index) {
|
|||||||
try {
|
try {
|
||||||
midiIn_->openPort(index);
|
midiIn_->openPort(index);
|
||||||
midiIn_->setCallback(&MidiController::midiCallback, this);
|
midiIn_->setCallback(&MidiController::midiCallback, this);
|
||||||
std::cout << "Opened MIDI port: "
|
std::cout << "Opened MIDI port: " << midiIn_->getPortName(index) << "\n";
|
||||||
<< midiIn_->getPortName(index) << "\n";
|
|
||||||
return true;
|
return true;
|
||||||
} catch (RtMidiError& e) {
|
} catch (RtMidiError& e) {
|
||||||
std::cerr << e.getMessage() << std::endl;
|
std::cerr << e.getMessage() << std::endl;
|
||||||
@@ -48,42 +48,45 @@ void MidiController::close() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// called by RtMidi on receive of a midi message. deltaTime is time since last midi message, not useful atm
|
||||||
void MidiController::midiCallback(double /*deltaTime*/, std::vector<unsigned char>* message, void* userData) {
|
void MidiController::midiCallback(double /*deltaTime*/, std::vector<unsigned char>* message, void* userData) {
|
||||||
auto* self = static_cast<MidiController*>(userData);
|
auto* self = static_cast<MidiController*>(userData);
|
||||||
if (!message || message->empty()) return;
|
if (!message || message->empty()) return;
|
||||||
self->handleMessage(*message);
|
self->handleMessage(*message); // pass to parsing function if valid
|
||||||
}
|
}
|
||||||
|
|
||||||
void MidiController::handleMessage(const std::vector<unsigned char>& msg) {
|
void MidiController::handleMessage(const std::vector<unsigned char>& msg) {
|
||||||
|
|
||||||
if(msg.size() <= 1) return;
|
if(msg.size() <= 1) return; // msg doesn't contain useful note info
|
||||||
|
|
||||||
uint8_t status = msg[0] & 0xF0;
|
uint8_t status = msg[0] & 0xF0;
|
||||||
uint8_t data1 = msg[1];
|
uint8_t data1 = msg[1];
|
||||||
uint8_t data2 = msg[2];
|
uint8_t data2 = msg[2];
|
||||||
|
|
||||||
if(status == 0xFE) return;
|
if(status == 0xFE) return; // "Active Sensing" -> 300ms heartbeat. could be useful to sense if this is missing for device failure detection
|
||||||
if(status == 0xF8) return;
|
if(status == 0xF8) return; // "Timing Clock" -> 24 pulses per quarter note, for steady rhythm. not useful for this instrument
|
||||||
|
|
||||||
|
// sustain pedal message event
|
||||||
if(status == 0xB0 && data1 == 64) {
|
if(status == 0xB0 && data1 == 64) {
|
||||||
handleSustain(data2 >= 64);
|
handleSustain(data2 >= 64);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
unsigned char note = msg.size() > 1 ? msg[1] : 0;
|
unsigned char note = msg.size() > 1 ? msg[1] : 0; // note number
|
||||||
unsigned char vel = msg.size() > 2 ? msg[2] : 0;
|
unsigned char vel = msg.size() > 2 ? msg[2] : 0; // velocity
|
||||||
|
|
||||||
// Note On (velocity > 0)
|
// note on (velocity > 0)
|
||||||
if (status == 0x90 && vel > 0) {
|
if (status == 0x90 && vel > 0) {
|
||||||
noteOn(note, vel);
|
noteOn(note, vel);
|
||||||
}
|
}
|
||||||
// Note Off (or Note On with velocity 0)
|
// note off (or note on with 0 velocity)
|
||||||
else if (status == 0x80 || (status == 0x90 && vel == 0)) {
|
else if (status == 0x80 || (status == 0x90 && vel == 0)) {
|
||||||
noteOff(note);
|
noteOff(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// construct note on event and add to noteQueue
|
||||||
void MidiController::noteOn(uint8_t note, uint8_t vel) {
|
void MidiController::noteOn(uint8_t note, uint8_t vel) {
|
||||||
sustainedNotes_.erase(note);
|
sustainedNotes_.erase(note);
|
||||||
|
|
||||||
@@ -95,6 +98,7 @@ void MidiController::noteOn(uint8_t note, uint8_t vel) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add note off event to noteQueue if no sustain active
|
||||||
void MidiController::noteOff(uint8_t note) {
|
void MidiController::noteOff(uint8_t note) {
|
||||||
if(sustainDown_) {
|
if(sustainDown_) {
|
||||||
sustainedNotes_.insert(note);
|
sustainedNotes_.insert(note);
|
||||||
@@ -108,6 +112,7 @@ void MidiController::noteOff(uint8_t note) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if sustain goes from on->off, then noteOff all the active ntoes
|
||||||
void MidiController::handleSustain(bool down) {
|
void MidiController::handleSustain(bool down) {
|
||||||
if(down == sustainDown_) return;
|
if(down == sustainDown_) return;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "NoteQueue.h"
|
#include "NoteQueue.h"
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
|
// add event to noteQueue, called by MidiController or keyboardController
|
||||||
bool NoteQueue::push(const NoteEvent& event) {
|
bool NoteQueue::push(const NoteEvent& event) {
|
||||||
size_t head = head_.load(std::memory_order_relaxed);
|
size_t head = head_.load(std::memory_order_relaxed);
|
||||||
size_t next = (head + 1) % SIZE;
|
size_t next = (head + 1) % SIZE;
|
||||||
@@ -14,6 +15,7 @@ bool NoteQueue::push(const NoteEvent& event) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// take event from noteQueue, called by synth
|
||||||
bool NoteQueue::pop(NoteEvent& event) {
|
bool NoteQueue::pop(NoteEvent& event) {
|
||||||
size_t tail = tail_.load(std::memory_order_relaxed);
|
size_t tail = tail_.load(std::memory_order_relaxed);
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ ParameterStore::ParameterStore() {
|
|||||||
resetToDefaults();
|
resetToDefaults();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set parameter value
|
||||||
void ParameterStore::set(ParamId id, float value) {
|
void ParameterStore::set(ParamId id, float value) {
|
||||||
values_[static_cast<size_t>(id)].store(value, std::memory_order_relaxed);
|
values_[static_cast<size_t>(id)].store(value, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set a whole envelope of parameters
|
||||||
void ParameterStore::set(EnvelopeId id, float depth, float a, float d, float s, float r) {
|
void ParameterStore::set(EnvelopeId id, float depth, float a, float d, float s, float r) {
|
||||||
|
|
||||||
EnvelopeParam params = ENV_PARAMS[static_cast<size_t>(id)];
|
EnvelopeParam params = ENV_PARAMS[static_cast<size_t>(id)];
|
||||||
@@ -19,6 +21,7 @@ void ParameterStore::set(EnvelopeId id, float depth, float a, float d, float s,
|
|||||||
set(params.r, r);
|
set(params.r, r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get a single parameter
|
||||||
float ParameterStore::get(ParamId id) const {
|
float ParameterStore::get(ParamId id) const {
|
||||||
return values_[static_cast<size_t>(id)].load(std::memory_order_relaxed);
|
return values_[static_cast<size_t>(id)].load(std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
@@ -28,3 +31,5 @@ void ParameterStore::resetToDefaults() {
|
|||||||
values_[i].store(PARAM_DEFS[i].def, std::memory_order_relaxed);
|
values_[i].store(PARAM_DEFS[i].def, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: applying parameter profiles will work similarly to above function
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ int main(int argc, char *argv[]) {
|
|||||||
|
|
||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
|
|
||||||
MainWindow window;
|
MainWindow window; // entry point goes to MainWindow::MainWindow()
|
||||||
window.show();
|
window.show();
|
||||||
|
|
||||||
int status = app.exec();
|
int status = app.exec(); // assembles ui
|
||||||
|
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ AudioEngine::~AudioEngine() {
|
|||||||
|
|
||||||
bool AudioEngine::start() {
|
bool AudioEngine::start() {
|
||||||
|
|
||||||
|
// initialize the audio engine
|
||||||
RtAudio::StreamParameters params;
|
RtAudio::StreamParameters params;
|
||||||
params.deviceId = audio_.getDefaultOutputDevice();
|
params.deviceId = audio_.getDefaultOutputDevice();
|
||||||
params.nChannels = channels_;
|
params.nChannels = channels_; // we're doing two duplicate channels for pseudo-mono
|
||||||
params.firstChannel = 0;
|
params.firstChannel = 0;
|
||||||
|
|
||||||
RtAudio::StreamOptions options;
|
RtAudio::StreamOptions options;
|
||||||
@@ -47,7 +48,8 @@ void AudioEngine::stop() {
|
|||||||
|
|
||||||
int32_t AudioEngine::audioCallback( void* outputBuffer, void*, uint32_t nFrames, double, RtAudioStreamStatus status, void* userData) {
|
int32_t AudioEngine::audioCallback( void* outputBuffer, void*, uint32_t nFrames, double, RtAudioStreamStatus status, void* userData) {
|
||||||
|
|
||||||
if (status) std::cerr << "Stream underflow!" << std::endl;
|
// error if process is too slow for the callback. If this is consistent, then need to optimize synth.process() or whatever cascades from it
|
||||||
|
if (status) std::cerr << "Stream underflow" << std::endl;
|
||||||
|
|
||||||
return static_cast<AudioEngine*>(userData)->process(static_cast<float*>(outputBuffer), nFrames);
|
return static_cast<AudioEngine*>(userData)->process(static_cast<float*>(outputBuffer), nFrames);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ void Envelope::noteOff() {
|
|||||||
if(state_ != State::Idle) state_ = State::Release;
|
if(state_ != State::Idle) state_ = State::Release;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns current value based on state and steps forward one sample
|
||||||
float Envelope::process() {
|
float Envelope::process() {
|
||||||
|
|
||||||
switch (state_) {
|
switch (state_) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ void Filter::setSampleRate(float sampleRate) {
|
|||||||
calculateCoefficients();
|
calculateCoefficients();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// recalculate filter based on params
|
||||||
void Filter::setParams(Type type, float frequency, float q) {
|
void Filter::setParams(Type type, float frequency, float q) {
|
||||||
type_ = type;
|
type_ = type;
|
||||||
frequency_ = std::min(frequency, sampleRate_ / 2.0f * 0.999f);
|
frequency_ = std::min(frequency, sampleRate_ / 2.0f * 0.999f);
|
||||||
@@ -20,6 +21,7 @@ void Filter::setParams(Type type, float frequency, float q) {
|
|||||||
calculateCoefficients();
|
calculateCoefficients();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update current state and output filtered value
|
||||||
float Filter::biquadProcess(float in) {
|
float Filter::biquadProcess(float in) {
|
||||||
|
|
||||||
// calculate filtered sample
|
// calculate filtered sample
|
||||||
@@ -32,6 +34,7 @@ float Filter::biquadProcess(float in) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// internal control system emulation
|
||||||
void Filter::calculateCoefficients() {
|
void Filter::calculateCoefficients() {
|
||||||
|
|
||||||
if(q_ < 0.001f) q_ = 0.001f;
|
if(q_ < 0.001f) q_ = 0.001f;
|
||||||
@@ -72,7 +75,7 @@ void Filter::calculateCoefficients() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize
|
// values need to be normalized
|
||||||
b0_ = b0 / a0;
|
b0_ = b0 / a0;
|
||||||
b1_ = b1 / a0;
|
b1_ = b1 / a0;
|
||||||
b2_ = b2 / a0;
|
b2_ = b2 / a0;
|
||||||
|
|||||||
@@ -2,3 +2,5 @@
|
|||||||
#include "Oscillator.h"
|
#include "Oscillator.h"
|
||||||
|
|
||||||
// placeholder
|
// placeholder
|
||||||
|
|
||||||
|
// will eventually hold wavetable sampling, store phase state, and all that silliness
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ ScopeBuffer::ScopeBuffer(size_t size) : buffer_(size) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add a single sample to the scope buffer, called by synth
|
||||||
void ScopeBuffer::push(float sample) {
|
void ScopeBuffer::push(float sample) {
|
||||||
size_t w = writeIndex_.fetch_add(1, std::memory_order_relaxed);
|
size_t w = writeIndex_.fetch_add(1, std::memory_order_relaxed);
|
||||||
buffer_[w % buffer_.size()] = sample;
|
buffer_[w % buffer_.size()] = sample;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// outputs value from the scope buffer, called by the scope widget
|
||||||
void ScopeBuffer::read(std::vector<float>& out) const {
|
void ScopeBuffer::read(std::vector<float>& out) const {
|
||||||
size_t w = writeIndex_.load(std::memory_order_relaxed);
|
size_t w = writeIndex_.load(std::memory_order_relaxed);
|
||||||
for (size_t i = 0; i < out.size(); i++) {
|
for (size_t i = 0; i < out.size(); i++) {
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ inline float Synth::getParam(ParamId id) {
|
|||||||
return params_[static_cast<size_t>(id)].current;
|
return params_[static_cast<size_t>(id)].current;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// called by audio engine before process (because it has the reference to the noteEvent store)
|
||||||
|
// this function consumes the noteEvents in the noteEvent store (whether produced by MIDI or keyboard)
|
||||||
void Synth::handleNoteEvent(const NoteEvent& event) {
|
void Synth::handleNoteEvent(const NoteEvent& event) {
|
||||||
|
|
||||||
lastTime = event.timestamp;
|
lastTime = event.timestamp;
|
||||||
@@ -45,7 +47,6 @@ void Synth::handleNoteEvent(const NoteEvent& event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: find quietest voice and assign a note to it instead of just the first inactive one
|
|
||||||
// find inactive voice and start it with the given note
|
// find inactive voice and start it with the given note
|
||||||
for(Voice& v : voices_) {
|
for(Voice& v : voices_) {
|
||||||
if(!v.isActive()) {
|
if(!v.isActive()) {
|
||||||
@@ -85,7 +86,7 @@ void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) {
|
|||||||
float params[PARAM_COUNT] = {0.0f};
|
float params[PARAM_COUNT] = {0.0f};
|
||||||
for(int i = 0; i < PARAM_COUNT; i++) {
|
for(int i = 0; i < PARAM_COUNT; i++) {
|
||||||
params[i] = params_[i].current;
|
params[i] = params_[i].current;
|
||||||
}
|
} // maybe take this outside the loop if performance is an issue
|
||||||
|
|
||||||
// foreach voice, process...
|
// foreach voice, process...
|
||||||
float mix = 0.0f;
|
float mix = 0.0f;
|
||||||
@@ -94,6 +95,7 @@ void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) {
|
|||||||
}
|
}
|
||||||
mix /= 4.0f; // for number of voices to prevent clipping
|
mix /= 4.0f; // for number of voices to prevent clipping
|
||||||
mix = tanh(mix); // really prevents clipping
|
mix = tanh(mix); // really prevents clipping
|
||||||
|
// TODO: these saturation function work kinda like magic, use them elsewhere
|
||||||
|
|
||||||
sampleOut = mix;
|
sampleOut = mix;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ Voice::Voice(SmoothedParam* params) : params_(params) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cascade sample rate to all descendant objects
|
||||||
void Voice::setSampleRate(float sampleRate) {
|
void Voice::setSampleRate(float sampleRate) {
|
||||||
sampleRate_ = sampleRate;
|
sampleRate_ = sampleRate;
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ void Voice::setSampleRate(float sampleRate) {
|
|||||||
//osc1_.setSampleRate(sampleRate);
|
//osc1_.setSampleRate(sampleRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// calculates oscillator frequency based on midi note
|
||||||
inline float Voice::noteToFrequency(uint8_t note) {
|
inline float Voice::noteToFrequency(uint8_t note) {
|
||||||
return SYNTH_PITCH_STANDARD * pow(2.0f, static_cast<float>(note - SYNTH_MIDI_HOME) / static_cast<float>(SYNTH_NOTES_PER_OCTAVE));
|
return SYNTH_PITCH_STANDARD * pow(2.0f, static_cast<float>(note - SYNTH_MIDI_HOME) / static_cast<float>(SYNTH_NOTES_PER_OCTAVE));
|
||||||
}
|
}
|
||||||
@@ -55,6 +57,7 @@ bool Voice::isActive() {
|
|||||||
return active_;
|
return active_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generates a single sample, called from synth.process()
|
||||||
float Voice::process(float* params, bool& scopeTrigger) {
|
float Voice::process(float* params, bool& scopeTrigger) {
|
||||||
|
|
||||||
// process all envelopes
|
// process all envelopes
|
||||||
@@ -69,11 +72,10 @@ float Voice::process(float* params, bool& scopeTrigger) {
|
|||||||
return 0.0f;
|
return 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// process all envelopes
|
||||||
float gainEnv = gainEnvelope_.process();
|
float gainEnv = gainEnvelope_.process();
|
||||||
float cutoffEnv = cutoffEnvelope_.process();
|
float cutoffEnv = cutoffEnvelope_.process();
|
||||||
float resonanceEnv = resonanceEnvelope_.process();
|
float resonanceEnv = resonanceEnvelope_.process();
|
||||||
// TODO: envelope is shared between all notes so this sequence involves a note change but only one envelope attack:
|
|
||||||
// NOTE_A_ON > NOTE_B_ON > NOTE_A_OFF and note B starts playing part-way through note A's envelope
|
|
||||||
|
|
||||||
// TODO: make pitchOffset variable for each oscillator (maybe three values like octave, semitone offset, and pitch offset in cents)
|
// TODO: make pitchOffset variable for each oscillator (maybe three values like octave, semitone offset, and pitch offset in cents)
|
||||||
float pitchOffset = 1.0f;
|
float pitchOffset = 1.0f;
|
||||||
@@ -120,6 +122,7 @@ float Voice::process(float* params, bool& scopeTrigger) {
|
|||||||
sampleOut = filter1_.biquadProcess(sampleOut);
|
sampleOut = filter1_.biquadProcess(sampleOut);
|
||||||
sampleOut = filter2_.biquadProcess(sampleOut);
|
sampleOut = filter2_.biquadProcess(sampleOut);
|
||||||
|
|
||||||
|
// state tracking, may keep this here even if oscillators store their own phase because it might help with scope triggering
|
||||||
phase_ += phaseInc;
|
phase_ += phaseInc;
|
||||||
if (phase_ > 2.0f * M_PI) {
|
if (phase_ > 2.0f * M_PI) {
|
||||||
phase_ -= 2.0f * M_PI;
|
phase_ -= 2.0f * M_PI;
|
||||||
|
|||||||
Reference in New Issue
Block a user