Switched to PortAudio

This commit is contained in:
2020-04-26 20:49:20 +02:00
parent e5654e5131
commit ea6c112158
18 changed files with 551 additions and 128 deletions

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "rtmidi"]
path = rtmidi
url = git@github.com:thestk/rtmidi.git

View File

@ -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

83
audiodecoder.cpp Normal file
View File

@ -0,0 +1,83 @@
#include "audiodecoder.h"
#include <QAudioBuffer>
#include <QMetaType>
#include <QCoreApplication>
#include <QDebug>
#include "audioformat.h"
AudioDecoder::AudioDecoder(QObject *parent) :
QObject(parent)
{
QObject::connect(&m_decoder, qOverload<QAudioDecoder::Error>(&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<QIODevice> 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<char>(), 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<std::shared_ptr<QIODevice>>();
}
Q_COREAPP_STARTUP_FUNCTION(registerMetaType)
}

40
audiodecoder.h Normal file
View File

@ -0,0 +1,40 @@
#pragma once
#include <memory>
#include <QObject>
#include <QAudioDecoder>
#include <QByteArray>
#include <QDateTime>
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<QIODevice> device);
private slots:
void error(const QAudioDecoder::Error error);
void finished();
void bufferReady();
void durationChanged(const qint64 duration);
private:
std::shared_ptr<QIODevice> m_device;
QAudioDecoder m_decoder;
QByteArray m_bytearray;
QDateTime m_lastProgressUpdate;
};
Q_DECLARE_METATYPE(std::shared_ptr<QIODevice>)

17
audioformat.cpp Normal file
View File

@ -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;
}

14
audioformat.h Normal file
View File

@ -0,0 +1,14 @@
#pragma once
#include <QAudioFormat>
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<sample_t, channelCount>;
const QAudioFormat &audioFormat();

52
audioplayer.cpp Normal file
View File

@ -0,0 +1,52 @@
#include "audioplayer.h"
#include <algorithm>
#include <cmath>
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<size_t>(std::distance(begin, end), m_buffer.frameCount()-m_position);
if (!frames)
{
m_playing = false;
emit playingChanged(false);
return;
}
std::transform(static_cast<const frame_t *>(begin), static_cast<const frame_t *>(begin+frames), m_buffer.constData<frame_t>() + 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);
}

45
audioplayer.h Normal file
View File

@ -0,0 +1,45 @@
#pragma once
#include <atomic>
#include <QObject>
#include <QAudioBuffer>
#include <QtGlobal>
#include <QDateTime>
#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;
};

View File

@ -8,9 +8,36 @@
#include <QMessageBox>
#include <QDebug>
#include "portaudio.h"
#include "jsonconverters.h"
#include "mainwindow.h"
namespace {
template<typename T>
class CleanupHelper
{
public:
CleanupHelper(T &&callback) :
m_callback{std::move(callback)}
{}
~CleanupHelper()
{
m_callback();
}
private:
T m_callback;
};
template<typename T>
auto makeCleanupHelper(T &&callback)
{
return CleanupHelper<T>{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}"

View File

@ -6,14 +6,54 @@
#include <QTimer>
#include <QAbstractEventDispatcher>
#include <QAudioDeviceInfo>
#include <QDebug>
#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<frame_t*>(outputBuffer);
static_cast<MainWindow*>(userData)->writeSamples(begin, begin+framesPerBuffer);
}
}
MainWindow::MainWindow(const presets::PresetsConfig &presetsConfig, QWidget *parent) :
QMainWindow{parent},
m_ui{std::make_unique<Ui::MainWindow>()},
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<int>(&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<PaStream, void(*)(PaStream*)>(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);
}
}

View File

@ -5,6 +5,9 @@
#include <QMainWindow>
#include <QSortFilterProxyModel>
#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 &current);
@ -34,9 +39,9 @@ private:
const std::unique_ptr<Ui::MainWindow> m_ui;
MidiInWrapper m_midiIn;
std::unique_ptr<PaStream, void(*)(PaStream*)> m_paStream;
QList<QAudioDeviceInfo> m_devices;
MidiInWrapper m_midiIn;
PresetsModel m_presetsModel;
QSortFilterProxyModel m_presetsProxyModel;

View File

@ -124,6 +124,23 @@
<item>
<widget class="QComboBox" name="comboBoxAudioDevice"/>
</item>
<item>
<widget class="QSpinBox" name="spinBoxBufferSize">
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>32</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButtonAudioDevice">
<property name="text">
<string>Open</string>
</property>
</widget>
</item>
</layout>
</item>
<item>

1
rtmidi

Submodule rtmidi deleted from 7ab18ef06b

View File

@ -3,7 +3,6 @@
#include <iterator>
#include <QEventLoop>
#include <QDebug>
#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)

View File

@ -5,14 +5,16 @@
#include <functional>
#include <QWidget>
#include <QNetworkAccessManager>
#include <QNetworkDiskCache>
#include <QThread>
#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<Ui::SamplesWidget> m_ui;
presets::Preset m_preset;
QNetworkDiskCache m_cache;
QNetworkAccessManager m_networkAccessManager;
QThread m_audioThread;
QThread m_decoderThread;
presets::Preset m_preset;
};

View File

@ -3,24 +3,18 @@
#include <QAbstractEventDispatcher>
#include <QSoundEffect>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QNetworkAccessManager>
#include <QMetaEnum>
#include <QDebug>
#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<QSoundEffect>(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<AudioDecoder>();
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<QNetworkReply::NetworkError>().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<QNetworkReply>{m_networkAccessManager->get(request)};
connect(m_reply.get(), &QNetworkReply::finished, this, &SampleWidget::requestFinished);
}
updateStatus();
}

View File

@ -3,13 +3,16 @@
#include <memory>
#include <QFrame>
#include <QAudioDeviceInfo>
#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<QIODevice> &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<Ui::SampleWidget> m_ui;
std::shared_ptr<QNetworkReply> m_reply;
std::unique_ptr<AudioDecoder> m_decoder;
AudioPlayer m_player;
QString m_presetId;
std::optional<presets::File> m_file;
std::unique_ptr<QSoundEffect> m_effect;
QThread *m_thread{};
QAudioDeviceInfo m_device;
QNetworkAccessManager *m_networkAccessManager{};
};

View File

@ -32,15 +32,15 @@
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="toolButtonReload">
<property name="text">
<string>↻</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>24</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton">
<property name="text">