Added DJ audio track player

This commit is contained in:
2020-04-27 22:59:30 +02:00
parent ea6c112158
commit e9c905eb67
30 changed files with 1326 additions and 79 deletions

View File

@ -14,7 +14,10 @@ SOURCES += \
audiodecoder.cpp \
audioformat.cpp \
audioplayer.cpp \
djwidget.cpp \
filesmodel.cpp \
flatfilesystemmodel.cpp \
graphrenderer.cpp \
jsonconverters.cpp \
main.cpp \
mainwindow.cpp \
@ -23,15 +26,22 @@ SOURCES += \
presetdetailwidget.cpp \
presets.cpp \
presetsmodel.cpp \
previewwidget.cpp \
sampleswidget.cpp \
samplewidget.cpp \
sequencerwidget.cpp
scratchwidget.cpp \
sequencerwidget.cpp \
synthisizer.cpp \
trackdeck.cpp
HEADERS += \
audiodecoder.h \
audioformat.h \
audioplayer.h \
djwidget.h \
filesmodel.h \
flatfilesystemmodel.h \
graphrenderer.h \
jsonconverters.h \
mainwindow.h \
midicontainers.h \
@ -39,16 +49,22 @@ HEADERS += \
presetdetailwidget.h \
presets.h \
presetsmodel.h \
previewwidget.h \
sampleswidget.h \
samplewidget.h \
sequencerwidget.h
scratchwidget.h \
sequencerwidget.h \
synthisizer.h \
trackdeck.h
FORMS += \
djwidget.ui \
mainwindow.ui \
presetdetailwidget.ui \
sampleswidget.ui \
samplewidget.ui \
sequencerwidget.ui
sequencerwidget.ui \
trackdeck.ui
RESOURCES += \
resources.qrc

View File

@ -22,10 +22,19 @@ AudioDecoder::AudioDecoder(QObject *parent) :
m_decoder.setAudioFormat(audioFormat());
}
void AudioDecoder::startDecoding(std::shared_ptr<QIODevice> device)
void AudioDecoder::startDecodingFilename(const QString &filename)
{
qDebug() << "called" << device.get();
if (m_decoder.state() == QAudioDecoder::DecodingState)
m_decoder.stop();
m_decoder.setSourceFilename(filename);
m_device = nullptr;
m_bytearray.clear();
m_decoder.start();
}
void AudioDecoder::startDecodingDevice(std::shared_ptr<QIODevice> device)
{
if (m_decoder.state() == QAudioDecoder::DecodingState)
m_decoder.stop();
@ -42,7 +51,6 @@ void AudioDecoder::error(const QAudioDecoder::Error error)
void AudioDecoder::finished()
{
qDebug() << "called";
emit decodingFinished(QAudioBuffer{std::move(m_bytearray), audioFormat()});
}
@ -71,7 +79,6 @@ void AudioDecoder::durationChanged(const qint64 duration)
const auto &format = m_decoder.audioFormat();
const auto reserve = (format.sampleSize()/8) * format.sampleRate() * format.channelCount() * (duration + 1000) / 1000;
m_bytearray.reserve(reserve);
qDebug() << "duration:" << duration << reserve;
}
namespace {

View File

@ -22,7 +22,8 @@ signals:
void decodingFinished(const QAudioBuffer &buffer);
public slots:
void startDecoding(std::shared_ptr<QIODevice> device);
void startDecodingFilename(const QString &filename);
void startDecodingDevice(std::shared_ptr<QIODevice> device);
private slots:
void error(const QAudioDecoder::Error error);

View File

@ -19,7 +19,14 @@ void AudioPlayer::writeSamples(frame_t *begin, frame_t *end)
if (!m_playing)
return;
const auto frames = std::min<size_t>(std::distance(begin, end), m_buffer.frameCount()-m_position);
// thread safe copies
auto position = m_position;
const auto speed = m_speed;
const auto buffer = m_buffer;
const auto volume = m_volume;
const auto &data = buffer.constData<frame_t>();
const auto frames = std::min<size_t>(std::distance(begin, end), buffer.frameCount()-position);
if (!frames)
{
@ -28,25 +35,80 @@ void AudioPlayer::writeSamples(frame_t *begin, frame_t *end)
return;
}
std::transform(static_cast<const frame_t *>(begin), static_cast<const frame_t *>(begin+frames), m_buffer.constData<frame_t>() + m_position, begin,
[](const frame_t &frame, const frame_t &frame2)->frame_t{
frame_t newFrame;
std::transform(std::begin(frame), std::end(frame), std::begin(frame2), std::begin(newFrame),
[](const sample_t &left, const sample_t &right) { return left + right; });
return newFrame;
bool ended{};
std::transform(static_cast<const frame_t *>(begin), static_cast<const frame_t *>(begin+frames), begin,
[&](frame_t frame)->frame_t{
if (ended)
return frame;
const auto index = std::size_t(position);
if (index >= buffer.frameCount())
{
ended = true;
return frame;
}
const frame_t &frame2 = data[index];
position += speed;
std::transform(std::cbegin(frame), std::cend(frame), std::begin(frame2), std::begin(frame),
[&volume](const sample_t &left, const sample_t &right) { return left + (right*volume); });
return frame;
});
m_position += frames;
m_position = position;
const auto now = QDateTime::currentDateTime();
if (m_lastPositionUpdate.isNull() || m_lastPositionUpdate.msecsTo(now) > 100)
emit positionChanged(m_position);
if (ended)
emit playingChanged(m_playing = false);
}
void AudioPlayer::setPlaying(bool playing)
{
m_playing = playing;
emit playingChanged(playing);
}
void AudioPlayer::setPosition(double position)
{
m_position = position;
emit positionChanged(position);
}
void AudioPlayer::setSpeed(float speed)
{
m_speed = speed;
emit speedChanged(speed);
}
void AudioPlayer::setVolume(float volume)
{
m_volume = volume;
emit volumeChanged(volume);
}
void AudioPlayer::setBuffer(const QAudioBuffer &buffer)
{
stop();
emit bufferChanged(m_buffer = buffer);
}
void AudioPlayer::togglePlaying()
{
emit playingChanged(m_playing = !m_playing);
}
void AudioPlayer::restart()
{
setPosition(0);
setPosition(0.);
setPlaying(true);
}
void AudioPlayer::stop()
{
setPosition(0);
setPosition(0.);
setPlaying(false);
}

View File

@ -20,25 +20,36 @@ public:
void writeSamples(frame_t *begin, frame_t *end);
bool playing() const { return m_playing; }
void setPlaying(bool playing) { m_playing = playing; emit playingChanged(playing); }
void setPlaying(bool playing);
std::size_t position() const { return m_position; }
void setPosition(std::size_t position) { m_position = position; emit positionChanged(position); }
double position() const { return m_position; }
void setPosition(double position);
float speed() const { return m_speed; }
void setSpeed(float speed);
float volume() const { return m_volume; }
void setVolume(float volume);
const QAudioBuffer &buffer() const { return m_buffer; }
void setBuffer(const QAudioBuffer &buffer) { emit bufferChanged(m_buffer = buffer); }
void setBuffer(const QAudioBuffer &buffer);
void togglePlaying();
void restart();
void stop();
signals:
void playingChanged(bool playing);
void positionChanged(std::size_t position);
void positionChanged(double position);
void speedChanged(float speed);
void volumeChanged(float volume);
void bufferChanged(const QAudioBuffer &buffer);
private:
bool m_playing{false};
std::size_t m_position{};
double m_position{};
float m_speed{1.f};
float m_volume{1.f};
QAudioBuffer m_buffer;
QDateTime m_lastPositionUpdate;

83
djwidget.cpp Normal file
View File

@ -0,0 +1,83 @@
#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(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::AllEntries|QDir::NoDotAndDotDot);
m_ui->treeViewFiles->setModel(&m_filesModel);
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_filesModel.m_root_path = rootIndex;
m_ui->treeViewFiles->setRootIndex(rootIndex);
}
}
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::directorySelected()
{
const auto selected = m_ui->treeViewDirectories->currentIndex();
if (selected.isValid())
{
const auto rootIndex = m_filesModel.setRootPath(m_directoryModel.filePath(selected));
m_filesModel.m_root_path = rootIndex;
m_ui->treeViewFiles->setRootIndex(rootIndex);
}
}

33
djwidget.h Normal file
View File

@ -0,0 +1,33 @@
#pragma once
#include <QWidget>
#include <QFileSystemModel>
#include <memory>
#include "audioformat.h"
#include "flatfilesystemmodel.h"
namespace Ui { class DjWidget; }
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);
private slots:
void directorySelected();
private:
const std::unique_ptr<Ui::DjWidget> m_ui;
QFileSystemModel m_directoryModel;
FlatFileSystemModel m_filesModel;
};

145
djwidget.ui Normal file
View File

@ -0,0 +1,145 @@
<?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="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>
<property name="rootIsDecorated">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>TrackDeck</class>
<extends>QWidget</extends>
<header>trackdeck.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

1
flatfilesystemmodel.cpp Normal file
View File

@ -0,0 +1 @@
#include "flatfilesystemmodel.h"

14
flatfilesystemmodel.h Normal file
View File

@ -0,0 +1,14 @@
#pragma once
#include <QFileSystemModel>
class FlatFileSystemModel : public QFileSystemModel
{
public:
bool hasChildren(const QModelIndex &parent = QModelIndex()) const override { return (parent == m_root_path) ? QFileSystemModel::hasChildren(parent) : false; };
bool canFetchMore(const QModelIndex &parent) const override { return (parent == m_root_path) ? QFileSystemModel::canFetchMore(parent) : false; };
void fetchMore(const QModelIndex &parent) override { if ((parent == m_root_path)) QFileSystemModel::fetchMore(parent); };
public:
QModelIndex m_root_path;
};

59
graphrenderer.cpp Normal file
View File

@ -0,0 +1,59 @@
#include "graphrenderer.h"
#include <iterator>
#include <QPixmap>
#include <QPainter>
#include <QPalette>
QPixmap GraphRenderer::render(const QSize &size, const frame_t *frameBegin, const frame_t *frameEnd, const QPalette &palette)
{
QPixmap pixmap{size};
QPainter painter;
painter.begin(&pixmap);
painter.fillRect(pixmap.rect(), palette.base());
painter.setPen(Qt::white);
painter.setBrush(Qt::black);
painter.drawRect(QRect({}, size));
render(size, frameBegin, frameEnd, painter, palette);
painter.end();
return pixmap;
}
void GraphRenderer::render(const QSize &size, const frame_t *frameBegin, const frame_t *frameEnd, QPainter &painter, const QPalette &palette)
{
if (frameEnd == frameBegin)
return;
painter.setPen(QPen{palette.color(QPalette::Text)});
painter.setBrush(palette.text());
const auto framesPerPixel = std::distance(frameBegin, frameEnd) / size.width();
for (int x = 0; x < size.width(); x++)
{
const frame_t *begin = frameBegin + (x * framesPerPixel);
const frame_t *end = begin + framesPerPixel;
frame_t min{1.f, 1.f}, max{-1.f, -1.f};
for (auto iter = begin; iter != end; iter++)
{
if ((*iter)[0] < min[0])
min[0] = (*iter)[0];
if ((*iter)[1] < min[1])
min[1] = (*iter)[1];
if ((*iter)[0] > max[0])
max[0] = (*iter)[0];
if ((*iter)[1] > max[1])
max[1] = (*iter)[1];
}
painter.drawLine(x, (size.height() / 2) - (min[0] * (size.height() / 2)),
x, (size.height() / 2) + (max[0] * (size.height() / 2)));
}
}

14
graphrenderer.h Normal file
View File

@ -0,0 +1,14 @@
#pragma once
class QPixmap;
class QSize;
class QPainter;
class QPalette;
#include "audioformat.h"
namespace GraphRenderer
{
QPixmap render(const QSize &size, const frame_t *frameBegin, const frame_t *frameEnd, const QPalette &palette);
void render(const QSize &size, const frame_t *frameBegin, const frame_t *frameEnd, QPainter &painter, const QPalette &palette);
}

View File

@ -1,6 +1,7 @@
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QEventLoop>
#include <QMetaEnum>
#include <QMessageBox>
#include <QTimer>
@ -53,7 +54,7 @@ int paCallback(const void *inputBuffer, void *outputBuffer,
MainWindow::MainWindow(const presets::PresetsConfig &presetsConfig, QWidget *parent) :
QMainWindow{parent},
m_ui{std::make_unique<Ui::MainWindow>()},
m_paStream(nullptr, DummyDeleter),
m_paStream{nullptr, DummyDeleter},
m_presetsModel{*presetsConfig.presets}
{
m_ui->setupUi(this);
@ -61,6 +62,16 @@ MainWindow::MainWindow(const presets::PresetsConfig &presetsConfig, QWidget *par
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->samplesWidget->injectDecodingThread(m_decoderThread);
m_ui->djWidget->injectDecodingThread(m_decoderThread);
connect(m_ui->sequencerWidget, &SequencerWidget::triggerSample, m_ui->samplesWidget, &SamplesWidget::sequencerTriggerSample);
updateMidiDevices();
@ -75,6 +86,7 @@ MainWindow::MainWindow(const presets::PresetsConfig &presetsConfig, QWidget *par
m_midiIn.openPort(index);
}
m_ui->comboBoxMidiController->setDisabled(m_midiIn.isPortOpen());
m_ui->pushButtonMidiController->setText(m_midiIn.isPortOpen() ? tr("Close") : tr("Open"));
});
@ -99,7 +111,11 @@ MainWindow::MainWindow(const presets::PresetsConfig &presetsConfig, QWidget *par
connect(m_ui->presetsView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &MainWindow::currentRowChanged);
}
MainWindow::~MainWindow() = default;
MainWindow::~MainWindow()
{
m_decoderThread.exit();
m_decoderThread.wait();
}
void MainWindow::selectFirstPreset()
{
@ -119,6 +135,15 @@ void MainWindow::writeSamples(frame_t *begin, frame_t *end)
std::fill(begin, end, frame_t{0.,0.});
m_ui->samplesWidget->writeSamples(begin, end);
m_ui->djWidget->writeSamples(begin, end);
m_synthisizer.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;
});
}
void MainWindow::openAudioDevice()
@ -126,6 +151,8 @@ 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
@ -169,6 +196,8 @@ void MainWindow::openAudioDevice()
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"));
}
}
@ -179,7 +208,10 @@ void MainWindow::messageReceived(const midi::MidiMessage &message)
.arg(message.flag?"true":"false", QMetaEnum::fromType<midi::Command>().valueToKey(int(message.cmd)))
.arg(message.channel).arg(message.note).arg(message.velocity), 1000);
m_ui->samplesWidget->messageReceived(message);
if (m_ui->comboBoxMidiType->currentIndex() == 0)
m_ui->samplesWidget->messageReceived(message);
else if (m_ui->comboBoxMidiType->currentIndex() == 1)
m_synthisizer.messageReceived(message);
}
void MainWindow::currentRowChanged(const QModelIndex &current)

View File

@ -4,6 +4,7 @@
#include <QMainWindow>
#include <QSortFilterProxyModel>
#include <QThread>
#include "portaudio.h"
@ -11,6 +12,7 @@
#include "presetsmodel.h"
#include "filesmodel.h"
#include "midiinwrapper.h"
#include "synthisizer.h"
namespace Ui { class MainWindow; }
namespace presets { struct PresetsConfig; }
@ -43,6 +45,10 @@ private:
MidiInWrapper m_midiIn;
QThread m_decoderThread;
Synthisizer m_synthisizer;
PresetsModel m_presetsModel;
QSortFilterProxyModel m_presetsProxyModel;

View File

@ -101,6 +101,19 @@
<item>
<widget class="QComboBox" name="comboBoxMidiController"/>
</item>
<item>
<widget class="QPushButton" name="pushButtonRefreshMidiControllers">
<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="pushButtonMidiController">
<property name="text">
@ -108,6 +121,20 @@
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBoxMidiType">
<item>
<property name="text">
<string>Samples</string>
</property>
</item>
<item>
<property name="text">
<string>Synthisizer</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
@ -124,6 +151,19 @@
<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">
@ -186,28 +226,38 @@
</layout>
</item>
<item>
<widget class="QTreeView" name="presetsView">
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="sortingEnabled">
<bool>true</bool>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QTreeView" name="presetsView">
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
</widget>
<widget class="QTabWidget" name="tabWidget">
<widget class="PresetDetailWidget" name="presetDetailWidget">
<attribute name="title">
<string/>
</attribute>
</widget>
<widget class="QTreeView" name="filesView">
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<attribute name="title">
<string/>
</attribute>
</widget>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QSplitter" name="splitter_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="PresetDetailWidget" name="presetDetailWidget"/>
<widget class="QTreeView" name="filesView">
<property name="rootIsDecorated">
<bool>false</bool>
</property>
</widget>
</widget>
<widget class="DjWidget" name="djWidget" native="true"/>
</widget>
<widget class="QWidget" name="layoutWidget">
<layout class="QVBoxLayout" stretch="1,0">
@ -254,6 +304,12 @@
<header>sequencerwidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>DjWidget</class>
<extends>QWidget</extends>
<header>djwidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>

View File

@ -12,5 +12,5 @@ PresetDetailWidget::~PresetDetailWidget() = default;
void PresetDetailWidget::setPreset(const presets::Preset &preset)
{
// TODO
}

88
previewwidget.cpp Normal file
View File

@ -0,0 +1,88 @@
#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());
if (m_buffer.isValid())
GraphRenderer::render(size(), m_buffer.constData<frame_t>(), m_buffer.constData<frame_t>() + m_buffer.frameCount(), painter, palette());
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
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;
};

View File

@ -16,13 +16,6 @@ SamplesWidget::SamplesWidget(QWidget *parent) :
m_cache.setCacheDirectory("cache");
m_networkAccessManager.setCache(&m_cache);
{
QEventLoop eventLoop;
connect(&m_decoderThread, &QThread::started, &eventLoop, &QEventLoop::quit);
m_decoderThread.start(QThread::HighestPriority);
eventLoop.exec();
}
connect(m_ui->checkBox, &QCheckBox::toggled, this, &SamplesWidget::updateWidgets);
connect(m_ui->pushButtonStopAll, &QAbstractButton::pressed, this, &SamplesWidget::stopAll);
@ -30,7 +23,6 @@ SamplesWidget::SamplesWidget(QWidget *parent) :
for (const auto &ref : getWidgets())
{
ref.get().injectNetworkAccessManager(m_networkAccessManager);
ref.get().injectDecodingThread(m_decoderThread);
connect(&ref.get(), &SampleWidget::chokeTriggered, this, &SamplesWidget::chokeTriggered);
}
@ -52,11 +44,7 @@ SamplesWidget::SamplesWidget(QWidget *parent) :
m_ui->sampleWidget_24->setNote(72);
}
SamplesWidget::~SamplesWidget()
{
m_decoderThread.exit();
m_decoderThread.wait();
}
SamplesWidget::~SamplesWidget() = default;
void SamplesWidget::setPreset(const presets::Preset &preset)
{
@ -94,6 +82,12 @@ void SamplesWidget::writeSamples(frame_t *begin, frame_t *end)
ref.get().writeSamples(begin, end);
}
void SamplesWidget::injectDecodingThread(QThread &thread)
{
for (const auto &ref : getWidgets())
ref.get().injectDecodingThread(thread);
}
void SamplesWidget::sequencerTriggerSample(int index)
{
const auto widgets = getWidgets();

View File

@ -7,7 +7,6 @@
#include <QWidget>
#include <QNetworkAccessManager>
#include <QNetworkDiskCache>
#include <QThread>
#include "audioformat.h"
#include "presets.h"
@ -30,6 +29,8 @@ public:
void writeSamples(frame_t *begin, frame_t *end);
void injectDecodingThread(QThread &thread);
public slots:
void sequencerTriggerSample(int index);
@ -46,7 +47,5 @@ private:
QNetworkDiskCache m_cache;
QNetworkAccessManager m_networkAccessManager;
QThread m_decoderThread;
presets::Preset m_preset;
};

View File

@ -23,6 +23,9 @@ SampleWidget::SampleWidget(QWidget *parent) :
{
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); });
@ -38,6 +41,8 @@ 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){
@ -84,6 +89,26 @@ void SampleWidget::setNote(quint8 note)
m_ui->noteSpinBox->setValue(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)
@ -121,10 +146,10 @@ 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::startDecoding);
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->startDecoding(m_reply);
m_decoder->startDecodingDevice(m_reply);
});
}
@ -136,7 +161,7 @@ void SampleWidget::writeSamples(frame_t *begin, frame_t *end)
void SampleWidget::updateStatus()
{
QPalette pal;
if (m_file && m_file->color)
if (m_file && m_file->color && m_player.buffer().isValid())
{
const auto bright = m_player.playing() ? 255 : 155;
const auto dark = m_player.playing() ?
@ -183,7 +208,6 @@ void SampleWidget::updateStatus()
void SampleWidget::requestFinished()
{
qDebug() << "called" << m_reply->error() << m_reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute);
if (m_reply->error() == QNetworkReply::NoError)
{
emit startDecoding(m_reply);
@ -193,9 +217,10 @@ void SampleWidget::requestFinished()
void SampleWidget::decodingFinished(const QAudioBuffer &buffer)
{
qDebug() << "called";
m_reply = nullptr;
m_player.setBuffer(buffer);
setSpeed(100);
setVolume(100);
updateStatus();
}

View File

@ -30,6 +30,12 @@ public:
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);

View File

@ -24,7 +24,7 @@
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="statusLabel">
<property name="text">
@ -32,21 +32,65 @@
</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>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>24</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>PushButton</string>
</property>
</widget>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QPushButton" name="pushButton">
<property name="text">
<string>PushButton</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">

45
scratchwidget.cpp Normal file
View File

@ -0,0 +1,45 @@
#include "scratchwidget.h"
#include <QPainter>
#include "graphrenderer.h"
ScratchWidget::ScratchWidget(QWidget *parent) :
QWidget{parent}
{
}
void ScratchWidget::paintEvent(QPaintEvent *event)
{
QPainter painter;
painter.begin(this);
painter.setPen({});
painter.setBrush(palette().window());
painter.drawRect(rect());
{
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)
{
}
void ScratchWidget::mouseReleaseEvent(QMouseEvent *event)
{
}
void ScratchWidget::mouseMoveEvent(QMouseEvent *event)
{
}

31
scratchwidget.h Normal file
View File

@ -0,0 +1,31 @@
#pragma once
#include <vector>
#include <QWidget>
#include <QAudioBuffer>
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(); }
double position() const { return m_position; }
void setPosition(double position) { m_position = position; repaint(); }
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
private:
QAudioBuffer m_buffer;
double m_position{};
std::vector<QPixmap> m_graphCache;
};

33
synthisizer.cpp Normal file
View File

@ -0,0 +1,33 @@
#include "synthisizer.h"
#include <cmath>
#include "midicontainers.h"
constexpr double pi = std::acos(-1);
void Synthisizer::writeSamples(frame_t *begin, frame_t *end)
{
if (m_frequency)
std::transform(begin, end, begin, [&](frame_t frame){
std::transform(std::cbegin(frame), std::cend(frame), std::begin(frame),
[value=std::sin(m_phase)](const sample_t &sample) { return sample + value; });
m_phase += pi*2./sampleRate*m_frequency;
if (m_phase >= pi*2.)
m_phase -= pi*2.;
return frame;
});
}
void Synthisizer::messageReceived(const midi::MidiMessage &message)
{
if (message.cmd == midi::Command::NoteOn)
m_frequency = 440.*std::pow(std::pow(2., 1./12.), message.note-48);
else if (message.cmd == midi::Command::NoteOff)
{
if (m_frequency == int16_t(440.*std::pow(std::pow(2., 1./12.), message.note-48)))
m_frequency = 0;
}
}

19
synthisizer.h Normal file
View File

@ -0,0 +1,19 @@
#pragma once
#include "audioformat.h"
namespace midi { class MidiMessage; }
class Synthisizer
{
public:
void setFrequency(int16_t frequency) { m_frequency = frequency; }
void writeSamples(frame_t *begin, frame_t *end);
void messageReceived(const midi::MidiMessage &message);
private:
int16_t m_frequency{};
double m_phase{};
};

145
trackdeck.cpp Normal file
View File

@ -0,0 +1,145 @@
#include "trackdeck.h"
#include "ui_trackdeck.h"
#include <QAbstractEventDispatcher>
#include <QDragEnterEvent>
#include <QDragLeaveEvent>
#include <QDropEvent>
#include <QMimeData>
#include <QTextStream>
#include <QUrl>
#include <QFileInfo>
#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();
connect(m_ui->pushButtonPlay, &QAbstractButton::pressed, &m_player, &AudioPlayer::togglePlaying);
connect(m_ui->pushButtonStop, &QAbstractButton::pressed, &m_player, &AudioPlayer::stop);
connect(m_ui->verticalSliderVolume, &QAbstractSlider::valueChanged, &m_player, [&player=m_player](int value){ player.setVolume(float(value)/100.f); });
connect(m_ui->horizontalSliderSpeed, &QAbstractSlider::valueChanged, &m_player, [&player=m_player](int value){ player.setSpeed(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_player, &AudioPlayer::positionChanged, m_ui->scratchWidget, &ScratchWidget::setPosition);
connect(&m_player, &AudioPlayer::playingChanged, m_ui->pushButtonPlay, [&button=*m_ui->pushButtonPlay](bool playing){ button.setText(playing ? tr("▮▮") : tr("")); });
}
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->previewWidget->setBuffer(buffer);
m_ui->scratchWidget->setBuffer(buffer);
m_ui->progressBar->hide();
}

48
trackdeck.h Normal file
View File

@ -0,0 +1,48 @@
#pragma once
#include <QWidget>
#include <memory>
#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);
private:
const std::unique_ptr<Ui::TrackDeck> m_ui;
std::unique_ptr<AudioDecoder> m_decoder;
AudioPlayer m_player;
QString m_filename;
};

195
trackdeck.ui Normal file
View File

@ -0,0 +1,195 @@
<?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>400</width>
<height>300</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">
<item>
<widget class="QLabel" name="labelTitle">
<property name="font">
<font>
<pointsize>20</pointsize>
</font>
</property>
<property name="text">
<string>Track</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>24</number>
</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="horizontalSliderSpeed">
<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::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBothSides</enum>
</property>
<property name="tickInterval">
<number>10</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>
<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>
</layout>
</item>
<item>
<widget class="QSlider" name="verticalSliderVolume">
<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>previewwidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>ScratchWidget</class>
<extends>QWidget</extends>
<header>scratchwidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>