diff --git a/CMakeLists.txt b/CMakeLists.txt index bc93e0f..4c8eee5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,6 +44,7 @@ qt_add_executable(metabolus src/MainWindow.cpp src/MainWindow.h src/MainWindow.ui + src/ParameterStore.h src/synth/AudioEngine.cpp src/synth/AudioEngine.h src/synth/Synth.cpp diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 2639a5c..7b1ec74 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -1,6 +1,8 @@ #include "MainWindow.h" + #include "ui_MainWindow.h" +#include "ParameterStore.h" 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); // slider business + // TODO: smart slider widget connect(ui->slider, &QSlider::valueChanged, this, &MainWindow::onSliderValueChanged); connect(ui->inputMin, &QLineEdit::editingFinished, this, &MainWindow::onMinChanged); 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_->start(); + // init defaults + // TODO:: there's gotta be a better way + ui->slider->setValue(PARAM_DEFS[static_cast(ParamId::Osc1Frequency)].def); + ui->slider->setMinimum(PARAM_DEFS[static_cast(ParamId::Osc1Frequency)].min); + ui->slider->setMaximum(PARAM_DEFS[static_cast(ParamId::Osc1Frequency)].max); + ui->inputValue->setText(QString::number(PARAM_DEFS[static_cast(ParamId::Osc1Frequency)].def)); + ui->inputMin->setText(QString::number(PARAM_DEFS[static_cast(ParamId::Osc1Frequency)].min)); + ui->inputMax->setText(QString::number(PARAM_DEFS[static_cast(ParamId::Osc1Frequency)].max)); } MainWindow::~MainWindow() { @@ -49,7 +60,8 @@ void MainWindow::onSliderValueChanged(int value) { QSignalBlocker blocker(ui->inputValue); ui->inputValue->setText(QString::number(value)); - audio_->setFrequency(static_cast(value)); + // forward value so synthesizer can read + audio_->parameters()->set(ParamId::Osc1Frequency, static_cast(value)); } // allows only values within the min, max to be set by the text field diff --git a/src/MainWindow.h b/src/MainWindow.h index dae69c1..2f54513 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -42,5 +42,4 @@ private: void syncValueToUi(int value); AudioEngine* audio_ = nullptr; - }; diff --git a/src/ParameterStore.h b/src/ParameterStore.h new file mode 100644 index 0000000..e4d55c6 --- /dev/null +++ b/src/ParameterStore.h @@ -0,0 +1,80 @@ + +#pragma once + +#include +#include +#include + +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(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(ParamId::Count); + +class ParameterStore { + +public: + + ParameterStore() { resetToDefaults(); } + ~ParameterStore() = default; + + // TODO: move into implementation file ? idk + void set(ParamId id, float value) { + values_[static_cast(id)].store(value, std::memory_order_relaxed); + } + float get(ParamId id) const { + return values_[static_cast(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, PARAM_COUNT> values_; + +}; diff --git a/src/synth/AudioEngine.cpp b/src/synth/AudioEngine.cpp index 94e5d60..d518c66 100644 --- a/src/synth/AudioEngine.cpp +++ b/src/synth/AudioEngine.cpp @@ -1,18 +1,17 @@ #include "AudioEngine.h" -#define _USE_MATH_DEFINES -#include #include -#ifndef M_PI // I hate my stupid chungus life - #define M_PI 3.14159265358979323846 -#endif - -AudioEngine::AudioEngine() { +AudioEngine::AudioEngine() : synth_(params_) { if(audio_.getDeviceCount() < 1) { throw std::runtime_error("No audio devices found"); } + + // TODO: get audio configurations + + synth_.setSampleRate(sampleRate_); + } AudioEngine::~AudioEngine() { @@ -23,24 +22,17 @@ bool AudioEngine::start() { RtAudio::StreamParameters params; params.deviceId = audio_.getDefaultOutputDevice(); - params.nChannels = 2; + params.nChannels = channels_; params.firstChannel = 0; RtAudio::StreamOptions options; options.flags = RTAUDIO_MINIMIZE_LATENCY; - /* - try { - audio_.openStream(¶ms, nullptr, RTAUDIO_FLOAT32, sampleRate_, &bufferFrames_, &AudioEngine::audioCallback, this, &options); - audio_.startStream(); - } catch(RtAudioError& e) { - std::cerr << e.getMessage() << std::endl; - return false; - } - */ + // TODO: error check this please audio_.openStream(¶ms, nullptr, RTAUDIO_FLOAT32, sampleRate_, &bufferFrames_, &AudioEngine::audioCallback, this, &options); audio_.startStream(); + // sanity check std::cout << "sample rate: " << sampleRate_ << " buffer frames: " << bufferFrames_ << std::endl; return true; @@ -52,10 +44,6 @@ void AudioEngine::stop() { 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) { 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) { - const float sr = static_cast(sampleRate_); - float target = targetFreq_.load(std::memory_order_relaxed); - float freqStep = (target - currentFreq_) / nFrames; - for (uint32_t i = 0; i < nFrames; ++i) { - currentFreq_ += freqStep; - - 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; - } + // pass to synth + synth_.process(out, nFrames, sampleRate_); return 0; } diff --git a/src/synth/AudioEngine.h b/src/synth/AudioEngine.h index 1c99700..0229e93 100644 --- a/src/synth/AudioEngine.h +++ b/src/synth/AudioEngine.h @@ -5,6 +5,8 @@ #include #include +#include "Synth.h" + class AudioEngine { public: @@ -13,19 +15,19 @@ public: bool start(); void stop(); - - void setFrequency(float freq); + ParameterStore* parameters() { return ¶ms_; } private: 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); + ParameterStore params_; + Synth synth_; + + // TODO: id like a yml config file or something for these RtAudio audio_; uint32_t sampleRate_ = 44100; - uint32_t bufferFrames_ = 256; - - std::atomic targetFreq_{ 400.0f }; - float currentFreq_ = 440.0f; - float phase_ = 0.0f; + uint32_t bufferFrames_ = 256; // time per buffer = BF/SR (256/44100 = 5.8ms) + uint32_t channels_ = 2; // stereo }; diff --git a/src/synth/Synth.cpp b/src/synth/Synth.cpp index b946eac..5d72d54 100644 --- a/src/synth/Synth.cpp +++ b/src/synth/Synth.cpp @@ -1,115 +1,51 @@ -/* - #include "Synth.h" -#include -#include #include +#include -Synth::Synth(QObject *parent) : QObject(parent) { +#ifndef M_PI // I hate my stupid chungus life + #define M_PI 3.14159265358979323846 +#endif - format_.setSampleRate(44100); - format_.setChannelCount(1); - format_.setSampleFormat(QAudioFormat::Int16); +Synth::Synth(const ParameterStore& params) : paramStore_(params) { - QAudioDevice device = QMediaDevices::defaultAudioOutput(); +} - if (!device.isFormatSupported(format_)) { - format_ = device.preferredFormat(); +void Synth::updateParams() { + for(size_t i = 0; i < PARAM_COUNT; i++) { + params_[i].target = paramStore_.get(static_cast(i)); } - - audioSink_ = new QAudioSink(device, format_, this); - audioSink_->setBufferSize(4096); } -Synth::~Synth() { - - stop(); +inline float Synth::getParam(ParamId id) { + return params_[static_cast(id)].current; } -void Synth::start() { - - // std::cout << "Synth::start()" << std::endl; - // if (audioSink_->state() == QAudio::ActiveState) - // return; - - // audioDevice_ = audioSink_->start(); +void Synth::process(float* out, uint32_t nFrames, uint32_t sampleRate) { -} + // 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_) { - audioSink_->stop(); - audioDevice_ = nullptr; - } -} + for(auto& p : params_) p.update(); // TODO: profile this -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(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); - - const int channels = format_.channelCount(); - 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(buffer.data()); - int frames = bytes / (sizeof(int16_t) * channels); - - for (int i = 0; i < frames; ++i) { - int16_t s = static_cast(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(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; - } + // sampling business + phase_ += phaseInc; + if (phase_ > 2.0f * M_PI) phase_ -= 2.0f * M_PI; } - 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); -} - -*/ +} \ No newline at end of file diff --git a/src/synth/Synth.h b/src/synth/Synth.h index c8b742e..c2a8f55 100644 --- a/src/synth/Synth.h +++ b/src/synth/Synth.h @@ -1,52 +1,38 @@ #pragma once -/* - -#include -#include -#include -#include +#include "../ParameterStore.h" #include -struct AudioConfig { - int sampleRate = 44100; - int channelCount = 1; - QAudioFormat::SampleFormat sampleFormat = QAudioFormat::Int16; - int bufferSize = 4096; // bytes +struct SmoothedParam { + float current = 0.0f; + float target = 0.0f; + float gain = 0.001f; + + inline void update() { + current += gain * (target - current); + } }; -class Synth : public QObject { - Q_OBJECT +class Synth { public: - explicit Synth(QObject *parent = nullptr); - ~Synth(); + Synth(const ParameterStore& params); + ~Synth() = default; - // audioSink is the media consumer for the audio data - QAudioSink* audioSink() { return audioSink_; } - - // 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); + void process(float* out, uint32_t nFrames, uint32_t sampleRate); + void setSampleRate(uint32_t sampleRate) { sampleRate_ = sampleRate; } private: - QAudioFormat format_; - QAudioSink *audioSink_ = nullptr; - QIODevice *audioDevice_ = nullptr; - std::atomic 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 params_; + uint32_t sampleRate_; + float phase_ = 0.0f; - - float freq = 400.0f; }; - -*/