diff --git a/CMakeLists.txt b/CMakeLists.txt index a80c7a0..0e3e9d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,8 +57,8 @@ qt_add_executable(metabolus src/synth/Envelope.h src/synth/Synth.cpp src/synth/Synth.h - src/synth/SynthBuffer.cpp - src/synth/SynthBuffer.h + src/synth/ScopeBuffer.cpp + src/synth/ScopeBuffer.h resources/resources.qrc src/ui/widgets/SmartSlider/SmartSlider.cpp src/ui/widgets/SmartSlider/SmartSlider.h diff --git a/src/synth/Envelope.h b/src/synth/Envelope.h index df1e77b..c0ea207 100644 --- a/src/synth/Envelope.h +++ b/src/synth/Envelope.h @@ -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 diff --git a/src/synth/ScopeBuffer.cpp b/src/synth/ScopeBuffer.cpp new file mode 100644 index 0000000..96ff31f --- /dev/null +++ b/src/synth/ScopeBuffer.cpp @@ -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& 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]; + } +} diff --git a/src/synth/ScopeBuffer.h b/src/synth/ScopeBuffer.h new file mode 100644 index 0000000..988ebb5 --- /dev/null +++ b/src/synth/ScopeBuffer.h @@ -0,0 +1,33 @@ + +#pragma once + +#include +#include + +class ScopeBuffer { +public: + + explicit ScopeBuffer(size_t size); + ~ScopeBuffer() = default; + + // add/read from the buffer + void push(float sample); + void read(std::vector& 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 buffer_; + std::atomic writeIndex_{0}; + + int32_t trigger_ = 0; // units in array indices + int32_t wavelength_ = 400; + +}; diff --git a/src/synth/Synth.cpp b/src/synth/Synth.cpp index 9176c51..2dc8d69 100644 --- a/src/synth/Synth.cpp +++ b/src/synth/Synth.cpp @@ -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(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; + } + } } } \ No newline at end of file diff --git a/src/ui/MainWindow.cpp b/src/ui/MainWindow.cpp index f965e50..2b0c407 100644 --- a/src/ui/MainWindow.cpp +++ b/src/ui/MainWindow.cpp @@ -16,7 +16,7 @@ MainWindow::MainWindow(QWidget *parent) : setFocusPolicy(Qt::StrongFocus); // connect scope - ui_->scopeWidget->setScopeBuffer(&audioEngine_->scopeBuffer()); + ui_->scope->setScopeBuffer(&audio_->scopeBuffer()); // Connect buttons to slots connect(ui_->buttonReset, &QPushButton::clicked, this, &MainWindow::onResetClicked); diff --git a/src/ui/MainWindow.ui b/src/ui/MainWindow.ui index 446f821..4952e45 100644 --- a/src/ui/MainWindow.ui +++ b/src/ui/MainWindow.ui @@ -7,7 +7,7 @@ 0 0 800 - 800 + 600 @@ -39,7 +39,7 @@ 150 - 130 + 230 500 360 @@ -55,7 +55,7 @@ 200 - 540 + 20 400 200 diff --git a/src/ui/widgets/Scope/Scope.cpp b/src/ui/widgets/Scope/Scope.cpp new file mode 100644 index 0000000..dce472f --- /dev/null +++ b/src/ui/widgets/Scope/Scope.cpp @@ -0,0 +1,62 @@ + +#include "Scope.h" +#include "ui_Scope.h" + +#include "../../../synth/ScopeBuffer.h" + +#include +#include + +Scope::Scope(QWidget* parent) : QWidget(parent), ui_(new Ui::Scope), samples_(512) { + + timer_.setInterval(16); // ~60 hz + connect(&timer_, &QTimer::timeout, this, QOverload<>::of(&Scope::update)); + timer_.start(); +} + +Scope::~Scope() { + delete ui_; +} + +void Scope::setScopeBuffer(ScopeBuffer* buffer) { + buffer_ = buffer; +} + +void Scope::paintEvent(QPaintEvent*) { + if (!buffer_) return; + + int32_t wavelength = buffer_->wavelength(); + int32_t trigger = buffer_->trigger(); + + buffer_->read(samples_); + + // auto scale amplitude. disabled because it hides the envelope effects + float maxAmp = 1.0f; + //for (float s : samples_) { + // maxAmp = std::max(maxAmp, std::abs(s)); + //} + // TODO: if you get really bored you can start adding scope display options + + float scaleY = (height() * 0.45f) / maxAmp; + float midY = height() / 2.0f; + + QPainter p(this); + QColor gray(20, 20, 20); + p.fillRect(rect(), gray); + + // got caught playing around + QPen pen; + pen.setWidthF(2.0f); + QColor green(50, 255, 70); + pen.setColor(green); + p.setPen(pen); + + for (int32_t i = 1; i < samples_.size(); i++) { + p.drawLine( + (i - 1) * width() / samples_.size(), + midY - samples_[i - 1] * scaleY, + i * width() / samples_.size(), + midY - samples_[i] * scaleY + ); + } +} \ No newline at end of file diff --git a/src/ui/widgets/Scope/Scope.h b/src/ui/widgets/Scope/Scope.h new file mode 100644 index 0000000..0689df9 --- /dev/null +++ b/src/ui/widgets/Scope/Scope.h @@ -0,0 +1,35 @@ + +#pragma once + +#include +#include +#include + +// forward declaration +class ScopeBuffer; + +QT_BEGIN_NAMESPACE +namespace Ui { class Scope; } +QT_END_NAMESPACE + +class Scope : public QWidget { + Q_OBJECT +public: + explicit Scope(QWidget* parent = nullptr); + ~Scope(); + + void setScopeBuffer(ScopeBuffer* buffer); + +protected: + // autocalled on QT's refresh loop + // scope drawing happens here + void paintEvent(QPaintEvent*) override; + +private: + + Ui::Scope* ui_; + + ScopeBuffer* buffer_ = nullptr; + std::vector samples_; + QTimer timer_; +}; diff --git a/src/ui/widgets/Scope/Scope.ui b/src/ui/widgets/Scope/Scope.ui new file mode 100644 index 0000000..846d38e --- /dev/null +++ b/src/ui/widgets/Scope/Scope.ui @@ -0,0 +1,25 @@ + + + Scope + + + + 0 + 0 + 400 + 200 + + + + + 0 + 0 + + + + Form + + + + +