From 7e87429e08ef735109ecb15c54897650cef3c547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20L=C3=B3pez-Cabanillas?= Date: Sat, 30 Apr 2016 17:44:12 +0200 Subject: [PATCH] MIDI file playback using Sonivox library --- cmdlnsynth/main.cpp | 9 +- guisynth/guisynth.qrc | 3 + guisynth/mainwindow.cpp | 82 ++++++++++++++++- guisynth/mainwindow.h | 15 ++++ guisynth/mainwindow.ui | 82 +++++++++++++++-- guisynth/open.png | Bin 0 -> 261 bytes guisynth/play.png | Bin 0 -> 159 bytes guisynth/stop.png | Bin 0 -> 174 bytes libsvoxeas/CMakeLists.txt | 6 +- libsvoxeas/filewrapper.cpp | 78 ++++++++++++++++ libsvoxeas/filewrapper.h | 44 +++++++++ libsvoxeas/libsvoxeas.pro | 12 ++- libsvoxeas/synthcontroller.cpp | 3 +- libsvoxeas/synthrenderer.cpp | 157 ++++++++++++++++++++++++++++++--- libsvoxeas/synthrenderer.h | 20 ++++- 15 files changed, 479 insertions(+), 32 deletions(-) create mode 100644 guisynth/open.png create mode 100644 guisynth/play.png create mode 100644 guisynth/stop.png create mode 100644 libsvoxeas/filewrapper.cpp create mode 100644 libsvoxeas/filewrapper.h diff --git a/cmdlnsynth/main.cpp b/cmdlnsynth/main.cpp index 78869e4..10fd10c 100644 --- a/cmdlnsynth/main.cpp +++ b/cmdlnsynth/main.cpp @@ -41,9 +41,16 @@ int main(int argc, char *argv[]) signal(SIGINT, signalHandler); signal(SIGTERM, signalHandler); synth = new SynthController(); - QObject::connect(synth, &SynthController::finished, &app, &QCoreApplication::quit); + QObject::connect(synth->renderer(), &SynthRenderer::playbackStopped, &app, &QCoreApplication::quit); QObject::connect(&app, &QCoreApplication::aboutToQuit, synth, &QObject::deleteLater); synth->renderer()->initReverb(EAS_PARAM_REVERB_HALL); + QStringList args = app.arguments(); + for(int i = 1; i < args.length(); ++i) { + QFile argFile(args[i]); + if (argFile.exists()) { + synth->renderer()->playFile(argFile.fileName()); + } + } synth->start(); return app.exec(); } diff --git a/guisynth/guisynth.qrc b/guisynth/guisynth.qrc index 619648d..5fbb3a7 100644 --- a/guisynth/guisynth.qrc +++ b/guisynth/guisynth.qrc @@ -1,5 +1,8 @@ icon.png + open.png + play.png + stop.png diff --git a/guisynth/mainwindow.cpp b/guisynth/mainwindow.cpp index ef99141..6828a7b 100644 --- a/guisynth/mainwindow.cpp +++ b/guisynth/mainwindow.cpp @@ -18,13 +18,15 @@ */ #include +#include #include "mainwindow.h" #include "ui_mainwindow.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow), - m_synth(new SynthController(this)) + m_synth(new SynthController(this)), + m_state(InitialState) { ui->setupUi(this); ui->combo_Reverb->addItem(QStringLiteral("Large Hall"), 0); @@ -45,9 +47,16 @@ MainWindow::MainWindow(QWidget *parent) : connect(ui->combo_Chorus, SIGNAL(currentIndexChanged(int)), SLOT(chorusTypeChanged(int))); connect(ui->dial_Reverb, &QDial::valueChanged, this, &MainWindow::reverbChanged); connect(ui->dial_Chorus, &QDial::valueChanged, this, &MainWindow::chorusChanged); + connect(ui->openButton, &QToolButton::clicked, this, &MainWindow::openFile); + connect(ui->playButton, &QToolButton::clicked, this, &MainWindow::playSong); + connect(ui->stopButton, &QToolButton::clicked, this, &MainWindow::stopSong); + connect(m_synth->renderer(), &SynthRenderer::playbackStopped, this, &MainWindow::songStopped); ui->combo_Reverb->setCurrentIndex(1); ui->dial_Reverb->setValue(25800); + + m_songFile = QString(); + updateState(EmptyState); } MainWindow::~MainWindow() @@ -100,3 +109,74 @@ MainWindow::chorusChanged(int value) { m_synth->renderer()->setChorusLevel(value); } + +void +MainWindow::openFile() +{ + m_songFile = QFileDialog::getOpenFileName(this, + tr("Open MIDI file"), QDir::homePath(), + tr("MIDI Files (*.mid *.midi *.kar)")); + if (m_songFile.isEmpty()) { + ui->lblSong->setText("[empty]"); + updateState(EmptyState); + } else { + QFileInfo f(m_songFile); + ui->lblSong->setText(f.fileName()); + updateState(StoppedState); + } +} + +void +MainWindow::playSong() +{ + if (m_state == StoppedState) { + m_synth->renderer()->startPlayback(m_songFile); + updateState(PlayingState); + } +} + +void +MainWindow::stopSong() +{ + if (m_state == PlayingState) { + m_synth->renderer()->stopPlayback(); + updateState(StoppedState); + } +} + +void +MainWindow::songStopped() +{ + if (m_state != StoppedState) { + updateState(StoppedState); + } +} + +void +MainWindow::updateState(PlayerState newState) +{ + //qDebug() << Q_FUNC_INFO << newState; + if (m_state != newState) { + switch (newState) { + case EmptyState: + ui->playButton->setEnabled(false); + ui->stopButton->setEnabled(false); + ui->openButton->setEnabled(true); + break; + case PlayingState: + ui->playButton->setEnabled(false); + ui->stopButton->setEnabled(true); + ui->openButton->setEnabled(false); + break; + case StoppedState: + ui->stopButton->setEnabled(true); + ui->playButton->setEnabled(true); + ui->playButton->setChecked(false); + ui->openButton->setEnabled(true); + break; + default: + break; + } + m_state = newState; + } +} diff --git a/guisynth/mainwindow.h b/guisynth/mainwindow.h index 6bd1e54..0b344ab 100644 --- a/guisynth/mainwindow.h +++ b/guisynth/mainwindow.h @@ -23,6 +23,13 @@ #include #include "synthcontroller.h" +enum PlayerState { + InitialState, + EmptyState, + PlayingState, + StoppedState +}; + namespace Ui { class MainWindow; } @@ -34,6 +41,7 @@ class MainWindow : public QMainWindow public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); + void updateState(PlayerState newState); protected: virtual void showEvent(QShowEvent *ev); @@ -44,10 +52,17 @@ private slots: void chorusTypeChanged(int index); void reverbChanged(int value); void chorusChanged(int value); + void songStopped(); + + void openFile(); + void playSong(); + void stopSong(); private: Ui::MainWindow *ui; SynthController *m_synth; + QString m_songFile; + PlayerState m_state; }; #endif // MAINWINDOW_H diff --git a/guisynth/mainwindow.ui b/guisynth/mainwindow.ui index 359e6e3..f6aae3c 100644 --- a/guisynth/mainwindow.ui +++ b/guisynth/mainwindow.ui @@ -6,8 +6,8 @@ 0 0 - 250 - 167 + 190 + 208 @@ -19,7 +19,73 @@ - + + + + + + ... + + + + :/play.png:/play.png + + + true + + + + + + + ... + + + + :/stop.png:/stop.png + + + + + + + ... + + + + :/open.png:/open.png + + + false + + + false + + + + + + + [empty] + + + + + + + + + Qt::Vertical + + + + 20 + 8 + + + + + Reverb @@ -29,7 +95,7 @@ - + Chorus @@ -39,24 +105,24 @@ - + 32765 - + 32765 - + - + diff --git a/guisynth/open.png b/guisynth/open.png new file mode 100644 index 0000000000000000000000000000000000000000..1d87c7d6326b7cba709c34672ffd1e4878473511 GIT binary patch literal 261 zcmV+g0s8)lP)#}Y{DZ!f=xO|1|hTL9!INEq@##@thBOarwyqp-qg?@sH)d}jFGwp zRMnZe^k#;XlK&GKTC$m890SaDjAOtw?MNwkOU$B|2sJq)^Mwy0a#f{Id(o23b^ru3 zJ7IQBBH~Y`&MYw75ki27{36tYyQ^gZm_zB02!w5;F3d6Or-O*NJA7Tyg-2``5idjB zHejCnu>E<(MX!T(MR9NvcoHKEP`yks#z#*oBA|8~+hw{ddb`eFHR|0u$Bpju00000 LNkvXXu0mjfRibL! literal 0 HcmV?d00001 diff --git a/guisynth/play.png b/guisynth/play.png new file mode 100644 index 0000000000000000000000000000000000000000..e42006033438ec7229c33f4e360bc9a531c25726 GIT binary patch literal 159 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Ea{HEjtmSN`?>!lvI6;>1s;*b z3=G^tAk28_ZrvZCprfaYV@SoE2!jjsZAi6Alk~7-zjE(hW5+sLJ~(ezW^G>;OXk;vd$@?2>_KM BD@_0Z literal 0 HcmV?d00001 diff --git a/guisynth/stop.png b/guisynth/stop.png new file mode 100644 index 0000000000000000000000000000000000000000..ba1b9c609c89c6afda75d28df09da2b3ec2fa2f8 GIT binary patch literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!D3?x-;bCrM;OS+@4BLl<6e(pbstUx|nfKQ0) z|NsAi%+N!Z>wpwvNswQ#gPGOLZy-*Rx4R2N2dk_Hki%Kv5n0T@zzsB%TYzz`^4?OQ zAbW|YuPgfnP8k+ + + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#include +#include "filewrapper.h" + +static int +readAt(void *handle, void *buffer, int pos, int size) { + return ((FileWrapper*)handle)->readAt(buffer, pos, size); +} + +static int +size(void *handle) { + return ((FileWrapper*)handle)->size(); +} + +FileWrapper::FileWrapper(const QString path) +{ + //qDebug() << Q_FUNC_INFO << path; + m_file = new QFile(path); + m_file->open(QIODevice::ReadOnly); + m_Base = 0; + m_Length = m_file->size(); +} + +FileWrapper::FileWrapper(const char *path) +{ + //qDebug("FileWrapper(path=%s)", path); + m_file = new QFile(path); + m_file->open(QIODevice::ReadOnly); + m_Base = 0; + m_Length = m_file->size(); +} + +FileWrapper::~FileWrapper() { + //qDebug("~FileWrapper"); + m_file->close(); +} + +int +FileWrapper::readAt(void *buffer, int offset, int size) { + //qDebug("readAt(%p, %d, %d)", buffer, offset, size); + m_file->seek(offset); + if (offset + size > m_Length) { + size = m_Length - offset; + } + return m_file->read((char *)buffer, size); +} + +int +FileWrapper::size() { + //qDebug("size() = %d", int(mLength)); + return m_Length; +} + +EAS_FILE_LOCATOR +FileWrapper::getLocator() { + m_easFile.handle = this; + m_easFile.readAt = ::readAt; + m_easFile.size = ::size; + return &m_easFile; +} diff --git a/libsvoxeas/filewrapper.h b/libsvoxeas/filewrapper.h new file mode 100644 index 0000000..a6f7861 --- /dev/null +++ b/libsvoxeas/filewrapper.h @@ -0,0 +1,44 @@ +/* + Sonivox EAS Synthesizer for Qt applications + Copyright (C) 2016, Pedro Lopez-Cabanillas + + This library is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +#ifndef MIDIIOWRAPPER_H +#define MIDIIOWRAPPER_H + +#include +#include +#include + +class FileWrapper +{ +public: + FileWrapper(const QString path); + FileWrapper(const char *path); + ~FileWrapper(); + EAS_FILE_LOCATOR getLocator(); + int readAt(void *buffer, int offset, int size); + int size(); + +private: + QFile *m_file; + off64_t m_Base; + int64_t m_Length; + EAS_FILE m_easFile; +}; + +#endif // MIDIIOWRAPPER_H diff --git a/libsvoxeas/libsvoxeas.pro b/libsvoxeas/libsvoxeas.pro index 0aa1ecd..f097566 100644 --- a/libsvoxeas/libsvoxeas.pro +++ b/libsvoxeas/libsvoxeas.pro @@ -8,18 +8,24 @@ QT += core QT -= gui TEMPLATE = lib TARGET = svoxeas -CONFIG(release): DEFINES += QT_NO_DEBUG_OUTPUT +CONFIG(debug, debug|release) { + message(DEBUG) +} else { + DEFINES += QT_NO_DEBUG_OUTPUT +} DEPENDPATH += ../sonivox INCLUDEPATH += ../sonivox/host_src HEADERS += \ synthcontroller.h \ - synthrenderer.h + synthrenderer.h \ + filewrapper.h SOURCES += \ synthcontroller.cpp \ - synthrenderer.cpp + synthrenderer.cpp \ + filewrapper.cpp QMAKE_LFLAGS += -L../sonivox LIBS += -lsonivox diff --git a/libsvoxeas/synthcontroller.cpp b/libsvoxeas/synthcontroller.cpp index 6d31eed..fbb72c0 100644 --- a/libsvoxeas/synthcontroller.cpp +++ b/libsvoxeas/synthcontroller.cpp @@ -34,8 +34,7 @@ SynthController::~SynthController() { qDebug() << Q_FUNC_INFO; if (m_renderingThread.isRunning()) { - m_renderingThread.quit(); - m_renderingThread.wait(); + stop(); } } diff --git a/libsvoxeas/synthrenderer.cpp b/libsvoxeas/synthrenderer.cpp index 2958551..3102b63 100644 --- a/libsvoxeas/synthrenderer.cpp +++ b/libsvoxeas/synthrenderer.cpp @@ -28,12 +28,15 @@ #include #include #include "synthrenderer.h" +#include "filewrapper.h" -SynthRenderer::SynthRenderer(QObject *parent) : QObject(parent) +SynthRenderer::SynthRenderer(QObject *parent) : QObject(parent), + m_Stopped(true), + m_isPlaying(false) { initALSA(); initEAS(); - initPulse(); + initPulse(); } void @@ -98,8 +101,8 @@ SynthRenderer::initEAS() } m_easData = dataHandle; - m_easHandle = handle; - assert(m_easHandle != 0); + m_streamHandle = handle; + assert(m_streamHandle != 0); m_sampleRate = easConfig->sampleRate; m_bufferSize = easConfig->mixBufferSize; m_channels = easConfig->numChannels; @@ -127,13 +130,13 @@ SynthRenderer::initPulse() bufattr.prebuf = (int32_t)-1; /* Just initialize to same value as tlength */ bufattr.fragsize = (int32_t)-1; /* Not used */ - m_handle = pa_simple_new (server, "SonivoxEAS", PA_STREAM_PLAYBACK, - device, "Synthesizer", &samplespec, + m_pulseHandle = pa_simple_new (server, "SonivoxEAS", PA_STREAM_PLAYBACK, + device, "Synthesizer output", &samplespec, NULL, /* pa_channel_map */ &bufattr, &err); - if (!m_handle) + if (!m_pulseHandle) { qCritical() << "Failed to create PulseAudio connection"; } @@ -149,8 +152,8 @@ SynthRenderer::~SynthRenderer() delete m_codec; EAS_RESULT eas_res; - if (m_easData != 0 && m_easHandle != 0) { - eas_res = EAS_CloseMIDIStream(m_easData, m_easHandle); + if (m_easData != 0 && m_streamHandle != 0) { + eas_res = EAS_CloseMIDIStream(m_easData, m_streamHandle); if (eas_res != EAS_SUCCESS) { qWarning() << "EAS_CloseMIDIStream error: " << eas_res; } @@ -160,7 +163,7 @@ SynthRenderer::~SynthRenderer() } } - pa_simple_free(m_handle); + pa_simple_free(m_pulseHandle); qDebug() << Q_FUNC_INFO; } @@ -212,11 +215,18 @@ SynthRenderer::run() m_Client->setRealTimeInput(false); m_Client->startSequencerInput(); m_Stopped = false; + m_isPlaying = false; + if (m_files.length() > 0) { + preparePlayback(); + } while (!stopped()) { EAS_RESULT eas_res; EAS_I32 numGen = 0; size_t bytes = 0; QCoreApplication::sendPostedEvents(); + if (m_isPlaying) { + getPlaybackLocation(); + } if (m_easData != 0) { EAS_PCM *buffer = (EAS_PCM *) data; @@ -226,11 +236,23 @@ SynthRenderer::run() } bytes += (size_t) numGen * sizeof(EAS_PCM) * m_channels; // hand over to pulseaudio the rendered buffer - if (pa_simple_write (m_handle, data, bytes, &pa_err) < 0) + if (pa_simple_write (m_pulseHandle, data, bytes, &pa_err) < 0) { qWarning() << "Error writing to PulseAudio connection:" << pa_err; } } + if (m_isPlaying && playbackCompleted()) { + closePlayback(); + if (m_files.length() == 0) { + m_isPlaying = false; + emit playbackStopped(); + } else { + preparePlayback(); + } + } + } + if (m_isPlaying) { + closePlayback(); } m_Client->stopSequencerInput(); } catch (const SequencerError& err) { @@ -266,12 +288,12 @@ SynthRenderer::writeMIDIData(SequencerEvent *ev) EAS_I32 count; EAS_U8 buffer[256]; - if (m_easData != 0 && m_easHandle != 0) + if (m_easData != 0 && m_streamHandle != 0) { count = m_codec->decode((unsigned char *)&buffer, sizeof(buffer), ev->getHandle()); if (count > 0) { qDebug() << Q_FUNC_INFO << QByteArray((char *)&buffer, count).toHex(); - eas_res = EAS_WriteMIDIStream(m_easData, m_easHandle, buffer, count); + eas_res = EAS_WriteMIDIStream(m_easData, m_streamHandle, buffer, count); if (eas_res != EAS_SUCCESS) { qWarning() << "EAS_WriteMIDIStream error: " << eas_res; } @@ -332,3 +354,112 @@ SynthRenderer::setChorusLevel(int amount) qWarning() << "EAS_SetParameter error:" << eas_res; } } + +void +SynthRenderer::playFile(const QString fileName) +{ + qDebug() << Q_FUNC_INFO << fileName; + m_files.append(fileName); +} + +void +SynthRenderer::preparePlayback() +{ + EAS_HANDLE handle; + EAS_RESULT result; + EAS_I32 playTime; + + m_currentFile = new FileWrapper(m_files.first()); + m_files.removeFirst(); + + /* call EAS library to open file */ + if ((result = EAS_OpenFile(m_easData, m_currentFile->getLocator(), &handle)) != EAS_SUCCESS) + { + qWarning() << "EAS_OpenFile" << result; + return; + } + + /* prepare to play the file */ + if ((result = EAS_Prepare(m_easData, handle)) != EAS_SUCCESS) + { + qWarning() << "EAS_Prepare" << result; + return; + } + + /* get play length */ + if ((result = EAS_ParseMetaData(m_easData, handle, &playTime)) != EAS_SUCCESS) + { + qWarning() << "EAS_ParseMetaData. result=" << result; + return; + } + else + { + qDebug() << "EAS_ParseMetaData. playTime=" << playTime; + } + + qDebug() << Q_FUNC_INFO; + m_fileHandle = handle; + m_isPlaying = true; +} + +bool +SynthRenderer::playbackCompleted() +{ + EAS_RESULT result; + EAS_STATE state = EAS_STATE_EMPTY; + if ((result = EAS_State(m_easData, m_fileHandle, &state)) != EAS_SUCCESS) + { + qWarning() << "EAS_State:" << result; + } + //qDebug() << Q_FUNC_INFO << state; + /* is playback complete */ + return ((state == EAS_STATE_STOPPED) || (state == EAS_STATE_ERROR)); +} + +void +SynthRenderer::closePlayback() +{ + qDebug() << Q_FUNC_INFO; + EAS_RESULT result = EAS_SUCCESS; + /* close the input file */ + if ((result = EAS_CloseFile(m_easData, m_fileHandle)) != EAS_SUCCESS) + { + qWarning() << "EAS_CloseFile" << result; + } + m_fileHandle = 0; + delete m_currentFile; + m_currentFile = 0; + m_isPlaying = false; +} + +long +SynthRenderer::getPlaybackLocation() +{ + EAS_I32 playTime = 0; + EAS_RESULT result = EAS_SUCCESS; + /* get the current time */ + if ((result = EAS_GetLocation(m_easData, m_fileHandle, &playTime)) != EAS_SUCCESS) + { + qWarning() << "EAS_GetLocation" << result; + } + //qDebug() << Q_FUNC_INFO << playTime; + return playTime; +} + +void +SynthRenderer::startPlayback(const QString fileName) +{ + if (!stopped()) + { + playFile(fileName); + preparePlayback(); + } +} + +void +SynthRenderer::stopPlayback() +{ + if (!stopped()) { + closePlayback(); + } +} diff --git a/libsvoxeas/synthrenderer.h b/libsvoxeas/synthrenderer.h index 4f294c5..7453411 100644 --- a/libsvoxeas/synthrenderer.h +++ b/libsvoxeas/synthrenderer.h @@ -25,6 +25,7 @@ #include #include #include "eas.h" +#include "filewrapper.h" using namespace drumstick; @@ -45,12 +46,21 @@ class SynthRenderer : public QObject void setReverbWet(int amount); void setChorusLevel(int amount); + void playFile(const QString fileName); + void startPlayback(const QString fileName); + void stopPlayback(); + private: void initALSA(); void initEAS(); void initPulse(); void writeMIDIData(SequencerEvent *ev); + void preparePlayback(); + bool playbackCompleted(); + void closePlayback(); + long getPlaybackLocation(); + public slots: void subscription(MidiPort* port, Subscription* subs); void sequencerEvent( SequencerEvent* ev ); @@ -58,10 +68,14 @@ public slots: signals: void finished(); + void playbackStopped(); private: bool m_Stopped; + bool m_isPlaying; + QReadWriteLock m_mutex; + QStringList m_files; /* Drumstick ALSA*/ MidiClient* m_Client; @@ -71,10 +85,12 @@ public slots: /* SONiVOX EAS */ int m_sampleRate, m_bufferSize, m_channels; EAS_DATA_HANDLE m_easData; - EAS_HANDLE m_easHandle; + EAS_HANDLE m_streamHandle; + EAS_HANDLE m_fileHandle; + FileWrapper *m_currentFile; /* pulseaudio */ - pa_simple *m_handle; + pa_simple *m_pulseHandle; }; #endif /*SYNTHRENDERER_H_*/