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 +
+
+ + +