Implemented basic BPM mechanism

This commit is contained in:
2020-04-29 19:48:41 +02:00
parent 2919ca2db6
commit 81cc81fe3a
18 changed files with 426 additions and 122 deletions

View File

@ -16,7 +16,6 @@ SOURCES += \
audioplayer.cpp \
djwidget.cpp \
filesmodel.cpp \
flatfilesystemmodel.cpp \
graphrenderer.cpp \
jsonconverters.cpp \
main.cpp \
@ -40,7 +39,6 @@ HEADERS += \
audioplayer.h \
djwidget.h \
filesmodel.h \
flatfilesystemmodel.h \
graphrenderer.h \
jsonconverters.h \
mainwindow.h \

View File

@ -4,7 +4,7 @@ const QAudioFormat &audioFormat()
{
static const QAudioFormat audioFormat = [](){
QAudioFormat format;
format.setSampleRate(sampleRate);
format.setSampleRate(frameRate);
format.setChannelCount(channelCount);
format.setSampleSize(sampleSize);
format.setCodec(codec);

View File

@ -2,7 +2,7 @@
#include <QAudioFormat>
static constexpr int sampleRate = 44100;
static constexpr int frameRate = 44100;
static constexpr int channelCount = 2;
static constexpr int sampleSize = 32;
static constexpr auto codec = "audio/pcm";

View File

@ -24,7 +24,9 @@ void AudioPlayer::writeSamples(frame_t *begin, frame_t *end)
const auto speed = m_speed;
const auto buffer = m_buffer;
const auto volume = m_volume;
const auto stopOnEnd = m_stopOnEnd;
const auto &data = buffer.constData<frame_t>();
const auto loop = m_loop;
const auto frames = std::min<size_t>(std::distance(begin, end), buffer.frameCount()-position);
@ -38,18 +40,27 @@ void AudioPlayer::writeSamples(frame_t *begin, frame_t *end)
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)
if (ended && stopOnEnd)
return frame;
const auto index = std::size_t(position);
if (index >= buffer.frameCount())
const auto index = std::ptrdiff_t(position);
position += speed;
if (loop)
{
if (speed < 0 && position < loop->first)
position = loop->second;
else if (speed > 0 && position > loop->second)
position = loop->first;
}
if ((speed < 0.f && index < 0) || (speed > 0.f && 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); });
@ -65,7 +76,7 @@ void AudioPlayer::writeSamples(frame_t *begin, frame_t *end)
m_lastPositionUpdate = now;
}
if (ended && m_stopOnEnd)
if (ended && stopOnEnd)
{
m_playing = false;
emit playingChanged(m_playing);

View File

@ -1,6 +1,7 @@
#pragma once
#include <atomic>
#include <optional>
#include <QObject>
#include <QAudioBuffer>
@ -37,6 +38,9 @@ public:
const QAudioBuffer &buffer() const { return m_buffer; }
void setBuffer(const QAudioBuffer &buffer);
const std::optional<std::pair<double, double>> &loop() const { return m_loop; }
void setLoop(const std::optional<std::pair<double, double>> &loop) { m_loop = loop; }
void togglePlaying();
void restart();
void stop();
@ -56,6 +60,7 @@ private:
float m_volume{1.f};
bool m_stopOnEnd{true};
QAudioBuffer m_buffer;
std::optional<std::pair<double, double>> m_loop;
QDateTime m_lastPositionUpdate;
};

View File

@ -39,9 +39,7 @@ DjWidget::DjWidget(QWidget *parent) :
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);
m_ui->treeViewFiles->setRootIndex(m_filesModel.setRootPath(m_directoryModel.filePath(index)));
}
}
@ -75,9 +73,5 @@ 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);
}
m_ui->treeViewFiles->setRootIndex(m_filesModel.setRootPath(m_directoryModel.filePath(selected)));
}

View File

@ -6,7 +6,6 @@
#include <memory>
#include "audioformat.h"
#include "flatfilesystemmodel.h"
namespace Ui { class DjWidget; }
@ -29,5 +28,5 @@ private:
const std::unique_ptr<Ui::DjWidget> m_ui;
QFileSystemModel m_directoryModel;
FlatFileSystemModel m_filesModel;
QFileSystemModel m_filesModel;
};

View File

@ -124,6 +124,12 @@
<property name="rootIsDecorated">
<bool>false</bool>
</property>
<property name="itemsExpandable">
<bool>false</bool>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
</widget>
</item>
</layout>

View File

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

View File

@ -1,14 +0,0 @@
#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;
};

View File

@ -76,6 +76,8 @@ MainWindow::MainWindow(const presets::PresetsConfig &presetsConfig, QWidget *par
updateMidiDevices();
connect(m_ui->pushButtonRefreshMidiControllers, &QAbstractButton::pressed, this, &MainWindow::updateMidiDevices);
connect(m_ui->pushButtonMidiController, &QAbstractButton::pressed, this, [this](){
if (m_midiIn.isPortOpen())
m_midiIn.closePort();
@ -92,6 +94,8 @@ MainWindow::MainWindow(const presets::PresetsConfig &presetsConfig, QWidget *par
updateAudioDevices();
connect(m_ui->pushButtonRefreshAudioDevices, &QAbstractButton::pressed, this, &MainWindow::updateAudioDevices);
m_ui->comboBoxAudioDevice->setCurrentIndex(Pa_GetDefaultOutputDevice());
connect(m_ui->pushButtonAudioDevice, &QAbstractButton::pressed, this, &MainWindow::openAudioDevice);
@ -171,7 +175,7 @@ void MainWindow::openAudioDevice()
PaStream *stream{};
if (PaError err = Pa_OpenStream(&stream, NULL, &outputParameters, sampleRate, m_ui->spinBoxBufferSize->value(), paNoFlag, &paCallback, this); err != paNoError)
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;

View File

@ -7,8 +7,6 @@
#include "graphrenderer.h"
constexpr auto theWidth = 100;
ScratchWidget::ScratchWidget(QWidget *parent) :
QWidget{parent}
{
@ -26,7 +24,7 @@ void ScratchWidget::paintEvent(QPaintEvent *event)
painter.setBrush(palette().window());
painter.drawRect(rect());
if (m_buffer.isValid() && m_position < m_buffer.frameCount() - sampleRate)
if (m_buffer.isValid() && m_position < m_buffer.frameCount() - m_framesPerBeat)
{
{
QPen pen{Qt::blue};
@ -36,8 +34,8 @@ void ScratchWidget::paintEvent(QPaintEvent *event)
const auto doit = [&](int offset)
{
int x = ((width()/2)-(float(m_position % sampleRate) / sampleRate * theWidth)) + (theWidth*offset);
const auto pixmap = getPixmap((m_position/sampleRate)+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);
};
@ -69,6 +67,7 @@ void ScratchWidget::mousePressEvent(QMouseEvent *event)
m_timestamp = QDateTime::currentDateTime();
setMouseTracking(true);
emit scratchBegin();
emit scratchSpeed(0.f);
}
}
@ -93,7 +92,7 @@ void ScratchWidget::mouseMoveEvent(QMouseEvent *event)
int dx = m_mouseX - event->x();
int dt = m_timestamp.msecsTo(now);
emit scratchSpeed(float(dx) / dt * 5.f);
emit scratchSpeed(float(dx) / dt * m_framesPerBeat / m_beatWidth / 50);
m_mouseX = event->x();
m_timestamp = now;
@ -114,14 +113,14 @@ QPixmap ScratchWidget::getPixmap(int index)
return *pixmap;
}
if (!m_buffer.isValid() || index < 0 || index >= m_buffer.frameCount()/sampleRate)
if (!m_buffer.isValid() || index < 0 || index >= m_buffer.frameCount()/m_framesPerBeat)
{
qWarning() << index;
return {};
}
const auto *begin = m_buffer.constData<frame_t>() + (index*sampleRate);
const auto pixmap = GraphRenderer::render(QSize{theWidth, height()}, begin, begin+sampleRate, palette());
const auto *begin = m_buffer.constData<frame_t>() + (index*m_framesPerBeat);
const auto pixmap = GraphRenderer::render(QSize{m_beatWidth, height()}, begin, begin+m_framesPerBeat, palette());
m_graphCache.insert(index, new QPixmap{pixmap});

View File

@ -6,6 +6,8 @@
#include <QDateTime>
#include <QTimer>
#include "audioformat.h"
class ScratchWidget : public QWidget
{
Q_OBJECT
@ -16,8 +18,14 @@ public:
const QAudioBuffer &buffer() const { return m_buffer; }
void setBuffer(const QAudioBuffer &buffer) { m_buffer = buffer; m_graphCache.clear(); repaint(); }
std::size_t position() const { return m_position; }
void setPosition(std::size_t position) { m_position = position; 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();
@ -37,9 +45,12 @@ private:
QPixmap getPixmap(int index);
QAudioBuffer m_buffer;
std::size_t m_position{};
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;

View File

@ -8,17 +8,19 @@ constexpr double pi = std::acos(-1);
void Synthisizer::writeSamples(frame_t *begin, frame_t *end)
{
if (m_frequency)
const auto frequency = m_frequency;
if (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;
m_phase += pi*2./frameRate*m_actualFrequency;
if (m_phase >= pi*2.)
m_phase -= pi*2.;
return frame;
});
m_actualFrequency = float(m_actualFrequency+m_frequency)/2.f;
}
void Synthisizer::messageReceived(const midi::MidiMessage &message)

View File

@ -15,5 +15,6 @@ public:
private:
int16_t m_frequency{};
int16_t m_actualFrequency{};
double m_phase{};
};

View File

@ -9,6 +9,7 @@
#include <QTextStream>
#include <QUrl>
#include <QFileInfo>
#include <QButtonGroup>
#include <QDebug>
#include "audiodecoder.h"
@ -20,12 +21,36 @@ TrackDeck::TrackDeck(QWidget *parent) :
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, qOverload<int>(&QButtonGroup::buttonClicked), [&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->verticalSliderVolume, &QAbstractSlider::valueChanged, &m_player, [&player=m_player](int value){ player.setVolume(float(value)/100.f); });
connect(m_ui->horizontalSliderSpeed, &QAbstractSlider::valueChanged, this, &TrackDeck::speedChanged);
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);
@ -33,6 +58,15 @@ TrackDeck::TrackDeck(QWidget *parent) :
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;
@ -141,6 +175,12 @@ void TrackDeck::decodingProgress(int progress, int total)
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(false);
m_ui->previewWidget->setBuffer(buffer);
m_ui->scratchWidget->setBuffer(buffer);
m_ui->progressBar->hide();
@ -148,8 +188,8 @@ void TrackDeck::decodingFinished(const QAudioBuffer &buffer)
void TrackDeck::scratchBegin()
{
disconnect(m_ui->horizontalSliderSpeed, &QAbstractSlider::valueChanged, this, &TrackDeck::speedChanged);
m_ui->horizontalSliderSpeed->setEnabled(false);
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);
@ -169,8 +209,8 @@ void TrackDeck::scratchEnd()
m_player.setStopOnEnd(m_stopOnEndBeforeScratch);
disconnect(m_ui->scratchWidget, &ScratchWidget::scratchSpeed, &m_player, &AudioPlayer::setSpeed);
m_ui->horizontalSliderSpeed->setEnabled(true);
connect(m_ui->horizontalSliderSpeed, &QAbstractSlider::valueChanged, this, &TrackDeck::speedChanged);
m_ui->sliderSpeed->setEnabled(true);
connect(m_ui->sliderSpeed, &QAbstractSlider::valueChanged, this, &TrackDeck::speedChanged);
connect(&m_player, &AudioPlayer::playingChanged, this, &TrackDeck::updatePlayButtonText);
}
@ -181,5 +221,46 @@ void TrackDeck::speedChanged(int value)
void TrackDeck::updatePlayButtonText(bool playing)
{
m_ui->pushButtonPlay->setText(playing ? tr("▮▮") : tr(""));
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);
const auto beatsPerSecond = frameRate/framesPerBeat;
const auto bpm = 60.*beatsPerSecond;
qDebug() << "framesPerBeat =" << framesPerBeat << "beatsPerSecond =" << beatsPerSecond << "bpm =" << bpm;
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);
const auto beatsPerSecond = frameRate/framesPerBeat;
const auto bpm = 60.*beatsPerSecond;
qDebug() << "framesPerBeat =" << framesPerBeat << "beatsPerSecond =" << beatsPerSecond << "bpm =" << bpm;
m_ui->pushButtonBpm->setText(tr("BPM tap"));
m_ui->doubleSpinBoxBpm->setValue(bpm);
for (auto *btn : m_loopGroup.buttons())
btn->setEnabled(true);
m_bpmTap = {};
}
void TrackDeck::updatePlaybackBpm()
{
m_ui->labelPlaybackBpm->setText(QString::number(m_ui->doubleSpinBoxBpm->value() * m_ui->sliderSpeed->value() / 100., 'f', 2));
}

View File

@ -1,8 +1,12 @@
#pragma once
#include <QWidget>
#include <memory>
#include <optional>
#include <tuple>
#include <QWidget>
#include <QButtonGroup>
#include <QTimer>
#include "audioformat.h"
#include "audioplayer.h"
@ -40,10 +44,15 @@ private slots:
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;
@ -53,4 +62,8 @@ private:
bool m_playingBeforeScratch;
float m_speedBeforeScratch;
bool m_stopOnEndBeforeScratch;
QTimer m_timer;
std::optional<std::tuple<double, double, int>> m_bpmTap;
};

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
<width>752</width>
<height>500</height>
</rect>
</property>
<property name="sizePolicy">
@ -24,21 +24,12 @@
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,0">
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,0,0">
<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">
<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">
@ -46,6 +37,42 @@
</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">
@ -60,12 +87,101 @@
</spacer>
</item>
<item>
<widget class="QSlider" name="horizontalSliderSpeed">
<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>
<widget class="QSlider" name="sliderZoom">
<property name="maximumSize">
<size>
<width>100</width>
<height>16777215</height>
</size>
</property>
<property name="minimum">
<number>80</number>
<number>25</number>
</property>
<property name="maximum">
<number>120</number>
<number>200</number>
</property>
<property name="value">
<number>100</number>
@ -77,7 +193,7 @@
<enum>QSlider::TicksBothSides</enum>
</property>
<property name="tickInterval">
<number>10</number>
<number>50</number>
</property>
</widget>
</item>
@ -110,53 +226,132 @@
</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>
<layout class="QHBoxLayout" name="horizontalLayout_3" stretch="1,0">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<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>
<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="verticalSliderVolume">
<widget class="QSlider" name="sliderVolume">
<property name="maximum">
<number>100</number>
</property>