From 783330990ee6a1368868ef107f6d2adf17f362aa Mon Sep 17 00:00:00 2001 From: Blitblank Date: Sun, 21 Dec 2025 22:49:10 -0600 Subject: [PATCH] audio checkpoint --- CMakeLists.txt | 10 +++- src/MainWindow.cpp | 15 +++-- src/MainWindow.h | 7 +++ src/main.cpp | 5 ++ src/synth/AudioStream.cpp | 27 +++++++++ src/synth/AudioStream.h | 23 ++++++++ src/synth/Synth.cpp | 112 ++++++++++++++++++++++++++++++++++++++ src/synth/Synth.h | 48 ++++++++++++++++ 8 files changed, 241 insertions(+), 6 deletions(-) create mode 100644 src/synth/AudioStream.cpp create mode 100644 src/synth/AudioStream.h create mode 100644 src/synth/Synth.cpp create mode 100644 src/synth/Synth.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 5ea247f..cdf24a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ project(metabolus LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt6 REQUIRED COMPONENTS Widgets) +find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia) qt_standard_project_setup() @@ -13,9 +13,15 @@ qt_add_executable(metabolus src/MainWindow.cpp src/MainWindow.h src/MainWindow.ui + src/synth/AudioStream.cpp + src/synth/AudioStream.h + src/synth/Synth.cpp + src/synth/Synth.h resources/resources.qrc ) target_link_libraries(metabolus - PRIVATE Qt6::Widgets + PRIVATE + Qt6::Widgets + Qt6::Multimedia ) \ No newline at end of file diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 41bdb84..4e5fdeb 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -2,10 +2,8 @@ #include "MainWindow.h" #include "ui_MainWindow.h" -MainWindow::MainWindow(QWidget *parent) - : QMainWindow(parent), - ui(new Ui::MainWindow) -{ +MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { + ui->setupUi(this); // Initialize UI @@ -22,6 +20,13 @@ MainWindow::MainWindow(QWidget *parent) connect(ui->inputStep, &QLineEdit::editingFinished, this, &MainWindow::onStepChanged); connect(ui->inputValue, &QLineEdit::editingFinished, this, &MainWindow::onValueChanged); + // synth business + synth_ = new Synth(this); + audioStream_ = new AudioStream(synth_, this); + + audioStream_->start(); + synth_->audioSink()->start(audioStream_); + } MainWindow::~MainWindow() { @@ -46,6 +51,8 @@ void MainWindow::updateCounterLabel() { void MainWindow::onSliderValueChanged(int value) { QSignalBlocker blocker(ui->inputValue); ui->inputValue->setText(QString::number(value)); + + synth_->setFrequency(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 1f63fbf..7534a14 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -3,6 +3,9 @@ #include +#include "synth/Synth.h" +#include "synth/AudioStream.h" + QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; @@ -38,4 +41,8 @@ private: void applySliderRange(); void applySliderStep(); void syncValueToUi(int value); + + Synth *synth_ = nullptr; + AudioStream *audioStream_ = nullptr; + }; diff --git a/src/main.cpp b/src/main.cpp index 6280c3f..4fc7f97 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,7 +2,12 @@ #include #include "MainWindow.h" +#include + int main(int argc, char *argv[]) { + + // std::cout << "Main()" << std::endl; + QApplication app(argc, argv); MainWindow window; diff --git a/src/synth/AudioStream.cpp b/src/synth/AudioStream.cpp new file mode 100644 index 0000000..e8dd5da --- /dev/null +++ b/src/synth/AudioStream.cpp @@ -0,0 +1,27 @@ + +#include "AudioStream.h" + +#include + +AudioStream::AudioStream(Synth *synth, QObject *parent) : QIODevice(parent), synth_(synth) { + +} + +void AudioStream::start() { + + // std::cout << "AudioStream::start()" << std::endl; + open(QIODevice::ReadOnly); +} + +void AudioStream::stop() { + + close(); +} + +qint64 AudioStream::readData(char *data, qint64 maxlen) { + + // std::cout << "sample out " << std::endl; + QByteArray samples = synth_->generateSamples(maxlen); + memcpy(data, samples.constData(), samples.size()); + return samples.size(); +} diff --git a/src/synth/AudioStream.h b/src/synth/AudioStream.h new file mode 100644 index 0000000..ffd4242 --- /dev/null +++ b/src/synth/AudioStream.h @@ -0,0 +1,23 @@ + +#pragma once + +#include +#include "Synth.h" + +class AudioStream : public QIODevice +{ + Q_OBJECT + +public: + explicit AudioStream(Synth *synth, QObject *parent = nullptr); + + void start(); + void stop(); + +protected: + qint64 readData(char *data, qint64 maxlen) override; + qint64 writeData(const char *, qint64) override { return 0; } + +private: + Synth *synth_; +}; diff --git a/src/synth/Synth.cpp b/src/synth/Synth.cpp new file mode 100644 index 0000000..e6f865d --- /dev/null +++ b/src/synth/Synth.cpp @@ -0,0 +1,112 @@ + +#include "Synth.h" + +#include +#include +#include + +Synth::Synth(QObject *parent) : QObject(parent) { + + format_.setSampleRate(44100); + format_.setChannelCount(1); + format_.setSampleFormat(QAudioFormat::Int16); + + QAudioDevice device = QMediaDevices::defaultAudioOutput(); + + if (!device.isFormatSupported(format_)) { + format_ = device.preferredFormat(); + } + + audioSink_ = new QAudioSink(device, format_, this); + audioSink_->setBufferSize(4096); +} + +Synth::~Synth() { + + stop(); +} + +void Synth::start() { + + /* + // std::cout << "Synth::start()" << std::endl; + if (audioSink_->state() == QAudio::ActiveState) + return; + + audioDevice_ = audioSink_->start(); + */ +} + +void Synth::stop() { + + if (audioSink_) { + audioSink_->stop(); + audioDevice_ = nullptr; + } +} + +void Synth::setFrequency(float frequency) { + + frequency_ = qMax(1.0f, frequency); +} + +QByteArray Synth::generateSamples(qint64 bytes) { + + 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; + } + } + + 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 new file mode 100644 index 0000000..1440bae --- /dev/null +++ b/src/synth/Synth.h @@ -0,0 +1,48 @@ + +#pragma once + +#include +#include +#include +#include + +#include + +struct AudioConfig { + int sampleRate = 44100; + int channelCount = 1; + QAudioFormat::SampleFormat sampleFormat = QAudioFormat::Int16; + int bufferSize = 4096; // bytes +}; + +class Synth : public QObject { + Q_OBJECT + +public: + explicit Synth(QObject *parent = nullptr); + ~Synth(); + + // 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); + +private: + QAudioFormat format_; + QAudioSink *audioSink_ = nullptr; + QIODevice *audioDevice_ = nullptr; + + std::atomic frequency_{440.0f}; + float phase_ = 0.0f; + + float freq = 400.0f; +}; \ No newline at end of file