Skip to content

Commit d1333a5

Browse files
jpcimapaulfd
authored andcommitted
Add tool to capture Dimension EG curves
1 parent 5a7aa90 commit d1333a5

File tree

5 files changed

+764
-0
lines changed

5 files changed

+764
-0
lines changed

CMakeLists.txt

+5
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ option (SFIZZ_LV2 "Enable LV2 plug-in build [default: ON]" ON)
3030
option (SFIZZ_VST "Enable VST plug-in build [default: OFF]" OFF)
3131
option (SFIZZ_BENCHMARKS "Enable benchmarks build [default: OFF]" OFF)
3232
option (SFIZZ_TESTS "Enable tests build [default: OFF]" OFF)
33+
option (SFIZZ_DEVTOOLS "Enable developer tools build [default: OFF]" OFF)
3334
option (SFIZZ_SHARED "Enable shared library build [default: ON]" ON)
3435
option (SFIZZ_USE_VCPKG "Assume that sfizz is build using vcpkg [default: OFF]" OFF)
3536
option (SFIZZ_STATIC_LIBSNDFILE "Link libsndfile statically [default: OFF]" OFF)
@@ -64,6 +65,10 @@ if (SFIZZ_TESTS)
6465
add_subdirectory (tests)
6566
endif()
6667

68+
if (SFIZZ_DEVTOOLS)
69+
add_subdirectory (devtools)
70+
endif()
71+
6772
# Put it at the end so that the vst/lv2 directories are registered
6873
if (NOT MSVC)
6974
include(SfizzUninstall)

devtools/CMakeLists.txt

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
###############################
2+
# Developer tools
3+
4+
find_package(PkgConfig)
5+
if(PKGCONFIG_FOUND)
6+
pkg_check_modules(JACK "jack")
7+
endif()
8+
find_package(Qt5 COMPONENTS Widgets)
9+
10+
if(JACK_FOUND AND TARGET Qt5::Widgets)
11+
add_executable(sfizz_capture_eg CaptureEG.cpp)
12+
target_include_directories(sfizz_capture_eg PRIVATE . ${JACK_INCLUDE_DIRS})
13+
target_link_libraries(sfizz_capture_eg PRIVATE sfizz-sndfile Qt5::Widgets ${JACK_LIBRARIES})
14+
set_target_properties(sfizz_capture_eg PROPERTIES AUTOUIC ON)
15+
endif()

devtools/CaptureEG.cpp

+330
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
// SPDX-License-Identifier: BSD-2-Clause
2+
3+
// This code is part of the sfizz library and is licensed under a BSD 2-clause
4+
// license. You should have receive a LICENSE.md file along with the code.
5+
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
6+
7+
#include "CaptureEG.h"
8+
#include "ui_CaptureEG.h"
9+
#include <QMessageBox>
10+
#include <QFileDialog>
11+
#include <QMouseEvent>
12+
#include <QDrag>
13+
#include <QMimeData>
14+
#include <QTimer>
15+
#include <QStandardPaths>
16+
#include <QFileInfo>
17+
#include <QDir>
18+
#include <QDebug>
19+
#include <sndfile.hh>
20+
#include <cmath>
21+
22+
Application::Application(int& argc, char *argv[])
23+
: QApplication(argc, argv), _ui(new Ui::MainWindow)
24+
{
25+
setApplicationName("SfizzCaptureEG");
26+
}
27+
28+
Application::~Application()
29+
{
30+
}
31+
32+
bool Application::init()
33+
{
34+
_cacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
35+
if (_cacheDir.isEmpty()) {
36+
QMessageBox::critical(nullptr, tr("Error"), tr("Cannot determine the cache directory."));
37+
return false;
38+
}
39+
QDir(_cacheDir).mkpath(".");
40+
41+
///
42+
jack_client_t* client = jack_client_open(
43+
applicationName().toUtf8().data(), JackNoStartServer, nullptr);
44+
45+
if (!client) {
46+
QMessageBox::critical(nullptr, tr("Error"), tr("Cannot register a new JACK client."));
47+
return false;
48+
}
49+
_client.reset(client);
50+
51+
std::string clientName = jack_get_client_name(client);
52+
53+
if (!(_portAudioIn = jack_port_register(client, "audio_in", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0)) ||
54+
!(_portMidiOut = jack_port_register(client, "midi_out", JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput, 0)))
55+
{
56+
QMessageBox::critical(nullptr, tr("Error"), tr("Cannot register the JACK client ports."));
57+
return false;
58+
}
59+
60+
jack_set_process_callback(client, &processAudio, this);
61+
62+
if (jack_activate(client) != 0) {
63+
QMessageBox::critical(nullptr, tr("Error"), tr("Cannot activate the JACK client."));
64+
return false;
65+
}
66+
67+
// Try to connect Dimension if it exists
68+
{
69+
const char** synthAudioPorts = jack_get_ports(client, "^Dimension Pro:", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput);
70+
const char** synthMidiPorts = jack_get_ports(client, "^Dimension Pro:", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput);
71+
if (synthAudioPorts && *synthAudioPorts)
72+
jack_connect(client, *synthAudioPorts, jack_port_name(_portAudioIn));
73+
if (synthMidiPorts && *synthMidiPorts)
74+
jack_connect(client, jack_port_name(_portMidiOut), *synthMidiPorts);
75+
jack_free(synthAudioPorts);
76+
jack_free(synthMidiPorts);
77+
}
78+
79+
// Allocate capture buffer (capacity 30 seconds)
80+
double sampleRate = jack_get_sample_rate(client);
81+
_captureCapacity = static_cast<size_t>(std::ceil(30.0 * sampleRate));
82+
_captureBuffer.reset(new float[_captureCapacity]);
83+
// Always capture a minimum of 0.5 seconds (ensure not stopping too early)
84+
_captureMinFrames = static_cast<size_t>(std::ceil(0.5 * sampleRate));
85+
86+
///
87+
QMainWindow* window = new QMainWindow;
88+
_window = window;
89+
_ui->setupUi(window);
90+
window->setWindowTitle(applicationDisplayName());
91+
window->adjustSize();
92+
window->setFixedSize(window->size());
93+
window->show();
94+
95+
_ui->dragFileLabel->setDragFilePath(getSfzPath());
96+
_ui->dragFileLabel->setPixmap(
97+
QIcon::fromTheme("text-x-generic").pixmap(_ui->dragFileLabel->size()));
98+
99+
_ui->releaseTimeVal->setRange(0.0, 10.0);
100+
_ui->releaseTimeVal->setValue(5.0);
101+
102+
_ui->internalGainVal->setRange(0.1, 2.0);
103+
_ui->internalGainVal->setValue(0.342); // default to match Dimension
104+
105+
_ui->saveButton->setEnabled(false);
106+
107+
connect(
108+
_ui->envelopeEdit, &QPlainTextEdit::textChanged,
109+
this, [this]() { onSfzTextChanged(); });
110+
111+
connect(
112+
_ui->captureButton, &QPushButton::clicked,
113+
this, [this]() { engageCapture(); });
114+
115+
connect(
116+
_ui->saveButton, &QPushButton::clicked,
117+
this, [this]() { saveCapture(); });
118+
119+
_sfzUpdateTimer = new QTimer;
120+
_sfzUpdateTimer->setInterval(500);
121+
_sfzUpdateTimer->setSingleShot(true);
122+
connect(_sfzUpdateTimer, &QTimer::timeout, this, [this]() { performSfzUpdate(); });
123+
124+
_idleTimer = new QTimer;
125+
_idleTimer->setInterval(50);
126+
_idleTimer->setSingleShot(false);
127+
connect(_idleTimer, &QTimer::timeout, this, [this]() { performIdleChecks(); });
128+
129+
_idleTimer->start();
130+
131+
onSfzTextChanged();
132+
133+
return true;
134+
}
135+
136+
int Application::processAudio(unsigned numFrames, void* arg)
137+
{
138+
auto* self = static_cast<Application*>(arg);
139+
140+
const float* audioIn = static_cast<float*>(jack_port_get_buffer(self->_portAudioIn, numFrames));
141+
void* midiOut = jack_port_get_buffer(self->_portMidiOut, numFrames);
142+
143+
jack_midi_clear_buffer(midiOut);
144+
145+
if (self->_captureStatus != CaptureEngaged)
146+
return 0;
147+
148+
bool over = false;
149+
150+
size_t captureIndex = self->_captureFill;
151+
const size_t captureCapacity = self->_captureCapacity;
152+
const size_t captureMinFrames = self->_captureMinFrames;
153+
float* captureBuffer = self->_captureBuffer.get();
154+
155+
constexpr float silentThreshold = 1e-4; // -80 dB
156+
157+
long tt = self->_framesLeftToTrigger;
158+
long tr = self->_framesLeftToRelease;
159+
160+
for (size_t i = 0; i < numFrames && !over; ++i) {
161+
if (tt == 0) {
162+
const unsigned char noteOn[3] = {0x90, 69, 127};
163+
jack_midi_event_write(midiOut, i, noteOn, sizeof(noteOn));
164+
}
165+
if (tr == 0) {
166+
const unsigned char noteOff[3] = {0x90, 69, 0};
167+
jack_midi_event_write(midiOut, i, noteOff, sizeof(noteOff));
168+
}
169+
--tt;
170+
--tr;
171+
if (captureIndex == captureCapacity)
172+
over = true;
173+
else {
174+
captureBuffer[captureIndex++] = audioIn[i];
175+
if (tr < 0 && captureIndex >= captureMinFrames && audioIn[i] < silentThreshold)
176+
over = true;
177+
}
178+
}
179+
180+
self->_captureFill = captureIndex;
181+
self->_framesLeftToTrigger = tt;
182+
self->_framesLeftToRelease = tr;
183+
184+
if (over)
185+
self->_captureStatus = CaptureOver;
186+
187+
return 0;
188+
}
189+
190+
QString Application::getSfzPath() const
191+
{
192+
return _cacheDir + "/CaptureEG.sfz";
193+
}
194+
195+
QString Application::getSamplePath() const
196+
{
197+
return _cacheDir + "/CaptureEG.wav";
198+
}
199+
200+
void Application::onSfzTextChanged()
201+
{
202+
_ui->dragFileLabel->setEnabled(false);
203+
_sfzUpdateTimer->start();
204+
}
205+
206+
void Application::engageCapture()
207+
{
208+
if (_captureStatus != CaptureIdle)
209+
return;
210+
211+
_ui->saveButton->setEnabled(false);
212+
213+
const double sampleRate = jack_get_sample_rate(_client.get());
214+
215+
_framesLeftToTrigger = 0;
216+
_framesLeftToRelease = static_cast<size_t>(std::ceil(sampleRate * _ui->releaseTimeVal->value()));
217+
_captureFill = 0;
218+
219+
_captureStatus = CaptureEngaged;
220+
}
221+
222+
void Application::saveCapture()
223+
{
224+
if (_captureStatus != CaptureIdle)
225+
return;
226+
227+
QString filePath = QFileDialog::getSaveFileName(
228+
_window, tr("Save data"), QString(), tr("Data files (*.dat)"));
229+
if (filePath.isEmpty())
230+
return;
231+
232+
QFile file(filePath);
233+
file.open(QFile::WriteOnly|QFile::Truncate);
234+
QTextStream stream(&file);
235+
236+
const float *data = _captureBuffer.get();
237+
size_t size = _captureFill;
238+
double sampleRate = jack_get_sample_rate(_client.get());
239+
double scaleFactor = 1.0 / _ui->internalGainVal->value();
240+
241+
for (size_t i = 0; i < size; ++i)
242+
stream << (i / sampleRate) << ' ' << (scaleFactor * data[i]) << '\n';
243+
}
244+
245+
void Application::performSfzUpdate()
246+
{
247+
const QString sfzPath = getSfzPath();
248+
const QString samplePath = getSamplePath();
249+
250+
QString code;
251+
code += "<region>\n";
252+
code += "sample="; code += QFileInfo(samplePath).fileName(); code += "\n";
253+
code += _ui->envelopeEdit->toPlainText();
254+
255+
QFile sfzFile(sfzPath);
256+
sfzFile.open(QFile::WriteOnly|QFile::Truncate);
257+
sfzFile.write(code.toUtf8());
258+
sfzFile.close();
259+
260+
if (!QFile::exists(samplePath)) {
261+
// generate all-1s sound file of 30 seconds length
262+
constexpr float sampleRate = 44100.0;
263+
constexpr float duration = 30.0;
264+
constexpr size_t channels = 2;
265+
size_t frames = static_cast<size_t>(std::ceil(sampleRate * duration));
266+
267+
SndfileHandle snd(
268+
samplePath.toUtf8().data(),
269+
SFM_WRITE, SF_FORMAT_PCM_16|SF_FORMAT_WAV, 2, sampleRate);
270+
271+
float frameData[channels];
272+
for (size_t i = 0; i < channels; ++i)
273+
frameData[i] = 1.0;
274+
275+
for (size_t i = 0; i < frames; ++i)
276+
snd.writef(frameData, 1);
277+
278+
snd.writeSync();
279+
280+
if (snd.error())
281+
QFile::remove(samplePath);
282+
}
283+
284+
_ui->dragFileLabel->setEnabled(true);
285+
}
286+
287+
void Application::performIdleChecks()
288+
{
289+
if (_captureStatus == CaptureOver) {
290+
_captureStatus = CaptureIdle;
291+
_ui->saveButton->setEnabled(true);
292+
}
293+
}
294+
295+
//------------------------------------------------------------------------------
296+
297+
void DragFileLabel::setDragFilePath(const QString& path)
298+
{
299+
_dragFilePath = path;
300+
}
301+
302+
void DragFileLabel::mousePressEvent(QMouseEvent *event)
303+
{
304+
if (!_dragFilePath.isEmpty() && event->button() == Qt::LeftButton && rect().contains(event->pos())) {
305+
QMimeData *mimeData = new QMimeData;
306+
mimeData->setUrls(QList<QUrl>() << QUrl::fromLocalFile(_dragFilePath));
307+
308+
QDrag *drag = new QDrag(this);
309+
drag->setMimeData(mimeData);
310+
drag->exec();
311+
drag->deleteLater();
312+
313+
event->accept();
314+
return;
315+
}
316+
317+
QLabel::mousePressEvent(event);
318+
}
319+
320+
//------------------------------------------------------------------------------
321+
322+
int main(int argc, char* argv[])
323+
{
324+
Application app(argc, argv);
325+
326+
if (!app.init())
327+
return 1;
328+
329+
return app.exec();
330+
}

0 commit comments

Comments
 (0)