diff --git a/.gitmodules b/.gitmodules index ae3ece6..e69de29 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "rtmidi"] - path = rtmidi - url = git@github.com:thestk/rtmidi.git diff --git a/DrumMachine.pro b/DrumMachine.pro index 85fbc6c..ae307ef 100755 --- a/DrumMachine.pro +++ b/DrumMachine.pro @@ -6,19 +6,14 @@ release: QMAKE_CXXFLAGS_RELEASE -= -O1 release: QMAKE_CXXFLAGS_RELEASE -= -O2 release: QMAKE_CXXFLAGS_RELEASE += -O3 -ffast-math -march=native -mtune=native -win32: { - DEFINES += __WINDOWS_MM__ - LIBS += -lwinmm -} - -unix: { - DEFINES += __LINUX_ALSA__ - LIBS += -lasound -} +LIBS += -lrtmidi -lportaudio DEFINES += QT_DEPRECATED_WARNINGS QT_DISABLE_DEPRECATED_BEFORE=0x060000 SOURCES += \ + audiodecoder.cpp \ + audioformat.cpp \ + audioplayer.cpp \ filesmodel.cpp \ jsonconverters.cpp \ main.cpp \ @@ -28,12 +23,14 @@ SOURCES += \ presetdetailwidget.cpp \ presets.cpp \ presetsmodel.cpp \ - rtmidi/RtMidi.cpp \ sampleswidget.cpp \ samplewidget.cpp \ sequencerwidget.cpp HEADERS += \ + audiodecoder.h \ + audioformat.h \ + audioplayer.h \ filesmodel.h \ jsonconverters.h \ mainwindow.h \ @@ -42,7 +39,6 @@ HEADERS += \ presetdetailwidget.h \ presets.h \ presetsmodel.h \ - rtmidi/RtMidi.h \ sampleswidget.h \ samplewidget.h \ sequencerwidget.h diff --git a/audiodecoder.cpp b/audiodecoder.cpp new file mode 100644 index 0000000..f64899f --- /dev/null +++ b/audiodecoder.cpp @@ -0,0 +1,83 @@ +#include "audiodecoder.h" + +#include +#include +#include +#include + +#include "audioformat.h" + +AudioDecoder::AudioDecoder(QObject *parent) : + QObject(parent) +{ + QObject::connect(&m_decoder, qOverload(&QAudioDecoder::error), + this, &AudioDecoder::error); + QObject::connect(&m_decoder, &QAudioDecoder::finished, + this, &AudioDecoder::finished); + QObject::connect(&m_decoder, &QAudioDecoder::bufferReady, + this, &AudioDecoder::bufferReady); + QObject::connect(&m_decoder, &QAudioDecoder::durationChanged, + this, &AudioDecoder::durationChanged); + + m_decoder.setAudioFormat(audioFormat()); +} + +void AudioDecoder::startDecoding(std::shared_ptr device) +{ + qDebug() << "called" << device.get(); + + if (m_decoder.state() == QAudioDecoder::DecodingState) + m_decoder.stop(); + + m_decoder.setSourceDevice(device.get()); + m_device = device; + m_bytearray.clear(); + m_decoder.start(); +} + +void AudioDecoder::error(const QAudioDecoder::Error error) +{ + qDebug() << "error:" << error << m_decoder.errorString() << m_decoder.sourceFilename(); +} + +void AudioDecoder::finished() +{ + qDebug() << "called"; + emit decodingFinished(QAudioBuffer{std::move(m_bytearray), audioFormat()}); +} + +void AudioDecoder::bufferReady() +{ + const auto &format = m_decoder.audioFormat(); + const auto buffer = m_decoder.read(); + + Q_ASSERT(buffer.byteCount() == buffer.sampleCount() * (format.sampleSize()/8)); + + m_bytearray.append(buffer.constData(), buffer.byteCount()); + + const auto now = QDateTime::currentDateTime(); + if (m_lastProgressUpdate.isNull() || m_lastProgressUpdate.msecsTo(now) > 100) + { + emit progress(m_decoder.position(), m_decoder.duration()); + m_lastProgressUpdate = now; + } +} + +void AudioDecoder::durationChanged(const qint64 duration) +{ + if (duration == -1) + return; + + const auto &format = m_decoder.audioFormat(); + const auto reserve = (format.sampleSize()/8) * format.sampleRate() * format.channelCount() * (duration + 1000) / 1000; + m_bytearray.reserve(reserve); + qDebug() << "duration:" << duration << reserve; +} + +namespace { +void registerMetaType() +{ + qRegisterMetaType>(); +} +Q_COREAPP_STARTUP_FUNCTION(registerMetaType) +} diff --git a/audiodecoder.h b/audiodecoder.h new file mode 100644 index 0000000..6282a56 --- /dev/null +++ b/audiodecoder.h @@ -0,0 +1,40 @@ +#pragma once + +#include + +#include +#include +#include +#include + +class QAudioBuffer; +class QIODevice; + +class AudioDecoder : public QObject +{ + Q_OBJECT + +public: + explicit AudioDecoder(QObject *parent = nullptr); + +signals: + void progress(int progress, int total); + void decodingFinished(const QAudioBuffer &buffer); + +public slots: + void startDecoding(std::shared_ptr device); + +private slots: + void error(const QAudioDecoder::Error error); + void finished(); + void bufferReady(); + void durationChanged(const qint64 duration); + +private: + std::shared_ptr m_device; + QAudioDecoder m_decoder; + QByteArray m_bytearray; + QDateTime m_lastProgressUpdate; +}; + +Q_DECLARE_METATYPE(std::shared_ptr) diff --git a/audioformat.cpp b/audioformat.cpp new file mode 100644 index 0000000..b653e9f --- /dev/null +++ b/audioformat.cpp @@ -0,0 +1,17 @@ +#include "audioformat.h" + +const QAudioFormat &audioFormat() +{ + static const QAudioFormat audioFormat = [](){ + QAudioFormat format; + format.setSampleRate(sampleRate); + format.setChannelCount(channelCount); + format.setSampleSize(sampleSize); + format.setCodec(codec); + format.setByteOrder(byteOrder); + format.setSampleType(sampleType); + return format; + }(); + + return audioFormat; +} diff --git a/audioformat.h b/audioformat.h new file mode 100644 index 0000000..e6b504e --- /dev/null +++ b/audioformat.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +static constexpr int sampleRate = 44100; +static constexpr int channelCount = 2; +static constexpr int sampleSize = 32; +static constexpr auto codec = "audio/pcm"; +static constexpr QAudioFormat::Endian byteOrder = QAudioFormat::LittleEndian; +static constexpr QAudioFormat::SampleType sampleType = QAudioFormat::Float; +using sample_t = float; +using frame_t = std::array; + +const QAudioFormat &audioFormat(); diff --git a/audioplayer.cpp b/audioplayer.cpp new file mode 100644 index 0000000..eba50ba --- /dev/null +++ b/audioplayer.cpp @@ -0,0 +1,52 @@ +#include "audioplayer.h" + +#include +#include + +AudioPlayer::AudioPlayer(QObject *parent) : + QObject{parent} +{ +} + +AudioPlayer::AudioPlayer(const QAudioBuffer &buffer, QObject *parent) : + QObject{parent}, + m_buffer{buffer} +{ +} + +void AudioPlayer::writeSamples(frame_t *begin, frame_t *end) +{ + if (!m_playing) + return; + + const auto frames = std::min(std::distance(begin, end), m_buffer.frameCount()-m_position); + + if (!frames) + { + m_playing = false; + emit playingChanged(false); + return; + } + + std::transform(static_cast(begin), static_cast(begin+frames), m_buffer.constData() + m_position, begin, + [](const frame_t &frame, const frame_t &frame2)->frame_t{ + frame_t newFrame; + std::transform(std::begin(frame), std::end(frame), std::begin(frame2), std::begin(newFrame), + [](const sample_t &left, const sample_t &right) { return left + right; }); + return newFrame; + }); + + m_position += frames; +} + +void AudioPlayer::restart() +{ + setPosition(0); + setPlaying(true); +} + +void AudioPlayer::stop() +{ + setPosition(0); + setPlaying(false); +} diff --git a/audioplayer.h b/audioplayer.h new file mode 100644 index 0000000..8fed489 --- /dev/null +++ b/audioplayer.h @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include "audioformat.h" + +class AudioPlayer : public QObject +{ + Q_OBJECT + +public: + explicit AudioPlayer(QObject *parent = nullptr); + explicit AudioPlayer(const QAudioBuffer &buffer, QObject *parent = nullptr); + + void writeSamples(frame_t *begin, frame_t *end); + + bool playing() const { return m_playing; } + void setPlaying(bool playing) { m_playing = playing; emit playingChanged(playing); } + + std::size_t position() const { return m_position; } + void setPosition(std::size_t position) { m_position = position; emit positionChanged(position); } + + const QAudioBuffer &buffer() const { return m_buffer; } + void setBuffer(const QAudioBuffer &buffer) { emit bufferChanged(m_buffer = buffer); } + + void restart(); + void stop(); + +signals: + void playingChanged(bool playing); + void positionChanged(std::size_t position); + void bufferChanged(const QAudioBuffer &buffer); + +private: + bool m_playing{false}; + std::size_t m_position{}; + QAudioBuffer m_buffer; + + QDateTime m_lastPositionUpdate; +}; diff --git a/main.cpp b/main.cpp index 8dfa0ac..837d232 100755 --- a/main.cpp +++ b/main.cpp @@ -8,9 +8,36 @@ #include #include +#include "portaudio.h" + #include "jsonconverters.h" #include "mainwindow.h" +namespace { +template +class CleanupHelper +{ +public: + CleanupHelper(T &&callback) : + m_callback{std::move(callback)} + {} + + ~CleanupHelper() + { + m_callback(); + } + +private: + T m_callback; +}; + +template +auto makeCleanupHelper(T &&callback) +{ + return CleanupHelper{std::move(callback)}; +} +} + int main(int argc, char *argv[]) { QApplication app(argc, argv); @@ -23,6 +50,18 @@ int main(int argc, char *argv[]) qDebug() << "sslLibraryVersionString" << QSslSocket::sslLibraryVersionString(); qDebug() << "sslLibraryBuildVersionString" << QSslSocket::sslLibraryBuildVersionString(); + if (PaError err = Pa_Initialize(); err != paNoError) + { + QMessageBox::warning({}, QApplication::translate("main", "Error initializing PortAudio!"), QApplication::translate("main", "Error initializing PortAudio!") + "\n\n" + Pa_GetErrorText(err)); + return 1; + } + + auto helper0 = makeCleanupHelper([](){ + qDebug() << "helper0"; + if (PaError err = Pa_Terminate(); err != paNoError) + fprintf(stderr, "Could not terminate PortAudio!\n"); + }); + qSetMessagePattern("%{time dd.MM.yyyy HH:mm:ss.zzz} " "[" "%{if-debug}D%{endif}" diff --git a/mainwindow.cpp b/mainwindow.cpp index bce1d3b..2d5d4ac 100755 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -6,14 +6,54 @@ #include #include #include +#include #include "presets.h" #include "midiinwrapper.h" #include "midicontainers.h" +namespace { +void DummyDeleter(PaStream *stream) {} +void PaStreamCloser(PaStream *stream) +{ + if (PaError err = Pa_CloseStream(stream); err != paNoError) + fprintf(stderr, "Could not close stream!\n"); +} +void PaStreamStopperAndCloser(PaStream *stream) +{ + if (PaError err = Pa_StopStream(stream); err != paNoError) + fprintf(stderr, "Could not stop stream!\n"); + PaStreamCloser(stream); +} + +void paStreamFinished(void* userData) +{ + printf("Stream Completed\n"); +} + +int paCallback(const void *inputBuffer, void *outputBuffer, + unsigned long framesPerBuffer, + const PaStreamCallbackTimeInfo* timeInfo, + PaStreamCallbackFlags statusFlags, + void *userData) +{ + Q_UNUSED(inputBuffer) + Q_ASSERT(outputBuffer); + Q_ASSERT(framesPerBuffer); + Q_UNUSED(timeInfo) + Q_UNUSED(statusFlags) + Q_ASSERT(userData); + + auto begin = static_cast(outputBuffer); + + static_cast(userData)->writeSamples(begin, begin+framesPerBuffer); +} +} + MainWindow::MainWindow(const presets::PresetsConfig &presetsConfig, QWidget *parent) : QMainWindow{parent}, m_ui{std::make_unique()}, + m_paStream(nullptr, DummyDeleter), m_presetsModel{*presetsConfig.presets} { m_ui->setupUi(this); @@ -40,19 +80,9 @@ MainWindow::MainWindow(const presets::PresetsConfig &presetsConfig, QWidget *par updateAudioDevices(); - { - const auto index = m_devices.indexOf(QAudioDeviceInfo::defaultOutputDevice()); - if (index != -1) - m_ui->comboBoxAudioDevice->setCurrentIndex(index); - } + m_ui->comboBoxAudioDevice->setCurrentIndex(Pa_GetDefaultOutputDevice()); - { - const auto callback = [this](int index){ - m_ui->samplesWidget->setAudioDevice(m_devices.at(index)); - }; - connect(m_ui->comboBoxAudioDevice, qOverload(&QComboBox::currentIndexChanged), m_ui->samplesWidget, callback); - callback(m_ui->comboBoxAudioDevice->currentIndex()); - } + connect(m_ui->pushButtonAudioDevice, &QAbstractButton::pressed, this, &MainWindow::openAudioDevice); m_presetsProxyModel.setFilterCaseSensitivity(Qt::CaseInsensitive); m_presetsProxyModel.setSourceModel(&m_presetsModel); @@ -84,6 +114,65 @@ void MainWindow::selectFirstPreset() } } +void MainWindow::writeSamples(frame_t *begin, frame_t *end) +{ + std::fill(begin, end, frame_t{0.,0.}); + + m_ui->samplesWidget->writeSamples(begin, end); +} + +void MainWindow::openAudioDevice() +{ + if (m_paStream) + { + m_paStream = nullptr; + m_ui->pushButtonAudioDevice->setText(tr("Open")); + } + else + { + PaDeviceIndex index = m_ui->comboBoxAudioDevice->currentIndex(); + + const PaDeviceInfo* pInfo = Pa_GetDeviceInfo(index); + + const PaStreamParameters outputParameters { + .device = index, + .channelCount = channelCount, + .sampleFormat = paFloat32, /* 32 bit floating point output */ + .suggestedLatency = pInfo->defaultLowOutputLatency, + .hostApiSpecificStreamInfo = NULL + }; + + PaStream *stream{}; + + if (PaError err = Pa_OpenStream(&stream, NULL, &outputParameters, sampleRate, m_ui->spinBoxBufferSize->value(), paNoFlag, &paCallback, this); err != paNoError) + { + QMessageBox::warning(this, tr("Error opening stream!"), tr("Error opening stream!") + "\n\n" + Pa_GetErrorText(err)); + return; + } + + auto smartPtr = std::unique_ptr(stream, PaStreamCloser); + stream = nullptr; + + if (PaError err = Pa_SetStreamFinishedCallback(smartPtr.get(), &paStreamFinished); err != paNoError) + { + QMessageBox::warning(this, tr("Error setting finished callback!"), tr("Error setting finished callback!") + "\n\n" + Pa_GetErrorText(err)); + return; + } + + if (PaError err = Pa_StartStream(smartPtr.get()); err != paNoError) + { + QMessageBox::warning(this, tr("Error starting stream!"), tr("Error starting stream!") + "\n\n" + Pa_GetErrorText(err)); + return; + } + + // stream has been started and from now on we not only need to delete it, but also stop it first + smartPtr.get_deleter() = PaStreamStopperAndCloser; + + m_paStream = std::move(smartPtr); + m_ui->pushButtonAudioDevice->setText(tr("Close")); + } +} + void MainWindow::messageReceived(const midi::MidiMessage &message) { m_ui->statusbar->showMessage(tr("Received midi message: flag: %0 cmd: %1 channel: %2 note: %3 velocity: %4") @@ -120,8 +209,11 @@ void MainWindow::updateAudioDevices() { m_ui->comboBoxAudioDevice->clear(); - m_devices = QAudioDeviceInfo::availableDevices(QAudio::AudioOutput); + const auto count = Pa_GetDeviceCount(); - for (const auto &device : m_devices) - m_ui->comboBoxAudioDevice->addItem(device.deviceName()); + for (PaDeviceIndex i = 0; i < count; i++) + { + const auto info = Pa_GetDeviceInfo(i); + m_ui->comboBoxAudioDevice->addItem(info->name); + } } diff --git a/mainwindow.h b/mainwindow.h index ed71c92..6103851 100755 --- a/mainwindow.h +++ b/mainwindow.h @@ -5,6 +5,9 @@ #include #include +#include "portaudio.h" + +#include "audioformat.h" #include "presetsmodel.h" #include "filesmodel.h" #include "midiinwrapper.h" @@ -12,7 +15,6 @@ namespace Ui { class MainWindow; } namespace presets { struct PresetsConfig; } namespace midi { struct MidiMessage; } -class QAudioDeviceInfo; class MainWindow : public QMainWindow { @@ -24,7 +26,10 @@ public: void selectFirstPreset(); + void writeSamples(frame_t *begin, frame_t *end); + private slots: + void openAudioDevice(); void messageReceived(const midi::MidiMessage &message); void currentRowChanged(const QModelIndex ¤t); @@ -34,9 +39,9 @@ private: const std::unique_ptr m_ui; - MidiInWrapper m_midiIn; + std::unique_ptr m_paStream; - QList m_devices; + MidiInWrapper m_midiIn; PresetsModel m_presetsModel; QSortFilterProxyModel m_presetsProxyModel; diff --git a/mainwindow.ui b/mainwindow.ui index 1e6a8a2..506e6b1 100755 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -124,6 +124,23 @@ + + + + 65535 + + + 32 + + + + + + + Open + + + diff --git a/rtmidi b/rtmidi deleted file mode 160000 index 7ab18ef..0000000 --- a/rtmidi +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7ab18ef06b549e7cdd247c492e055093263dd4f3 diff --git a/sampleswidget.cpp b/sampleswidget.cpp index d364ad3..5e0f560 100755 --- a/sampleswidget.cpp +++ b/sampleswidget.cpp @@ -3,7 +3,6 @@ #include -#include #include #include "midicontainers.h" @@ -14,10 +13,13 @@ SamplesWidget::SamplesWidget(QWidget *parent) : { m_ui->setupUi(this); + m_cache.setCacheDirectory("cache"); + m_networkAccessManager.setCache(&m_cache); + { QEventLoop eventLoop; - connect(&m_audioThread, &QThread::started, &eventLoop, &QEventLoop::quit); - m_audioThread.start(QThread::HighestPriority); + connect(&m_decoderThread, &QThread::started, &eventLoop, &QEventLoop::quit); + m_decoderThread.start(QThread::HighestPriority); eventLoop.exec(); } @@ -26,7 +28,11 @@ SamplesWidget::SamplesWidget(QWidget *parent) : connect(m_ui->pushButtonStopAll, &QAbstractButton::pressed, this, &SamplesWidget::stopAll); for (const auto &ref : getWidgets()) + { + ref.get().injectNetworkAccessManager(m_networkAccessManager); + ref.get().injectDecodingThread(m_decoderThread); connect(&ref.get(), &SampleWidget::chokeTriggered, this, &SamplesWidget::chokeTriggered); + } m_ui->sampleWidget_1->setNote(48); m_ui->sampleWidget_2->setNote(50); @@ -48,8 +54,8 @@ SamplesWidget::SamplesWidget(QWidget *parent) : SamplesWidget::~SamplesWidget() { - m_audioThread.exit(); - m_audioThread.wait(); + m_decoderThread.exit(); + m_decoderThread.wait(); } void SamplesWidget::setPreset(const presets::Preset &preset) @@ -82,13 +88,10 @@ void SamplesWidget::messageReceived(const midi::MidiMessage &message) } } -void SamplesWidget::setAudioDevice(const QAudioDeviceInfo &device) +void SamplesWidget::writeSamples(frame_t *begin, frame_t *end) { for (const auto &ref : getWidgets()) - { - connect(&ref.get(), &SampleWidget::chokeTriggered, this, &SamplesWidget::chokeTriggered); - ref.get().setupAudioThread(device, m_audioThread); - } + ref.get().writeSamples(begin, end); } void SamplesWidget::sequencerTriggerSample(int index) diff --git a/sampleswidget.h b/sampleswidget.h index 9acda04..1bdb79b 100755 --- a/sampleswidget.h +++ b/sampleswidget.h @@ -5,14 +5,16 @@ #include #include +#include +#include #include +#include "audioformat.h" #include "presets.h" namespace Ui { class SamplesWidget; } namespace midi { class MidiMessage; } class SampleWidget; -class QAudioDeviceInfo; class SamplesWidget : public QWidget { @@ -26,7 +28,7 @@ public: void messageReceived(const midi::MidiMessage &message); - void setAudioDevice(const QAudioDeviceInfo &device); + void writeSamples(frame_t *begin, frame_t *end); public slots: void sequencerTriggerSample(int index); @@ -41,7 +43,10 @@ private: const std::unique_ptr m_ui; - presets::Preset m_preset; + QNetworkDiskCache m_cache; + QNetworkAccessManager m_networkAccessManager; - QThread m_audioThread; + QThread m_decoderThread; + + presets::Preset m_preset; }; diff --git a/samplewidget.cpp b/samplewidget.cpp index 4a66e6d..3104725 100755 --- a/samplewidget.cpp +++ b/samplewidget.cpp @@ -3,24 +3,18 @@ #include #include +#include +#include +#include +#include #include +#include "audiodecoder.h" + namespace { QString toString(QString value) { return value; } QString toString(int value) { return QString::number(value); } QString toString(bool value) { return value?"true":"false"; } -QString toString(QSoundEffect::Status value) -{ - switch (value) - { - case QSoundEffect::Null: return "Null"; - case QSoundEffect::Loading: return "Loading"; - case QSoundEffect::Ready: return "Ready"; - case QSoundEffect::Error: return "Error"; - } - - return QString{"Unknown (%0)"}.arg(value); -} } SampleWidget::SampleWidget(QWidget *parent) : @@ -29,31 +23,22 @@ SampleWidget::SampleWidget(QWidget *parent) : { m_ui->setupUi(this); + connect(&m_player, &AudioPlayer::playingChanged, this, &SampleWidget::updateStatus); + connect(m_ui->pushButton, &QAbstractButton::pressed, this, [this](){ pressed(127); }); connect(m_ui->pushButton, &QAbstractButton::released, this, &SampleWidget::released); - connect(m_ui->toolButtonReload, &QAbstractButton::pressed, this, &SampleWidget::createEffect); updateStatus(); } -SampleWidget::~SampleWidget() -{ - destroyEffect(); -} +SampleWidget::~SampleWidget() = default; void SampleWidget::setFile(const QString &presetId, const presets::File &file) { m_presetId = presetId; m_file = file; - if (m_effect) - { - auto sampleUrl = this->sampleUrl(); - if (!sampleUrl.isEmpty()) - QMetaObject::invokeMethod(m_effect.get(), [&effect=*m_effect,sampleUrl=std::move(sampleUrl)](){ - effect.setSource(sampleUrl); - }); - } + startRequest(); const auto setupLabel = [&](const auto &value, QLabel *label){ QString text; @@ -110,10 +95,7 @@ void SampleWidget::pressed(quint8 velocity) { Q_UNUSED(velocity) - if (m_effect) - { - QMetaObject::invokeMethod(m_effect.get(), &QSoundEffect::play); - } + m_player.restart(); if (m_file && m_file->choke && *m_file->choke) emit chokeTriggered(*m_file->choke); @@ -125,41 +107,39 @@ void SampleWidget::released() void SampleWidget::forceStop() { - if (m_effect) - QMetaObject::invokeMethod(m_effect.get(), &QSoundEffect::stop); + m_player.setPlaying(false); } -void SampleWidget::setupAudioThread(const QAudioDeviceInfo &device, QThread &thread) +void SampleWidget::injectNetworkAccessManager(QNetworkAccessManager &networkAccessManager) { - m_device = device; - m_thread = &thread; - - createEffect(); + m_networkAccessManager = &networkAccessManager; + if (m_file) + startRequest(); } -void SampleWidget::createEffect() +void SampleWidget::injectDecodingThread(QThread &thread) { - QMetaObject::invokeMethod(QAbstractEventDispatcher::instance(m_thread), [this](){ - m_effect = std::make_unique(m_device); - - connect(m_effect.get(), &QSoundEffect::playingChanged, this, &SampleWidget::updateStatus); - connect(m_effect.get(), &QSoundEffect::statusChanged, this, &SampleWidget::updateStatus); - - const auto sampleUrl = this->sampleUrl(); - if (!sampleUrl.isEmpty()) - m_effect->setSource(sampleUrl); - - QMetaObject::invokeMethod(this, &SampleWidget::updateStatus); + QMetaObject::invokeMethod(QAbstractEventDispatcher::instance(&thread), [this](){ + m_decoder = std::make_unique(); + connect(this, &SampleWidget::startDecoding, m_decoder.get(), &AudioDecoder::startDecoding); + connect(m_decoder.get(), &AudioDecoder::decodingFinished, this, &SampleWidget::decodingFinished); + if (m_reply && m_reply->isFinished() && m_reply->error() == QNetworkReply::NoError) + m_decoder->startDecoding(m_reply); }); } +void SampleWidget::writeSamples(frame_t *begin, frame_t *end) +{ + m_player.writeSamples(begin, end); +} + void SampleWidget::updateStatus() { QPalette pal; - if (m_effect && m_file && m_file->color) + if (m_file && m_file->color) { - const auto bright = m_effect->isPlaying() ? 255 : 155; - const auto dark = m_effect->isPlaying() ? + const auto bright = m_player.playing() ? 255 : 155; + const auto dark = m_player.playing() ? #if !defined(Q_OS_WIN) 80 : 0 #else @@ -183,22 +163,52 @@ void SampleWidget::updateStatus() } setPalette(pal); - if (!m_effect) - m_ui->statusLabel->setText(tr("No player")); + if (m_reply) + { + if (!m_reply->isFinished()) + m_ui->statusLabel->setText(tr("Downloading...")); + else if (m_reply->error() != QNetworkReply::NoError) + m_ui->statusLabel->setText(QMetaEnum::fromType().valueToKey(m_reply->error())); + else + { + if (!m_decoder) + m_ui->statusLabel->setText(tr("Waiting for decoder thread...")); + else + m_ui->statusLabel->setText(tr("Decoding")); + } + } else - m_ui->statusLabel->setText(toString(m_effect->status())); + m_ui->statusLabel->setText(m_player.playing() ? tr("Playing") : tr("Ready")); } -void SampleWidget::destroyEffect() +void SampleWidget::requestFinished() { - if (m_effect) - QMetaObject::invokeMethod(m_effect.get(), [effect=m_effect.release()](){ delete effect; }); + qDebug() << "called" << m_reply->error() << m_reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute); + if (m_reply->error() == QNetworkReply::NoError) + { + emit startDecoding(m_reply); + } + updateStatus(); } -QUrl SampleWidget::sampleUrl() const +void SampleWidget::decodingFinished(const QAudioBuffer &buffer) { - if (!m_file || !m_file->filename) - return {}; - - return QUrl{QString{"https://brunner.ninja/komposthaufen/dpm/presets/extracted/%0/%1"}.arg(m_presetId, *m_file->filename)}; + qDebug() << "called"; + m_reply = nullptr; + m_player.setBuffer(buffer); + updateStatus(); +} + +void SampleWidget::startRequest() +{ + if (m_networkAccessManager && m_file->filename) + { + QNetworkRequest request{QUrl{QString{"https://brunner.ninja/komposthaufen/dpm/presets/extracted/%0/%1"}.arg(m_presetId, *m_file->filename)}}; + request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache); + request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, true); + m_reply = std::shared_ptr{m_networkAccessManager->get(request)}; + connect(m_reply.get(), &QNetworkReply::finished, this, &SampleWidget::requestFinished); + } + + updateStatus(); } diff --git a/samplewidget.h b/samplewidget.h index 56e4251..8270f3a 100755 --- a/samplewidget.h +++ b/samplewidget.h @@ -3,13 +3,16 @@ #include #include -#include +#include "audioformat.h" #include "presets.h" +#include "audioplayer.h" namespace Ui { class SampleWidget; } -class QThread; -class QSoundEffect; +class QNetworkAccessManager; +class QNetworkReply; +class QAudioBuffer; +class AudioDecoder; class SampleWidget : public QFrame { @@ -34,27 +37,33 @@ public: void forceStop(); - void setupAudioThread(const QAudioDeviceInfo &device, QThread &thread); + void injectNetworkAccessManager(QNetworkAccessManager &networkAccessManager); + void injectDecodingThread(QThread &thread); + + void writeSamples(frame_t *begin, frame_t *end); signals: void chokeTriggered(int choke); + void startDecoding(const std::shared_ptr &device); private slots: - void createEffect(); void updateStatus(); + void requestFinished(); + void decodingFinished(const QAudioBuffer &buffer); private: - void destroyEffect(); - - QUrl sampleUrl() const; + void startRequest(); const std::unique_ptr m_ui; + std::shared_ptr m_reply; + + std::unique_ptr m_decoder; + + AudioPlayer m_player; + QString m_presetId; std::optional m_file; - std::unique_ptr m_effect; - - QThread *m_thread{}; - QAudioDeviceInfo m_device; + QNetworkAccessManager *m_networkAccessManager{}; }; diff --git a/samplewidget.ui b/samplewidget.ui index b696f55..9016064 100755 --- a/samplewidget.ui +++ b/samplewidget.ui @@ -32,15 +32,15 @@ - - - - - - - + + + + 24 + + +