From d1333a5ab369fcdab126ae07cdef45e5875c64e4 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Thu, 26 Mar 2020 16:24:37 +0100 Subject: [PATCH] Add tool to capture Dimension EG curves --- CMakeLists.txt | 5 + devtools/CMakeLists.txt | 15 ++ devtools/CaptureEG.cpp | 330 +++++++++++++++++++++++++++++++++++++++ devtools/CaptureEG.h | 80 ++++++++++ devtools/CaptureEG.ui | 334 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 764 insertions(+) create mode 100644 devtools/CMakeLists.txt create mode 100644 devtools/CaptureEG.cpp create mode 100644 devtools/CaptureEG.h create mode 100644 devtools/CaptureEG.ui diff --git a/CMakeLists.txt b/CMakeLists.txt index 55a80114..daf8e57b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,7 @@ option (SFIZZ_LV2 "Enable LV2 plug-in build [default: ON]" ON) option (SFIZZ_VST "Enable VST plug-in build [default: OFF]" OFF) option (SFIZZ_BENCHMARKS "Enable benchmarks build [default: OFF]" OFF) option (SFIZZ_TESTS "Enable tests build [default: OFF]" OFF) +option (SFIZZ_DEVTOOLS "Enable developer tools build [default: OFF]" OFF) option (SFIZZ_SHARED "Enable shared library build [default: ON]" ON) option (SFIZZ_USE_VCPKG "Assume that sfizz is build using vcpkg [default: OFF]" OFF) option (SFIZZ_STATIC_LIBSNDFILE "Link libsndfile statically [default: OFF]" OFF) @@ -64,6 +65,10 @@ if (SFIZZ_TESTS) add_subdirectory (tests) endif() +if (SFIZZ_DEVTOOLS) + add_subdirectory (devtools) +endif() + # Put it at the end so that the vst/lv2 directories are registered if (NOT MSVC) include(SfizzUninstall) diff --git a/devtools/CMakeLists.txt b/devtools/CMakeLists.txt new file mode 100644 index 00000000..e4250d9c --- /dev/null +++ b/devtools/CMakeLists.txt @@ -0,0 +1,15 @@ +############################### +# Developer tools + +find_package(PkgConfig) +if(PKGCONFIG_FOUND) +pkg_check_modules(JACK "jack") +endif() +find_package(Qt5 COMPONENTS Widgets) + +if(JACK_FOUND AND TARGET Qt5::Widgets) + add_executable(sfizz_capture_eg CaptureEG.cpp) + target_include_directories(sfizz_capture_eg PRIVATE . ${JACK_INCLUDE_DIRS}) + target_link_libraries(sfizz_capture_eg PRIVATE sfizz-sndfile Qt5::Widgets ${JACK_LIBRARIES}) + set_target_properties(sfizz_capture_eg PROPERTIES AUTOUIC ON) +endif() diff --git a/devtools/CaptureEG.cpp b/devtools/CaptureEG.cpp new file mode 100644 index 00000000..171248ca --- /dev/null +++ b/devtools/CaptureEG.cpp @@ -0,0 +1,330 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#include "CaptureEG.h" +#include "ui_CaptureEG.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Application::Application(int& argc, char *argv[]) + : QApplication(argc, argv), _ui(new Ui::MainWindow) +{ + setApplicationName("SfizzCaptureEG"); +} + +Application::~Application() +{ +} + +bool Application::init() +{ + _cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation); + if (_cacheDir.isEmpty()) { + QMessageBox::critical(nullptr, tr("Error"), tr("Cannot determine the cache directory.")); + return false; + } + QDir(_cacheDir).mkpath("."); + + /// + jack_client_t* client = jack_client_open( + applicationName().toUtf8().data(), JackNoStartServer, nullptr); + + if (!client) { + QMessageBox::critical(nullptr, tr("Error"), tr("Cannot register a new JACK client.")); + return false; + } + _client.reset(client); + + std::string clientName = jack_get_client_name(client); + + if (!(_portAudioIn = jack_port_register(client, "audio_in", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0)) || + !(_portMidiOut = jack_port_register(client, "midi_out", JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0))) + { + QMessageBox::critical(nullptr, tr("Error"), tr("Cannot register the JACK client ports.")); + return false; + } + + jack_set_process_callback(client, &processAudio, this); + + if (jack_activate(client) != 0) { + QMessageBox::critical(nullptr, tr("Error"), tr("Cannot activate the JACK client.")); + return false; + } + + // Try to connect Dimension if it exists + { + const char** synthAudioPorts = jack_get_ports(client, "^Dimension Pro:", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput); + const char** synthMidiPorts = jack_get_ports(client, "^Dimension Pro:", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput); + if (synthAudioPorts && *synthAudioPorts) + jack_connect(client, *synthAudioPorts, jack_port_name(_portAudioIn)); + if (synthMidiPorts && *synthMidiPorts) + jack_connect(client, jack_port_name(_portMidiOut), *synthMidiPorts); + jack_free(synthAudioPorts); + jack_free(synthMidiPorts); + } + + // Allocate capture buffer (capacity 30 seconds) + double sampleRate = jack_get_sample_rate(client); + _captureCapacity = static_cast(std::ceil(30.0 * sampleRate)); + _captureBuffer.reset(new float[_captureCapacity]); + // Always capture a minimum of 0.5 seconds (ensure not stopping too early) + _captureMinFrames = static_cast(std::ceil(0.5 * sampleRate)); + + /// + QMainWindow* window = new QMainWindow; + _window = window; + _ui->setupUi(window); + window->setWindowTitle(applicationDisplayName()); + window->adjustSize(); + window->setFixedSize(window->size()); + window->show(); + + _ui->dragFileLabel->setDragFilePath(getSfzPath()); + _ui->dragFileLabel->setPixmap( + QIcon::fromTheme("text-x-generic").pixmap(_ui->dragFileLabel->size())); + + _ui->releaseTimeVal->setRange(0.0, 10.0); + _ui->releaseTimeVal->setValue(5.0); + + _ui->internalGainVal->setRange(0.1, 2.0); + _ui->internalGainVal->setValue(0.342); // default to match Dimension + + _ui->saveButton->setEnabled(false); + + connect( + _ui->envelopeEdit, &QPlainTextEdit::textChanged, + this, [this]() { onSfzTextChanged(); }); + + connect( + _ui->captureButton, &QPushButton::clicked, + this, [this]() { engageCapture(); }); + + connect( + _ui->saveButton, &QPushButton::clicked, + this, [this]() { saveCapture(); }); + + _sfzUpdateTimer = new QTimer; + _sfzUpdateTimer->setInterval(500); + _sfzUpdateTimer->setSingleShot(true); + connect(_sfzUpdateTimer, &QTimer::timeout, this, [this]() { performSfzUpdate(); }); + + _idleTimer = new QTimer; + _idleTimer->setInterval(50); + _idleTimer->setSingleShot(false); + connect(_idleTimer, &QTimer::timeout, this, [this]() { performIdleChecks(); }); + + _idleTimer->start(); + + onSfzTextChanged(); + + return true; +} + +int Application::processAudio(unsigned numFrames, void* arg) +{ + auto* self = static_cast(arg); + + const float* audioIn = static_cast(jack_port_get_buffer(self->_portAudioIn, numFrames)); + void* midiOut = jack_port_get_buffer(self->_portMidiOut, numFrames); + + jack_midi_clear_buffer(midiOut); + + if (self->_captureStatus != CaptureEngaged) + return 0; + + bool over = false; + + size_t captureIndex = self->_captureFill; + const size_t captureCapacity = self->_captureCapacity; + const size_t captureMinFrames = self->_captureMinFrames; + float* captureBuffer = self->_captureBuffer.get(); + + constexpr float silentThreshold = 1e-4; // -80 dB + + long tt = self->_framesLeftToTrigger; + long tr = self->_framesLeftToRelease; + + for (size_t i = 0; i < numFrames && !over; ++i) { + if (tt == 0) { + const unsigned char noteOn[3] = {0x90, 69, 127}; + jack_midi_event_write(midiOut, i, noteOn, sizeof(noteOn)); + } + if (tr == 0) { + const unsigned char noteOff[3] = {0x90, 69, 0}; + jack_midi_event_write(midiOut, i, noteOff, sizeof(noteOff)); + } + --tt; + --tr; + if (captureIndex == captureCapacity) + over = true; + else { + captureBuffer[captureIndex++] = audioIn[i]; + if (tr < 0 && captureIndex >= captureMinFrames && audioIn[i] < silentThreshold) + over = true; + } + } + + self->_captureFill = captureIndex; + self->_framesLeftToTrigger = tt; + self->_framesLeftToRelease = tr; + + if (over) + self->_captureStatus = CaptureOver; + + return 0; +} + +QString Application::getSfzPath() const +{ + return _cacheDir + "/CaptureEG.sfz"; +} + +QString Application::getSamplePath() const +{ + return _cacheDir + "/CaptureEG.wav"; +} + +void Application::onSfzTextChanged() +{ + _ui->dragFileLabel->setEnabled(false); + _sfzUpdateTimer->start(); +} + +void Application::engageCapture() +{ + if (_captureStatus != CaptureIdle) + return; + + _ui->saveButton->setEnabled(false); + + const double sampleRate = jack_get_sample_rate(_client.get()); + + _framesLeftToTrigger = 0; + _framesLeftToRelease = static_cast(std::ceil(sampleRate * _ui->releaseTimeVal->value())); + _captureFill = 0; + + _captureStatus = CaptureEngaged; +} + +void Application::saveCapture() +{ + if (_captureStatus != CaptureIdle) + return; + + QString filePath = QFileDialog::getSaveFileName( + _window, tr("Save data"), QString(), tr("Data files (*.dat)")); + if (filePath.isEmpty()) + return; + + QFile file(filePath); + file.open(QFile::WriteOnly|QFile::Truncate); + QTextStream stream(&file); + + const float *data = _captureBuffer.get(); + size_t size = _captureFill; + double sampleRate = jack_get_sample_rate(_client.get()); + double scaleFactor = 1.0 / _ui->internalGainVal->value(); + + for (size_t i = 0; i < size; ++i) + stream << (i / sampleRate) << ' ' << (scaleFactor * data[i]) << '\n'; +} + +void Application::performSfzUpdate() +{ + const QString sfzPath = getSfzPath(); + const QString samplePath = getSamplePath(); + + QString code; + code += "\n"; + code += "sample="; code += QFileInfo(samplePath).fileName(); code += "\n"; + code += _ui->envelopeEdit->toPlainText(); + + QFile sfzFile(sfzPath); + sfzFile.open(QFile::WriteOnly|QFile::Truncate); + sfzFile.write(code.toUtf8()); + sfzFile.close(); + + if (!QFile::exists(samplePath)) { + // generate all-1s sound file of 30 seconds length + constexpr float sampleRate = 44100.0; + constexpr float duration = 30.0; + constexpr size_t channels = 2; + size_t frames = static_cast(std::ceil(sampleRate * duration)); + + SndfileHandle snd( + samplePath.toUtf8().data(), + SFM_WRITE, SF_FORMAT_PCM_16|SF_FORMAT_WAV, 2, sampleRate); + + float frameData[channels]; + for (size_t i = 0; i < channels; ++i) + frameData[i] = 1.0; + + for (size_t i = 0; i < frames; ++i) + snd.writef(frameData, 1); + + snd.writeSync(); + + if (snd.error()) + QFile::remove(samplePath); + } + + _ui->dragFileLabel->setEnabled(true); +} + +void Application::performIdleChecks() +{ + if (_captureStatus == CaptureOver) { + _captureStatus = CaptureIdle; + _ui->saveButton->setEnabled(true); + } +} + +//------------------------------------------------------------------------------ + +void DragFileLabel::setDragFilePath(const QString& path) +{ + _dragFilePath = path; +} + +void DragFileLabel::mousePressEvent(QMouseEvent *event) +{ + if (!_dragFilePath.isEmpty() && event->button() == Qt::LeftButton && rect().contains(event->pos())) { + QMimeData *mimeData = new QMimeData; + mimeData->setUrls(QList() << QUrl::fromLocalFile(_dragFilePath)); + + QDrag *drag = new QDrag(this); + drag->setMimeData(mimeData); + drag->exec(); + drag->deleteLater(); + + event->accept(); + return; + } + + QLabel::mousePressEvent(event); +} + +//------------------------------------------------------------------------------ + +int main(int argc, char* argv[]) +{ + Application app(argc, argv); + + if (!app.init()) + return 1; + + return app.exec(); +} diff --git a/devtools/CaptureEG.h b/devtools/CaptureEG.h new file mode 100644 index 00000000..3d4a61d5 --- /dev/null +++ b/devtools/CaptureEG.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: BSD-2-Clause + +// This code is part of the sfizz library and is licensed under a BSD 2-clause +// license. You should have receive a LICENSE.md file along with the code. +// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + +#pragma once +#include +#include +#include +#include +#include +#include +#include +class QMainWindow; +namespace Ui { class MainWindow; } + +class Application : public QApplication { +public: + Application(int& argc, char *argv[]); + ~Application(); + bool init(); + +private: + static int processAudio(unsigned numFrames, void* arg); + +private: + QMainWindow* _window = nullptr; + std::unique_ptr _ui; + + QTimer* _sfzUpdateTimer = nullptr; + QTimer* _idleTimer = nullptr; + QString _cacheDir; + + QString getSfzPath() const; + QString getSamplePath() const; + + void onSfzTextChanged(); + void engageCapture(); + void saveCapture(); + + void performSfzUpdate(); + void performIdleChecks(); + +private: + // capture status + enum CaptureStatus { CaptureIdle, CaptureEngaged, CaptureOver }; + std::atomic _captureStatus { CaptureIdle }; + // + bool _capturing = false; + long _framesLeftToTrigger = 0; + long _framesLeftToRelease = 0; + std::unique_ptr _captureBuffer; + size_t _captureCapacity = 0; + size_t _captureMinFrames = 0; + size_t _captureFill = 0; + +private: + struct jack_client_delete { + void operator()(jack_client_t* x) const noexcept { jack_client_close(x); } + }; + + std::unique_ptr _client; + jack_port_t* _portAudioIn = nullptr; + jack_port_t* _portMidiOut = nullptr; +}; + +//------------------------------------------------------------------------------ + +class DragFileLabel : public QLabel { +public: + using QLabel::QLabel; + + void setDragFilePath(const QString& path); + + void mousePressEvent(QMouseEvent *event) override; + +private: + QString _dragFilePath; +}; diff --git a/devtools/CaptureEG.ui b/devtools/CaptureEG.ui new file mode 100644 index 00000000..7cfe4c62 --- /dev/null +++ b/devtools/CaptureEG.ui @@ -0,0 +1,334 @@ + + + MainWindow + + + + 0 + 0 + 833 + 337 + + + + MainWindow + + + + + + + + + + Step 1 + + + + + + + 75 + true + + + + Configure player + + + + + + + <html><head/><body><p>Connect audio and MIDI to Dimension synth.</p><p>Initialize the program.</p></body></html> + + + + + + + QFrame::Box + + + QFrame::Raised + + + + + + Synth internal gain + + + + + + + 3 + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Step 2 + + + + + + + 75 + true + + + + Enter the time of release + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Step 3 + + + + + + + 75 + true + + + + Edit envelope opcodes + + + + + + + ampeg_attack=1 +ampeg_hold=0 +ampeg_decay=1 +ampeg_sustain=50 +ampeg_release=1 + + + + + + + + + + + + + + Step 4 + + + + + + Qt::Horizontal + + + + 58 + 20 + + + + + + + + + + + 100 + 100 + + + + QFrame::Panel + + + QFrame::Raised + + + + + + + + 75 + true + + + + Drag SFZ to player + + + + + + + + + Qt::Horizontal + + + + 57 + 20 + + + + + + + + + + + Step 5 + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Start capture + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Step 6 + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Save data + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + DragFileLabel + QLabel +
CaptureEG.h
+
+
+ + +