From e9c905eb6734f678a367d3b880c601bfe3f7eca1 Mon Sep 17 00:00:00 2001 From: 0xFEEDC0DE64 Date: Mon, 27 Apr 2020 22:59:30 +0200 Subject: [PATCH] Added DJ audio track player --- DrumMachine.pro | 22 ++++- audiodecoder.cpp | 15 +++- audiodecoder.h | 3 +- audioplayer.cpp | 82 ++++++++++++++--- audioplayer.h | 23 +++-- djwidget.cpp | 83 +++++++++++++++++ djwidget.h | 33 +++++++ djwidget.ui | 145 ++++++++++++++++++++++++++++++ flatfilesystemmodel.cpp | 1 + flatfilesystemmodel.h | 14 +++ graphrenderer.cpp | 59 ++++++++++++ graphrenderer.h | 14 +++ mainwindow.cpp | 38 +++++++- mainwindow.h | 6 ++ mainwindow.ui | 90 +++++++++++++++---- presetdetailwidget.cpp | 2 +- previewwidget.cpp | 88 ++++++++++++++++++ previewwidget.h | 35 ++++++++ sampleswidget.cpp | 20 ++--- sampleswidget.h | 5 +- samplewidget.cpp | 35 ++++++-- samplewidget.h | 6 ++ samplewidget.ui | 70 ++++++++++++--- scratchwidget.cpp | 45 ++++++++++ scratchwidget.h | 31 +++++++ synthisizer.cpp | 33 +++++++ synthisizer.h | 19 ++++ trackdeck.cpp | 145 ++++++++++++++++++++++++++++++ trackdeck.h | 48 ++++++++++ trackdeck.ui | 195 ++++++++++++++++++++++++++++++++++++++++ 30 files changed, 1326 insertions(+), 79 deletions(-) create mode 100644 djwidget.cpp create mode 100644 djwidget.h create mode 100644 djwidget.ui create mode 100644 flatfilesystemmodel.cpp create mode 100644 flatfilesystemmodel.h create mode 100644 graphrenderer.cpp create mode 100644 graphrenderer.h create mode 100644 previewwidget.cpp create mode 100644 previewwidget.h create mode 100644 scratchwidget.cpp create mode 100644 scratchwidget.h create mode 100644 synthisizer.cpp create mode 100644 synthisizer.h create mode 100644 trackdeck.cpp create mode 100644 trackdeck.h create mode 100644 trackdeck.ui diff --git a/DrumMachine.pro b/DrumMachine.pro index ae307ef..ddd8080 100755 --- a/DrumMachine.pro +++ b/DrumMachine.pro @@ -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 diff --git a/audiodecoder.cpp b/audiodecoder.cpp index f64899f..d13ad42 100644 --- a/audiodecoder.cpp +++ b/audiodecoder.cpp @@ -22,10 +22,19 @@ AudioDecoder::AudioDecoder(QObject *parent) : m_decoder.setAudioFormat(audioFormat()); } -void AudioDecoder::startDecoding(std::shared_ptr 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 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 { diff --git a/audiodecoder.h b/audiodecoder.h index 6282a56..21357c0 100644 --- a/audiodecoder.h +++ b/audiodecoder.h @@ -22,7 +22,8 @@ signals: void decodingFinished(const QAudioBuffer &buffer); public slots: - void startDecoding(std::shared_ptr device); + void startDecodingFilename(const QString &filename); + void startDecodingDevice(std::shared_ptr device); private slots: void error(const QAudioDecoder::Error error); diff --git a/audioplayer.cpp b/audioplayer.cpp index eba50ba..edeea4a 100644 --- a/audioplayer.cpp +++ b/audioplayer.cpp @@ -19,7 +19,14 @@ void AudioPlayer::writeSamples(frame_t *begin, frame_t *end) if (!m_playing) return; - const auto frames = std::min(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(); + + const auto frames = std::min(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(begin), static_cast(begin+frames), m_buffer.constData() + 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(begin), static_cast(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); } diff --git a/audioplayer.h b/audioplayer.h index 8fed489..f9f82a4 100644 --- a/audioplayer.h +++ b/audioplayer.h @@ -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; diff --git a/djwidget.cpp b/djwidget.cpp new file mode 100644 index 0000000..46d94ab --- /dev/null +++ b/djwidget.cpp @@ -0,0 +1,83 @@ +#include "djwidget.h" +#include "ui_djwidget.h" + +#include +#include + +DjWidget::DjWidget(QWidget *parent) : + QWidget{parent}, + m_ui{std::make_unique()} +{ + 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(begin), static_cast(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); + } +} diff --git a/djwidget.h b/djwidget.h new file mode 100644 index 0000000..6a519f1 --- /dev/null +++ b/djwidget.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +#include + +#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 m_ui; + + QFileSystemModel m_directoryModel; + FlatFileSystemModel m_filesModel; +}; diff --git a/djwidget.ui b/djwidget.ui new file mode 100644 index 0000000..0d48128 --- /dev/null +++ b/djwidget.ui @@ -0,0 +1,145 @@ + + + DjWidget + + + + 0 + 0 + 765 + 519 + + + + Form + + + + + + + + + + + QFrame::VLine + + + QFrame::Sunken + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + -100 + + + 100 + + + Qt::Horizontal + + + true + + + QSlider::TicksBothSides + + + 50 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + + true + + + + + + + + + + Load in Deck A + + + + + + + Load in Deck B + + + + + + + + + true + + + false + + + + + + + + + + + + TrackDeck + QWidget +
trackdeck.h
+ 1 +
+
+ + +
diff --git a/flatfilesystemmodel.cpp b/flatfilesystemmodel.cpp new file mode 100644 index 0000000..835f076 --- /dev/null +++ b/flatfilesystemmodel.cpp @@ -0,0 +1 @@ +#include "flatfilesystemmodel.h" diff --git a/flatfilesystemmodel.h b/flatfilesystemmodel.h new file mode 100644 index 0000000..99cc3b7 --- /dev/null +++ b/flatfilesystemmodel.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +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; +}; diff --git a/graphrenderer.cpp b/graphrenderer.cpp new file mode 100644 index 0000000..a1f1c25 --- /dev/null +++ b/graphrenderer.cpp @@ -0,0 +1,59 @@ +#include "graphrenderer.h" + +#include + +#include +#include +#include + +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))); + } +} diff --git a/graphrenderer.h b/graphrenderer.h new file mode 100644 index 0000000..71eef46 --- /dev/null +++ b/graphrenderer.h @@ -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); +} diff --git a/mainwindow.cpp b/mainwindow.cpp index 2d5d4ac..8bd00e9 100755 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -1,6 +1,7 @@ #include "mainwindow.h" #include "ui_mainwindow.h" +#include #include #include #include @@ -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()}, - 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().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 ¤t) diff --git a/mainwindow.h b/mainwindow.h index 6103851..883d6d5 100755 --- a/mainwindow.h +++ b/mainwindow.h @@ -4,6 +4,7 @@ #include #include +#include #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; diff --git a/mainwindow.ui b/mainwindow.ui index 506e6b1..dfdc9c4 100755 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -101,6 +101,19 @@ + + + + + 32 + 16777215 + + + + + + + @@ -108,6 +121,20 @@ + + + + + Samples + + + + + Synthisizer + + + + @@ -124,6 +151,19 @@ + + + + + 32 + 16777215 + + + + + + + @@ -186,28 +226,38 @@ - - - false - - - true + + + Qt::Horizontal + + + false + + + true + + + + + + + + + + + false + + + + + + - - - Qt::Horizontal - - - - - false - - - + @@ -254,6 +304,12 @@
sequencerwidget.h
1 + + DjWidget + QWidget +
djwidget.h
+ 1 +
diff --git a/presetdetailwidget.cpp b/presetdetailwidget.cpp index 1813739..bca3f02 100755 --- a/presetdetailwidget.cpp +++ b/presetdetailwidget.cpp @@ -12,5 +12,5 @@ PresetDetailWidget::~PresetDetailWidget() = default; void PresetDetailWidget::setPreset(const presets::Preset &preset) { - + // TODO } diff --git a/previewwidget.cpp b/previewwidget.cpp new file mode 100644 index 0000000..d353424 --- /dev/null +++ b/previewwidget.cpp @@ -0,0 +1,88 @@ +#include "previewwidget.h" + +#include +#include +#include + +#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(), m_buffer.constData() + 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); +} diff --git a/previewwidget.h b/previewwidget.h new file mode 100644 index 0000000..52f371e --- /dev/null +++ b/previewwidget.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +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; +}; diff --git a/sampleswidget.cpp b/sampleswidget.cpp index 5e0f560..5e32a81 100755 --- a/sampleswidget.cpp +++ b/sampleswidget.cpp @@ -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(); diff --git a/sampleswidget.h b/sampleswidget.h index 1bdb79b..d5ffda8 100755 --- a/sampleswidget.h +++ b/sampleswidget.h @@ -7,7 +7,6 @@ #include #include #include -#include #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; }; diff --git a/samplewidget.cpp b/samplewidget.cpp index 3104725..86f6f07 100755 --- a/samplewidget.cpp +++ b/samplewidget.cpp @@ -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 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(); - 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(); } diff --git a/samplewidget.h b/samplewidget.h index 8270f3a..d458a98 100755 --- a/samplewidget.h +++ b/samplewidget.h @@ -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 choke() const; void pressed(quint8 velocity); diff --git a/samplewidget.ui b/samplewidget.ui index 9016064..6472c87 100755 --- a/samplewidget.ui +++ b/samplewidget.ui @@ -24,7 +24,7 @@
- + @@ -32,21 +32,65 @@ + + + + + 32 + 32 + + + + 80 + + + 120 + + + 100 + + + 10.000000000000000 + + + true + + + - - - 24 - - - - - - - PushButton - - + + + + + PushButton + + + + + + + + 32 + 32 + + + + 100 + + + 100 + + + 10.000000000000000 + + + true + + + + diff --git a/scratchwidget.cpp b/scratchwidget.cpp new file mode 100644 index 0000000..2e30543 --- /dev/null +++ b/scratchwidget.cpp @@ -0,0 +1,45 @@ +#include "scratchwidget.h" + +#include + +#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) +{ + +} diff --git a/scratchwidget.h b/scratchwidget.h new file mode 100644 index 0000000..d95ea7f --- /dev/null +++ b/scratchwidget.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include +#include + +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 m_graphCache; +}; diff --git a/synthisizer.cpp b/synthisizer.cpp new file mode 100644 index 0000000..d6cdcc9 --- /dev/null +++ b/synthisizer.cpp @@ -0,0 +1,33 @@ +#include "synthisizer.h" + +#include + +#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; + } +} diff --git a/synthisizer.h b/synthisizer.h new file mode 100644 index 0000000..9d867e0 --- /dev/null +++ b/synthisizer.h @@ -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{}; +}; diff --git a/trackdeck.cpp b/trackdeck.cpp new file mode 100644 index 0000000..3e4e0ea --- /dev/null +++ b/trackdeck.cpp @@ -0,0 +1,145 @@ +#include "trackdeck.h" +#include "ui_trackdeck.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "audiodecoder.h" + +TrackDeck::TrackDeck(QWidget *parent) : + QWidget{parent}, + m_ui{std::make_unique()} +{ + 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(); + 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(); +} diff --git a/trackdeck.h b/trackdeck.h new file mode 100644 index 0000000..d7e8db7 --- /dev/null +++ b/trackdeck.h @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include + +#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 m_ui; + + std::unique_ptr m_decoder; + + AudioPlayer m_player; + + QString m_filename; +}; diff --git a/trackdeck.ui b/trackdeck.ui new file mode 100644 index 0000000..b98a0aa --- /dev/null +++ b/trackdeck.ui @@ -0,0 +1,195 @@ + + + TrackDeck + + + + 0 + 0 + 400 + 300 + + + + + 0 + 0 + + + + true + + + Form + + + + + + + + + 20 + + + + Track + + + + + + + + + 24 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + 80 + + + 120 + + + 100 + + + Qt::Horizontal + + + QSlider::TicksBothSides + + + 10 + + + + + + + + 32 + 16777215 + + + + + + + + + + + + 32 + 16777215 + + + + + + + + + + + + + + 0 + 0 + + + + + 0 + 50 + + + + + 16777215 + 50 + + + + + + + + + 0 + 0 + + + + + 0 + 100 + + + + + 16777215 + 100 + + + + + + + + + + 100 + + + 100 + + + Qt::Vertical + + + QSlider::TicksBothSides + + + 25 + + + + + + + + PreviewWidget + QWidget +
previewwidget.h
+ 1 +
+ + ScratchWidget + QWidget +
scratchwidget.h
+ 1 +
+
+ + +