Moved widgets in subfolder

This commit is contained in:
2022-12-27 10:15:13 +01:00
parent e612fa163d
commit 818f462ae8
37 changed files with 175 additions and 132 deletions

102
widgets/djwidget.cpp Normal file
View File

@@ -0,0 +1,102 @@
#include "djwidget.h"
#include "ui_djwidget.h"
#include <QDebug>
#include <QStandardPaths>
DjWidget::DjWidget(QWidget *parent) :
QWidget{parent},
m_ui{std::make_unique<Ui::DjWidget>()}
{
m_ui->setupUi(this);
const auto createSlot = [this](TrackDeck &trackDeck){
return [this,&trackDeck](){
const auto index = m_ui->treeViewFiles->currentIndex();
if (!index.isValid())
return;
trackDeck.loadTrack(m_filesModel.filePath(m_filesTableModel.mapToSource(m_sortFilterProxyModel.mapToSource(index))));
};
};
connect(m_ui->pushButtonLoadDeckA, &QAbstractButton::pressed, m_ui->trackDeckA, createSlot(*m_ui->trackDeckA));
connect(m_ui->pushButtonLoadDeckB, &QAbstractButton::pressed, m_ui->trackDeckB, createSlot(*m_ui->trackDeckB));
m_directoryModel.setFilter(QDir::Dirs|QDir::Drives|QDir::NoDotAndDotDot);
const auto rootIndex = m_directoryModel.setRootPath(QDir::homePath());
m_ui->treeViewDirectories->setModel(&m_directoryModel);
m_ui->treeViewDirectories->setRootIndex(rootIndex);
for (auto i = 0, count = m_directoryModel.columnCount(); i < count; i++)
m_ui->treeViewDirectories->setColumnHidden(i, i!=0);
connect(m_ui->treeViewDirectories->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &DjWidget::directorySelected);
m_filesModel.setFilter(QDir::Files);
m_filesTableModel.setSourceModel(&m_filesModel);
m_sortFilterProxyModel.setFilterCaseSensitivity(Qt::CaseInsensitive);
m_sortFilterProxyModel.setSourceModel(&m_filesTableModel);
m_ui->treeViewFiles->setModel(&m_sortFilterProxyModel);
if (const auto locations = QStandardPaths::standardLocations(QStandardPaths::MusicLocation); !locations.isEmpty())
{
const auto index = m_directoryModel.index(locations.first());
m_ui->treeViewDirectories->selectionModel()->select(index, QItemSelectionModel::Clear|QItemSelectionModel::Select|QItemSelectionModel::Current|QItemSelectionModel::Rows);
const auto rootIndex = m_filesModel.setRootPath(m_directoryModel.filePath(index));
m_filesTableModel.setRootIndex(rootIndex);
}
connect(m_ui->lineEditSearch, &QLineEdit::textChanged, &m_sortFilterProxyModel, &QSortFilterProxyModel::setFilterFixedString);
}
DjWidget::~DjWidget() = default;
void DjWidget::injectDecodingThread(QThread &thread)
{
m_ui->trackDeckA->injectDecodingThread(thread);
m_ui->trackDeckB->injectDecodingThread(thread);
}
void DjWidget::writeSamples(frame_t *begin, frame_t *end)
{
const auto count = std::distance(begin, end);
for (TrackDeck *trackDeck : {m_ui->trackDeckA, m_ui->trackDeckB})
{
frame_t buffer[count];
std::fill(buffer, buffer+count, frame_t{0.f,0.f});
trackDeck->writeSamples(buffer, buffer+count);
std::transform(static_cast<const frame_t *>(begin), static_cast<const frame_t *>(end), buffer, begin, [volume=1.f](const frame_t &frame, const frame_t &frame2){
frame_t newFrame;
std::transform(std::cbegin(frame), std::cend(frame), std::begin(frame2), std::begin(newFrame),
[&volume](const sample_t &left, const sample_t &right) { return left + (right*volume); });
return newFrame;
});
}
}
void DjWidget::loadSettings(DrumMachineSettings &settings)
{
}
void DjWidget::unsendColors()
{
}
void DjWidget::sendColors()
{
}
void DjWidget::messageReceived(const midi::MidiMessage &message)
{
}
void DjWidget::directorySelected()
{
const auto selected = m_ui->treeViewDirectories->currentIndex();
if (selected.isValid())
{
const auto rootIndex = m_filesModel.setRootPath(m_directoryModel.filePath(selected));
m_filesTableModel.setRootIndex(rootIndex);
}
}

46
widgets/djwidget.h Normal file
View File

@@ -0,0 +1,46 @@
#pragma once
#include <QWidget>
#include <QFileSystemModel>
#include <QSortFilterProxyModel>
#include <memory>
#include "audioformat.h"
#include "midicontainers.h"
#include "treetotableproxymodel.h"
namespace Ui { class DjWidget; }
class DrumMachineSettings;
class DjWidget : public QWidget
{
Q_OBJECT
public:
explicit DjWidget(QWidget *parent = nullptr);
~DjWidget() override;
void injectDecodingThread(QThread &thread);
void writeSamples(frame_t *begin, frame_t *end);
void loadSettings(DrumMachineSettings &settings);
void unsendColors();
void sendColors();
signals:
void sendMidi(const midi::MidiMessage &midiMsg);
public slots:
void messageReceived(const midi::MidiMessage &message);
private slots:
void directorySelected();
private:
const std::unique_ptr<Ui::DjWidget> m_ui;
QFileSystemModel m_directoryModel;
QFileSystemModel m_filesModel;
TreeToTableProxyModel m_filesTableModel;
QSortFilterProxyModel m_sortFilterProxyModel;
};

149
widgets/djwidget.ui Normal file
View File

@@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DjWidget</class>
<widget class="QWidget" name="DjWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>765</width>
<height>519</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,1">
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0,1">
<item>
<widget class="TrackDeck" name="trackDeckA" native="true"/>
</item>
<item>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::VLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item>
<widget class="TrackDeck" name="trackDeckB" native="true"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<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="QSlider" name="horizontalSlider">
<property name="minimum">
<number>-100</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="invertedAppearance">
<bool>true</bool>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBothSides</enum>
</property>
<property name="tickInterval">
<number>50</number>
</property>
</widget>
</item>
<item>
<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>
</layout>
</item>
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QTreeView" name="treeViewDirectories">
<property name="headerHidden">
<bool>true</bool>
</property>
</widget>
<widget class="QWidget" name="layoutWidget">
<layout class="QVBoxLayout">
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QLineEdit" name="lineEditSearch">
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButtonLoadDeckA">
<property name="text">
<string>Load in Deck A</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButtonLoadDeckB">
<property name="text">
<string>Load in Deck B</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTreeView" name="treeViewFiles">
<property name="dragEnabled">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>TrackDeck</class>
<extends>QWidget</extends>
<header>widgets/trackdeck.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

150
widgets/drumpadwidget.cpp Normal file
View File

@@ -0,0 +1,150 @@
#include "drumpadwidget.h"
#include "ui_drumpadwidget.h"
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QNetworkAccessManager>
#include <QMessageBox>
#include "jsonconverters.h"
DrumPadWidget::DrumPadWidget(QWidget *parent) :
QSplitter{parent},
m_ui{std::make_unique<Ui::DrumPadWidget>()}
{
m_ui->setupUi(this);
connect(m_ui->samplesWidget, &SamplesWidget::sendMidi, this, &DrumPadWidget::sendMidi);
connect(m_ui->sequencerWidget, &SequencerWidget::triggerSample, m_ui->samplesWidget, &SamplesWidget::sequencerTriggerSample);
m_presetsProxyModel.setFilterCaseSensitivity(Qt::CaseInsensitive);
m_presetsProxyModel.setSortRole(Qt::EditRole);
m_presetsProxyModel.setSourceModel(&m_presetsModel);
m_ui->presetsView->setModel(&m_presetsProxyModel);
m_presetsProxyModel.setFilterKeyColumn(1);
connect(m_ui->lineEdit, &QLineEdit::textChanged, &m_presetsProxyModel, &QSortFilterProxyModel::setFilterFixedString);
m_ui->filesView->setModel(&m_filesModel);
connect(m_ui->presetsView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &DrumPadWidget::currentRowChanged);
connect(m_ui->pushButtonRefresh, &QAbstractButton::pressed, this, &DrumPadWidget::loadPresets);
}
DrumPadWidget::~DrumPadWidget() = default;
void DrumPadWidget::selectFirstPreset()
{
if (!m_presetsProxyModel.rowCount())
return;
const auto index = m_presetsProxyModel.index(0, 0);
if (index.isValid())
{
m_ui->presetsView->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
currentRowChanged(index);
}
}
void DrumPadWidget::writeSamples(frame_t *begin, frame_t *end)
{
m_ui->samplesWidget->writeSamples(begin, end);
}
void DrumPadWidget::injectNetworkAccessManager(QNetworkAccessManager &networkAccessManager)
{
m_networkAccessManager = &networkAccessManager;
loadPresets();
m_ui->samplesWidget->injectNetworkAccessManager(networkAccessManager);
}
void DrumPadWidget::injectDecodingThread(QThread &thread)
{
m_ui->samplesWidget->injectDecodingThread(thread);
}
void DrumPadWidget::loadSettings(DrumMachineSettings &settings)
{
m_ui->samplesWidget->loadSettings(settings);
}
void DrumPadWidget::unsendColors()
{
m_ui->samplesWidget->unsendColors();
}
void DrumPadWidget::sendColors()
{
m_ui->samplesWidget->sendColors();
}
void DrumPadWidget::messageReceived(const midi::MidiMessage &message)
{
m_ui->samplesWidget->messageReceived(message);
}
void DrumPadWidget::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->sequencerWidget->setPreset(preset);
m_ui->samplesWidget->setPreset(preset);
}
void DrumPadWidget::loadPresets()
{
if (!m_networkAccessManager)
{
qWarning() << "no network access manager available";
return;
}
m_ui->pushButtonRefresh->setEnabled(false);
QNetworkRequest request{QUrl{"https://brunner.ninja/komposthaufen/dpm/presets_config_v12.json"}};
request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, true);
m_reply = std::unique_ptr<QNetworkReply>(m_networkAccessManager->get(request));
connect(m_reply.get(), &QNetworkReply::finished, this, &DrumPadWidget::requestFinished);
}
void DrumPadWidget::requestFinished()
{
if (!m_reply)
{
qWarning() << "no valid reply";
return;
}
auto reply = std::move(m_reply);
if (reply->error() != QNetworkReply::NoError)
{
QMessageBox::warning(this, tr("Could not load presets!"), tr("Could not load presets!") + "\n\n" + reply->errorString());
return;
}
try
{
auto result = json_converters::parsePresetsConfig(json_converters::loadJson(reply->readAll()));
if (!result.presets)
throw std::runtime_error("presets missing in response");
m_presetsModel.setPresets(std::move(*std::move(result).presets));
selectFirstPreset();
}
catch (const std::exception &e)
{
QMessageBox::warning(this, tr("error"), tr("error") + "\n\n" + QString::fromStdString(e.what()));
}
}

60
widgets/drumpadwidget.h Normal file
View File

@@ -0,0 +1,60 @@
#pragma once
#include <QSplitter>
#include <QSortFilterProxyModel>
#include <memory>
#include "audioformat.h"
#include "midicontainers.h"
#include "presetsmodel.h"
#include "filesmodel.h"
namespace Ui { class DrumPadWidget; }
class SamplesWidget;
class SequencerWidget;
class QModelIndex;
class QNetworkAccessManager;
class QThread;
class DrumMachineSettings;
class QNetworkReply;
class DrumPadWidget : public QSplitter
{
Q_OBJECT
public:
explicit DrumPadWidget(QWidget *parent = nullptr);
~DrumPadWidget() override;
void selectFirstPreset();
void writeSamples(frame_t *begin, frame_t *end);
void injectNetworkAccessManager(QNetworkAccessManager &networkAccessManager);
void injectDecodingThread(QThread &thread);
void loadSettings(DrumMachineSettings &settings);
void unsendColors();
void sendColors();
signals:
void sendMidi(const midi::MidiMessage &midiMsg);
public slots:
void messageReceived(const midi::MidiMessage &message);
private slots:
void currentRowChanged(const QModelIndex &current);
void loadPresets();
void requestFinished();
private:
const std::unique_ptr<Ui::DrumPadWidget> m_ui;
PresetsModel m_presetsModel;
QSortFilterProxyModel m_presetsProxyModel;
FilesModel m_filesModel;
QNetworkAccessManager *m_networkAccessManager{};
std::unique_ptr<QNetworkReply> m_reply;
};

108
widgets/drumpadwidget.ui Normal file
View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DrumPadWidget</class>
<widget class="QSplitter">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>805</width>
<height>525</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QWidget" name="layoutWidget">
<layout class="QVBoxLayout">
<item>
<layout class="QVBoxLayout">
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QLineEdit" name="lineEdit">
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButtonRefresh">
<property name="maximumSize">
<size>
<width>32</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>↻</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>
</item>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>2</number>
</property>
<widget class="PresetDetailWidget" name="presetDetailWidget">
<attribute name="title">
<string>Properties</string>
</attribute>
</widget>
<widget class="QTreeView" name="filesView">
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<attribute name="title">
<string>Samples</string>
</attribute>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QSplitter">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="SequencerWidget" name="sequencerWidget" />
<widget class="SamplesWidget" name="samplesWidget" />
</widget>
</widget>
<customwidgets>
<customwidget>
<class>SamplesWidget</class>
<extends>QWidget</extends>
<header>widgets/sampleswidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>PresetDetailWidget</class>
<extends>QScrollArea</extends>
<header>widgets/presetdetailwidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>SequencerWidget</class>
<extends>QWidget</extends>
<header>widgets/sequencerwidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

372
widgets/mainwindow.cpp Executable file
View File

@@ -0,0 +1,372 @@
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QEventLoop>
#include <QMetaEnum>
#include <QMessageBox>
#include <QTimer>
#include <QAbstractEventDispatcher>
#include <QAudioDeviceInfo>
#include <QDebug>
#include "presets.h"
#include "midiinwrapper.h"
#include "midicontainers.h"
#include "sampleswidget.h"
#include "sequencerwidget.h"
namespace {
void DummyDeleter(PaStream *stream);
void PaStreamCloser(PaStream *stream);
void PaStreamStopperAndCloser(PaStream *stream);
void paStreamFinished(void* userData);
int paCallback(const void *inputBuffer, void *outputBuffer,
unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo* timeInfo,
PaStreamCallbackFlags statusFlags,
void *userData);
} // namespace
MainWindow::MainWindow(QWidget *parent) :
QMainWindow{parent},
m_ui{std::make_unique<Ui::MainWindow>()},
m_paStream{nullptr, DummyDeleter},
m_midiIn{RtMidi::UNSPECIFIED, "DrumMachine"},
m_midiOut{RtMidi::UNSPECIFIED, "DrumMachine"}
{
m_ui->setupUi(this);
m_cache.setCacheDirectory("cache");
m_cache.setMaximumCacheSize(2ull * 1024 * 1024 * 1024);
m_networkAccessManager.setCache(&m_cache);
m_ui->drumPadWidget->injectNetworkAccessManager(m_networkAccessManager);
connect(&m_midiIn, &MidiInWrapper::messageReceived, this, &MainWindow::messageReceived);
{
QEventLoop eventLoop;
connect(&m_decoderThread, &QThread::started, &eventLoop, &QEventLoop::quit);
m_decoderThread.start(QThread::HighestPriority);
eventLoop.exec();
}
m_ui->drumPadWidget->injectDecodingThread(m_decoderThread);
m_ui->djWidget->injectDecodingThread(m_decoderThread);
updateAudioDevices();
connect(m_ui->pushButtonRefreshAudioDevices, &QAbstractButton::pressed, this, &MainWindow::updateAudioDevices);
if (const auto &lastAudioDevice = m_settings.lastAudioDevice(); !lastAudioDevice.isEmpty())
{
if (const auto index = m_ui->comboBoxAudioDevice->findText(lastAudioDevice); index >= 0)
m_ui->comboBoxAudioDevice->setCurrentIndex(index);
else
goto paDefault;
}
else
{
paDefault:
m_ui->comboBoxAudioDevice->setCurrentIndex(Pa_GetDefaultOutputDevice());
}
m_ui->spinBoxBufferSize->setValue(m_settings.framesPerBuffer());
connect(m_ui->pushButtonAudioDevice, &QAbstractButton::pressed, this, &MainWindow::openAudioDevice);
updateMidiInDevices();
connect(m_ui->pushButtonRefreshMidiIn, &QAbstractButton::pressed, this, &MainWindow::updateMidiInDevices);
if (const auto &lastMidiInDevice = m_settings.lastMidiInDevice(); !lastMidiInDevice.isEmpty())
{
if (const auto index = m_ui->comboBoxMidiIn->findText(lastMidiInDevice); index >= 0)
m_ui->comboBoxMidiIn->setCurrentIndex(index);
}
connect(m_ui->pushButtonMidiIn, &QAbstractButton::pressed, this, &MainWindow::openMidiInDevice);
updateMidiOutDevices();
connect(m_ui->pushButtonRefreshMidiOut, &QAbstractButton::pressed, this, &MainWindow::updateMidiOutDevices);
if (const auto &lastMidiOutDevice = m_settings.lastMidiOutDevice(); !lastMidiOutDevice.isEmpty())
{
if (const auto index = m_ui->comboBoxMidiOut->findText(lastMidiOutDevice); index >= 0)
m_ui->comboBoxMidiOut->setCurrentIndex(index);
}
connect(m_ui->pushButtonMidiOut, &QAbstractButton::pressed, this, &MainWindow::openMidiOutDevice);
loadSettings();
connect(m_ui->drumPadWidget, &DrumPadWidget::sendMidi, this, &MainWindow::sendMidi);
connect(m_ui->djWidget, &DjWidget::sendMidi, this, &MainWindow::sendMidi);
connect(m_ui->synthisizerWidget, &SynthisizerWidget::sendMidi, this, &MainWindow::sendMidi);
m_lastIndex = m_ui->tabWidget->currentIndex();
connect(m_ui->tabWidget, &QTabWidget::currentChanged, this, &MainWindow::currentChanged);
}
MainWindow::~MainWindow()
{
m_paStream = nullptr;
m_decoderThread.exit();
m_decoderThread.wait();
}
int MainWindow::writeSamples(frame_t *begin, frame_t *end)
{
std::fill(begin, end, frame_t{0.,0.});
m_ui->drumPadWidget->writeSamples(begin, end);
m_ui->djWidget->writeSamples(begin, end);
m_ui->synthisizerWidget->writeSamples(begin, end);
std::transform(begin, end, begin, [factor=float(m_ui->horizontalSliderMaster->value())/100.f](frame_t frame){
std::transform(std::cbegin(frame), std::cend(frame), std::begin(frame), [&factor](const sample_t &sample){
return sample*factor;
});
return frame;
});
return paContinue;
}
void MainWindow::openAudioDevice()
{
if (m_paStream)
{
m_paStream = nullptr;
m_ui->comboBoxAudioDevice->setEnabled(true);
m_ui->spinBoxBufferSize->setEnabled(true);
m_ui->pushButtonAudioDevice->setText(tr("Open"));
}
else
{
PaDeviceIndex index = m_ui->comboBoxAudioDevice->currentIndex();
const PaDeviceInfo* pInfo = Pa_GetDeviceInfo(index);
const PaStreamParameters outputParameters {
.device = index,
.channelCount = channelCount,
.sampleFormat = paFloat32, /* 32 bit floating point output */
.suggestedLatency = pInfo->defaultLowOutputLatency,
.hostApiSpecificStreamInfo = NULL
};
PaStream *stream{};
if (PaError err = Pa_OpenStream(&stream, NULL, &outputParameters, frameRate, m_ui->spinBoxBufferSize->value(), paNoFlag, &paCallback, this); err != paNoError)
{
QMessageBox::warning(this, tr("Error opening stream!"), tr("Error opening stream!") + "\n\n" + Pa_GetErrorText(err));
return;
}
auto smartPtr = std::unique_ptr<PaStream, void(*)(PaStream*)>(stream, PaStreamCloser);
stream = nullptr;
if (PaError err = Pa_SetStreamFinishedCallback(smartPtr.get(), &paStreamFinished); err != paNoError)
{
QMessageBox::warning(this, tr("Error setting finished callback!"), tr("Error setting finished callback!") + "\n\n" + Pa_GetErrorText(err));
return;
}
if (PaError err = Pa_StartStream(smartPtr.get()); err != paNoError)
{
QMessageBox::warning(this, tr("Error starting stream!"), tr("Error starting stream!") + "\n\n" + Pa_GetErrorText(err));
return;
}
// stream has been started and from now on we not only need to delete it, but also stop it first
smartPtr.get_deleter() = PaStreamStopperAndCloser;
m_paStream = std::move(smartPtr);
m_ui->comboBoxAudioDevice->setEnabled(false);
m_ui->spinBoxBufferSize->setEnabled(false);
m_ui->pushButtonAudioDevice->setText(tr("Close"));
m_settings.setLastAudioDevice(m_ui->comboBoxAudioDevice->currentText());
m_settings.setFramesPerBuffer(m_ui->spinBoxBufferSize->value());
}
}
void MainWindow::openMidiInDevice()
{
if (m_midiIn.isPortOpen())
m_midiIn.closePort();
else
{
const auto index = m_ui->comboBoxMidiIn->currentIndex();
if (index != -1)
m_midiIn.openPort(index, "Input");
m_settings.setLastMidiInDevice(m_ui->comboBoxMidiIn->currentText());
}
m_ui->comboBoxMidiIn->setDisabled(m_midiIn.isPortOpen());
m_ui->pushButtonMidiIn->setText(m_midiIn.isPortOpen() ? tr("Close") : tr("Open"));
}
void MainWindow::openMidiOutDevice()
{
if (m_midiOut.isPortOpen())
{
qDebug() << "closing port";
unsendColors(m_ui->tabWidget->currentIndex());
m_midiOut.closePort();
}
else
{
const auto index = m_ui->comboBoxMidiOut->currentIndex();
if (index != -1)
{
m_midiOut.openPort(index, "Output");
m_settings.setLastMidiOutDevice(m_ui->comboBoxMidiOut->currentText());
sendColors(m_ui->tabWidget->currentIndex());
}
}
m_ui->comboBoxMidiOut->setDisabled(m_midiOut.isPortOpen());
m_ui->pushButtonMidiOut->setText(m_midiOut.isPortOpen() ? tr("Close") : tr("Open"));
}
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);
if (m_ui->tabWidget->currentIndex() == 0)
m_ui->drumPadWidget->messageReceived(message);
else if (m_ui->tabWidget->currentIndex() == 1)
m_ui->djWidget->messageReceived(message);
else if (m_ui->tabWidget->currentIndex() == 2)
m_ui->synthisizerWidget->messageReceived(message);
}
void MainWindow::sendMidi(const midi::MidiMessage &midiMsg)
{
if (m_midiOut.isPortOpen())
m_midiOut.sendMessage(midiMsg);
}
void MainWindow::currentChanged(int index)
{
unsendColors(m_lastIndex);
m_lastIndex = index;
sendColors(index);
}
void MainWindow::updateAudioDevices()
{
m_ui->comboBoxAudioDevice->clear();
const auto count = Pa_GetDeviceCount();
for (PaDeviceIndex i = 0; i < count; i++)
{
const auto info = Pa_GetDeviceInfo(i);
m_ui->comboBoxAudioDevice->addItem(info->name);
}
}
void MainWindow::updateMidiInDevices()
{
m_ui->comboBoxMidiIn->clear();
for (const auto &name : m_midiIn.portNames())
m_ui->comboBoxMidiIn->addItem(name);
m_ui->pushButtonMidiIn->setEnabled(m_ui->comboBoxMidiIn->count() > 0);
}
void MainWindow::updateMidiOutDevices()
{
m_ui->comboBoxMidiOut->clear();
for (const auto &name : m_midiOut.portNames())
m_ui->comboBoxMidiOut->addItem(name);
m_ui->pushButtonMidiOut->setEnabled(m_ui->comboBoxMidiOut->count() > 0);
}
void MainWindow::loadSettings()
{
m_ui->drumPadWidget->loadSettings(m_settings);
m_ui->djWidget->loadSettings(m_settings);
m_ui->synthisizerWidget->loadSettings(m_settings);
}
void MainWindow::unsendColors(int index)
{
if (index == 0)
m_ui->drumPadWidget->unsendColors();
else if (index == 1)
m_ui->djWidget->unsendColors();
else if (index == 2)
m_ui->synthisizerWidget->unsendColors();
}
void MainWindow::sendColors(int index)
{
if (index == 0)
m_ui->drumPadWidget->sendColors();
else if (index == 1)
m_ui->djWidget->sendColors();
else if (index == 2)
m_ui->synthisizerWidget->sendColors();
return;
// this was just for debugging all the available colors on novation launchpad mk1
int k{0};
for (int j = 0; j < 128; j+= 16)
{
qDebug() << k;
for (auto i = 0; i < 8; i++)
{
midi::MidiMessage midiMsg;
midiMsg.channel = 0;
midiMsg.cmd = midi::Command::NoteOn;
midiMsg.flag = true;
midiMsg.note = i + j;
midiMsg.velocity = k++;
sendMidi(midiMsg);
}
}
}
namespace {
void DummyDeleter(PaStream *stream)
{
Q_UNUSED(stream);
}
void PaStreamCloser(PaStream *stream)
{
if (PaError err = Pa_CloseStream(stream); err != paNoError)
fprintf(stderr, "Could not close stream!\n");
}
void PaStreamStopperAndCloser(PaStream *stream)
{
if (PaError err = Pa_StopStream(stream); err != paNoError)
fprintf(stderr, "Could not stop stream!\n");
PaStreamCloser(stream);
}
void paStreamFinished(void* userData)
{
printf("Stream Completed\n");
}
int paCallback(const void *inputBuffer, void *outputBuffer,
unsigned long framesPerBuffer,
const PaStreamCallbackTimeInfo* timeInfo,
PaStreamCallbackFlags statusFlags,
void *userData)
{
Q_UNUSED(inputBuffer)
Q_ASSERT(outputBuffer);
Q_ASSERT(framesPerBuffer);
Q_UNUSED(timeInfo)
Q_UNUSED(statusFlags)
Q_ASSERT(userData);
auto begin = static_cast<frame_t*>(outputBuffer);
return static_cast<MainWindow*>(userData)->writeSamples(begin, begin+framesPerBuffer);
}
} // namespace

62
widgets/mainwindow.h Executable file
View File

@@ -0,0 +1,62 @@
#pragma once
#include <memory>
#include <QMainWindow>
#include <QThread>
#include <QNetworkAccessManager>
#include <QNetworkDiskCache>
#include "portaudio.h"
#include "audioformat.h"
#include "midiinwrapper.h"
#include "midioutwrapper.h"
#include "drummachinesettings.h"
namespace Ui { class MainWindow; }
namespace presets { struct PresetsConfig; }
namespace midi { struct MidiMessage; }
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow() override;
int writeSamples(frame_t *begin, frame_t *end);
private slots:
void openAudioDevice();
void openMidiInDevice();
void openMidiOutDevice();
void messageReceived(const midi::MidiMessage &message);
void sendMidi(const midi::MidiMessage &midiMsg);
void currentChanged(int index);
private:
void updateAudioDevices();
void updateMidiInDevices();
void updateMidiOutDevices();
void loadSettings();
void unsendColors(int index);
void sendColors(int index);
const std::unique_ptr<Ui::MainWindow> m_ui;
DrumMachineSettings m_settings;
std::unique_ptr<PaStream, void(*)(PaStream*)> m_paStream;
MidiInWrapper m_midiIn;
MidiOutWrapper m_midiOut;
QNetworkAccessManager m_networkAccessManager;
QNetworkDiskCache m_cache;
QThread m_decoderThread;
int m_lastIndex;
};

261
widgets/mainwindow.ui Executable file
View File

@@ -0,0 +1,261 @@
<?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>1520</width>
<height>890</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="widget">
<layout class="QVBoxLayout" stretch="0,0,0">
<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="labelDrumMachine">
<property name="text">
<string>&lt;b&gt;DrumMachine&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="QComboBox" name="comboBoxAudioDevice"/>
</item>
<item>
<widget class="QPushButton" name="pushButtonRefreshAudioDevices">
<property name="maximumSize">
<size>
<width>32</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>↻</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="spinBoxBufferSize">
<property name="maximum">
<number>65535</number>
</property>
<property name="value">
<number>32</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButtonAudioDevice">
<property name="text">
<string>Open</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="labelMidiIn">
<property name="text">
<string>Midi in:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBoxMidiIn"/>
</item>
<item>
<widget class="QPushButton" name="pushButtonRefreshMidiIn">
<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="pushButtonMidiIn">
<property name="text">
<string>Open</string>
</property>
</widget>
</item>
<item>
<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>
<widget class="QLabel" name="labelMidiOut">
<property name="text">
<string>Midi out:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBoxMidiOut"/>
</item>
<item>
<widget class="QPushButton" name="pushButtonRefreshMidiOut">
<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="pushButtonMidiOut">
<property name="text">
<string>Open</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QTabWidget" name="tabWidget">
<widget class="DrumPadWidget" name="drumPadWidget">
<attribute name="title">
<string>DrumPad</string>
</attribute>
</widget>
<widget class="DjWidget" name="djWidget">
<attribute name="title">
<string>DJ</string>
</attribute>
</widget>
<widget class="SynthisizerWidget" name="synthisizerWidget">
<attribute name="title">
<string>Synthisizer</string>
</attribute>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1520</width>
<height>23</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<customwidgets>
<customwidget>
<class>DjWidget</class>
<extends>QWidget</extends>
<header>widgets/djwidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>DrumPadWidget</class>
<extends>QWidget</extends>
<header>widgets/drumpadwidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>SynthisizerWidget</class>
<extends>QWidget</extends>
<header>widgets/synthisizerwidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

16
widgets/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)
{
// TODO
}

22
widgets/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
widgets/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>

90
widgets/previewwidget.cpp Normal file
View File

@@ -0,0 +1,90 @@
#include "previewwidget.h"
#include <QPainter>
#include <QPen>
#include <QMouseEvent>
#include "graphrenderer.h"
PreviewWidget::PreviewWidget(QWidget *parent) :
QWidget(parent)
{
}
void PreviewWidget::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event)
if (m_graphCache.isNull() || m_graphCache.size() != size())
{
m_graphCache = QPixmap{size()};
QPainter painter;
painter.begin(&m_graphCache);
painter.setBrush(palette().base());
painter.drawRect(m_graphCache.rect());
painter.setPen(QPen{palette().color(QPalette::Text)});
painter.setBrush(palette().text());
if (m_buffer.isValid())
GraphRenderer::render(m_graphCache.rect(), m_buffer.constData<frame_t>(), m_buffer.constData<frame_t>() + m_buffer.frameCount(), painter);
painter.end();
}
QPainter painter;
painter.begin(this);
painter.drawPixmap(0, 0, m_graphCache);
if (m_buffer.frameCount())
{
std::size_t currentSample = double(m_position) / m_buffer.frameCount() * width();
QPen pen(Qt::red);
pen.setWidth(3);
painter.setPen(pen);
painter.drawLine(currentSample, 0, currentSample, height());
}
painter.end();
}
void PreviewWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
{
setMouseTracking(true);
clicked(event->x(), event->y());
}
QWidget::mousePressEvent(event);
}
void PreviewWidget::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
setMouseTracking(false);
QWidget::mouseReleaseEvent(event);
}
void PreviewWidget::mouseMoveEvent(QMouseEvent *event)
{
if (event->buttons().testFlag(Qt::LeftButton))
clicked(event->x(), event->y());
QWidget::mouseMoveEvent(event);
}
void PreviewWidget::clicked(int x, int y)
{
if (!m_buffer.isValid())
return;
const auto samplesPerPixel = m_buffer.frameCount() / width();
emit positionSelected(x * samplesPerPixel);
}

35
widgets/previewwidget.h Normal file
View File

@@ -0,0 +1,35 @@
#pragma once
#include <QWidget>
#include <QPixmap>
#include <QAudioBuffer>
class PreviewWidget : public QWidget
{
Q_OBJECT
public:
explicit PreviewWidget(QWidget *parent = nullptr);
const QAudioBuffer &buffer() const { return m_buffer; }
void setBuffer(const QAudioBuffer &buffer) { m_buffer = buffer; m_graphCache = {}; repaint(); }
double position() const { return m_position; }
void setPosition(double position) { m_position = position; repaint(); }
signals:
void positionSelected(double sample);
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
private:
void clicked(int x, int y);
QAudioBuffer m_buffer;
double m_position{};
QPixmap m_graphCache;
};

175
widgets/sampleswidget.cpp Executable file
View File

@@ -0,0 +1,175 @@
#include "sampleswidget.h"
#include "ui_sampleswidget.h"
#include <iterator>
#include <QDebug>
#include "midicontainers.h"
SamplesWidget::SamplesWidget(QWidget *parent) :
QWidget{parent},
m_ui{std::make_unique<Ui::SamplesWidget>()}
{
m_ui->setupUi(this);
connect(m_ui->checkBox, &QCheckBox::toggled, this, &SamplesWidget::updateWidgets);
connect(m_ui->pushButtonStopAll, &QAbstractButton::pressed, this, &SamplesWidget::stopAll);
quint8 padNr{};
for (SampleWidget &widget : getWidgets())
{
widget.setPadNr(padNr++);
connect(&widget, &SampleWidget::chokeTriggered, this, &SamplesWidget::chokeTriggered);
connect(&widget, &SampleWidget::sendMidi, this, &SamplesWidget::sendMidi);
}
}
SamplesWidget::~SamplesWidget() = default;
void SamplesWidget::loadSettings(DrumMachineSettings &settings)
{
for (SampleWidget &widget : getWidgets())
widget.loadSettings(settings);
}
void SamplesWidget::setPreset(const presets::Preset &preset)
{
m_preset = 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 (SampleWidget &widget : getWidgets())
{
if (widget.isLearning())
{
widget.learn(message.channel, message.note);
}
else if (widget.channel() == message.channel && widget.note() == message.note)
{
if (message.cmd == midi::Command::NoteOff || (message.cmd == midi::Command::NoteOn && message.velocity == 0))
widget.released();
else if (message.cmd == midi::Command::NoteOn)
widget.pressed(message.velocity);
}
}
}
void SamplesWidget::writeSamples(frame_t *begin, frame_t *end)
{
for (SampleWidget &widget : getWidgets())
widget.writeSamples(begin, end);
}
void SamplesWidget::injectNetworkAccessManager(QNetworkAccessManager &networkAccessManager)
{
for (SampleWidget &widget : getWidgets())
widget.injectNetworkAccessManager(networkAccessManager);
}
void SamplesWidget::injectDecodingThread(QThread &thread)
{
for (SampleWidget &widget : getWidgets())
widget.injectDecodingThread(thread);
}
void SamplesWidget::unsendColors()
{
for (SampleWidget &widget : getWidgets())
widget.unsendColor();
}
void SamplesWidget::sendColors()
{
for (SampleWidget &widget : getWidgets())
widget.sendColor();
}
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::chokeTriggered(int choke)
{
for (SampleWidget &widget : getWidgets())
{
if (&widget == sender())
continue;
if (widget.choke() && *widget.choke() && *widget.choke() == choke)
widget.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::stopAll()
{
for (SampleWidget &widget : getWidgets())
widget.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)
};
}

57
widgets/sampleswidget.h Executable file
View File

@@ -0,0 +1,57 @@
#pragma once
#include <memory>
#include <array>
#include <functional>
#include <QWidget>
#include "audioformat.h"
#include "presets.h"
namespace Ui { class SamplesWidget; }
namespace midi { class MidiMessage; }
class QNetworkAccessManager;
class SampleWidget;
class DrumMachineSettings;
class SamplesWidget : public QWidget
{
Q_OBJECT
public:
explicit SamplesWidget(QWidget *parent = nullptr);
~SamplesWidget() override;
void loadSettings(DrumMachineSettings &settings);
void setPreset(const presets::Preset &preset);
void messageReceived(const midi::MidiMessage &message);
void writeSamples(frame_t *begin, frame_t *end);
void injectNetworkAccessManager(QNetworkAccessManager &networkAccessManager);
void injectDecodingThread(QThread &thread);
void unsendColors();
void sendColors();
signals:
void sendMidi(const midi::MidiMessage &midiMsg);
public slots:
void sequencerTriggerSample(int index);
private slots:
void chokeTriggered(int choke);
void updateWidgets();
void stopAll();
private:
std::array<std::reference_wrapper<SampleWidget>, 24> getWidgets();
const std::unique_ptr<Ui::SamplesWidget> m_ui;
presets::Preset m_preset;
};

184
widgets/sampleswidget.ui Executable file
View File

@@ -0,0 +1,184 @@
<?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="0,0,0,0,0" columnstretch="0,0,0,0,0,0,0,0,0">
<item row="3" column="3">
<widget class="SampleWidget" name="sampleWidget_9" native="true"/>
</item>
<item row="4" column="3">
<widget class="SampleWidget" name="sampleWidget_12" native="true"/>
</item>
<item row="1" column="5">
<widget class="SampleWidget" name="sampleWidget_13" native="true"/>
</item>
<item row="1" column="7">
<widget class="SampleWidget" name="sampleWidget_15" native="true"/>
</item>
<item row="2" column="7">
<widget class="SampleWidget" name="sampleWidget_18" native="true"/>
</item>
<item row="4" column="6">
<widget class="SampleWidget" name="sampleWidget_23" native="true"/>
</item>
<item row="0" 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="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="3" column="2">
<widget class="SampleWidget" name="sampleWidget_8" native="true"/>
</item>
<item row="4" column="7">
<widget class="SampleWidget" name="sampleWidget_24" native="true"/>
</item>
<item row="1" 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="1" column="1">
<widget class="SampleWidget" name="sampleWidget_1" native="true"/>
</item>
<item row="3" column="5">
<widget class="SampleWidget" name="sampleWidget_19" native="true"/>
</item>
<item row="3" column="7">
<widget class="SampleWidget" name="sampleWidget_21" native="true"/>
</item>
<item row="1" 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="2" column="2">
<widget class="SampleWidget" name="sampleWidget_5" native="true"/>
</item>
<item row="1" column="2">
<widget class="SampleWidget" name="sampleWidget_2" native="true"/>
</item>
<item row="2" column="6">
<widget class="SampleWidget" name="sampleWidget_17" native="true"/>
</item>
<item row="3" column="6">
<widget class="SampleWidget" name="sampleWidget_20" native="true"/>
</item>
<item row="1" 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="2" column="5">
<widget class="SampleWidget" name="sampleWidget_16" native="true"/>
</item>
<item row="2" column="1">
<widget class="SampleWidget" name="sampleWidget_4" native="true"/>
</item>
<item row="4" column="5">
<widget class="SampleWidget" name="sampleWidget_22" native="true"/>
</item>
<item row="4" column="2">
<widget class="SampleWidget" name="sampleWidget_11" native="true"/>
</item>
<item row="1" column="3">
<widget class="SampleWidget" name="sampleWidget_3" native="true"/>
</item>
<item row="1" column="6">
<widget class="SampleWidget" name="sampleWidget_14" native="true"/>
</item>
<item row="4" column="1">
<widget class="SampleWidget" name="sampleWidget_10" native="true"/>
</item>
<item row="2" column="3">
<widget class="SampleWidget" name="sampleWidget_6" native="true"/>
</item>
<item row="3" column="1">
<widget class="SampleWidget" name="sampleWidget_7" native="true"/>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SampleWidget</class>
<extends>QWidget</extends>
<header>widgets/samplewidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

340
widgets/samplewidget.cpp Executable file
View File

@@ -0,0 +1,340 @@
#include "samplewidget.h"
#include "ui_samplewidget.h"
#include <QAbstractEventDispatcher>
#include <QSoundEffect>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QNetworkAccessManager>
#include <QMetaEnum>
#include <QDebug>
#include "audiodecoder.h"
#include "drummachinesettings.h"
#include "midicontainers.h"
namespace {
QString toString(QString value) { return value; }
QString toString(int value) { return QString::number(value); }
QString toString(bool value) { return value?"true":"false"; }
}
SampleWidget::SampleWidget(QWidget *parent) :
QFrame{parent},
m_ui{std::make_unique<Ui::SampleWidget>()}
{
m_ui->setupUi(this);
connect(m_ui->dialSpeed, &QAbstractSlider::valueChanged, &m_player, [&player=m_player](int value){ player.setSpeed(float(value)/100.f); });
connect(m_ui->dialVolume, &QAbstractSlider::valueChanged, &m_player, [&player=m_player](int value){ player.setVolume(float(value)/100.f); });
connect(&m_player, &AudioPlayer::playingChanged, this, &SampleWidget::updateStatus);
connect(m_ui->pushButton, &QAbstractButton::pressed, this, [this](){ pressed(127); });
connect(m_ui->pushButton, &QAbstractButton::released, this, &SampleWidget::released);
connect(m_ui->toolButtonLearn, &QAbstractButton::pressed, this, &SampleWidget::learnPressed);
updateStatus();
}
SampleWidget::~SampleWidget() = default;
void SampleWidget::loadSettings(DrumMachineSettings &settings)
{
m_ui->channelSpinBox->setValue(settings.padChannel(m_padNr));
m_ui->noteSpinBox->setValue(settings.padNote(m_padNr));
m_settings = &settings;
}
void SampleWidget::setFile(const QString &presetId, const presets::File &file)
{
m_presetId = presetId;
m_file = file;
m_player.setBuffer({});
startRequest();
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);
if (m_settings)
m_settings->setPadChannel(m_padNr, channel);
}
quint8 SampleWidget::note() const
{
return m_ui->noteSpinBox->value();
}
void SampleWidget::setNote(quint8 note)
{
m_ui->noteSpinBox->setValue(note);
if (m_settings)
m_settings->setPadNote(m_padNr, note);
}
int SampleWidget::speed() const
{
return m_ui->dialSpeed->value();
}
void SampleWidget::setSpeed(int speed)
{
m_ui->dialSpeed->setValue(speed);
}
int SampleWidget::volume() const
{
return m_ui->dialVolume->value();
}
void SampleWidget::setVolume(int volume)
{
m_ui->dialVolume->setValue(volume);
}
std::optional<int> SampleWidget::choke() const
{
if (!m_file)
return {};
return m_file->choke;
}
void SampleWidget::pressed(quint8 velocity)
{
Q_UNUSED(velocity)
m_player.restart();
if (m_file && m_file->choke && *m_file->choke)
emit chokeTriggered(*m_file->choke);
}
void SampleWidget::released()
{
}
void SampleWidget::forceStop()
{
m_player.setPlaying(false);
}
void SampleWidget::injectNetworkAccessManager(QNetworkAccessManager &networkAccessManager)
{
m_networkAccessManager = &networkAccessManager;
if (m_file)
startRequest();
}
void SampleWidget::injectDecodingThread(QThread &thread)
{
QMetaObject::invokeMethod(QAbstractEventDispatcher::instance(&thread), [this](){
m_decoder = std::make_unique<AudioDecoder>();
connect(this, &SampleWidget::startDecoding, m_decoder.get(), &AudioDecoder::startDecodingDevice);
connect(m_decoder.get(), &AudioDecoder::decodingFinished, this, &SampleWidget::decodingFinished);
if (m_reply && m_reply->isFinished() && m_reply->error() == QNetworkReply::NoError)
m_decoder->startDecodingDevice(m_reply);
});
}
void SampleWidget::writeSamples(frame_t *begin, frame_t *end)
{
m_player.writeSamples(begin, end);
}
void SampleWidget::learn(quint8 channel, quint8 note)
{
setChannel(channel);
setNote(note);
if (m_learning)
learnPressed();
}
void SampleWidget::unsendColor()
{
m_sendColors = false;
midi::MidiMessage midiMsg;
midiMsg.channel = m_ui->channelSpinBox->value();
midiMsg.cmd = midi::Command::NoteOn;
midiMsg.flag = true;
midiMsg.note = m_ui->noteSpinBox->value();
midiMsg.velocity = 0;
emit sendMidi(midiMsg);
}
void SampleWidget::sendColor()
{
m_sendColors = true;
midi::MidiMessage midiMsg;
midiMsg.channel = m_ui->channelSpinBox->value();
midiMsg.cmd = midi::Command::NoteOn;
midiMsg.flag = true;
midiMsg.note = m_ui->noteSpinBox->value();
if (m_file && m_file->color && m_player.buffer().isValid())
{
const auto &color = *m_file->color;
if (color == "purple")
midiMsg.velocity = m_player.playing() ? 43 : 18;
else if (color == "red")
midiMsg.velocity = m_player.playing() ? 3 : 1;
else if (color == "yellow")
midiMsg.velocity = m_player.playing() ? 58 : 33;
else if (color == "green")
midiMsg.velocity = m_player.playing() ? 56 : 16;
else if (color == "blue")
midiMsg.velocity = m_player.playing() ? 49 : 51;
else
goto noColor;
}
else
{
noColor:
midiMsg.velocity = 0;
}
emit sendMidi(midiMsg);
}
void SampleWidget::updateStatus()
{
QPalette pal;
if (m_file && m_file->color && m_player.buffer().isValid())
{
const auto bright = m_player.playing() ? 255 : 155;
const auto dark = m_player.playing() ?
#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_sendColors)
sendColor();
if (m_reply)
{
if (!m_reply->isFinished())
m_ui->statusLabel->setText(tr("Downloading..."));
else if (m_reply->error() != QNetworkReply::NoError)
m_ui->statusLabel->setText(QMetaEnum::fromType<QNetworkReply::NetworkError>().valueToKey(m_reply->error()));
else
{
if (!m_decoder)
m_ui->statusLabel->setText(tr("Waiting for decoder thread..."));
else
m_ui->statusLabel->setText(tr("Decoding"));
}
}
else
m_ui->statusLabel->setText(m_player.playing() ? tr("Playing") : tr("Ready"));
}
void SampleWidget::requestFinished()
{
if (m_reply->error() == QNetworkReply::NoError)
{
emit startDecoding(m_reply);
}
updateStatus();
}
void SampleWidget::decodingFinished(const QAudioBuffer &buffer)
{
m_reply = nullptr;
m_player.setBuffer(buffer);
setSpeed(100);
setVolume(100);
updateStatus();
}
void SampleWidget::learnPressed()
{
auto palette = m_ui->toolButtonLearn->palette();
if (m_learning)
{
palette.setColor(m_ui->toolButtonLearn->backgroundRole(), m_oldColor);
palette.setBrush(m_ui->toolButtonLearn->backgroundRole(), m_oldBrush);
}
else
{
m_oldColor = palette.color(m_ui->toolButtonLearn->backgroundRole());
m_oldBrush = palette.brush(m_ui->toolButtonLearn->backgroundRole());
palette.setColor(m_ui->toolButtonLearn->backgroundRole(), Qt::red);
palette.setBrush(m_ui->toolButtonLearn->backgroundRole(), Qt::red);
}
m_ui->toolButtonLearn->setPalette(palette);
m_learning = !m_learning;
qDebug() << m_learning;
}
void SampleWidget::startRequest()
{
if (m_networkAccessManager && m_file->filename)
{
QNetworkRequest request{QUrl{QString{"https://brunner.ninja/komposthaufen/dpm/presets/extracted/%0/%1"}.arg(m_presetId, *m_file->filename)}};
request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, true);
m_reply = std::shared_ptr<QNetworkReply>{m_networkAccessManager->get(request)};
connect(m_reply.get(), &QNetworkReply::finished, this, &SampleWidget::requestFinished);
}
updateStatus();
}

100
widgets/samplewidget.h Executable file
View File

@@ -0,0 +1,100 @@
#pragma once
#include <memory>
#include <QFrame>
#include "audioformat.h"
#include "presets.h"
#include "audioplayer.h"
#include "midicontainers.h"
namespace Ui { class SampleWidget; }
class QNetworkAccessManager;
class QNetworkReply;
class QAudioBuffer;
class AudioDecoder;
class DrumMachineSettings;
class SampleWidget : public QFrame
{
Q_OBJECT
public:
explicit SampleWidget(QWidget *parent = nullptr);
~SampleWidget() override;
quint8 padNr() const { return m_padNr; }
void setPadNr(quint8 padNr) { m_padNr = padNr; }
void loadSettings(DrumMachineSettings &settings);
void setFile(const QString &presetId, const presets::File &file);
quint8 channel() const;
void setChannel(quint8 channel);
quint8 note() const;
void setNote(quint8 note);
int speed() const;
void setSpeed(int speed);
int volume() const;
void setVolume(int volume);
std::optional<int> choke() const;
void pressed(quint8 velocity);
void released();
void forceStop();
void injectNetworkAccessManager(QNetworkAccessManager &networkAccessManager);
void injectDecodingThread(QThread &thread);
void writeSamples(frame_t *begin, frame_t *end);
bool isLearning() const { return m_learning; }
void learn(quint8 channel, quint8 note);
void unsendColor();
void sendColor();
signals:
void chokeTriggered(int choke);
void startDecoding(std::shared_ptr<QIODevice> device);
void sendMidi(const midi::MidiMessage &midiMsg);
private slots:
void updateStatus();
void requestFinished();
void decodingFinished(const QAudioBuffer &buffer);
void learnPressed();
private:
void startRequest();
const std::unique_ptr<Ui::SampleWidget> m_ui;
DrumMachineSettings *m_settings{};
std::shared_ptr<QNetworkReply> m_reply;
std::unique_ptr<AudioDecoder> m_decoder;
AudioPlayer m_player;
QString m_presetId;
std::optional<presets::File> m_file;
QNetworkAccessManager *m_networkAccessManager{};
quint8 m_padNr{};
bool m_learning{};
QColor m_oldColor;
QBrush m_oldBrush;
bool m_sendColors{};
};

156
widgets/samplewidget.ui Executable file
View File

@@ -0,0 +1,156 @@
<?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>140</width>
<height>157</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_5">
<item>
<widget class="QLabel" name="statusLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<widget class="QDial" name="dialSpeed">
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="minimum">
<number>80</number>
</property>
<property name="maximum">
<number>120</number>
</property>
<property name="value">
<number>100</number>
</property>
<property name="notchTarget">
<double>10.000000000000000</double>
</property>
<property name="notchesVisible">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QPushButton" name="pushButton">
<property name="maximumSize">
<size>
<width>32</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>▶</string>
</property>
</widget>
</item>
<item>
<widget class="QDial" name="dialVolume">
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>100</number>
</property>
<property name="notchTarget">
<double>10.000000000000000</double>
</property>
<property name="notchesVisible">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QSpinBox" name="channelSpinBox"/>
</item>
<item>
<widget class="QSpinBox" name="noteSpinBox"/>
</item>
<item>
<widget class="QToolButton" name="toolButtonLearn">
<property name="text">
<string>...</string>
</property>
</widget>
</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>
</widget>
</item>
<item>
<widget class="QLabel" name="loopedLabel">
<property name="frameShape">
<enum>QFrame::Panel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</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>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

142
widgets/scratchwidget.cpp Normal file
View File

@@ -0,0 +1,142 @@
#include "scratchwidget.h"
#include <QPainter>
#include <QRect>
#include <QMouseEvent>
#include <QDebug>
#include "graphrenderer.h"
ScratchWidget::ScratchWidget(QWidget *parent) :
QWidget{parent}
{
connect(&m_timer, &QTimer::timeout, this, &ScratchWidget::timeout);
m_timer.setSingleShot(true);
m_timer.setInterval(100);
}
void ScratchWidget::paintEvent(QPaintEvent *event)
{
QPainter painter;
painter.begin(this);
painter.setPen({});
painter.setBrush(palette().window());
painter.drawRect(rect());
if (m_buffer.isValid() && m_position < m_buffer.frameCount() - m_framesPerBeat)
{
{
QPen pen{Qt::blue};
pen.setWidth(3);
painter.setPen(pen);
}
const auto doit = [&](int offset)
{
int x = ((width()/2)-(float(m_position % m_framesPerBeat) / m_framesPerBeat * m_beatWidth)) + (m_beatWidth*offset);
const auto pixmap = getPixmap((m_position/m_framesPerBeat)+offset);
if (!pixmap.isNull())
painter.drawPixmap(x, 0, pixmap);
};
doit(-2);
doit(-1);
doit(0);
doit(1);
doit(2);
}
{
QPen pen{Qt::red};
pen.setWidth(3);
painter.setPen(pen);
}
painter.drawLine(width()/2, 0, width()/2, height());
painter.end();
}
void ScratchWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
{
m_scratching = true;
m_mouseX = event->x();
m_timestamp = QDateTime::currentDateTime();
setMouseTracking(true);
emit scratchBegin();
emit scratchSpeed(0.f);
}
}
void ScratchWidget::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
{
m_timer.stop();
emit scratchSpeed(1.f);
m_scratching = false;
setMouseTracking(false);
emit scratchEnd();
}
}
void ScratchWidget::mouseMoveEvent(QMouseEvent *event)
{
if (m_scratching)
{
const auto now = QDateTime::currentDateTime();
int dx = m_mouseX - event->x();
int dt = m_timestamp.msecsTo(now);
emit scratchSpeed(float(dx) / dt * m_framesPerBeat / m_beatWidth / 50);
m_mouseX = event->x();
m_timestamp = now;
m_timer.start();
}
}
void ScratchWidget::timeout()
{
emit scratchSpeed(0.f);
}
QPixmap ScratchWidget::getPixmap(int index)
{
{
const auto pixmap = m_graphCache[index];
if (pixmap)
return *pixmap;
}
if (!m_buffer.isValid() || index < 0 || index >= m_buffer.frameCount()/m_framesPerBeat)
{
qWarning() << index;
return {};
}
const auto *begin = m_buffer.constData<frame_t>() + (index*m_framesPerBeat);
const auto pixmap = new QPixmap{QSize{m_beatWidth, height()}};
{
QPainter painter;
painter.begin(pixmap);
painter.setPen(Qt::blue);
painter.setBrush(palette().base());
painter.drawRect(pixmap->rect());
painter.setPen(QPen{palette().color(QPalette::Text)});
painter.setBrush(palette().text());
GraphRenderer::render(pixmap->rect(), begin, begin+m_framesPerBeat, painter);
}
m_graphCache.insert(index, pixmap);
return *pixmap;
}

59
widgets/scratchwidget.h Normal file
View File

@@ -0,0 +1,59 @@
#pragma once
#include <QWidget>
#include <QAudioBuffer>
#include <QCache>
#include <QDateTime>
#include <QTimer>
#include "audioformat.h"
class ScratchWidget : public QWidget
{
Q_OBJECT
public:
explicit ScratchWidget(QWidget *parent = nullptr);
const QAudioBuffer &buffer() const { return m_buffer; }
void setBuffer(const QAudioBuffer &buffer) { m_buffer = buffer; m_graphCache.clear(); repaint(); }
std::ptrdiff_t position() const { return m_position; }
void setPosition(std::ptrdiff_t position) { m_position = position; repaint(); }
int beatWidth() const { return m_beatWidth; };
void setBeatWidth(int beatWidth) { m_beatWidth = beatWidth; m_graphCache.clear(); repaint(); };
int framesPerBeat() const { return m_framesPerBeat; }
void setFramesPerBeat(int framesPerBeat) { m_framesPerBeat = framesPerBeat; m_graphCache.clear(); repaint(); }
signals:
void scratchBegin();
void scratchSpeed(float speed);
void scratchEnd();
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
private slots:
void timeout();
private:
QPixmap getPixmap(int index);
QAudioBuffer m_buffer;
std::ptrdiff_t m_position{};
QCache<int, QPixmap> m_graphCache;
int m_beatWidth{100};
int m_framesPerBeat{frameRate/4};
bool m_scratching{};
bool m_dragging{};
int m_mouseX;
QDateTime m_timestamp;
QTimer m_timer;
};

153
widgets/sequencerwidget.cpp Executable file
View File

@@ -0,0 +1,153 @@
#include "sequencerwidget.h"
#include "ui_sequencerwidget.h"
#include <algorithm>
#include <QDebug>
#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);
for (int i = 0; i < 24; i++)
qobject_cast<QLabel*>(m_ui->gridLayout->itemAtPosition(2+(i*2), 0)->widget())->setText(*preset.files->at(i).filename);
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));
}

47
widgets/sequencerwidget.h Executable file
View File

@@ -0,0 +1,47 @@
#pragma once
#include <memory>
#include <vector>
#include <array>
#include <QWidget>
#include <QTimer>
namespace Ui { class SequencerWidget; }
namespace presets { class Preset; class Sequence; }
class QLabel;
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{};
std::array<QLabel*, 24> m_sampleLabels;
};

522
widgets/sequencerwidget.ui Executable file
View File

@@ -0,0 +1,522 @@
<?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>912</width>
<height>611</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,1">
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,0,0,0,1">
<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>
<item>
<widget class="QSlider" name="horizontalSlider">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>878</width>
<height>921</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout" columnstretch="0,0,1">
<item row="38" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="34" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="19" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="7" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="14" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="15" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="41" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="44" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="47" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="29" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="32" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="12" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="27" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="48" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="11" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="28" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="18" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="16" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="13" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="25" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="40" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="30" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="26" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="43" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="42" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="39" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="21" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="31" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="23" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="37" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="35" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="9" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="20" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="17" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="36" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="0" column="1" rowspan="49" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::VLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="33" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="22" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="46" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="45" column="0" colspan="3">
<widget class="QFrame">
<property name="frameShape">
<enum>QFrame::HLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
</widget>
</item>
<item row="24" column="0">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -0,0 +1,46 @@
#include "synthisizerwidget.h"
#include "ui_synthisizerwidget.h"
#include <cmath>
#include "midicontainers.h"
SynthisizerWidget::SynthisizerWidget(QWidget *parent) :
QWidget{parent},
m_ui{std::make_unique<Ui::SynthisizerWidget>()}
{
m_ui->setupUi(this);
connect(m_ui->horizontalSliderVolume, &QSlider::valueChanged,
&m_synthisizer, [&synthisizer=m_synthisizer](int value){ synthisizer.setVolume(float(value) / 100.f); });
}
SynthisizerWidget::~SynthisizerWidget() = default;
void SynthisizerWidget::writeSamples(frame_t *begin, frame_t *end)
{
m_synthisizer.writeSamples(begin, end);
}
void SynthisizerWidget::loadSettings(DrumMachineSettings &settings)
{
}
void SynthisizerWidget::unsendColors()
{
}
void SynthisizerWidget::sendColors()
{
}
void SynthisizerWidget::messageReceived(const midi::MidiMessage &message)
{
if (message.cmd == midi::Command::NoteOff || (message.cmd == midi::Command::NoteOn && message.velocity == 0))
{
if (m_synthisizer.frequency() == int16_t(440.*std::pow(std::pow(2., 1./12.), message.note-48)))
m_synthisizer.setFrequency(0);
}
else if (message.cmd == midi::Command::NoteOn)
m_synthisizer.setFrequency(440.*std::pow(std::pow(2., 1./12.), message.note-48));
}

View File

@@ -0,0 +1,35 @@
#pragma once
#include <QWidget>
#include <memory>
#include "synthisizer.h"
#include "audioformat.h"
namespace Ui { class SynthisizerWidget; }
class DrumMachineSettings;
class SynthisizerWidget : public QWidget
{
Q_OBJECT
public:
explicit SynthisizerWidget(QWidget *parent = nullptr);
~SynthisizerWidget() override;
void writeSamples(frame_t *begin, frame_t *end);
void loadSettings(DrumMachineSettings &settings);
void unsendColors();
void sendColors();
signals:
void sendMidi(const midi::MidiMessage &midiMsg);
public slots:
void messageReceived(const midi::MidiMessage &message);
private:
const std::unique_ptr<Ui::SynthisizerWidget> m_ui;
Synthisizer m_synthisizer;
};

View File

@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>SynthisizerWidget</class>
<widget class="QWidget" name="SynthisizerWidget">
<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>
<widget class="QLabel" name="label">
<property name="geometry">
<rect>
<x>30</x>
<y>60</y>
<width>221</width>
<height>31</height>
</rect>
</property>
<property name="text">
<string>Hier könnte ihr Synthisizer stehen.</string>
</property>
</widget>
<widget class="QSlider" name="horizontalSliderVolume">
<property name="geometry">
<rect>
<x>110</x>
<y>110</y>
<width>160</width>
<height>16</height>
</rect>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
<widget class="QLabel" name="labelVolume">
<property name="geometry">
<rect>
<x>20</x>
<y>110</y>
<width>55</width>
<height>18</height>
</rect>
</property>
<property name="text">
<string>Volume:</string>
</property>
<property name="buddy">
<cstring>horizontalSliderVolume</cstring>
</property>
</widget>
</widget>
<resources/>
<connections/>
</ui>

264
widgets/trackdeck.cpp Normal file
View File

@@ -0,0 +1,264 @@
#include "trackdeck.h"
#include "ui_trackdeck.h"
#include <QAbstractEventDispatcher>
#include <QDragEnterEvent>
#include <QDragLeaveEvent>
#include <QDropEvent>
#include <QMimeData>
#include <QTextStream>
#include <QUrl>
#include <QFileInfo>
#include <QButtonGroup>
#include <QDebug>
#include "audiodecoder.h"
TrackDeck::TrackDeck(QWidget *parent) :
QWidget{parent},
m_ui{std::make_unique<Ui::TrackDeck>()}
{
m_ui->setupUi(this);
m_ui->progressBar->hide();
m_loopGroup.addButton(m_ui->pushButtonLoopOff, -1);
m_loopGroup.addButton(m_ui->pushButtonLoop1, 1);
m_loopGroup.addButton(m_ui->pushButtonLoop2, 2);
m_loopGroup.addButton(m_ui->pushButtonLoop4, 4);
m_loopGroup.addButton(m_ui->pushButtonLoop8, 8);
connect(&m_loopGroup, &QButtonGroup::idClicked, [&player=m_player,&bpmInput=*m_ui->doubleSpinBoxBpm](int id){
const auto position = player.position();
const auto bpm = bpmInput.value();
const auto beatsPerSecond = bpm / 60.;
const auto framesPerBeat = frameRate / beatsPerSecond;
switch (id)
{
case 1: player.setLoop(std::make_pair(position, position + (framesPerBeat/4))); break;
case 2: player.setLoop(std::make_pair(position, position + (framesPerBeat/2))); break;
case 4: player.setLoop(std::make_pair(position, position + framesPerBeat)); break;
case 8: player.setLoop(std::make_pair(position, position + (framesPerBeat*2))); break;
default: player.setLoop({});
}
});
connect(m_ui->pushButtonBpm, &QAbstractButton::pressed, this, &TrackDeck::bpmTap);
connect(m_ui->pushButtonPlay, &QAbstractButton::pressed, &m_player, &AudioPlayer::togglePlaying);
connect(m_ui->pushButtonStop, &QAbstractButton::pressed, &m_player, &AudioPlayer::stop);
connect(m_ui->sliderZoom, &QAbstractSlider::valueChanged, m_ui->scratchWidget, &ScratchWidget::setBeatWidth);
connect(m_ui->sliderSpeed, &QAbstractSlider::valueChanged, this, &TrackDeck::speedChanged);
connect(m_ui->sliderVolume, &QAbstractSlider::valueChanged, &m_player, [&player=m_player](int value){ player.setVolume(float(value)/100.f); });
connect(m_ui->previewWidget, &PreviewWidget::positionSelected, &m_player, &AudioPlayer::setPosition);
connect(&m_player, &AudioPlayer::positionChanged, m_ui->previewWidget, &PreviewWidget::setPosition);
connect(m_ui->scratchWidget, &ScratchWidget::scratchBegin, this, &TrackDeck::scratchBegin);
connect(m_ui->scratchWidget, &ScratchWidget::scratchEnd, this, &TrackDeck::scratchEnd);
connect(&m_player, &AudioPlayer::positionChanged, m_ui->scratchWidget, &ScratchWidget::setPosition);
connect(&m_player, &AudioPlayer::playingChanged, this, &TrackDeck::updatePlayButtonText);
connect(m_ui->sliderSpeed, &QAbstractSlider::valueChanged, this, &TrackDeck::updatePlaybackBpm);
connect(m_ui->doubleSpinBoxBpm, &QDoubleSpinBox::valueChanged, this, &TrackDeck::updatePlaybackBpm);
connect(&m_timer, &QTimer::timeout, this, &TrackDeck::timeout);
m_timer.setSingleShot(true);
m_timer.setInterval(1000);
updatePlaybackBpm();
}
TrackDeck::~TrackDeck() = default;
void TrackDeck::loadTrack(const QString &filename)
{
m_filename = filename;
emit startDecoding(filename);
m_ui->progressBar->show();
m_ui->progressBar->setValue(0);
}
void TrackDeck::injectDecodingThread(QThread &thread)
{
QMetaObject::invokeMethod(QAbstractEventDispatcher::instance(&thread), [this](){
m_decoder = std::make_unique<AudioDecoder>();
connect(this, &TrackDeck::startDecoding, m_decoder.get(), &AudioDecoder::startDecodingFilename);
connect(m_decoder.get(), &AudioDecoder::progress, this, &TrackDeck::decodingProgress);
connect(m_decoder.get(), &AudioDecoder::decodingFinished, this, &TrackDeck::decodingFinished);
if (!m_filename.isEmpty())
m_decoder->startDecodingFilename(m_filename);
});
}
void TrackDeck::writeSamples(frame_t *begin, frame_t *end)
{
m_player.writeSamples(begin, end);
}
void TrackDeck::dragEnterEvent(QDragEnterEvent *event)
{
if (!event->mimeData()->hasFormat("text/uri-list"))
return;
auto data = event->mimeData()->data("text/uri-list");
QTextStream textStream(data, QIODevice::ReadOnly | QIODevice::Text);
if (textStream.atEnd())
return;
const QUrl url(textStream.readLine());
if (!url.isLocalFile())
return;
const QFileInfo fileInfo(url.toLocalFile());
if (!fileInfo.exists())
return;
if (!fileInfo.isFile())
return;
event->acceptProposedAction();
}
void TrackDeck::dragLeaveEvent(QDragLeaveEvent *event)
{
Q_UNUSED(event)
}
void TrackDeck::dropEvent(QDropEvent *event)
{
if (!event->mimeData()->hasFormat("text/uri-list"))
{
qWarning() << "wrong type";
return;
}
auto data = event->mimeData()->data("text/uri-list");
QTextStream textStream(data, QIODevice::ReadOnly | QIODevice::Text);
if (textStream.atEnd())
{
qWarning() << "no lines";
return;
}
const QUrl url(textStream.readLine());
if (!url.isLocalFile())
{
qWarning() << "isnt local file";
return;
}
const QFileInfo fileInfo(url.toLocalFile());
if (!fileInfo.exists())
{
qWarning() << "doesnt exist";
return;
}
if (!fileInfo.isFile())
{
qWarning() << "isnt file";
return;
}
loadTrack(url.toLocalFile());
}
void TrackDeck::decodingProgress(int progress, int total)
{
m_ui->progressBar->setMaximum(total);
m_ui->progressBar->setValue(progress);
}
void TrackDeck::decodingFinished(const QAudioBuffer &buffer)
{
m_player.setBuffer(buffer);
m_ui->labelTitle->setText(QFileInfo{m_filename}.fileName());
for (auto *btn : m_loopGroup.buttons())
btn->setEnabled(true);
m_ui->previewWidget->setBuffer(buffer);
m_ui->scratchWidget->setBuffer(buffer);
m_ui->progressBar->hide();
}
void TrackDeck::scratchBegin()
{
disconnect(m_ui->sliderSpeed, &QAbstractSlider::valueChanged, this, &TrackDeck::speedChanged);
m_ui->sliderSpeed->setEnabled(false);
connect(m_ui->scratchWidget, &ScratchWidget::scratchSpeed, &m_player, &AudioPlayer::setSpeed);
disconnect(&m_player, &AudioPlayer::playingChanged, this, &TrackDeck::updatePlayButtonText);
m_playingBeforeScratch = m_player.playing();
m_player.setPlaying(true);
m_speedBeforeScratch = m_player.speed();
m_stopOnEndBeforeScratch = m_player.stopOnEnd();
m_player.setStopOnEnd(false);
}
void TrackDeck::scratchEnd()
{
m_player.setPlaying(m_playingBeforeScratch);
m_player.setSpeed(m_speedBeforeScratch);
m_player.setStopOnEnd(m_stopOnEndBeforeScratch);
disconnect(m_ui->scratchWidget, &ScratchWidget::scratchSpeed, &m_player, &AudioPlayer::setSpeed);
m_ui->sliderSpeed->setEnabled(true);
connect(m_ui->sliderSpeed, &QAbstractSlider::valueChanged, this, &TrackDeck::speedChanged);
connect(&m_player, &AudioPlayer::playingChanged, this, &TrackDeck::updatePlayButtonText);
}
void TrackDeck::speedChanged(int value)
{
m_player.setSpeed(float(value)/100.f);
}
void TrackDeck::updatePlayButtonText(bool playing)
{
m_ui->pushButtonBpm->setEnabled(playing);
m_ui->pushButtonPlay->setText(playing ? tr("▮▮") : tr(""));
}
void TrackDeck::bpmTap()
{
const auto position = m_player.position();
if (m_bpmTap)
{
std::get<1>(*m_bpmTap) = position;
std::get<2>(*m_bpmTap)++;
const auto framesPerBeat = (std::get<1>(*m_bpmTap)-std::get<0>(*m_bpmTap))/std::get<2>(*m_bpmTap);
m_ui->scratchWidget->setFramesPerBeat(framesPerBeat);
const auto beatsPerSecond = frameRate/framesPerBeat;
const auto bpm = 60.*beatsPerSecond;
m_ui->doubleSpinBoxBpm->setValue(bpm);
}
else
{
m_bpmTap = std::make_tuple(position, position, 0);
}
m_ui->pushButtonBpm->setText(QString::number(std::get<2>(*m_bpmTap)));
m_timer.start();
}
void TrackDeck::timeout()
{
const auto framesPerBeat = (std::get<1>(*m_bpmTap)-std::get<0>(*m_bpmTap))/std::get<2>(*m_bpmTap);
m_ui->scratchWidget->setFramesPerBeat(framesPerBeat);
const auto beatsPerSecond = frameRate/framesPerBeat;
const auto bpm = 60.*beatsPerSecond;
m_ui->pushButtonBpm->setText(tr("BPM tap"));
m_ui->doubleSpinBoxBpm->setValue(bpm);
m_bpmTap = {};
}
void TrackDeck::updatePlaybackBpm()
{
m_ui->labelPlaybackBpm->setText(QString::number(m_ui->doubleSpinBoxBpm->value() * m_ui->sliderSpeed->value() / 100., 'f', 2));
}

69
widgets/trackdeck.h Normal file
View File

@@ -0,0 +1,69 @@
#pragma once
#include <memory>
#include <optional>
#include <tuple>
#include <QWidget>
#include <QButtonGroup>
#include <QTimer>
#include "audioformat.h"
#include "audioplayer.h"
namespace Ui { class TrackDeck; }
class AudioDecoder;
class QAudioBuffer;
class TrackDeck : public QWidget
{
Q_OBJECT
public:
explicit TrackDeck(QWidget *parent = nullptr);
~TrackDeck() override;
void loadTrack(const QString &filename);
void injectDecodingThread(QThread &thread);
void writeSamples(frame_t *begin, frame_t *end);
signals:
void startDecoding(const QString &filename);
protected:
void dragEnterEvent(QDragEnterEvent *event) override;
void dragLeaveEvent(QDragLeaveEvent *event) override;
void dropEvent(QDropEvent *event) override;
private slots:
void decodingProgress(int progress, int total);
void decodingFinished(const QAudioBuffer &buffer);
void scratchBegin();
void scratchEnd();
void speedChanged(int value);
void updatePlayButtonText(bool playing);
void bpmTap();
void timeout();
void updatePlaybackBpm();
private:
const std::unique_ptr<Ui::TrackDeck> m_ui;
QButtonGroup m_loopGroup;
std::unique_ptr<AudioDecoder> m_decoder;
AudioPlayer m_player;
QString m_filename;
bool m_playingBeforeScratch;
float m_speedBeforeScratch;
bool m_stopOnEndBeforeScratch;
QTimer m_timer;
std::optional<std::tuple<double, double, int>> m_bpmTap;
};

414
widgets/trackdeck.ui Normal file
View File

@@ -0,0 +1,414 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TrackDeck</class>
<widget class="QWidget" name="TrackDeck">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>567</width>
<height>283</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="acceptDrops">
<bool>true</bool>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,0">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,0">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5" stretch="1,0">
<item>
<widget class="QLabel" name="labelTitle"/>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>24</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="pushButtonBpm">
<property name="enabled">
<bool>false</bool>
</property>
<property name="maximumSize">
<size>
<width>60</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>BPM tap</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="doubleSpinBoxBpm">
<property name="suffix">
<string>BPM</string>
</property>
<property name="minimum">
<double>80.000000000000000</double>
</property>
<property name="maximum">
<double>250.000000000000000</double>
</property>
<property name="value">
<double>130.000000000000000</double>
</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="QSlider" name="sliderZoom">
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="minimum">
<number>25</number>
</property>
<property name="maximum">
<number>200</number>
</property>
<property name="value">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBothSides</enum>
</property>
<property name="tickInterval">
<number>50</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButtonPlay">
<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>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3" stretch="1,0">
<item>
<layout class="QVBoxLayout" name="verticalLayout" stretch="1,1,0">
<item>
<widget class="PreviewWidget" name="previewWidget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>50</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>50</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="ScratchWidget" name="scratchWidget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>100</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>100</height>
</size>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QLabel" name="labelLoop">
<property name="text">
<string>Loop:</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButtonLoopOff">
<property name="maximumSize">
<size>
<width>16</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>-</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButtonLoop1">
<property name="maximumSize">
<size>
<width>16</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>1</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButtonLoop2">
<property name="maximumSize">
<size>
<width>16</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>2</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButtonLoop4">
<property name="maximumSize">
<size>
<width>16</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>4</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButtonLoop8">
<property name="maximumSize">
<size>
<width>16</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>8</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<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>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3" stretch="0,1">
<item>
<widget class="QLabel" name="labelPlaybackBpm">
<property name="font">
<font>
<pointsize>8</pointsize>
</font>
</property>
<property name="text">
<string>100.00</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4" stretch="0,1,0">
<property name="spacing">
<number>0</number>
</property>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QSlider" name="sliderSpeed">
<property name="minimum">
<number>80</number>
</property>
<property name="maximum">
<number>120</number>
</property>
<property name="value">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBothSides</enum>
</property>
<property name="tickInterval">
<number>10</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QSlider" name="sliderVolume">
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>100</number>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBothSides</enum>
</property>
<property name="tickInterval">
<number>25</number>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>PreviewWidget</class>
<extends>QWidget</extends>
<header>widgets/previewwidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>ScratchWidget</class>
<extends>QWidget</extends>
<header>widgets/scratchwidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>