add parameter store

This commit is contained in:
2025-12-23 23:15:32 -06:00
parent 7e771f2f28
commit 4d5cb4974b
8 changed files with 165 additions and 173 deletions

View File

@@ -44,6 +44,7 @@ qt_add_executable(metabolus
src/MainWindow.cpp src/MainWindow.cpp
src/MainWindow.h src/MainWindow.h
src/MainWindow.ui src/MainWindow.ui
src/ParameterStore.h
src/synth/AudioEngine.cpp src/synth/AudioEngine.cpp
src/synth/AudioEngine.h src/synth/AudioEngine.h
src/synth/Synth.cpp src/synth/Synth.cpp

View File

@@ -1,6 +1,8 @@
#include "MainWindow.h" #include "MainWindow.h"
#include "ui_MainWindow.h" #include "ui_MainWindow.h"
#include "ParameterStore.h"
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) {
@@ -14,6 +16,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
connect(ui->buttonReset, &QPushButton::clicked, this, &MainWindow::onResetClicked); connect(ui->buttonReset, &QPushButton::clicked, this, &MainWindow::onResetClicked);
// slider business // slider business
// TODO: smart slider widget
connect(ui->slider, &QSlider::valueChanged, this, &MainWindow::onSliderValueChanged); connect(ui->slider, &QSlider::valueChanged, this, &MainWindow::onSliderValueChanged);
connect(ui->inputMin, &QLineEdit::editingFinished, this, &MainWindow::onMinChanged); connect(ui->inputMin, &QLineEdit::editingFinished, this, &MainWindow::onMinChanged);
connect(ui->inputMax, &QLineEdit::editingFinished, this, &MainWindow::onMaxChanged); connect(ui->inputMax, &QLineEdit::editingFinished, this, &MainWindow::onMaxChanged);
@@ -24,6 +27,14 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi
audio_ = new AudioEngine(); audio_ = new AudioEngine();
audio_->start(); audio_->start();
// init defaults
// TODO:: there's gotta be a better way
ui->slider->setValue(PARAM_DEFS[static_cast<size_t>(ParamId::Osc1Frequency)].def);
ui->slider->setMinimum(PARAM_DEFS[static_cast<size_t>(ParamId::Osc1Frequency)].min);
ui->slider->setMaximum(PARAM_DEFS[static_cast<size_t>(ParamId::Osc1Frequency)].max);
ui->inputValue->setText(QString::number(PARAM_DEFS[static_cast<size_t>(ParamId::Osc1Frequency)].def));
ui->inputMin->setText(QString::number(PARAM_DEFS[static_cast<size_t>(ParamId::Osc1Frequency)].min));
ui->inputMax->setText(QString::number(PARAM_DEFS[static_cast<size_t>(ParamId::Osc1Frequency)].max));
} }
MainWindow::~MainWindow() { MainWindow::~MainWindow() {
@@ -49,7 +60,8 @@ void MainWindow::onSliderValueChanged(int value) {
QSignalBlocker blocker(ui->inputValue); QSignalBlocker blocker(ui->inputValue);
ui->inputValue->setText(QString::number(value)); ui->inputValue->setText(QString::number(value));
audio_->setFrequency(static_cast<float>(value)); // forward value so synthesizer can read
audio_->parameters()->set(ParamId::Osc1Frequency, static_cast<float>(value));
} }
// allows only values within the min, max to be set by the text field // allows only values within the min, max to be set by the text field

View File

@@ -42,5 +42,4 @@ private:
void syncValueToUi(int value); void syncValueToUi(int value);
AudioEngine* audio_ = nullptr; AudioEngine* audio_ = nullptr;
}; };

80
src/ParameterStore.h Normal file
View File

@@ -0,0 +1,80 @@
#pragma once
#include <cstdint>
#include <array>
#include <atomic>
enum class ParamId : uint16_t {
Osc1Frequency,
Osc1Gain,
Osc1VolumeEnvA,
Osc1VolumeEnvD,
Osc1VolumeEnvS,
Osc1VolumeEnvR,
FilterCutoffEnvA,
FilterCutoffEnvD,
FilterCutoffEnvS,
FilterCutoffEnvR,
FilterResonanceEnvA,
FilterResonanceEnvD,
FilterResonanceEnvS,
FilterResonanceEnvR,
// ... and so on
// this list could be like 200 long if I really wanted to
Count // for sizing
};
struct ParamDef {
float def;
float min;
float max;
};
// TODO: make these configurable via yml file too
// TODO: and then when I have full on profile saving there will be a default profile to load from
constexpr std::array<ParamDef, static_cast<size_t>(ParamId::Count)> PARAM_DEFS {{
{ 100.0f, 20.0f, 600.0f}, // Osc1Freq
{ 0.8f, 0.0f, 1.0f}, // Osc1Gain
{ 10.0f, 0.0f, 1000.0f}, // Osc1VolumeEnvA,
{ 10.0f, 0.0f, 1000.0f}, // Osc1VolumeEnvD,
{ 10.0f, 0.0f, 1000.0f}, // Osc1VolumeEnvS,
{ 10.0f, 0.0f, 1000.0f}, // Osc1VolumeEnvR,
{ 10.0f, 0.0f, 1000.0f}, // FilterCutoffEnvA,
{ 10.0f, 0.0f, 1000.0f}, // FilterCutoffEnvD,
{ 10.0f, 0.0f, 1000.0f}, // FilterCutoffEnvS,
{ 10.0f, 0.0f, 1000.0f}, // FilterCutoffEnvR,
{ 10.0f, 0.0f, 1000.0f}, // FilterResonanceEnvA,
{ 10.0f, 0.0f, 1000.0f}, // FilterResonanceEnvD,
{ 10.0f, 0.0f, 1000.0f}, // FilterResonanceEnvS,
{ 10.0f, 0.0f, 1000.0f}, // FilterResonanceEnvR,
}};
constexpr size_t PARAM_COUNT = static_cast<size_t>(ParamId::Count);
class ParameterStore {
public:
ParameterStore() { resetToDefaults(); }
~ParameterStore() = default;
// TODO: move into implementation file ? idk
void set(ParamId id, float value) {
values_[static_cast<size_t>(id)].store(value, std::memory_order_relaxed);
}
float get(ParamId id) const {
return values_[static_cast<size_t>(id)].load(std::memory_order_relaxed);
}
void resetToDefaults() {
for(size_t i = 0; i < PARAM_COUNT; i++) {
values_[i].store(PARAM_DEFS[i].def, std::memory_order_relaxed);
}
}
private:
std::array<std::atomic<float>, PARAM_COUNT> values_;
};

View File

@@ -1,18 +1,17 @@
#include "AudioEngine.h" #include "AudioEngine.h"
#define _USE_MATH_DEFINES
#include <cmath>
#include <iostream> #include <iostream>
#ifndef M_PI // I hate my stupid chungus life AudioEngine::AudioEngine() : synth_(params_) {
#define M_PI 3.14159265358979323846
#endif
AudioEngine::AudioEngine() {
if(audio_.getDeviceCount() < 1) { if(audio_.getDeviceCount() < 1) {
throw std::runtime_error("No audio devices found"); throw std::runtime_error("No audio devices found");
} }
// TODO: get audio configurations
synth_.setSampleRate(sampleRate_);
} }
AudioEngine::~AudioEngine() { AudioEngine::~AudioEngine() {
@@ -23,24 +22,17 @@ bool AudioEngine::start() {
RtAudio::StreamParameters params; RtAudio::StreamParameters params;
params.deviceId = audio_.getDefaultOutputDevice(); params.deviceId = audio_.getDefaultOutputDevice();
params.nChannels = 2; params.nChannels = channels_;
params.firstChannel = 0; params.firstChannel = 0;
RtAudio::StreamOptions options; RtAudio::StreamOptions options;
options.flags = RTAUDIO_MINIMIZE_LATENCY; options.flags = RTAUDIO_MINIMIZE_LATENCY;
/* // TODO: error check this please
try {
audio_.openStream(&params, nullptr, RTAUDIO_FLOAT32, sampleRate_, &bufferFrames_, &AudioEngine::audioCallback, this, &options);
audio_.startStream();
} catch(RtAudioError& e) {
std::cerr << e.getMessage() << std::endl;
return false;
}
*/
audio_.openStream(&params, nullptr, RTAUDIO_FLOAT32, sampleRate_, &bufferFrames_, &AudioEngine::audioCallback, this, &options); audio_.openStream(&params, nullptr, RTAUDIO_FLOAT32, sampleRate_, &bufferFrames_, &AudioEngine::audioCallback, this, &options);
audio_.startStream(); audio_.startStream();
// sanity check
std::cout << "sample rate: " << sampleRate_ << " buffer frames: " << bufferFrames_ << std::endl; std::cout << "sample rate: " << sampleRate_ << " buffer frames: " << bufferFrames_ << std::endl;
return true; return true;
@@ -52,10 +44,6 @@ void AudioEngine::stop() {
if(audio_.isStreamOpen()) audio_.closeStream(); if(audio_.isStreamOpen()) audio_.closeStream();
} }
void AudioEngine::setFrequency(float freq) {
targetFreq_.store(freq, std::memory_order_relaxed);
}
int32_t AudioEngine::audioCallback( void* outputBuffer, void*, uint32_t nFrames, double, RtAudioStreamStatus status, void* userData) { int32_t AudioEngine::audioCallback( void* outputBuffer, void*, uint32_t nFrames, double, RtAudioStreamStatus status, void* userData) {
if (status) std::cerr << "Stream underflow!" << std::endl; if (status) std::cerr << "Stream underflow!" << std::endl;
@@ -64,21 +52,9 @@ int32_t AudioEngine::audioCallback( void* outputBuffer, void*, uint32_t nFrames,
} }
int32_t AudioEngine::process(float* out, uint32_t nFrames) { int32_t AudioEngine::process(float* out, uint32_t nFrames) {
const float sr = static_cast<float>(sampleRate_);
float target = targetFreq_.load(std::memory_order_relaxed);
float freqStep = (target - currentFreq_) / nFrames;
for (uint32_t i = 0; i < nFrames; ++i) { // pass to synth
currentFreq_ += freqStep; synth_.process(out, nFrames, sampleRate_);
float phaseInc = 2.0f * M_PI * currentFreq_ / sr;
out[2*i] = std::sin(phase_); // left
out[2*i+1] = std::sin(phase_); // right
phase_ += phaseInc;
if (phase_ > 2.0f * M_PI) phase_ -= 2.0f * M_PI;
}
return 0; return 0;
} }

View File

@@ -5,6 +5,8 @@
#include <stdint.h> #include <stdint.h>
#include <atomic> #include <atomic>
#include "Synth.h"
class AudioEngine { class AudioEngine {
public: public:
@@ -13,19 +15,19 @@ public:
bool start(); bool start();
void stop(); void stop();
ParameterStore* parameters() { return &params_; }
void setFrequency(float freq);
private: private:
static int32_t audioCallback(void* outputBuffer, void* inputBuffer, uint32_t nFrames, double streamTime, RtAudioStreamStatus status, void* userData); static int32_t audioCallback(void* outputBuffer, void* inputBuffer, uint32_t nFrames, double streamTime, RtAudioStreamStatus status, void* userData);
int32_t process(float* out, uint32_t nFrames); int32_t process(float* out, uint32_t nFrames);
ParameterStore params_;
Synth synth_;
// TODO: id like a yml config file or something for these
RtAudio audio_; RtAudio audio_;
uint32_t sampleRate_ = 44100; uint32_t sampleRate_ = 44100;
uint32_t bufferFrames_ = 256; uint32_t bufferFrames_ = 256; // time per buffer = BF/SR (256/44100 = 5.8ms)
uint32_t channels_ = 2; // stereo
std::atomic<float> targetFreq_{ 400.0f };
float currentFreq_ = 440.0f;
float phase_ = 0.0f;
}; };

View File

@@ -1,115 +1,51 @@
/*
#include "Synth.h" #include "Synth.h"
#include <QMediaDevices>
#include <QtMath>
#include <iostream> #include <iostream>
#include <cmath>
Synth::Synth(QObject *parent) : QObject(parent) { #ifndef M_PI // I hate my stupid chungus life
#define M_PI 3.14159265358979323846
#endif
format_.setSampleRate(44100); Synth::Synth(const ParameterStore& params) : paramStore_(params) {
format_.setChannelCount(1);
format_.setSampleFormat(QAudioFormat::Int16);
QAudioDevice device = QMediaDevices::defaultAudioOutput(); }
if (!device.isFormatSupported(format_)) { void Synth::updateParams() {
format_ = device.preferredFormat(); for(size_t i = 0; i < PARAM_COUNT; i++) {
params_[i].target = paramStore_.get(static_cast<ParamId>(i));
} }
audioSink_ = new QAudioSink(device, format_, this);
audioSink_->setBufferSize(4096);
} }
Synth::~Synth() { inline float Synth::getParam(ParamId id) {
return params_[static_cast<size_t>(id)].current;
stop();
} }
void Synth::start() { void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) {
// std::cout << "Synth::start()" << std::endl;
// if (audioSink_->state() == QAudio::ActiveState)
// return;
// audioDevice_ = audioSink_->start();
} // yeah really only need to update this once per buffer if its ~6ms latency
updateParams();
void Synth::stop() { for (uint32_t i = 0; i < nFrames; i++) {
if (audioSink_) { for(auto& p : params_) p.update(); // TODO: profile this
audioSink_->stop();
audioDevice_ = nullptr;
}
}
void Synth::setFrequency(float frequency) { // based on oscillator frequency
float frequency = getParam(ParamId::Osc1Frequency); // this will come from a midi controller
float phaseInc = 2.0f * M_PI * frequency / static_cast<float>(sampleRate);
frequency_ = qMax(1.0f, frequency); // sample generation
} float gain = getParam(ParamId::Osc1Gain);
float sample = std::sin(phase_) * gain;
QByteArray Synth::generateSamples(qint64 bytes) { // write to buffer
out[2*i] = sample; // left
out[2*i+1] = sample; // right
QByteArray buffer(bytes, Qt::Uninitialized); // sampling business
phase_ += phaseInc;
const int channels = format_.channelCount(); if (phase_ > 2.0f * M_PI) phase_ -= 2.0f * M_PI;
const int sampleRate = format_.sampleRate();
//const float phaseInc = 2.0f * M_PI * frequency_ / sampleRate;
freq += 1.0f;
const float phaseInc = 2.0f * M_PI * freq / sampleRate;
if (format_.sampleFormat() == QAudioFormat::Int16) {
int16_t* out = reinterpret_cast<int16_t*>(buffer.data());
int frames = bytes / (sizeof(int16_t) * channels);
for (int i = 0; i < frames; ++i) {
int16_t s = static_cast<int16_t>(32767 * std::sin(phase_));
for (int c = 0; c < channels; ++c)
*out++ = s;
phase_ += phaseInc;
}
}
else if (format_.sampleFormat() == QAudioFormat::Float) {
float* out = reinterpret_cast<float*>(buffer.data());
int frames = bytes / (sizeof(float) * channels);
for (int i = 0; i < frames; ++i) {
float s = std::sin(phase_);
for (int c = 0; c < channels; ++c)
*out++ = s;
phase_ += phaseInc;
}
} }
return buffer; }
}
void Synth::applyConfig(const AudioConfig& config) {
// map struct values to the QAudioFormat
QAudioFormat format;
format.setSampleRate(config.sampleRate);
format.setChannelCount(config.channelCount);
format.setSampleFormat(config.sampleFormat);
// must create a new device
QAudioDevice device = QMediaDevices::defaultAudioOutput();
if (!device.isFormatSupported(format)) {
std::cout << "Requested format not supported, using preferred format" << std::endl;
format = device.preferredFormat();
}
format_ = format;
// and must create a new audioSink
delete audioSink_;
audioSink_ = new QAudioSink(device, format_, this);
audioSink_->setBufferSize(config.bufferSize);
}
*/

View File

@@ -1,52 +1,38 @@
#pragma once #pragma once
/* #include "../ParameterStore.h"
#include <QObject>
#include <QAudioFormat>
#include <QAudioSink>
#include <QIODevice>
#include <atomic> #include <atomic>
struct AudioConfig { struct SmoothedParam {
int sampleRate = 44100; float current = 0.0f;
int channelCount = 1; float target = 0.0f;
QAudioFormat::SampleFormat sampleFormat = QAudioFormat::Int16; float gain = 0.001f;
int bufferSize = 4096; // bytes
inline void update() {
current += gain * (target - current);
}
}; };
class Synth : public QObject { class Synth {
Q_OBJECT
public: public:
explicit Synth(QObject *parent = nullptr); Synth(const ParameterStore& params);
~Synth(); ~Synth() = default;
// audioSink is the media consumer for the audio data void process(float* out, uint32_t nFrames, uint32_t sampleRate);
QAudioSink* audioSink() { return audioSink_; } void setSampleRate(uint32_t sampleRate) { sampleRate_ = sampleRate; }
// audio config setter/getter
void applyConfig(const AudioConfig& config);
const QAudioFormat& format() const { return format_; }
// synth commands
void start();
void stop();
void setFrequency(float frequency);
// bread and butter right here
QByteArray generateSamples(qint64 bytes);
private: private:
QAudioFormat format_;
QAudioSink *audioSink_ = nullptr;
QIODevice *audioDevice_ = nullptr;
std::atomic<float> frequency_{440.0f}; void updateParams();
inline float getParam(ParamId);
const ParameterStore& paramStore_;
// smoothed params creates a buffer in case the thread controlling paramStore gets blocked
std::array<SmoothedParam, PARAM_COUNT> params_;
uint32_t sampleRate_;
float phase_ = 0.0f; float phase_ = 0.0f;
float freq = 400.0f;
}; };
*/