This commit is contained in:
2026-01-14 23:06:56 -06:00
parent 0450482773
commit b160c8ae69
13 changed files with 50 additions and 22 deletions

View File

@@ -21,9 +21,10 @@ AudioEngine::~AudioEngine() {
bool AudioEngine::start() {
// initialize the audio engine
RtAudio::StreamParameters params;
params.deviceId = audio_.getDefaultOutputDevice();
params.nChannels = channels_;
params.nChannels = channels_; // we're doing two duplicate channels for pseudo-mono
params.firstChannel = 0;
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) {
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);
}

View File

@@ -14,6 +14,7 @@ void Envelope::noteOff() {
if(state_ != State::Idle) state_ = State::Release;
}
// returns current value based on state and steps forward one sample
float Envelope::process() {
switch (state_) {

View File

@@ -13,6 +13,7 @@ void Filter::setSampleRate(float sampleRate) {
calculateCoefficients();
}
// recalculate filter based on params
void Filter::setParams(Type type, float frequency, float q) {
type_ = type;
frequency_ = std::min(frequency, sampleRate_ / 2.0f * 0.999f);
@@ -20,6 +21,7 @@ void Filter::setParams(Type type, float frequency, float q) {
calculateCoefficients();
}
// update current state and output filtered value
float Filter::biquadProcess(float in) {
// calculate filtered sample
@@ -32,6 +34,7 @@ float Filter::biquadProcess(float in) {
return out;
}
// internal control system emulation
void Filter::calculateCoefficients() {
if(q_ < 0.001f) q_ = 0.001f;
@@ -72,7 +75,7 @@ void Filter::calculateCoefficients() {
break;
}
// Normalize
// values need to be normalized
b0_ = b0 / a0;
b1_ = b1 / a0;
b2_ = b2 / a0;

View File

@@ -2,3 +2,5 @@
#include "Oscillator.h"
// placeholder
// will eventually hold wavetable sampling, store phase state, and all that silliness

View File

@@ -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) {
size_t w = writeIndex_.fetch_add(1, std::memory_order_relaxed);
buffer_[w % buffer_.size()] = sample;
}
// outputs value from the scope buffer, called by the scope widget
void ScopeBuffer::read(std::vector<float>& out) const {
size_t w = writeIndex_.load(std::memory_order_relaxed);
for (size_t i = 0; i < out.size(); i++) {

View File

@@ -30,6 +30,8 @@ inline float Synth::getParam(ParamId id) {
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) {
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
for(Voice& v : voices_) {
if(!v.isActive()) {
@@ -85,7 +86,7 @@ void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) {
float params[PARAM_COUNT] = {0.0f};
for(int i = 0; i < PARAM_COUNT; i++) {
params[i] = params_[i].current;
}
} // maybe take this outside the loop if performance is an issue
// foreach voice, process...
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 = tanh(mix); // really prevents clipping
// TODO: these saturation function work kinda like magic, use them elsewhere
sampleOut = mix;

View File

@@ -7,6 +7,7 @@ Voice::Voice(SmoothedParam* params) : params_(params) {
}
// cascade sample rate to all descendant objects
void Voice::setSampleRate(float sampleRate) {
sampleRate_ = sampleRate;
@@ -23,6 +24,7 @@ void Voice::setSampleRate(float sampleRate) {
//osc1_.setSampleRate(sampleRate);
}
// calculates oscillator frequency based on midi 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));
}
@@ -55,6 +57,7 @@ bool Voice::isActive() {
return active_;
}
// generates a single sample, called from synth.process()
float Voice::process(float* params, bool& scopeTrigger) {
// process all envelopes
@@ -69,11 +72,10 @@ float Voice::process(float* params, bool& scopeTrigger) {
return 0.0f;
}
// process all envelopes
float gainEnv = gainEnvelope_.process();
float cutoffEnv = cutoffEnvelope_.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)
float pitchOffset = 1.0f;
@@ -120,6 +122,7 @@ float Voice::process(float* params, bool& scopeTrigger) {
sampleOut = filter1_.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;
if (phase_ > 2.0f * M_PI) {
phase_ -= 2.0f * M_PI;