add scope widget

This commit is contained in:
2025-12-26 20:30:41 -06:00
parent cc11cfe63a
commit 6b52f8fa4f
10 changed files with 194 additions and 13 deletions

View File

@@ -22,10 +22,10 @@ public:
// setters
void setSampleRate(float sampleRate) { sampleRate_ = sampleRate; }
void set(float a, float d, float s, float r) { setAttack(a); setDecay(d); setSustain(s); setRelease(r); }
void setAttack(float seconds) { attack_ = std::max(seconds, 0.0001f); }
void setDecay(float seconds) { decay_ = std::max(seconds, 0.0001f); }
void setAttack(float seconds) { attack_ = std::max(seconds, 0.001f); }
void setDecay(float seconds) { decay_ = std::max(seconds, 0.001f); }
void setSustain(float level) { sustain_ = level; }
void setRelease(float seconds) { release_ = std::max(seconds, 0.0001f); }
void setRelease(float seconds) { release_ = std::max(seconds, 0.001f); }
// values close to zero introduce that popping sound on noteOn/noteOffs
// note events

19
src/synth/ScopeBuffer.cpp Normal file
View File

@@ -0,0 +1,19 @@
#include "ScopeBuffer.h"
ScopeBuffer::ScopeBuffer(size_t size) : buffer_(size) {
}
void ScopeBuffer::push(float sample) {
size_t w = writeIndex_.fetch_add(1, std::memory_order_relaxed);
buffer_[w % buffer_.size()] = sample;
}
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++) {
size_t idx = (w + trigger_ + i * wavelength_ / out.size()) % buffer_.size();
out[i] = buffer_[idx];
}
}

33
src/synth/ScopeBuffer.h Normal file
View File

@@ -0,0 +1,33 @@
#pragma once
#include <vector>
#include <atomic>
class ScopeBuffer {
public:
explicit ScopeBuffer(size_t size);
~ScopeBuffer() = default;
// add/read from the buffer
void push(float sample);
void read(std::vector<float>& out) const;
// setters/getters
void setTrigger(int32_t trigger) { trigger_ = trigger; }
void setWavelength(int32_t wavelength) { wavelength_ = wavelength; }
int32_t trigger() { return trigger_; }
int32_t wavelength() { return wavelength_; }
// NOTE: there are limits to the wavelengths that the scope can show cleanly due to the size of the audio buffer
// at a buffer size of 256 at 44100hz the min visible steady frequency is ~172hz
private:
std::vector<float> buffer_;
std::atomic<size_t> writeIndex_{0};
int32_t trigger_ = 0; // units in array indices
int32_t wavelength_ = 400;
};

View File

@@ -65,6 +65,7 @@ void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) {
updateParams();
float sampleOut = 0.0f;
bool triggered = false;
for (uint32_t i = 0; i < nFrames; i++) {
@@ -82,6 +83,7 @@ void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) {
if(!gainEnvelope_.isActive()) {
out[2*i] = 0.0f;
out[2*i+1] = 0.0f;
scope_->push(0.0f);
continue;
}
@@ -92,8 +94,8 @@ void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) {
// TODO: wavetables should be scaled by their RMS for equal loudness (prelim standard = 0.707)
float sineSample = std::sin(phase_);
float squareSample = (phase_ >= M_PI) ? 0.707f : -0.707f;
float sawSample = phase_ * 4.0f / M_PI * frequency_ / static_cast<float>(sampleRate);
sampleOut = sawSample * gain;
float sawSample = ((phase_ / M_PI) - 1.0f) / 0.577f * 0.707f;
sampleOut = squareSample * gain;
// write to buffer
out[2*i] = sampleOut; // left
@@ -102,12 +104,17 @@ void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) {
// write to scope buffer
if (scope_) {
scope_->push(sampleOut); // visualization tap
// set trigger info here too
}
// sampling business
phase_ += phaseInc;
if (phase_ > 2.0f * M_PI) phase_ -= 2.0f * M_PI;
if (phase_ > 2.0f * M_PI) {
phase_ -= 2.0f * M_PI;
if(!triggered) {
scope_->setTrigger(i); // this is where we consider the start of a waveform
triggered = true;
}
}
}
}