Imported existing sources

This commit is contained in:
2020-04-25 20:53:49 +02:00
parent 87b151d347
commit 202a0a8a0a
33 changed files with 3065 additions and 0 deletions

3
.gitmodules vendored Normal file
View File

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

53
DrumMachine.pro Executable file
View File

@ -0,0 +1,53 @@
QT = core multimedia gui widgets network
CONFIG += c++17
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
}
DEFINES += QT_DEPRECATED_WARNINGS QT_DISABLE_DEPRECATED_BEFORE=0x060000
SOURCES += \
filesmodel.cpp \
jsonconverters.cpp \
main.cpp \
mainwindow.cpp \
midicontainers.cpp \
midiinwrapper.cpp \
presetdetailwidget.cpp \
presets.cpp \
presetsmodel.cpp \
rtmidi/RtMidi.cpp \
sampleswidget.cpp \
samplewidget.cpp \
sequencerwidget.cpp
HEADERS += \
filesmodel.h \
jsonconverters.h \
mainwindow.h \
midicontainers.h \
midiinwrapper.h \
presetdetailwidget.h \
presets.h \
presetsmodel.h \
rtmidi/RtMidi.h \
sampleswidget.h \
samplewidget.h \
sequencerwidget.h
FORMS += \
mainwindow.ui \
presetdetailwidget.ui \
sampleswidget.ui \
samplewidget.ui \
sequencerwidget.ui
RESOURCES += \
resources.qrc

129
filesmodel.cpp Executable file
View File

@ -0,0 +1,129 @@
#include "filesmodel.h"
#include <iterator>
#include <QFont>
#include <QColor>
enum {
ColumnFilename,
ColumnColor,
ColumnStopOnRelease,
ColumnLooped,
ColumnChoke,
NumberOfColumns
};
FilesModel::~FilesModel() = default;
const presets::File &FilesModel::getFile(const QModelIndex &index) const
{
return getFile(index.row());
}
const presets::File &FilesModel::getFile(int row) const
{
return m_files->at(row);
}
void FilesModel::setPreset(const presets::Preset &preset)
{
beginResetModel();
m_files = preset.files;
endResetModel();
}
int FilesModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
if (!m_files)
return 0;
return std::size(*m_files);
}
int FilesModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return NumberOfColumns;
}
QVariant FilesModel::data(const QModelIndex &index, int role) const
{
if (role != Qt::DisplayRole && role != Qt::EditRole && role != Qt::FontRole && role != Qt::ForegroundRole)
return {};
if (!m_files)
return {};
if (index.column() < 0)
return {};
if (index.column() >= NumberOfColumns)
return {};
if (index.row() < 0)
return {};
if (index.row() >= std::size(*m_files))
return {};
const auto &file = getFile(index);
const auto handleData = [&](const auto &val) -> QVariant
{
if (!val)
{
if (role == Qt::DisplayRole)
return this->tr("(null)");
else if (role == Qt::FontRole)
{
QFont font;
font.setItalic(true);
return font;
}
else if (role == Qt::ForegroundRole)
return QColor{Qt::gray};
return {};
}
if (role == Qt::DisplayRole || role == Qt::EditRole)
return *val;
return {};
};
switch (index.column())
{
case ColumnFilename: return handleData(file.filename);
case ColumnColor: return handleData(file.color);
case ColumnStopOnRelease: return handleData(file.stopOnRelease);
case ColumnLooped: return handleData(file.looped);
case ColumnChoke: return handleData(file.choke);
}
Q_UNREACHABLE();
}
QVariant FilesModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (role != Qt::DisplayRole && role != Qt::EditRole)
return {};
if (orientation != Qt::Horizontal)
return {};
if (section < 0)
return {};
if (section >= NumberOfColumns)
return {};
switch (section)
{
case ColumnFilename: return tr("filename");
case ColumnColor: return tr("color");
case ColumnStopOnRelease: return tr("stopOnRelease");
case ColumnLooped: return tr("looped");
case ColumnChoke: return tr("choke");
}
Q_UNREACHABLE();
}

31
filesmodel.h Executable file
View File

@ -0,0 +1,31 @@
#pragma once
#include <array>
#include <QAbstractTableModel>
#include "presets.h"
namespace presets { class Preset; class File; }
class FilesModel : public QAbstractTableModel
{
Q_OBJECT
public:
using QAbstractTableModel::QAbstractTableModel;
~FilesModel() override;
const presets::File &getFile(const QModelIndex &index) const;
const presets::File &getFile(int row) const;
void setPreset(const presets::Preset &preset);
int rowCount(const QModelIndex &parent) const override;
int columnCount(const QModelIndex &parent) const override;
QVariant data(const QModelIndex &index, int role) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
private:
std::optional<std::array<presets::File, 24>> m_files;
};

396
jsonconverters.cpp Executable file
View File

@ -0,0 +1,396 @@
#include "jsonconverters.h"
#include <stdexcept>
#include <QJsonParseError>
namespace json_converters
{
QJsonObject loadJson(const QByteArray &buffer)
{
QJsonParseError error;
QJsonDocument document{QJsonDocument::fromJson(buffer, &error)};
if (error.error != QJsonParseError::NoError)
throw std::runtime_error{QString{"Could not parse JSON because %0"}.arg(error.errorString()).toStdString()};
if (!document.isObject())
throw std::runtime_error{"JSON is not an object"};
return document.object();
}
QString parseString(const QJsonValue &jsonValue)
{
if (!jsonValue.isString())
throw std::runtime_error{"json value for string is not a string"};
return jsonValue.toString();
}
std::vector<QString> parseStringVector(const QJsonValue &jsonValue)
{
if (!jsonValue.isArray())
throw std::runtime_error{"json value for string vector is not an array"};
std::vector<QString> vector;
for (const auto &jsonValue : jsonValue.toArray())
vector.emplace_back(parseString(jsonValue));
return vector;
}
int parseInt(const QJsonValue &jsonValue)
{
if (!jsonValue.isDouble())
throw std::runtime_error{"json value for int is not a double"};
return jsonValue.toDouble();
}
bool parseBool(const QJsonValue &jsonValue)
{
if (!jsonValue.isBool())
throw std::runtime_error{"json value for bool is not a bool"};
return jsonValue.toBool();
}
std::vector<int> parseIntVector(const QJsonValue &jsonValue)
{
if (!jsonValue.isArray())
throw std::runtime_error{"json value for int vector is not an array"};
std::vector<int> vector;
for (const auto &jsonValue : jsonValue.toArray())
vector.emplace_back(parseInt(jsonValue));
return vector;
}
presets::PresetsConfig parsePresetsConfig(const QJsonObject &jsonObj)
{
presets::PresetsConfig presetConfig;
for (auto iter = std::cbegin(jsonObj); iter != std::cend(jsonObj); iter++)
{
if (iter.key() == "categories")
presetConfig.categories = parseCategoryVector(iter.value());
else if (iter.key() == "presets")
presetConfig.presets = parsePresetMap(iter.value());
else
throw std::runtime_error{QString{"unknown key %0 for PresetConfig"}.arg(iter.key()).toStdString()};
}
return presetConfig;
}
std::vector<presets::Category> parseCategoryVector(const QJsonValue &jsonValue)
{
if (!jsonValue.isArray())
throw std::runtime_error{"json value for vector of Category is not an array"};
std::vector<presets::Category> vector;
for (const auto &jsonValue : jsonValue.toArray())
vector.emplace_back(parseCategory(jsonValue));
return vector;
}
std::map<QString, presets::Preset> parsePresetMap(const QJsonValue &jsonValue)
{
if (!jsonValue.isObject())
throw std::runtime_error{"json value for Preset map is not an object"};
const auto jsonObj = jsonValue.toObject();
std::map<QString, presets::Preset> map;
for (auto iter = std::cbegin(jsonObj); iter != std::cend(jsonObj); iter++)
map[iter.key()] = parsePreset(iter.value());
return map;
}
presets::Category parseCategory(const QJsonValue &jsonValue)
{
if (!jsonValue.isObject())
throw std::runtime_error{"json value for Category is not an object"};
const auto jsonObj = jsonValue.toObject();
presets::Category category;
for (auto iter = std::cbegin(jsonObj); iter != std::cend(jsonObj); iter++)
{
if (iter.key() == "title")
category.title = parseString(iter.value());
else if (iter.key() == "filter")
category.filter = parseFilter(iter.value());
else
throw std::runtime_error{QString{"unknown key %0 for Category"}.arg(iter.key()).toStdString()};
}
return category;
}
presets::Filter parseFilter(const QJsonValue &jsonValue)
{
if (!jsonValue.isObject())
throw std::runtime_error{"json value for Filters is not an object"};
const auto jsonObj = jsonValue.toObject();
presets::Filter filters;
for (auto iter = std::cbegin(jsonObj); iter != std::cend(jsonObj); iter++)
{
if (iter.key() == "tags")
filters.tags = parseStringVector(iter.value());
else
throw std::runtime_error{QString{"unknown key %0 for Filters"}.arg(iter.key()).toStdString()};
}
return filters;
}
presets::Preset parsePreset(const QJsonValue &jsonValue)
{
if (!jsonValue.isObject())
throw std::runtime_error{"json value for Preset is not an object"};
const auto jsonObj = jsonValue.toObject();
presets::Preset preset;
for (auto iter = std::cbegin(jsonObj); iter != std::cend(jsonObj); iter++)
{
const auto key = iter.key();
if (key == "id")
preset.id = parseString(iter.value());
else if (key == "name")
preset.name = parseString(iter.value());
else if (key == "author")
preset.author = parseString(iter.value());
else if (key == "orderBy")
preset.orderBy = parseString(iter.value());
else if (key == "version")
preset.version = parseString(iter.value());
else if (key == "tempo")
preset.tempo = parseInt(iter.value());
else if (key == "icon")
preset.icon = parseString(iter.value());
else if (key == "price")
preset.price = parseInt(iter.value());
else if (key == "priceForSession")
preset.priceForSession = parseInt(iter.value());
else if (key == "hasInfo")
preset.hasInfo = parseBool(iter.value());
else if (key == "tags")
preset.tags = parseStringVector(iter.value());
else if (key == "DELETED")
preset.DELETED = parseBool(iter.value());
else if (key == "difficulty")
preset.difficulty = parseInt(iter.value());
else if (key == "sample")
preset.sample = parseInt(iter.value());
else if (key == "audioPreview1Name")
preset.audioPreview1Name = parseString(iter.value());
else if (key == "audioPreview1URL")
preset.audioPreview1URL = parseString(iter.value());
else if (key == "audioPreview2Name")
preset.audioPreview2Name = parseString(iter.value());
else if (key == "audioPreview2URL")
preset.audioPreview2URL = parseString(iter.value());
else if (key == "imagePreview1")
preset.imagePreview1 = parseString(iter.value());
else if (key == "videoPreview")
preset.videoPreview = parseString(iter.value());
else if (key == "videoTutorial")
preset.videoTutorial = parseString(iter.value());
else if (key == "files")
preset.files = parseFileArray(iter.value());
else if (key == "beatSchool")
preset.beatSchool = parseSequenceVectorMap(iter.value());
else if (key == "easyPlay")
preset.easyPlay = parseSequenceVectorMap(iter.value());
else if (key == "middleDescription")
{}
else
throw std::runtime_error{QString{"unknown key %0 for Preset"}.arg(key).toStdString()};
}
return preset;
}
std::array<presets::File, 24> parseFileArray(const QJsonValue &jsonValue)
{
if (!jsonValue.isObject())
throw std::runtime_error{"json value for File array is not an object"};
const auto jsonObj = jsonValue.toObject();
if (jsonObj.size() != 24)
throw std::runtime_error{"json value for File array doesn't have exactly 24 entries"};
std::array<presets::File, 24> array;
for (auto iter = std::cbegin(jsonObj); iter != std::cend(jsonObj); iter++)
{
bool ok;
const auto index = iter.key().toInt(&ok);
if (!ok || index < 1 || index >= 25)
throw std::runtime_error{QString{"unknown key %0 for File"}.arg(iter.key()).toStdString()};
array[index - 1] = parseFile(iter.value());
}
return array;
}
presets::File parseFile(const QJsonValue &jsonValue)
{
if (!jsonValue.isObject())
throw std::runtime_error{"json value for File is not an object"};
const auto jsonObj = jsonValue.toObject();
presets::File file;
for (auto iter = std::cbegin(jsonObj); iter != std::cend(jsonObj); iter++)
{
const auto key = iter.key();
if (key == "filename")
file.filename = parseString(iter.value());
else if (key == "color")
file.color = parseString(iter.value());
else if (key == "stopOnRelease")
file.stopOnRelease = parseString(iter.value());
else if (key == "looped")
file.looped = parseBool(iter.value());
else if (key == "choke")
file.choke = parseInt(iter.value());
else
throw std::runtime_error{QString{"unknown key %0 for File"}.arg(key).toStdString()};
}
return file;
}
std::vector<presets::Sequence> parseSequenceVector(const QJsonValue &jsonValue)
{
if (!jsonValue.isArray())
throw std::runtime_error{"json value for vector of Sequence is not an array"};
std::vector<presets::Sequence> vector;
for (const auto &jsonValue : jsonValue.toArray())
vector.emplace_back(parseSequence(jsonValue));
return vector;
}
presets::Sequence parseSequence(const QJsonValue &jsonValue)
{
if (!jsonValue.isObject())
throw std::runtime_error{"json value for File is not an object"};
const auto jsonObj = jsonValue.toObject();
presets::Sequence sequence;
for (auto iter = std::cbegin(jsonObj); iter != std::cend(jsonObj); iter++)
{
const auto key = iter.key();
if (key == "name")
sequence.name = parseString(iter.value());
else if (key == "id")
sequence.id = parseInt(iter.value());
else if (key == "version")
sequence.version = parseInt(iter.value());
else if (key == "orderBy")
sequence.orderBy = parseInt(iter.value());
else if (key == "sequencerSize")
sequence.sequencerSize = parseInt(iter.value());
else if (key == "pads")
sequence.pads = parseSequencePadVectorMap(iter.value());
else if (key == "embientPads")
sequence.embientPads = parseIntVector(iter.value());
else
throw std::runtime_error{QString{"unknown key %0 for Sequence"}.arg(key).toStdString()};
}
return sequence;
}
std::map<QString, std::vector<presets::Sequence>> parseSequenceVectorMap(const QJsonValue &jsonValue)
{
if (!jsonValue.isObject())
throw std::runtime_error{"json value for Sequence vector map is not an object"};
const auto jsonObj = jsonValue.toObject();
std::map<QString, std::vector<presets::Sequence>> map;
for (auto iter = std::cbegin(jsonObj); iter != std::cend(jsonObj); iter++)
map[iter.key()] = parseSequenceVector(iter.value());
return map;
}
presets::SequencePad parseSequencePad(const QJsonValue &jsonValue)
{
if (!jsonValue.isObject())
throw std::runtime_error{"json value for File is not an object"};
const auto jsonObj = jsonValue.toObject();
presets::SequencePad sequencePad;
for (auto iter = std::cbegin(jsonObj); iter != std::cend(jsonObj); iter++)
{
if (iter.key() == "start")
sequencePad.start = parseInt(iter.value());
else if (iter.key() == "duration")
sequencePad.duration = parseInt(iter.value());
else if (iter.key() == "embient")
sequencePad.embient = parseBool(iter.value());
else
throw std::runtime_error{QString{"unknown key %0 for Sequence"}.arg(iter.key()).toStdString()};
}
return sequencePad;
}
std::vector<presets::SequencePad> parseSequencePadVector(const QJsonValue &jsonValue)
{
if (!jsonValue.isArray())
throw std::runtime_error{"json value for vector of SequencePad is not an array"};
std::vector<presets::SequencePad> vector;
for (const auto &jsonValue : jsonValue.toArray())
vector.emplace_back(parseSequencePad(jsonValue));
return vector;
}
std::map<QString, std::vector<presets::SequencePad>> parseSequencePadVectorMap(const QJsonValue &jsonValue)
{
if (!jsonValue.isObject())
throw std::runtime_error{"json value for SequencePad vector map is not an object"};
const auto jsonObj = jsonValue.toObject();
std::map<QString, std::vector<presets::SequencePad>> map;
for (auto iter = std::cbegin(jsonObj); iter != std::cend(jsonObj); iter++)
map[iter.key()] = parseSequencePadVector(iter.value());
return map;
}
}

33
jsonconverters.h Executable file
View File

@ -0,0 +1,33 @@
#pragma once
#include <QJsonValue>
#include <QJsonArray>
#include <QJsonObject>
#include "presets.h"
namespace json_converters
{
QJsonObject loadJson(const QByteArray &buffer);
QString parseString(const QJsonValue &jsonValue);
std::vector<QString> parseStringVector(const QJsonValue &jsonValue);
int parseInt(const QJsonValue &jsonValue);
bool parseBool(const QJsonValue &jsonValue);
std::vector<int> parseIntVector(const QJsonValue &jsonValue);
presets::PresetsConfig parsePresetsConfig(const QJsonObject &jsonObj);
std::vector<presets::Category> parseCategoryVector(const QJsonValue &jsonValue);
std::map<QString, presets::Preset> parsePresetMap(const QJsonValue &jsonValue);
presets::Category parseCategory(const QJsonValue &jsonValue);
presets::Filter parseFilter(const QJsonValue &jsonValue);
presets::Preset parsePreset(const QJsonValue &jsonValue);
std::array<presets::File, 24> parseFileArray(const QJsonValue &jsonValue);
presets::File parseFile(const QJsonValue &jsonValue);
std::vector<presets::Sequence> parseSequenceVector(const QJsonValue &jsonValue);
presets::Sequence parseSequence(const QJsonValue &jsonValue);
std::map<QString, std::vector<presets::Sequence>> parseSequenceVectorMap(const QJsonValue &jsonValue);
presets::SequencePad parseSequencePad(const QJsonValue &jsonValue);
std::vector<presets::SequencePad> parseSequencePadVector(const QJsonValue &jsonValue);
std::map<QString, std::vector<presets::SequencePad>> parseSequencePadVectorMap(const QJsonValue &jsonValue);
}

85
main.cpp Executable file
View File

@ -0,0 +1,85 @@
#include <QApplication>
#include <QSslSocket>
#include <QNetworkAccessManager>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QEventLoop>
#include <QSplashScreen>
#include <QMessageBox>
#include <QDebug>
#include "jsonconverters.h"
#include "mainwindow.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QCoreApplication::setOrganizationDomain("brunner.ninja");
QCoreApplication::setOrganizationName("brunner.ninja");
QCoreApplication::setApplicationName("miditest");
QCoreApplication::setApplicationVersion("1.0");
qDebug() << "supportsSsl" << QSslSocket::supportsSsl();
qDebug() << "sslLibraryVersionString" << QSslSocket::sslLibraryVersionString();
qDebug() << "sslLibraryBuildVersionString" << QSslSocket::sslLibraryBuildVersionString();
qSetMessagePattern("%{time dd.MM.yyyy HH:mm:ss.zzz} "
"["
"%{if-debug}D%{endif}"
"%{if-info}I%{endif}"
"%{if-warning}W%{endif}"
"%{if-critical}C%{endif}"
"%{if-fatal}F%{endif}"
"] "
"%{function}(): "
"%{message}");
presets::PresetsConfig presetsConfig;
{
QSplashScreen splashScreen{QPixmap{":/drummachine/splashscreen.png"}};
splashScreen.showMessage(QCoreApplication::translate("main", "Loading list of presets..."));
splashScreen.show();
QEventLoop eventLoop;
QNetworkAccessManager manager;
const auto reply = std::unique_ptr<QNetworkReply>(manager.get(QNetworkRequest{QUrl{"https://brunner.ninja/komposthaufen/dpm/presets_config_v12.json"}}));
QObject::connect(reply.get(), &QNetworkReply::finished, &eventLoop, &QEventLoop::quit);
eventLoop.exec();
if (reply->error() != QNetworkReply::NoError)
{
QMessageBox::warning(nullptr, QCoreApplication::translate("main", "Could not load presets!"), QCoreApplication::translate("main", "Could not load presets!") + "\n\n" + reply->errorString());
return 1;
}
presetsConfig = json_converters::parsePresetsConfig(json_converters::loadJson(reply->readAll()));
}
#if !defined(Q_OS_WIN)
QPalette darkPalette;
darkPalette.setColor(QPalette::Window, QColor(53,53,53));
darkPalette.setColor(QPalette::WindowText, Qt::white);
darkPalette.setColor(QPalette::Base, QColor(25,25,25));
darkPalette.setColor(QPalette::AlternateBase, QColor(53,53,53));
darkPalette.setColor(QPalette::ToolTipBase, Qt::white);
darkPalette.setColor(QPalette::ToolTipText, Qt::white);
darkPalette.setColor(QPalette::Text, Qt::white);
darkPalette.setColor(QPalette::Button, QColor(53,53,53));
darkPalette.setColor(QPalette::ButtonText, Qt::white);
darkPalette.setColor(QPalette::BrightText, Qt::red);
darkPalette.setColor(QPalette::Link, QColor(42, 130, 218));
darkPalette.setColor(QPalette::Highlight, QColor(42, 130, 218));
darkPalette.setColor(QPalette::HighlightedText, Qt::black);
app.setPalette(darkPalette);
app.setStyleSheet("QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }");
#endif
MainWindow mainWindow{presetsConfig};
mainWindow.showMaximized();
mainWindow.selectFirstPreset();
return app.exec();
}

123
mainwindow.cpp Executable file
View File

@ -0,0 +1,123 @@
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMetaEnum>
#include <QMessageBox>
#include <QTimer>
#include <QAbstractEventDispatcher>
#include <QAudioDeviceInfo>
#include "presets.h"
#include "midiinwrapper.h"
#include "midicontainers.h"
MainWindow::MainWindow(const presets::PresetsConfig &presetsConfig, QWidget *parent) :
QMainWindow{parent},
m_ui{std::make_unique<Ui::MainWindow>()},
m_presetsModel{*presetsConfig.presets}
{
m_ui->setupUi(this);
connect(&m_midiIn, &MidiInWrapper::messageReceived, this, &MainWindow::messageReceived);
updateMidiDevices();
connect(m_ui->pushButtonMidiController, &QAbstractButton::pressed, this, [this](){
if (m_midiIn.isPortOpen())
m_midiIn.closePort();
else
{
const auto index = m_ui->comboBoxMidiController->currentIndex();
if (index != -1)
m_midiIn.openPort(index);
}
m_ui->pushButtonMidiController->setText(m_midiIn.isPortOpen() ? tr("Close") : tr("Open"));
});
updateAudioDevices();
{
const auto index = m_devices.indexOf(QAudioDeviceInfo::defaultOutputDevice());
if (index != -1)
m_ui->comboBoxAudioDevice->setCurrentIndex(index);
}
{
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());
}
m_presetsProxyModel.setFilterCaseSensitivity(Qt::CaseInsensitive);
m_presetsProxyModel.setSourceModel(&m_presetsModel);
m_ui->presetsView->setModel(&m_presetsProxyModel);
m_presetsProxyModel.setFilterKeyColumn(1);
connect(m_ui->lineEdit, &QLineEdit::textChanged, this, [=](){
m_presetsProxyModel.setFilterFixedString(m_ui->lineEdit->text());
});
m_ui->filesView->setModel(&m_filesModel);
connect(m_ui->presetsView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &MainWindow::currentRowChanged);
}
MainWindow::~MainWindow() = default;
void MainWindow::selectFirstPreset()
{
if (m_presetsProxyModel.rowCount())
{
const auto index = m_presetsProxyModel.index(0, 0);
if (index.isValid())
{
m_ui->presetsView->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
currentRowChanged(index);
}
}
}
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")
.arg(message.flag?"true":"false", QMetaEnum::fromType<midi::Command>().valueToKey(int(message.cmd)))
.arg(message.channel).arg(message.note).arg(message.velocity), 1000);
m_ui->samplesWidget->messageReceived(message);
}
void MainWindow::currentRowChanged(const QModelIndex &current)
{
if (!current.isValid())
return;
const auto &preset = m_presetsModel.getPreset(m_presetsProxyModel.mapToSource(current));
m_ui->presetDetailWidget->setPreset(preset);
m_filesModel.setPreset(preset);
m_ui->samplesWidget->setPreset(preset);
}
void MainWindow::updateMidiDevices()
{
m_ui->comboBoxMidiController->clear();
for (const auto &name : m_midiIn.portNames())
m_ui->comboBoxMidiController->addItem(name);
m_ui->pushButtonMidiController->setEnabled(m_ui->comboBoxMidiController->count() > 0);
}
void MainWindow::updateAudioDevices()
{
m_ui->comboBoxAudioDevice->clear();
m_devices = QAudioDeviceInfo::availableDevices(QAudio::AudioOutput);
for (const auto &device : m_devices)
m_ui->comboBoxAudioDevice->addItem(device.deviceName());
}

45
mainwindow.h Executable file
View File

@ -0,0 +1,45 @@
#pragma once
#include <memory>
#include <QMainWindow>
#include <QSortFilterProxyModel>
#include "presetsmodel.h"
#include "filesmodel.h"
#include "midiinwrapper.h"
namespace Ui { class MainWindow; }
namespace presets { struct PresetsConfig; }
namespace midi { struct MidiMessage; }
class QAudioDeviceInfo;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(const presets::PresetsConfig &presetsConfig, QWidget *parent = nullptr);
~MainWindow() override;
void selectFirstPreset();
private slots:
void messageReceived(const midi::MidiMessage &message);
void currentRowChanged(const QModelIndex &current);
private:
void updateMidiDevices();
void updateAudioDevices();
const std::unique_ptr<Ui::MainWindow> m_ui;
MidiInWrapper m_midiIn;
QList<QAudioDeviceInfo> m_devices;
PresetsModel m_presetsModel;
QSortFilterProxyModel m_presetsProxyModel;
FilesModel m_filesModel;
};

228
mainwindow.ui Executable file
View File

@ -0,0 +1,228 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1017</width>
<height>712</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="widget">
<layout class="QVBoxLayout" stretch="0,1">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="labelUltraDj">
<property name="text">
<string>&lt;b&gt;UltraDj&lt;/b&gt;</string>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="labelMaster">
<property name="text">
<string>Master:</string>
</property>
<property name="buddy">
<cstring>horizontalSliderMaster</cstring>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="horizontalSliderMaster">
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="labelMidiIn">
<property name="text">
<string>Midi in:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBoxMidiController"/>
</item>
<item>
<widget class="QPushButton" name="pushButtonMidiController">
<property name="text">
<string>Open</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QComboBox" name="comboBoxAudioDevice"/>
</item>
</layout>
</item>
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QSplitter" name="splitter_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="QWidget" name="layoutWidget">
<layout class="QVBoxLayout">
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QLineEdit" name="lineEdit">
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="selectRandomPushButton">
<property name="text">
<string>Select random</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTreeView" name="presetsView">
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QSplitter" name="splitter_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="PresetDetailWidget" name="presetDetailWidget"/>
<widget class="QTreeView" name="filesView">
<property name="rootIsDecorated">
<bool>false</bool>
</property>
</widget>
</widget>
</widget>
<widget class="SamplesWidget" name="samplesWidget" native="true"/>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1017</width>
<height>20</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<customwidgets>
<customwidget>
<class>SamplesWidget</class>
<extends>QWidget</extends>
<header>sampleswidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>PresetDetailWidget</class>
<extends>QScrollArea</extends>
<header>presetdetailwidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

23
midicontainers.cpp Executable file
View File

@ -0,0 +1,23 @@
#include "midicontainers.h"
#include <QCoreApplication>
namespace midi {
bool MidiMessage::operator==(const MidiMessage &other) const
{
return channel == other.channel &&
cmd == other.cmd &&
flag == other.flag &&
note == other.note &&
velocity == other.velocity;
}
}
namespace {
void registerMidiMessageMetatype()
{
qRegisterMetaType<midi::MidiMessage>();
}
Q_COREAPP_STARTUP_FUNCTION(registerMidiMessageMetatype)
}

31
midicontainers.h Executable file
View File

@ -0,0 +1,31 @@
#pragma once
#include <QObject>
namespace midi {
Q_NAMESPACE
enum class Command : uint8_t {
NoteOff,
NoteOn,
PolyphonicKeyPressure,
ControlChange,
ProgramChange,
ChannelPressure,
PitchBendChange
};
Q_ENUM_NS(Command)
struct MidiMessage
{
uint8_t channel:4;
Command cmd:3;
bool flag:1;
uint8_t note;
uint8_t velocity;
bool operator==(const MidiMessage &other) const;
};
}
Q_DECLARE_METATYPE(midi::MidiMessage)

73
midiinwrapper.cpp Executable file
View File

@ -0,0 +1,73 @@
#include "midiinwrapper.h"
#include <QCoreApplication>
#include <QMetaType>
#include <QDebug>
MidiInWrapper::MidiInWrapper(RtMidi::Api api, const QString& clientName, unsigned int queueSizeLimit, QObject *parent) :
QObject{parent},
midiIn{api, clientName.toStdString(), queueSizeLimit}
{
midiIn.ignoreTypes(false, false, false);
midiIn.setCallback(&mycallback, this);
}
void MidiInWrapper::openPort(unsigned int portNumber)
{
midiIn.openPort(portNumber);
}
void MidiInWrapper::openVirtualPort(const QString &portName)
{
midiIn.openVirtualPort(portName.toStdString());
}
void MidiInWrapper::closePort()
{
midiIn.closePort();
}
bool MidiInWrapper::isPortOpen() const
{
return midiIn.isPortOpen();
}
QStringList MidiInWrapper::portNames()
{
QStringList names;
const auto count = midiIn.getPortCount();
for (unsigned int i = 0; i < count; i++)
names.append(QString::fromStdString(midiIn.getPortName(i)));
return names;
}
void MidiInWrapper::mycallback(double deltatime, std::vector<unsigned char> *message, void *userData)
{
Q_UNUSED(deltatime)
if (!userData)
{
qCritical() << "called without userData pointer to wrapper";
return;
}
auto wrapper = static_cast<MidiInWrapper*>(userData);
if (!message)
{
qCritical() << "called without message pointer";
return;
}
if (message->size() < sizeof(midi::MidiMessage))
{
qCritical() << "called with message that is shorter than 3 bytes";
return;
}
const midi::MidiMessage &midiMessage = reinterpret_cast<const midi::MidiMessage &>(message->at(0));
wrapper->messageReceived(midiMessage);
}

34
midiinwrapper.h Executable file
View File

@ -0,0 +1,34 @@
#pragma once
#include <QObject>
#include <QString>
#include "rtmidi/RtMidi.h"
#include "midicontainers.h"
class MidiInWrapper : public QObject
{
Q_OBJECT
public:
MidiInWrapper(RtMidi::Api api = RtMidi::UNSPECIFIED,
const QString &clientName = "RtMidi Input Client",
unsigned int queueSizeLimit = 100,
QObject *parent = nullptr);
void openPort(unsigned int portNumber);
void openVirtualPort(const QString &portName);
void closePort();
bool isPortOpen() const;
QStringList portNames();
signals:
void messageReceived(const midi::MidiMessage &message);
private:
static void mycallback(double deltatime, std::vector<unsigned char> *message, void *userData);
RtMidiIn midiIn;
};

16
presetdetailwidget.cpp Executable file
View File

@ -0,0 +1,16 @@
#include "presetdetailwidget.h"
#include "ui_presetdetailwidget.h"
PresetDetailWidget::PresetDetailWidget(QWidget *parent) :
QScrollArea{parent},
m_ui{std::make_unique<Ui::PresetDetailWidget>()}
{
m_ui->setupUi(this);
}
PresetDetailWidget::~PresetDetailWidget() = default;
void PresetDetailWidget::setPreset(const presets::Preset &preset)
{
}

22
presetdetailwidget.h Executable file
View File

@ -0,0 +1,22 @@
#pragma once
#include <memory>
#include <QScrollArea>
namespace Ui { class PresetDetailWidget; }
namespace presets { class Preset; }
class PresetDetailWidget : public QScrollArea
{
Q_OBJECT
public:
explicit PresetDetailWidget(QWidget *parent = nullptr);
~PresetDetailWidget() override;
void setPreset(const presets::Preset &preset);
private:
const std::unique_ptr<Ui::PresetDetailWidget> m_ui;
};

306
presetdetailwidget.ui Executable file
View File

@ -0,0 +1,306 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PresetDetailWidget</class>
<widget class="QScrollArea" name="PresetDetailWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>320</width>
<height>633</height>
</rect>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>318</width>
<height>631</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="labelId">
<property name="text">
<string>id:</string>
</property>
<property name="buddy">
<cstring>lineEditId</cstring>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="labelName">
<property name="text">
<string>name:</string>
</property>
<property name="buddy">
<cstring>lineEditName</cstring>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelAuthor">
<property name="text">
<string>author:</string>
</property>
<property name="buddy">
<cstring>lineEditAuthor</cstring>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="labelOrderBy">
<property name="text">
<string>orderBy:</string>
</property>
<property name="buddy">
<cstring>lineEditOrderBy</cstring>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="labelVersion">
<property name="text">
<string>version:</string>
</property>
<property name="buddy">
<cstring>lineEditVersion</cstring>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="labelTempo">
<property name="text">
<string>tempo:</string>
</property>
<property name="buddy">
<cstring>spinBoxTempo</cstring>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="labelIcon">
<property name="text">
<string>icon:</string>
</property>
<property name="buddy">
<cstring>lineEditIcon</cstring>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="labelPrice">
<property name="text">
<string>price:</string>
</property>
<property name="buddy">
<cstring>spinBoxPrice</cstring>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="labelPriceForSession">
<property name="text">
<string>priceForSession:</string>
</property>
<property name="buddy">
<cstring>spinBoxPriceForSession</cstring>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="labelHasInfo">
<property name="text">
<string>hasInfo:</string>
</property>
<property name="buddy">
<cstring>checkBoxHasInfo</cstring>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel" name="labelTags">
<property name="text">
<string>tags:</string>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QLabel" name="labelDeleted">
<property name="text">
<string>DELETED:</string>
</property>
<property name="buddy">
<cstring>checkBoxDeleted</cstring>
</property>
</widget>
</item>
<item row="12" column="0">
<widget class="QLabel" name="labelDifficulty">
<property name="text">
<string>difficulty:</string>
</property>
<property name="buddy">
<cstring>spinBoxDifficulty</cstring>
</property>
</widget>
</item>
<item row="13" column="0">
<widget class="QLabel" name="labelSample">
<property name="text">
<string>sample:</string>
</property>
<property name="buddy">
<cstring>spinBoxSample</cstring>
</property>
</widget>
</item>
<item row="14" column="0">
<widget class="QLabel" name="labelAudioPreview1Name">
<property name="text">
<string>audioPreview1Name:</string>
</property>
<property name="buddy">
<cstring>lineEditAudioPreview1Name</cstring>
</property>
</widget>
</item>
<item row="15" column="0">
<widget class="QLabel" name="labelAudioPreview1URL">
<property name="text">
<string>audioPreview1URL:</string>
</property>
<property name="buddy">
<cstring>lineEditAudioPreview1URL</cstring>
</property>
</widget>
</item>
<item row="16" column="0">
<widget class="QLabel" name="labelAudioPreview2Name">
<property name="text">
<string>audioPreview2Name:</string>
</property>
<property name="buddy">
<cstring>lineEditAudioPreview2Name</cstring>
</property>
</widget>
</item>
<item row="17" column="0">
<widget class="QLabel" name="labelAudioPreview2URL">
<property name="text">
<string>audioPreview2URL:</string>
</property>
<property name="buddy">
<cstring>lineEditAudioPreview2URL</cstring>
</property>
</widget>
</item>
<item row="18" column="0">
<widget class="QLabel" name="labelImagePreview1">
<property name="text">
<string>imagePreview1:</string>
</property>
<property name="buddy">
<cstring>lineEditImagePreview1</cstring>
</property>
</widget>
</item>
<item row="19" column="0">
<widget class="QLabel" name="labelVideoPreview">
<property name="text">
<string>videoPreview:</string>
</property>
<property name="buddy">
<cstring>lineEditVideoPreview</cstring>
</property>
</widget>
</item>
<item row="20" column="0">
<widget class="QLabel" name="labelVideoTutorial">
<property name="text">
<string>videoTutorial:</string>
</property>
<property name="buddy">
<cstring>lineEditVideoTutorial</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="lineEditId"/>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="lineEditName"/>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="lineEditAuthor"/>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="lineEditOrderBy"/>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="lineEditVersion"/>
</item>
<item row="5" column="1">
<widget class="QSpinBox" name="spinBoxTempo"/>
</item>
<item row="6" column="1">
<widget class="QLineEdit" name="lineEditIcon"/>
</item>
<item row="7" column="1">
<widget class="QSpinBox" name="spinBoxPrice"/>
</item>
<item row="8" column="1">
<widget class="QSpinBox" name="spinBoxPriceForSession"/>
</item>
<item row="9" column="1">
<widget class="QCheckBox" name="checkBoxHasInfo">
<property name="text">
<string>CheckBox</string>
</property>
</widget>
</item>
<item row="11" column="1">
<widget class="QCheckBox" name="checkBoxDeleted">
<property name="text">
<string>CheckBox</string>
</property>
</widget>
</item>
<item row="12" column="1">
<widget class="QSpinBox" name="spinBoxDifficulty"/>
</item>
<item row="13" column="1">
<widget class="QSpinBox" name="spinBoxSample"/>
</item>
<item row="14" column="1">
<widget class="QLineEdit" name="lineEditAudioPreview1Name"/>
</item>
<item row="15" column="1">
<widget class="QLineEdit" name="lineEditAudioPreview1URL"/>
</item>
<item row="16" column="1">
<widget class="QLineEdit" name="lineEditAudioPreview2Name"/>
</item>
<item row="17" column="1">
<widget class="QLineEdit" name="lineEditAudioPreview2URL"/>
</item>
<item row="18" column="1">
<widget class="QLineEdit" name="lineEditImagePreview1"/>
</item>
<item row="19" column="1">
<widget class="QLineEdit" name="lineEditVideoPreview"/>
</item>
<item row="20" column="1">
<widget class="QLineEdit" name="lineEditVideoTutorial"/>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>

15
presets.cpp Executable file
View File

@ -0,0 +1,15 @@
#include "presets.h"
namespace presets
{
bool File::operator==(const File &other) const
{
return filename == other.filename &&
color == other.color &&
stopOnRelease == other.stopOnRelease &&
looped == other.looped &&
choke == other.choke;
}
}

84
presets.h Executable file
View File

@ -0,0 +1,84 @@
#pragma once
#include <vector>
#include <map>
#include <optional>
#include <QString>
namespace presets
{
struct Filter
{
std::optional<std::vector<QString>> tags;
};
struct Category
{
std::optional<QString> title;
std::optional<Filter> filter;
};
struct File
{
std::optional<QString> filename;
std::optional<QString> color; // purple, red, yellow, green, blue
std::optional<QString> stopOnRelease;
std::optional<bool> looped;
std::optional<int> choke;
bool operator==(const File &other) const;
};
struct SequencePad
{
std::optional<int> start;
std::optional<int> duration;
std::optional<bool> embient;
};
struct Sequence
{
std::optional<QString> name;
std::optional<int> id;
std::optional<int> version;
std::optional<int> orderBy;
std::optional<int> sequencerSize;
std::optional<std::map<QString, std::vector<SequencePad>>> pads;
std::optional<std::vector<int>> embientPads;
};
struct Preset
{
std::optional<QString> id;
std::optional<QString> name;
std::optional<QString> author;
std::optional<QString> orderBy;
std::optional<QString> version;
std::optional<int> tempo;
std::optional<QString> icon;
std::optional<int> price;
std::optional<int> priceForSession;
std::optional<bool> hasInfo;
std::optional<std::vector<QString>> tags;
std::optional<bool> DELETED;
std::optional<int> difficulty;
std::optional<int> sample;
std::optional<QString> audioPreview1Name;
std::optional<QString> audioPreview1URL;
std::optional<QString> audioPreview2Name;
std::optional<QString> audioPreview2URL;
std::optional<QString> imagePreview1;
std::optional<QString> videoPreview;
std::optional<QString> videoTutorial;
std::optional<std::array<presets::File, 24>> files;
std::optional<std::map<QString, std::vector<Sequence>>> beatSchool;
std::optional<std::map<QString, std::vector<Sequence>>> easyPlay;
};
struct PresetsConfig
{
std::optional<std::vector<Category>> categories;
std::optional<std::map<QString, Preset>> presets;
};
}

208
presetsmodel.cpp Executable file
View File

@ -0,0 +1,208 @@
#include "presetsmodel.h"
#include <iterator>
#include <QFont>
#include <QColor>
#include "presets.h"
enum {
ColumnId,
ColumnName,
ColumnAuthor,
ColumnOrderBy,
ColumnVersion,
ColumnTempo,
ColumnIcon,
ColumnPrice,
ColumnPriceForSession,
ColumnHasInfo,
ColumnTags,
ColumnDELETED,
ColumnDifficulty,
ColumnSample,
ColumnAudioPreview1Name,
ColumnAudioPreview1URL,
ColumnAudioPreview2Name,
ColumnAudioPreview2URL,
ColumnImagePreview1,
ColumnVideoPreview,
ColumnVideoTutorial,
NumberOfColumns
};
PresetsModel::PresetsModel(const std::map<QString, presets::Preset> &presets, QObject *parent) :
QAbstractTableModel{parent}
{
m_presets.reserve(std::size(presets));
for (const auto &pair : presets)
m_presets.emplace_back(pair.second);
}
PresetsModel::~PresetsModel() = default;
const presets::Preset &PresetsModel::getPreset(const QModelIndex &index) const
{
return getPreset(index.row());
}
const presets::Preset &PresetsModel::getPreset(int row) const
{
Q_ASSERT(row >= 0 && row < std::size(m_presets));
return m_presets.at(row);
}
int PresetsModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return std::size(m_presets);
}
int PresetsModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return NumberOfColumns;
}
QVariant PresetsModel::data(const QModelIndex &index, int role) const
{
if (role != Qt::DisplayRole && role != Qt::EditRole && role != Qt::FontRole && role != Qt::ForegroundRole)
return {};
if (index.column() < 0)
return {};
if (index.column() >= NumberOfColumns)
return {};
if (index.row() < 0)
return {};
if (index.row() >= std::size(m_presets))
return {};
const auto &preset = getPreset(index);
const auto handleData = [&](const auto &val) -> QVariant
{
if (!val)
{
if (role == Qt::DisplayRole)
return this->tr("(null)");
else if (role == Qt::FontRole)
{
QFont font;
font.setItalic(true);
return font;
}
else if (role == Qt::ForegroundRole)
return QColor{Qt::gray};
return {};
}
if (role == Qt::DisplayRole || role == Qt::EditRole)
return *val;
return {};
};
const auto handleStringVectorData = [&](const auto &val) -> QVariant
{
if (!val)
{
if (role == Qt::DisplayRole)
return this->tr("(null)");
else if (role == Qt::FontRole)
{
QFont font;
font.setItalic(true);
return font;
}
else if (role == Qt::ForegroundRole)
return QColor{Qt::gray};
return {};
}
if (role == Qt::DisplayRole || role == Qt::EditRole)
{
QString text;
for (auto iter = std::cbegin(*val); iter != std::cend(*val); iter++)
{
if (iter != std::cbegin(*val))
text += ", ";
text += *iter;
}
return text;
}
return {};
};
switch (index.column())
{
case ColumnId: return handleData(preset.id);
case ColumnName: return handleData(preset.name);
case ColumnAuthor: return handleData(preset.author);
case ColumnOrderBy: return handleData(preset.orderBy);
case ColumnVersion: return handleData(preset.version);
case ColumnTempo: return handleData(preset.tempo);
case ColumnIcon: return handleData(preset.icon);
case ColumnPrice: return handleData(preset.price);
case ColumnPriceForSession: return handleData(preset.priceForSession);
case ColumnHasInfo: return handleData(preset.hasInfo);
case ColumnTags: return handleStringVectorData(preset.tags);
case ColumnDELETED: return handleData(preset.DELETED);
case ColumnDifficulty: return handleData(preset.difficulty);
case ColumnSample: return handleData(preset.sample);
case ColumnAudioPreview1Name: return handleData(preset.audioPreview1Name);
case ColumnAudioPreview1URL: return handleData(preset.audioPreview1URL);
case ColumnAudioPreview2Name: return handleData(preset.audioPreview2Name);
case ColumnAudioPreview2URL: return handleData(preset.audioPreview2URL);
case ColumnImagePreview1: return handleData(preset.imagePreview1);
case ColumnVideoPreview: return handleData(preset.videoPreview);
case ColumnVideoTutorial: return handleData(preset.videoTutorial);
}
Q_UNREACHABLE();
}
QVariant PresetsModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (role != Qt::DisplayRole && role != Qt::EditRole)
return {};
if (orientation != Qt::Horizontal)
return {};
if (section < 0)
return {};
if (section >= NumberOfColumns)
return {};
switch (section)
{
case ColumnId: return tr("id");
case ColumnName: return tr("name");
case ColumnAuthor: return tr("author");
case ColumnOrderBy: return tr("orderBy");
case ColumnVersion: return tr("version");
case ColumnTempo: return tr("tempo");
case ColumnIcon: return tr("icon");
case ColumnPrice: return tr("price");
case ColumnPriceForSession: return tr("priceForSession");
case ColumnHasInfo: return tr("hasInfo");
case ColumnTags: return tr("tags");
case ColumnDELETED: return tr("DELETED");
case ColumnDifficulty: return tr("difficulty");
case ColumnSample: return tr("sample");
case ColumnAudioPreview1Name: return tr("audioPreview1Name");
case ColumnAudioPreview1URL: return tr("audioPreview1URL");
case ColumnAudioPreview2Name: return tr("audioPreview2Name");
case ColumnAudioPreview2URL: return tr("audioPreview2URL");
case ColumnImagePreview1: return tr("imagePreview1");
case ColumnVideoPreview: return tr("videoPreview");
case ColumnVideoTutorial: return tr("videoTutorial");
}
Q_UNREACHABLE();
}

27
presetsmodel.h Executable file
View File

@ -0,0 +1,27 @@
#pragma once
#include <vector>
#include <QAbstractTableModel>
namespace presets { class Preset; }
class PresetsModel : public QAbstractTableModel
{
Q_OBJECT
public:
PresetsModel(const std::map<QString, presets::Preset> &presets, QObject *parent = nullptr);
~PresetsModel() override;
const presets::Preset &getPreset(const QModelIndex &index) const;
const presets::Preset &getPreset(int row) const;
int rowCount(const QModelIndex &parent) const override;
int columnCount(const QModelIndex &parent) const override;
QVariant data(const QModelIndex &index, int role) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
private:
std::vector<presets::Preset> m_presets;
};

5
resources.qrc Normal file
View File

@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/drummachine">
<file>splashscreen.png</file>
</qresource>
</RCC>

1
rtmidi Submodule

Submodule rtmidi added at 7ab18ef06b

172
sampleswidget.cpp Executable file
View File

@ -0,0 +1,172 @@
#include "sampleswidget.h"
#include "ui_sampleswidget.h"
#include <iterator>
#include <QEventLoop>
#include <QDebug>
#include "midicontainers.h"
SamplesWidget::SamplesWidget(QWidget *parent) :
QWidget{parent},
m_ui{std::make_unique<Ui::SamplesWidget>()}
{
m_ui->setupUi(this);
{
QEventLoop eventLoop;
connect(&m_audioThread, &QThread::started, &eventLoop, &QEventLoop::quit);
m_audioThread.start(QThread::HighestPriority);
eventLoop.exec();
}
connect(m_ui->checkBox, &QCheckBox::toggled, this, &SamplesWidget::updateWidgets);
connect(m_ui->pushButtonStopAll, &QAbstractButton::pressed, this, &SamplesWidget::stopAll);
for (const auto &ref : getWidgets())
connect(&ref.get(), &SampleWidget::chokeTriggered, this, &SamplesWidget::chokeTriggered);
connect(m_ui->sequencerWidget, &SequencerWidget::triggerSample, this, &SamplesWidget::sequencerTriggerSample);
m_ui->sampleWidget_1->setNote(48);
m_ui->sampleWidget_2->setNote(50);
m_ui->sampleWidget_3->setNote(52);
m_ui->sampleWidget_4->setNote(53);
m_ui->sampleWidget_5->setNote(55);
m_ui->sampleWidget_6->setNote(57);
m_ui->sampleWidget_7->setNote(59);
m_ui->sampleWidget_8->setNote(60);
m_ui->sampleWidget_9->setNote(62);
m_ui->sampleWidget_10->setNote(64);
m_ui->sampleWidget_11->setNote(65);
m_ui->sampleWidget_12->setNote(67);
m_ui->sampleWidget_22->setNote(69);
m_ui->sampleWidget_23->setNote(71);
m_ui->sampleWidget_24->setNote(72);
}
SamplesWidget::~SamplesWidget()
{
m_audioThread.exit();
m_audioThread.wait();
}
void SamplesWidget::setPreset(const presets::Preset &preset)
{
m_preset = preset;
m_ui->sequencerWidget->setPreset(preset);
updateWidgets();
}
void SamplesWidget::messageReceived(const midi::MidiMessage &message)
{
if (message == midi::MidiMessage{.channel=0,.cmd=midi::Command::ControlChange,.flag=true,.note=64,.velocity=127})
{
m_ui->checkBox->toggle();
return;
}
if (message.cmd != midi::Command::NoteOn && message.cmd != midi::Command::NoteOff)
return;
for (const auto &ref : getWidgets())
{
if (ref.get().channel() == message.channel && ref.get().note() == message.note)
{
if (message.cmd == midi::Command::NoteOn)
ref.get().pressed(message.velocity);
else if (message.cmd == midi::Command::NoteOff)
ref.get().released();
}
}
}
void SamplesWidget::setAudioDevice(const QAudioDeviceInfo &device)
{
for (const auto &ref : getWidgets())
{
connect(&ref.get(), &SampleWidget::chokeTriggered, this, &SamplesWidget::chokeTriggered);
ref.get().setupAudioThread(device, m_audioThread);
}
}
void SamplesWidget::chokeTriggered(int choke)
{
for (const auto &ref : getWidgets())
{
if (&ref.get() == sender())
continue;
if (ref.get().choke() && *ref.get().choke() && *ref.get().choke() == choke)
ref.get().forceStop();
}
}
void SamplesWidget::updateWidgets()
{
const auto widgets = getWidgets();
auto files = *m_preset.files;
if (m_ui->checkBox->isChecked())
for (int i = 0; i < 12; i++)
std::swap(files[i], files[i+12]);
auto filesIter = std::cbegin(files);
auto widgetsIter = std::cbegin(widgets);
for (; filesIter != std::cend(files) && widgetsIter != std::cend(widgets); std::advance(filesIter, 1), std::advance(widgetsIter, 1))
widgetsIter->get().setFile(*m_preset.id, *filesIter);
}
void SamplesWidget::sequencerTriggerSample(int index)
{
const auto widgets = getWidgets();
if (index < 0 || index >= std::size(widgets))
{
qDebug() << "index out of range" << index;
return;
}
widgets[index].get().pressed(127);
}
void SamplesWidget::stopAll()
{
for (const auto &ref : getWidgets())
ref.get().forceStop();
}
std::array<std::reference_wrapper<SampleWidget>, 24> SamplesWidget::getWidgets()
{
return {
std::ref(*m_ui->sampleWidget_1),
std::ref(*m_ui->sampleWidget_2),
std::ref(*m_ui->sampleWidget_3),
std::ref(*m_ui->sampleWidget_4),
std::ref(*m_ui->sampleWidget_5),
std::ref(*m_ui->sampleWidget_6),
std::ref(*m_ui->sampleWidget_7),
std::ref(*m_ui->sampleWidget_8),
std::ref(*m_ui->sampleWidget_9),
std::ref(*m_ui->sampleWidget_10),
std::ref(*m_ui->sampleWidget_11),
std::ref(*m_ui->sampleWidget_12),
std::ref(*m_ui->sampleWidget_13),
std::ref(*m_ui->sampleWidget_14),
std::ref(*m_ui->sampleWidget_15),
std::ref(*m_ui->sampleWidget_16),
std::ref(*m_ui->sampleWidget_17),
std::ref(*m_ui->sampleWidget_18),
std::ref(*m_ui->sampleWidget_19),
std::ref(*m_ui->sampleWidget_20),
std::ref(*m_ui->sampleWidget_21),
std::ref(*m_ui->sampleWidget_22),
std::ref(*m_ui->sampleWidget_23),
std::ref(*m_ui->sampleWidget_24)
};
}

45
sampleswidget.h Executable file
View File

@ -0,0 +1,45 @@
#pragma once
#include <memory>
#include <array>
#include <functional>
#include <QWidget>
#include <QThread>
#include "presets.h"
namespace Ui { class SamplesWidget; }
namespace midi { class MidiMessage; }
class SampleWidget;
class QAudioDeviceInfo;
class SamplesWidget : public QWidget
{
Q_OBJECT
public:
explicit SamplesWidget(QWidget *parent = nullptr);
~SamplesWidget() override;
void setPreset(const presets::Preset &preset);
void messageReceived(const midi::MidiMessage &message);
void setAudioDevice(const QAudioDeviceInfo &device);
private slots:
void chokeTriggered(int choke);
void updateWidgets();
void sequencerTriggerSample(int index);
void stopAll();
private:
std::array<std::reference_wrapper<SampleWidget>, 24> getWidgets();
const std::unique_ptr<Ui::SamplesWidget> m_ui;
presets::Preset m_preset;
QThread m_audioThread;
};

253
sampleswidget.ui Executable file
View File

@ -0,0 +1,253 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SamplesWidget</class>
<widget class="QWidget" name="SamplesWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>660</width>
<height>421</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout" rowstretch="1,0,1,0,0,0,0,1" columnstretch="1,0,0,0,1,0,0,0,1">
<item row="3" column="5">
<widget class="SampleWidget" name="sampleWidget_13" native="true"/>
</item>
<item row="6" column="2">
<widget class="SampleWidget" name="sampleWidget_11" native="true"/>
</item>
<item row="2" column="4">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="1">
<widget class="SampleWidget" name="sampleWidget_4" native="true"/>
</item>
<item row="3" column="2">
<widget class="SampleWidget" name="sampleWidget_2" native="true"/>
</item>
<item row="4" column="7">
<widget class="SampleWidget" name="sampleWidget_18" native="true"/>
</item>
<item row="3" column="8">
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="6" column="5">
<widget class="SampleWidget" name="sampleWidget_22" native="true"/>
</item>
<item row="5" column="5">
<widget class="SampleWidget" name="sampleWidget_19" native="true"/>
</item>
<item row="5" column="2">
<widget class="SampleWidget" name="sampleWidget_8" native="true"/>
</item>
<item row="3" column="3">
<widget class="SampleWidget" name="sampleWidget_3" native="true"/>
</item>
<item row="5" column="7">
<widget class="SampleWidget" name="sampleWidget_21" native="true"/>
</item>
<item row="6" column="7">
<widget class="SampleWidget" name="sampleWidget_24" native="true"/>
</item>
<item row="5" column="1">
<widget class="SampleWidget" name="sampleWidget_7" native="true"/>
</item>
<item row="6" column="3">
<widget class="SampleWidget" name="sampleWidget_12" native="true"/>
</item>
<item row="4" column="2">
<widget class="SampleWidget" name="sampleWidget_5" native="true"/>
</item>
<item row="4" column="6">
<widget class="SampleWidget" name="sampleWidget_17" native="true"/>
</item>
<item row="3" column="7">
<widget class="SampleWidget" name="sampleWidget_15" native="true"/>
</item>
<item row="3" column="4">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="5" column="6">
<widget class="SampleWidget" name="sampleWidget_20" native="true"/>
</item>
<item row="3" column="1">
<widget class="SampleWidget" name="sampleWidget_1" native="true"/>
</item>
<item row="4" column="3">
<widget class="SampleWidget" name="sampleWidget_6" native="true"/>
</item>
<item row="4" column="5">
<widget class="SampleWidget" name="sampleWidget_16" native="true"/>
</item>
<item row="6" column="6">
<widget class="SampleWidget" name="sampleWidget_23" native="true"/>
</item>
<item row="3" column="6">
<widget class="SampleWidget" name="sampleWidget_14" native="true"/>
</item>
<item row="7" column="4">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="0">
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="6" column="1">
<widget class="SampleWidget" name="sampleWidget_10" native="true"/>
</item>
<item row="5" column="3">
<widget class="SampleWidget" name="sampleWidget_9" native="true"/>
</item>
<item row="1" column="0" colspan="9">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Sequencer</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="SequencerWidget" name="sequencerWidget" native="true"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox">
<property name="text">
<string>Swap left/right</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButtonStopAll">
<property name="text">
<string>Stop all</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="0" column="4">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SampleWidget</class>
<extends>QWidget</extends>
<header>samplewidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>SequencerWidget</class>
<extends>QWidget</extends>
<header>sequencerwidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

192
samplewidget.cpp Executable file
View File

@ -0,0 +1,192 @@
#include "samplewidget.h"
#include "ui_samplewidget.h"
#include <QAbstractEventDispatcher>
#include <QAudioDeviceInfo>
#include <QSoundEffect>
#include <QDebug>
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) :
QFrame{parent},
m_ui{std::make_unique<Ui::SampleWidget>()}
{
m_ui->setupUi(this);
connect(m_ui->pushButton, &QAbstractButton::pressed, this, [this](){ pressed(127); });
connect(m_ui->pushButton, &QAbstractButton::released, this, &SampleWidget::released);
updateStatus();
}
SampleWidget::~SampleWidget()
{
if (m_effect)
QMetaObject::invokeMethod(m_effect.get(), [effect=m_effect.release()](){ delete effect; });
}
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);
});
}
const auto setupLabel = [&](const auto &value, QLabel *label){
QString text;
QFont font;
QPalette pal;
if (value)
text = toString(*value);
else
{
text = tr("(null)");
font.setItalic(true);
pal.setColor(label->foregroundRole(), Qt::gray);
}
label->setText(text);
label->setFont(font);
label->setPalette(pal);
};
setupLabel(file.stopOnRelease, m_ui->stopOnReleaseLabel);
setupLabel(file.looped, m_ui->loopedLabel);
setupLabel(file.choke, m_ui->chokeLabel);
}
quint8 SampleWidget::channel() const
{
return m_ui->channelSpinBox->value();
}
void SampleWidget::setChannel(quint8 channel)
{
m_ui->channelSpinBox->setValue(channel);
}
quint8 SampleWidget::note() const
{
return m_ui->noteSpinBox->value();
}
void SampleWidget::setNote(quint8 note)
{
m_ui->noteSpinBox->setValue(note);
}
std::optional<int> SampleWidget::choke() const
{
if (!m_file)
return {};
return m_file->choke;
}
void SampleWidget::pressed(quint8 velocity)
{
Q_UNUSED(velocity)
if (m_effect)
QMetaObject::invokeMethod(m_effect.get(), &QSoundEffect::play);
if (m_file && m_file->choke && *m_file->choke)
emit chokeTriggered(*m_file->choke);
}
void SampleWidget::released()
{
}
void SampleWidget::forceStop()
{
if (m_effect)
QMetaObject::invokeMethod(m_effect.get(), &QSoundEffect::stop);
}
void SampleWidget::setupAudioThread(const QAudioDeviceInfo &device, QThread &thread)
{
const auto setupEffect = [this,device](){
m_effect = std::make_unique<QSoundEffect>(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), setupEffect);
//setupEffect();
}
void SampleWidget::updateStatus()
{
QPalette pal;
if (m_effect && m_file && m_file->color)
{
const auto bright = m_effect->isPlaying() ? 255 : 155;
const auto dark = m_effect->isPlaying() ?
#if !defined(Q_OS_WIN)
80 : 0
#else
180 : 80
#endif
;
const auto &color = *m_file->color;
if (color == "purple")
pal.setColor(QPalette::Window, QColor{bright, dark, bright});
else if (color == "red")
pal.setColor(QPalette::Window, QColor{bright, dark, dark});
else if (color == "yellow")
pal.setColor(QPalette::Window, QColor{bright, bright, dark});
else if (color == "green")
pal.setColor(QPalette::Window, QColor{dark, bright, dark});
else if (color == "blue")
pal.setColor(QPalette::Window, QColor{dark, dark, bright});
else
qWarning() << "unknown color:" << color;
}
setPalette(pal);
if (!m_effect)
m_ui->statusLabel->setText(tr("No player"));
else
m_ui->statusLabel->setText(toString(m_effect->status()));
}
QUrl SampleWidget::sampleUrl() const
{
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)};
}

54
samplewidget.h Executable file
View File

@ -0,0 +1,54 @@
#pragma once
#include <memory>
#include <QFrame>
#include "presets.h"
namespace Ui { class SampleWidget; }
class QThread;
class QAudioDeviceInfo;
class QSoundEffect;
class SampleWidget : public QFrame
{
Q_OBJECT
public:
explicit SampleWidget(QWidget *parent = nullptr);
~SampleWidget() override;
void setFile(const QString &presetId, const presets::File &file);
quint8 channel() const;
void setChannel(quint8 channel);
quint8 note() const;
void setNote(quint8 note);
std::optional<int> choke() const;
void pressed(quint8 velocity);
void released();
void forceStop();
void setupAudioThread(const QAudioDeviceInfo &device, QThread &thread);
signals:
void chokeTriggered(int choke);
private slots:
void updateStatus();
private:
QUrl sampleUrl() const;
const std::unique_ptr<Ui::SampleWidget> m_ui;
QString m_presetId;
std::optional<presets::File> m_file;
std::unique_ptr<QSoundEffect> m_effect;
};

108
samplewidget.ui Executable file
View File

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SampleWidget</class>
<widget class="QFrame" name="SampleWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
<property name="frameShape">
<enum>QFrame::Panel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="statusLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="toolButton">
<property name="text">
<string>↻</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>PushButton</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QSpinBox" name="channelSpinBox"/>
</item>
<item>
<widget class="QSpinBox" name="noteSpinBox"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="stopOnReleaseLabel">
<property name="frameShape">
<enum>QFrame::Panel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="loopedLabel">
<property name="frameShape">
<enum>QFrame::Panel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="chokeLabel">
<property name="frameShape">
<enum>QFrame::Panel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

148
sequencerwidget.cpp Executable file
View File

@ -0,0 +1,148 @@
#include "sequencerwidget.h"
#include "ui_sequencerwidget.h"
#include <algorithm>
#include "presets.h"
SequencerWidget::SequencerWidget(QWidget *parent) :
QWidget{parent},
m_ui{std::make_unique<Ui::SequencerWidget>()}
{
m_ui->setupUi(this);
connect(m_ui->spinBoxTempo, qOverload<int>(&QSpinBox::valueChanged), this, &SequencerWidget::tempoChanged);
connect(m_ui->comboBoxSequence, qOverload<int>(&QComboBox::currentIndexChanged), this, &SequencerWidget::sequenceSelected);
connect(m_ui->horizontalSlider, &QSlider::valueChanged, this, [=](int value){ m_pos = value; updateStatusLabel(); });
connect(m_ui->pushButtonPlayPause, &QAbstractButton::pressed, this, &SequencerWidget::playPause);
connect(m_ui->pushButtonStop, &QAbstractButton::pressed, this, &SequencerWidget::stop);
connect(&m_timer, &QTimer::timeout, this, &SequencerWidget::timeout);
updateStatusLabel();
}
SequencerWidget::~SequencerWidget() = default;
void SequencerWidget::setPreset(const presets::Preset &preset)
{
if (preset.tempo)
m_ui->spinBoxTempo->setValue(*preset.tempo);
m_selectedSequence = nullptr;
m_ui->horizontalSlider->setMaximum(0);
m_ui->comboBoxSequence->clear();
m_sequences.clear();
m_selectedSequence = nullptr;
const auto doit = [&](const QString &prefix, const std::optional<std::map<QString, std::vector<presets::Sequence>>> &value)
{
if (!value)
return;
for (const auto &pair : *value)
{
for (const auto &sequence : pair.second)
{
m_ui->comboBoxSequence->addItem(QString{"%0/%1/%2"}.arg(prefix, pair.first, sequence.name?*sequence.name:"(null)"));
m_sequences.emplace_back(sequence);
}
}
};
{
QSignalBlocker blocker{m_ui->comboBoxSequence};
doit("beatSchool", preset.beatSchool);
doit("easyPlay", preset.easyPlay);
}
sequenceSelected();
}
void SequencerWidget::playPause()
{
if (m_timer.isActive())
{
m_timer.stop();
m_ui->pushButtonPlayPause->setText(tr(""));
}
else
{
m_timer.start();
m_ui->pushButtonPlayPause->setText(tr("▮▮"));
}
}
void SequencerWidget::stop()
{
m_timer.stop();
m_ui->pushButtonPlayPause->setText(tr(""));
m_pos = 0;
m_ui->horizontalSlider->setValue(0);
updateStatusLabel();
}
void SequencerWidget::tempoChanged(int tempo)
{
m_timer.setInterval(1000. * 60. / tempo / 4.);
}
void SequencerWidget::sequenceSelected()
{
const auto index = m_ui->comboBoxSequence->currentIndex();
if (index == -1)
m_selectedSequence = nullptr;
else
m_selectedSequence = &m_sequences[index];
m_ui->horizontalSlider->setMaximum(m_selectedSequence && m_selectedSequence->sequencerSize ? *m_selectedSequence->sequencerSize-2 : 0);
m_ui->pushButtonPlayPause->setEnabled(m_selectedSequence != nullptr);
m_ui->pushButtonStop->setEnabled(m_selectedSequence != nullptr);
m_pos = 0;
m_ui->horizontalSlider->setValue(0);
updateStatusLabel();
}
void SequencerWidget::timeout()
{
if (m_selectedSequence && m_selectedSequence->pads)
{
for (const auto &pair : *m_selectedSequence->pads)
{
const auto iter = std::find_if(std::cbegin(pair.second), std::cend(pair.second), [&](const presets::SequencePad &sequencePad){
return sequencePad.start && *sequencePad.start == m_pos;
});
if (iter == std::cend(pair.second))
continue;
//TODO: iter->duration;
bool ok;
const auto index = pair.first.toInt(&ok);
if (!ok)
continue;
emit triggerSample(index);
}
}
m_pos++;
if (m_pos >= (m_selectedSequence && m_selectedSequence->sequencerSize ? *m_selectedSequence->sequencerSize-1 : -1))
m_pos = 0;
m_ui->horizontalSlider->setValue(m_pos);
updateStatusLabel();
}
void SequencerWidget::updateStatusLabel()
{
m_ui->labelStatus->setText(QString{"%0 / %1"}.arg(m_pos+1).arg(m_selectedSequence && m_selectedSequence->sequencerSize ? *m_selectedSequence->sequencerSize-1 : -1));
}

44
sequencerwidget.h Executable file
View File

@ -0,0 +1,44 @@
#pragma once
#include <memory>
#include <vector>
#include <QWidget>
#include <QTimer>
namespace Ui { class SequencerWidget; }
namespace presets { class Preset; class Sequence; }
class SequencerWidget : public QWidget
{
Q_OBJECT
public:
explicit SequencerWidget(QWidget *parent = nullptr);
~SequencerWidget() override;
void setPreset(const presets::Preset &preset);
signals:
void triggerSample(int index);
private slots:
void playPause();
void stop();
void tempoChanged(int tempo);
void sequenceSelected();
void timeout();
void updateStatusLabel();
private:
const std::unique_ptr<Ui::SequencerWidget> m_ui;
std::vector<presets::Sequence> m_sequences;
const presets::Sequence *m_selectedSequence{};
QTimer m_timer;
int m_pos;
};

78
sequencerwidget.ui Executable file
View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SequencerWidget</class>
<widget class="QWidget" name="SequencerWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>412</width>
<height>65</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QSpinBox" name="spinBoxTempo">
<property name="suffix">
<string>BPM</string>
</property>
<property name="maximum">
<number>999</number>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBoxSequence"/>
</item>
<item>
<widget class="QPushButton" name="pushButtonPlayPause">
<property name="maximumSize">
<size>
<width>32</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>▶</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButtonStop">
<property name="maximumSize">
<size>
<width>32</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>◼</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="labelStatus">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QSlider" name="horizontalSlider">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

BIN
splashscreen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB