add visual scope

This commit is contained in:
2026-06-13 19:31:46 -05:00
parent dbc1db37e1
commit e9fff9691b
8 changed files with 80 additions and 37 deletions

View File

@@ -24,18 +24,17 @@ int main(int argc, char* argv[]) {
NoteQueue queue = NoteQueue(); NoteQueue queue = NoteQueue();
ScopeBuffer scopeBuffer = ScopeBuffer(512); ScopeBuffer scopeBuffer = ScopeBuffer(512);
KeyboardController keyboard(&config, &logger, &queue); KeyboardController keyboard(&config, &logger, &queue);
Synth synth(&config, &logger, nullptr, &queue); Synth synth(&config, &logger, &scopeBuffer, &queue);
// audio synthesizer doohickey // audio synthesizer doohickey
AudioEngine audioEngine = AudioEngine(&config, &logger, &synth); AudioEngine audioEngine = AudioEngine(&config, &logger, &synth);
audioEngine.start(); audioEngine.start();
// attach backend gui components // attach backend gui components
//Scope scope; //scope.setBuffer(&scopeBuffer);
qmlRegisterType<TimerComponent>("AppDemo", 1, 0, "TimerComponent"); qmlRegisterType<TimerComponent>("AppDemo", 1, 0, "TimerComponent");
//qmlRegisterSingletonInstance("AppDemo", 1, 0, "Scope", &scope);
qmlRegisterType<Scope>("AppDemo", 1, 0, "Scope"); qmlRegisterType<Scope>("AppDemo", 1, 0, "Scope");
engine.rootContext()->setContextProperty("keyboardController", &keyboard); engine.rootContext()->setContextProperty("keyboardController", &keyboard);
engine.rootContext()->setContextProperty("scopeBuffer", &scopeBuffer);
// load qml // load qml
//engine.loadFromModule("sonobulus", "Main"); //engine.loadFromModule("sonobulus", "Main");

View File

@@ -31,7 +31,10 @@ float Instrument::process(bool& scopeTrigger) {
if(!active_ && envelope_ > 0.0f) envelope_ -= 0.01f; if(!active_ && envelope_ > 0.0f) envelope_ -= 0.01f;
phase_ += phaseIncrement_; phase_ += phaseIncrement_;
if(phase_ > 2.0f * pi) phase_ -= 2.0f * pi; if(phase_ > 2.0f * pi) {
phase_ -= 2.0f * pi;
scopeTrigger = true;
}
if(!isActive()) return 0.0f; if(!isActive()) return 0.0f;
return sin(phase_) * envelope_; return sin(phase_) * envelope_;

View File

@@ -3,6 +3,10 @@
#include <iostream> #include <iostream>
ScopeBuffer::ScopeBuffer(QObject *parent) : QObject(parent) {
}
ScopeBuffer::ScopeBuffer(size_t size) : buffer_(size) { ScopeBuffer::ScopeBuffer(size_t size) : buffer_(size) {
} }
@@ -27,21 +31,45 @@ Scope::Scope(QQuickItem *parent) : QQuickPaintedItem(parent) {
setAntialiasing(true); setAntialiasing(true);
} }
void Scope::setBuffer(ScopeBuffer* scopeBuffer) {
scopeBuffer_ = scopeBuffer;
buffer_ = std::vector<float>(scopeBuffer_->size());
}
void Scope::paint(QPainter *painter) { void Scope::paint(QPainter *painter) {
//std::cout << "onPaint" << std::endl; while(scopeBuffer_->spinlock()) {
// wait
}
static float phase = 0.0f; if(scopeBuffer_ != nullptr) scopeBuffer_->read(buffer_);
phase += 0.1f;
float sample = sin(phase);
QPen pen(Qt::blue, 4); // TODO: scale to max amplitude ?
float maxAmp = 1.0f;
/*
for(float s : buffer_) {
maxAmp = std::max(maxAmp, std::abs(s));
}
*/
float scaleY = (height() * 0.45f) / maxAmp;
float midY = height() / 2.0f;
painter->fillRect(0, 0, width(), height(), QColor(20, 20, 20));
QPen pen;
pen.setWidthF(3.0f);
QColor green(50, 255, 70);
pen.setColor(green);
painter->setPen(pen); painter->setPen(pen);
painter->setRenderHint(QPainter::Antialiasing); painter->setRenderHint(QPainter::Antialiasing);
painter->drawRect(10, 10, width() - 20*sample, height() - 20*sample); for(size_t i = 1; i < buffer_.size(); i++) {
painter->drawLine(
painter->setPen(QPen(Qt::red, 2)); i * width() / buffer_.size(),
painter->drawLine(0, 0, width(), height()); midY + buffer_[i-1] * scaleY,
i * width() / buffer_.size(),
midY + buffer_[i] * scaleY
);
}
} }

View File

@@ -8,10 +8,13 @@
#include <vector> #include <vector>
#include <atomic> #include <atomic>
class ScopeBuffer { class ScopeBuffer : public QObject {
Q_OBJECT // needed to attach to a qml component
public: public:
explicit ScopeBuffer(QObject* parent = nullptr);
ScopeBuffer(size_t size); ScopeBuffer(size_t size);
~ScopeBuffer() = default; ~ScopeBuffer() = default;
@@ -22,7 +25,8 @@ public:
void setWavelength(size_t wavelength) { wavelength_ = wavelength; } void setWavelength(size_t wavelength) { wavelength_ = wavelength; }
size_t trigger() { return trigger_; } size_t trigger() { return trigger_; }
size_t wavelength() { return wavelength_; } size_t wavelength() { return wavelength_; }
void spinlock(bool lock) { spinlock_ = lock; } void spinlock(bool lock) { spinlock_.store(lock, std::memory_order_relaxed); }
bool spinlock() { return spinlock_.load(std::memory_order_relaxed); }
size_t size() { return buffer_.size(); } size_t size() { return buffer_.size(); }
private: private:
@@ -33,20 +37,21 @@ private:
size_t trigger_ = 0; size_t trigger_ = 0;
size_t wavelength_ = 400; size_t wavelength_ = 400;
bool spinlock_ = false; std::atomic<bool> spinlock_ = false;
}; };
class Scope : public QQuickPaintedItem { class Scope : public QQuickPaintedItem {
Q_OBJECT Q_OBJECT
QML_ELEMENT Q_PROPERTY(ScopeBuffer* buffer WRITE setBuffer)
public: public:
explicit Scope(QQuickItem* parent = nullptr); explicit Scope(QQuickItem* parent = nullptr);
void paint(QPainter* painter) override; void paint(QPainter* painter) override;
void setBuffer(ScopeBuffer* scopeBuffer) { scopeBuffer_ = scopeBuffer; }
void setBuffer(ScopeBuffer* scopeBuffer);
std::vector<float> buffer_; std::vector<float> buffer_;
ScopeBuffer* scopeBuffer_; ScopeBuffer* scopeBuffer_;

View File

@@ -28,18 +28,22 @@ void Synth::process(float* out, size_t nFrames) {
handleNoteEvent(noteEvent); handleNoteEvent(noteEvent);
} }
// size_t lowestVoice = 0; size_t lowestVoice = 0;
// float lowestFreq = 100000.0f; float lowestFreq = 100000.0f;
// for(size_t i = 0; i < voices_.size(); i++) { for(size_t i = 0; i < voices_.size(); i++) {
// if(!voices_[i].isActive()) continue; if(!voices_[i].isActive()) continue;
// float currentFreq = voices_[i].frequency(); float currentFreq = voices_[i].frequency();
// if(currentFreq < lowestFreq) { if(currentFreq < lowestFreq) {
// lowestVoice = i; lowestVoice = i;
// lowestFreq = currentFreq; lowestFreq = currentFreq;
// } }
// } }
scope_->spinlock(true);
float sampleOut = 0.0f; float sampleOut = 0.0f;
bool triggered = false;
bool once = false;
for(size_t i = 0; i < nFrames; i++) { for(size_t i = 0; i < nFrames; i++) {
float mix = 0.0f; float mix = 0.0f;
@@ -47,7 +51,7 @@ void Synth::process(float* out, size_t nFrames) {
bool temp = false; bool temp = false;
//if(!voices_[j].isActive()) continue; //if(!voices_[j].isActive()) continue;
mix += voices_[j].process(temp); mix += voices_[j].process(temp);
// if(j == lowestVoice) triggered = temp; if(j == lowestVoice) triggered = temp;
} }
mix = tanh(mix/4.0f); // prevent clipping mix = tanh(mix/4.0f); // prevent clipping
@@ -55,13 +59,14 @@ void Synth::process(float* out, size_t nFrames) {
out[2*i] = sampleOut; out[2*i] = sampleOut;
out[2*i+1] = sampleOut; out[2*i+1] = sampleOut;
// if(scope_) scope_->push(sampleOut); if(scope_) scope_->push(sampleOut);
// if(triggered && !once) { if(triggered && !once) {
// scope_->setTrigger(i); scope_->setTrigger(i);
// once = true; once = true;
// }
} }
}
scope_->spinlock(false);
} }

View File

@@ -32,7 +32,7 @@ private:
ConfigService* config_; ConfigService* config_;
LoggerService* logger_; LoggerService* logger_;
ScopeBuffer* scope_ = nullptr; ScopeBuffer* scope_;;
NoteQueue* noteQueue_; NoteQueue* noteQueue_;
}; };

View File

@@ -21,6 +21,7 @@ public:
float process(bool& scopeTrigger); float process(bool& scopeTrigger);
uint8_t note() { return note_; } uint8_t note() { return note_; }
float frequency() { return noteToFrequency(note_); }
private: private:

View File

@@ -63,6 +63,8 @@ ApplicationWindow {
repeat: true repeat: true
onTriggered: scope.update() onTriggered: scope.update()
} }
buffer: scopeBuffer
} }
} }