Switched to PortAudio
This commit is contained in:
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "rtmidi"]
|
||||
path = rtmidi
|
||||
url = git@github.com:thestk/rtmidi.git
|
||||
|
@ -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
83
audiodecoder.cpp
Normal 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
40
audiodecoder.h
Normal 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
17
audioformat.cpp
Normal 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
14
audioformat.h
Normal 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
52
audioplayer.cpp
Normal 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
45
audioplayer.h
Normal 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;
|
||||
};
|
39
main.cpp
39
main.cpp
@ -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}"
|
||||
|
122
mainwindow.cpp
122
mainwindow.cpp
@ -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);
|
||||
}
|
||||
}
|
||||
|
11
mainwindow.h
11
mainwindow.h
@ -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 ¤t);
|
||||
|
||||
@ -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;
|
||||
|
@ -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
1
rtmidi
Submodule rtmidi deleted from 7ab18ef06b
@ -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)
|
||||
|
@ -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;
|
||||
};
|
||||
|
134
samplewidget.cpp
134
samplewidget.cpp
@ -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();
|
||||
}
|
||||
|
@ -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{};
|
||||
};
|
||||
|
@ -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">
|
||||
|
Reference in New Issue
Block a user