diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ae3ece6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "rtmidi"] + path = rtmidi + url = git@github.com:thestk/rtmidi.git diff --git a/DrumMachine.pro b/DrumMachine.pro new file mode 100755 index 0000000..6156b5c --- /dev/null +++ b/DrumMachine.pro @@ -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 diff --git a/filesmodel.cpp b/filesmodel.cpp new file mode 100755 index 0000000..5fc8a4a --- /dev/null +++ b/filesmodel.cpp @@ -0,0 +1,129 @@ +#include "filesmodel.h" + +#include + +#include +#include + +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(); +} diff --git a/filesmodel.h b/filesmodel.h new file mode 100755 index 0000000..677022d --- /dev/null +++ b/filesmodel.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include + +#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> m_files; +}; diff --git a/jsonconverters.cpp b/jsonconverters.cpp new file mode 100755 index 0000000..31ea86f --- /dev/null +++ b/jsonconverters.cpp @@ -0,0 +1,396 @@ +#include "jsonconverters.h" + +#include + +#include + +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 parseStringVector(const QJsonValue &jsonValue) +{ + if (!jsonValue.isArray()) + throw std::runtime_error{"json value for string vector is not an array"}; + + std::vector 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 parseIntVector(const QJsonValue &jsonValue) +{ + if (!jsonValue.isArray()) + throw std::runtime_error{"json value for int vector is not an array"}; + + std::vector 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 parseCategoryVector(const QJsonValue &jsonValue) +{ + if (!jsonValue.isArray()) + throw std::runtime_error{"json value for vector of Category is not an array"}; + + std::vector vector; + + for (const auto &jsonValue : jsonValue.toArray()) + vector.emplace_back(parseCategory(jsonValue)); + + return vector; +} + +std::map 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 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 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 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 parseSequenceVector(const QJsonValue &jsonValue) +{ + if (!jsonValue.isArray()) + throw std::runtime_error{"json value for vector of Sequence is not an array"}; + + std::vector 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> 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> 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 parseSequencePadVector(const QJsonValue &jsonValue) +{ + if (!jsonValue.isArray()) + throw std::runtime_error{"json value for vector of SequencePad is not an array"}; + + std::vector vector; + + for (const auto &jsonValue : jsonValue.toArray()) + vector.emplace_back(parseSequencePad(jsonValue)); + + return vector; +} + +std::map> 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> map; + + for (auto iter = std::cbegin(jsonObj); iter != std::cend(jsonObj); iter++) + map[iter.key()] = parseSequencePadVector(iter.value()); + + return map; +} + +} diff --git a/jsonconverters.h b/jsonconverters.h new file mode 100755 index 0000000..9470b94 --- /dev/null +++ b/jsonconverters.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +#include "presets.h" + +namespace json_converters +{ +QJsonObject loadJson(const QByteArray &buffer); + +QString parseString(const QJsonValue &jsonValue); +std::vector parseStringVector(const QJsonValue &jsonValue); +int parseInt(const QJsonValue &jsonValue); +bool parseBool(const QJsonValue &jsonValue); +std::vector parseIntVector(const QJsonValue &jsonValue); + +presets::PresetsConfig parsePresetsConfig(const QJsonObject &jsonObj); +std::vector parseCategoryVector(const QJsonValue &jsonValue); +std::map parsePresetMap(const QJsonValue &jsonValue); +presets::Category parseCategory(const QJsonValue &jsonValue); +presets::Filter parseFilter(const QJsonValue &jsonValue); +presets::Preset parsePreset(const QJsonValue &jsonValue); +std::array parseFileArray(const QJsonValue &jsonValue); +presets::File parseFile(const QJsonValue &jsonValue); +std::vector parseSequenceVector(const QJsonValue &jsonValue); +presets::Sequence parseSequence(const QJsonValue &jsonValue); +std::map> parseSequenceVectorMap(const QJsonValue &jsonValue); +presets::SequencePad parseSequencePad(const QJsonValue &jsonValue); +std::vector parseSequencePadVector(const QJsonValue &jsonValue); +std::map> parseSequencePadVectorMap(const QJsonValue &jsonValue); +} diff --git a/main.cpp b/main.cpp new file mode 100755 index 0000000..8dfa0ac --- /dev/null +++ b/main.cpp @@ -0,0 +1,85 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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(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(); +} diff --git a/mainwindow.cpp b/mainwindow.cpp new file mode 100755 index 0000000..5602e99 --- /dev/null +++ b/mainwindow.cpp @@ -0,0 +1,123 @@ +#include "mainwindow.h" +#include "ui_mainwindow.h" + +#include +#include +#include +#include +#include + +#include "presets.h" +#include "midiinwrapper.h" +#include "midicontainers.h" + +MainWindow::MainWindow(const presets::PresetsConfig &presetsConfig, QWidget *parent) : + QMainWindow{parent}, + m_ui{std::make_unique()}, + 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(&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().valueToKey(int(message.cmd))) + .arg(message.channel).arg(message.note).arg(message.velocity), 1000); + + m_ui->samplesWidget->messageReceived(message); +} + +void MainWindow::currentRowChanged(const QModelIndex ¤t) +{ + 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()); +} diff --git a/mainwindow.h b/mainwindow.h new file mode 100755 index 0000000..ed71c92 --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include +#include + +#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 ¤t); + +private: + void updateMidiDevices(); + void updateAudioDevices(); + + const std::unique_ptr m_ui; + + MidiInWrapper m_midiIn; + + QList m_devices; + + PresetsModel m_presetsModel; + QSortFilterProxyModel m_presetsProxyModel; + + FilesModel m_filesModel; +}; diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100755 index 0000000..ca925ae --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,228 @@ + + + MainWindow + + + + 0 + 0 + 1017 + 712 + + + + MainWindow + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + <b>UltraDj</b> + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Master: + + + horizontalSliderMaster + + + + + + + + 100 + 16777215 + + + + 100 + + + 100 + + + Qt::Horizontal + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Midi in: + + + + + + + + + + Open + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Qt::Horizontal + + + + Qt::Vertical + + + + + + + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Select random + + + + + + + + + false + + + true + + + + + + + + Qt::Horizontal + + + + + false + + + + + + + + + + + + + 0 + 0 + 1017 + 20 + + + + + + + + SamplesWidget + QWidget +
sampleswidget.h
+ 1 +
+ + PresetDetailWidget + QScrollArea +
presetdetailwidget.h
+ 1 +
+
+ + +
diff --git a/midicontainers.cpp b/midicontainers.cpp new file mode 100755 index 0000000..025eebd --- /dev/null +++ b/midicontainers.cpp @@ -0,0 +1,23 @@ +#include "midicontainers.h" + +#include + +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(); +} + +Q_COREAPP_STARTUP_FUNCTION(registerMidiMessageMetatype) +} diff --git a/midicontainers.h b/midicontainers.h new file mode 100755 index 0000000..f6bf8a7 --- /dev/null +++ b/midicontainers.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +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) diff --git a/midiinwrapper.cpp b/midiinwrapper.cpp new file mode 100755 index 0000000..969f98a --- /dev/null +++ b/midiinwrapper.cpp @@ -0,0 +1,73 @@ +#include "midiinwrapper.h" + +#include +#include +#include + +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 *message, void *userData) +{ + Q_UNUSED(deltatime) + + if (!userData) + { + qCritical() << "called without userData pointer to wrapper"; + return; + } + + auto wrapper = static_cast(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(message->at(0)); + wrapper->messageReceived(midiMessage); +} diff --git a/midiinwrapper.h b/midiinwrapper.h new file mode 100755 index 0000000..d7423bc --- /dev/null +++ b/midiinwrapper.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +#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 *message, void *userData); + + RtMidiIn midiIn; +}; diff --git a/presetdetailwidget.cpp b/presetdetailwidget.cpp new file mode 100755 index 0000000..1813739 --- /dev/null +++ b/presetdetailwidget.cpp @@ -0,0 +1,16 @@ +#include "presetdetailwidget.h" +#include "ui_presetdetailwidget.h" + +PresetDetailWidget::PresetDetailWidget(QWidget *parent) : + QScrollArea{parent}, + m_ui{std::make_unique()} +{ + m_ui->setupUi(this); +} + +PresetDetailWidget::~PresetDetailWidget() = default; + +void PresetDetailWidget::setPreset(const presets::Preset &preset) +{ + +} diff --git a/presetdetailwidget.h b/presetdetailwidget.h new file mode 100755 index 0000000..c0ee94e --- /dev/null +++ b/presetdetailwidget.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include + +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 m_ui; +}; diff --git a/presetdetailwidget.ui b/presetdetailwidget.ui new file mode 100755 index 0000000..8648f75 --- /dev/null +++ b/presetdetailwidget.ui @@ -0,0 +1,306 @@ + + + PresetDetailWidget + + + + 0 + 0 + 320 + 633 + + + + true + + + + + 0 + 0 + 318 + 631 + + + + + + + id: + + + lineEditId + + + + + + + name: + + + lineEditName + + + + + + + author: + + + lineEditAuthor + + + + + + + orderBy: + + + lineEditOrderBy + + + + + + + version: + + + lineEditVersion + + + + + + + tempo: + + + spinBoxTempo + + + + + + + icon: + + + lineEditIcon + + + + + + + price: + + + spinBoxPrice + + + + + + + priceForSession: + + + spinBoxPriceForSession + + + + + + + hasInfo: + + + checkBoxHasInfo + + + + + + + tags: + + + + + + + DELETED: + + + checkBoxDeleted + + + + + + + difficulty: + + + spinBoxDifficulty + + + + + + + sample: + + + spinBoxSample + + + + + + + audioPreview1Name: + + + lineEditAudioPreview1Name + + + + + + + audioPreview1URL: + + + lineEditAudioPreview1URL + + + + + + + audioPreview2Name: + + + lineEditAudioPreview2Name + + + + + + + audioPreview2URL: + + + lineEditAudioPreview2URL + + + + + + + imagePreview1: + + + lineEditImagePreview1 + + + + + + + videoPreview: + + + lineEditVideoPreview + + + + + + + videoTutorial: + + + lineEditVideoTutorial + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CheckBox + + + + + + + CheckBox + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presets.cpp b/presets.cpp new file mode 100755 index 0000000..4cdf931 --- /dev/null +++ b/presets.cpp @@ -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; +} + +} diff --git a/presets.h b/presets.h new file mode 100755 index 0000000..a53244a --- /dev/null +++ b/presets.h @@ -0,0 +1,84 @@ +#pragma once + +#include +#include +#include + +#include + +namespace presets +{ +struct Filter +{ + std::optional> tags; +}; + +struct Category +{ + std::optional title; + std::optional filter; +}; + +struct File +{ + std::optional filename; + std::optional color; // purple, red, yellow, green, blue + std::optional stopOnRelease; + std::optional looped; + std::optional choke; + + bool operator==(const File &other) const; +}; + +struct SequencePad +{ + std::optional start; + std::optional duration; + std::optional embient; +}; + +struct Sequence +{ + std::optional name; + std::optional id; + std::optional version; + std::optional orderBy; + std::optional sequencerSize; + std::optional>> pads; + std::optional> embientPads; +}; + +struct Preset +{ + std::optional id; + std::optional name; + std::optional author; + std::optional orderBy; + std::optional version; + std::optional tempo; + std::optional icon; + std::optional price; + std::optional priceForSession; + std::optional hasInfo; + std::optional> tags; + std::optional DELETED; + std::optional difficulty; + std::optional sample; + std::optional audioPreview1Name; + std::optional audioPreview1URL; + std::optional audioPreview2Name; + std::optional audioPreview2URL; + std::optional imagePreview1; + std::optional videoPreview; + std::optional videoTutorial; + std::optional> files; + std::optional>> beatSchool; + std::optional>> easyPlay; +}; + +struct PresetsConfig +{ + std::optional> categories; + std::optional> presets; +}; +} diff --git a/presetsmodel.cpp b/presetsmodel.cpp new file mode 100755 index 0000000..3e89fab --- /dev/null +++ b/presetsmodel.cpp @@ -0,0 +1,208 @@ +#include "presetsmodel.h" + +#include + +#include +#include + +#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 &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(); +} diff --git a/presetsmodel.h b/presetsmodel.h new file mode 100755 index 0000000..ecbd711 --- /dev/null +++ b/presetsmodel.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include + +namespace presets { class Preset; } + +class PresetsModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + PresetsModel(const std::map &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 m_presets; +}; diff --git a/resources.qrc b/resources.qrc new file mode 100644 index 0000000..3dff046 --- /dev/null +++ b/resources.qrc @@ -0,0 +1,5 @@ + + + splashscreen.png + + diff --git a/rtmidi b/rtmidi new file mode 160000 index 0000000..7ab18ef --- /dev/null +++ b/rtmidi @@ -0,0 +1 @@ +Subproject commit 7ab18ef06b549e7cdd247c492e055093263dd4f3 diff --git a/sampleswidget.cpp b/sampleswidget.cpp new file mode 100755 index 0000000..63a394e --- /dev/null +++ b/sampleswidget.cpp @@ -0,0 +1,172 @@ +#include "sampleswidget.h" +#include "ui_sampleswidget.h" + +#include + +#include +#include + +#include "midicontainers.h" + +SamplesWidget::SamplesWidget(QWidget *parent) : + QWidget{parent}, + m_ui{std::make_unique()} +{ + 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, 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) + }; +} diff --git a/sampleswidget.h b/sampleswidget.h new file mode 100755 index 0000000..293ad9d --- /dev/null +++ b/sampleswidget.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#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, 24> getWidgets(); + + const std::unique_ptr m_ui; + + presets::Preset m_preset; + + QThread m_audioThread; +}; diff --git a/sampleswidget.ui b/sampleswidget.ui new file mode 100755 index 0000000..26e2c96 --- /dev/null +++ b/sampleswidget.ui @@ -0,0 +1,253 @@ + + + SamplesWidget + + + + 0 + 0 + 660 + 421 + + + + Form + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Sequencer + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + Swap left/right + + + + + + + Stop all + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + SampleWidget + QWidget +
samplewidget.h
+ 1 +
+ + SequencerWidget + QWidget +
sequencerwidget.h
+ 1 +
+
+ + +
diff --git a/samplewidget.cpp b/samplewidget.cpp new file mode 100755 index 0000000..85dccae --- /dev/null +++ b/samplewidget.cpp @@ -0,0 +1,192 @@ +#include "samplewidget.h" +#include "ui_samplewidget.h" + +#include +#include +#include +#include + +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()} +{ + 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 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(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)}; +} diff --git a/samplewidget.h b/samplewidget.h new file mode 100755 index 0000000..55084ed --- /dev/null +++ b/samplewidget.h @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include + +#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 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 m_ui; + + QString m_presetId; + std::optional m_file; + + std::unique_ptr m_effect; +}; diff --git a/samplewidget.ui b/samplewidget.ui new file mode 100755 index 0000000..8fb911d --- /dev/null +++ b/samplewidget.ui @@ -0,0 +1,108 @@ + + + SampleWidget + + + + 0 + 0 + 400 + 300 + + + + Form + + + true + + + QFrame::Panel + + + QFrame::Sunken + + + + + + + + TextLabel + + + + + + + + + + + + + + + + PushButton + + + + + + + + + + + + + + + + + + + QFrame::Panel + + + QFrame::Sunken + + + TextLabel + + + + + + + QFrame::Panel + + + QFrame::Sunken + + + TextLabel + + + + + + + QFrame::Panel + + + QFrame::Sunken + + + TextLabel + + + + + + + + + + diff --git a/sequencerwidget.cpp b/sequencerwidget.cpp new file mode 100755 index 0000000..91c82d5 --- /dev/null +++ b/sequencerwidget.cpp @@ -0,0 +1,148 @@ +#include "sequencerwidget.h" +#include "ui_sequencerwidget.h" + +#include + +#include "presets.h" + +SequencerWidget::SequencerWidget(QWidget *parent) : + QWidget{parent}, + m_ui{std::make_unique()} +{ + m_ui->setupUi(this); + + connect(m_ui->spinBoxTempo, qOverload(&QSpinBox::valueChanged), this, &SequencerWidget::tempoChanged); + connect(m_ui->comboBoxSequence, qOverload(&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>> &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)); +} diff --git a/sequencerwidget.h b/sequencerwidget.h new file mode 100755 index 0000000..4d59ed8 --- /dev/null +++ b/sequencerwidget.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +#include +#include + +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 m_ui; + + std::vector m_sequences; + const presets::Sequence *m_selectedSequence{}; + + QTimer m_timer; + + int m_pos; +}; diff --git a/sequencerwidget.ui b/sequencerwidget.ui new file mode 100755 index 0000000..8c89b0c --- /dev/null +++ b/sequencerwidget.ui @@ -0,0 +1,78 @@ + + + SequencerWidget + + + + 0 + 0 + 412 + 65 + + + + Form + + + + + + + + BPM + + + 999 + + + + + + + + + + + 32 + 16777215 + + + + + + + + + + + + 32 + 16777215 + + + + + + + + + + + TextLabel + + + + + + + + + Qt::Horizontal + + + + + + + + diff --git a/splashscreen.png b/splashscreen.png new file mode 100644 index 0000000..73072cc Binary files /dev/null and b/splashscreen.png differ