add scope widget
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
19
src/synth/ScopeBuffer.cpp
Normal 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
33
src/synth/ScopeBuffer.h
Normal 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;
|
||||
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>800</height>
|
||||
<height>600</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -39,7 +39,7 @@
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>150</x>
|
||||
<y>130</y>
|
||||
<y>230</y>
|
||||
<width>500</width>
|
||||
<height>360</height>
|
||||
</rect>
|
||||
@@ -55,7 +55,7 @@
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>200</x>
|
||||
<y>540</y>
|
||||
<y>20</y>
|
||||
<width>400</width>
|
||||
<height>200</height>
|
||||
</rect>
|
||||
|
||||
62
src/ui/widgets/Scope/Scope.cpp
Normal file
62
src/ui/widgets/Scope/Scope.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
|
||||
#include "Scope.h"
|
||||
#include "ui_Scope.h"
|
||||
|
||||
#include "../../../synth/ScopeBuffer.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <iostream>
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
35
src/ui/widgets/Scope/Scope.h
Normal file
35
src/ui/widgets/Scope/Scope.h
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QTimer>
|
||||
#include <vector>
|
||||
|
||||
// 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<float> samples_;
|
||||
QTimer timer_;
|
||||
};
|
||||
25
src/ui/widgets/Scope/Scope.ui
Normal file
25
src/ui/widgets/Scope/Scope.ui
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>Scope</class>
|
||||
<widget class="QWidget" name="Scope">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>200</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
Reference in New Issue
Block a user