commit
3aaf44b636
16 changed files with 1002 additions and 191 deletions
|
|
@ -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_VST "Enable VST plug-in build [default: OFF]" OFF)
|
||||||
option (SFIZZ_BENCHMARKS "Enable benchmarks build [default: OFF]" OFF)
|
option (SFIZZ_BENCHMARKS "Enable benchmarks build [default: OFF]" OFF)
|
||||||
option (SFIZZ_TESTS "Enable tests 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_SHARED "Enable shared library build [default: ON]" ON)
|
||||||
option (SFIZZ_USE_VCPKG "Assume that sfizz is build using vcpkg [default: OFF]" OFF)
|
option (SFIZZ_USE_VCPKG "Assume that sfizz is build using vcpkg [default: OFF]" OFF)
|
||||||
option (SFIZZ_STATIC_LIBSNDFILE "Link libsndfile statically [default: OFF]" OFF)
|
option (SFIZZ_STATIC_LIBSNDFILE "Link libsndfile statically [default: OFF]" OFF)
|
||||||
|
|
@ -64,6 +65,10 @@ if (SFIZZ_TESTS)
|
||||||
add_subdirectory (tests)
|
add_subdirectory (tests)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (SFIZZ_DEVTOOLS)
|
||||||
|
add_subdirectory (devtools)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Put it at the end so that the vst/lv2 directories are registered
|
# Put it at the end so that the vst/lv2 directories are registered
|
||||||
if (NOT MSVC)
|
if (NOT MSVC)
|
||||||
include(SfizzUninstall)
|
include(SfizzUninstall)
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ public:
|
||||||
BENCHMARK_DEFINE_F(EnvelopeFixture, Scalar)(benchmark::State& state)
|
BENCHMARK_DEFINE_F(EnvelopeFixture, Scalar)(benchmark::State& state)
|
||||||
{
|
{
|
||||||
for (auto _ : state) {
|
for (auto _ : state) {
|
||||||
envelope.reset(region, midiState, 0, 0, sampleRate);
|
envelope.reset(region.amplitudeEG, region, midiState, 0, 0, sampleRate);
|
||||||
envelope.startRelease(releaseTime);
|
envelope.startRelease(releaseTime);
|
||||||
for (int offset = 0; offset < envelopeSize; offset += static_cast<int>(state.range(0)))
|
for (int offset = 0; offset < envelopeSize; offset += static_cast<int>(state.range(0)))
|
||||||
for (auto& out: output)
|
for (auto& out: output)
|
||||||
|
|
@ -56,7 +56,7 @@ BENCHMARK_DEFINE_F(EnvelopeFixture, Scalar)(benchmark::State& state)
|
||||||
BENCHMARK_DEFINE_F(EnvelopeFixture, Block)(benchmark::State& state)
|
BENCHMARK_DEFINE_F(EnvelopeFixture, Block)(benchmark::State& state)
|
||||||
{
|
{
|
||||||
for (auto _ : state) {
|
for (auto _ : state) {
|
||||||
envelope.reset(region, midiState, 0, 0, sampleRate);
|
envelope.reset(region.amplitudeEG, region, midiState, 0, 0, sampleRate);
|
||||||
envelope.startRelease(releaseTime);
|
envelope.startRelease(releaseTime);
|
||||||
for (int offset = 0; offset < envelopeSize; offset += static_cast<int>(state.range(0)))
|
for (int offset = 0; offset < envelopeSize; offset += static_cast<int>(state.range(0)))
|
||||||
envelope.getBlock(absl::MakeSpan(output));
|
envelope.getBlock(absl::MakeSpan(output));
|
||||||
|
|
|
||||||
15
devtools/CMakeLists.txt
Normal file
15
devtools/CMakeLists.txt
Normal file
|
|
@ -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()
|
||||||
365
devtools/CaptureEG.cpp
Normal file
365
devtools/CaptureEG.cpp
Normal file
|
|
@ -0,0 +1,365 @@
|
||||||
|
// 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 <QMessageBox>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QDrag>
|
||||||
|
#include <QMimeData>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <sndfile.hh>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
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<size_t>(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<size_t>(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<Application*>(arg);
|
||||||
|
|
||||||
|
const float* audioIn = static_cast<float*>(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<size_t>(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("Sound files (*.wav *.flac);;Data files (*.dat)"));
|
||||||
|
if (filePath.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
QString fileSuffix = QFileInfo(filePath).suffix();
|
||||||
|
if (fileSuffix.compare("wav", Qt::CaseInsensitive) == 0)
|
||||||
|
saveSoundFile(filePath, SF_FORMAT_WAV);
|
||||||
|
else if (fileSuffix.compare("flac", Qt::CaseInsensitive) == 0)
|
||||||
|
saveSoundFile(filePath, SF_FORMAT_FLAC);
|
||||||
|
else
|
||||||
|
savePlotData(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::saveSoundFile(const QString& path, int format)
|
||||||
|
{
|
||||||
|
const float *data = _captureBuffer.get();
|
||||||
|
size_t size = _captureFill;
|
||||||
|
double sampleRate = jack_get_sample_rate(_client.get());
|
||||||
|
double scaleFactor = 1.0 / _ui->internalGainVal->value();
|
||||||
|
size_t captureLatency = jack_get_buffer_size(_client.get());
|
||||||
|
|
||||||
|
SndfileHandle snd(path.toUtf8().data(), SFM_WRITE, format|SF_FORMAT_PCM_16, 1, sampleRate);
|
||||||
|
|
||||||
|
for (size_t i = captureLatency; i < size; ++i) {
|
||||||
|
float sample = scaleFactor * data[i];
|
||||||
|
sample = std::max(-1.0f, std::min(+1.0f, sample));
|
||||||
|
snd.write(&sample, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
snd.writeSync();
|
||||||
|
|
||||||
|
if (snd.error())
|
||||||
|
QFile::remove(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::savePlotData(const QString& path)
|
||||||
|
{
|
||||||
|
QFile file(path);
|
||||||
|
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();
|
||||||
|
size_t captureLatency = jack_get_buffer_size(_client.get());
|
||||||
|
|
||||||
|
for (size_t i = captureLatency; i < size; ++i)
|
||||||
|
stream << ((i - captureLatency) / sampleRate)
|
||||||
|
<< ' ' << (scaleFactor * data[i]) << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
void Application::performSfzUpdate()
|
||||||
|
{
|
||||||
|
const QString sfzPath = getSfzPath();
|
||||||
|
const QString samplePath = getSamplePath();
|
||||||
|
|
||||||
|
QString code;
|
||||||
|
code += "<region>\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<size_t>(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>() << 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();
|
||||||
|
}
|
||||||
83
devtools/CaptureEG.h
Normal file
83
devtools/CaptureEG.h
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
// 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 <QApplication>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QString>
|
||||||
|
#include <jack/jack.h>
|
||||||
|
#include <jack/midiport.h>
|
||||||
|
#include <memory>
|
||||||
|
#include <atomic>
|
||||||
|
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::MainWindow> _ui;
|
||||||
|
|
||||||
|
QTimer* _sfzUpdateTimer = nullptr;
|
||||||
|
QTimer* _idleTimer = nullptr;
|
||||||
|
QString _cacheDir;
|
||||||
|
|
||||||
|
QString getSfzPath() const;
|
||||||
|
QString getSamplePath() const;
|
||||||
|
|
||||||
|
void onSfzTextChanged();
|
||||||
|
void engageCapture();
|
||||||
|
void saveCapture();
|
||||||
|
|
||||||
|
void saveSoundFile(const QString& path, int format);
|
||||||
|
void savePlotData(const QString& path);
|
||||||
|
|
||||||
|
void performSfzUpdate();
|
||||||
|
void performIdleChecks();
|
||||||
|
|
||||||
|
private:
|
||||||
|
// capture status
|
||||||
|
enum CaptureStatus { CaptureIdle, CaptureEngaged, CaptureOver };
|
||||||
|
std::atomic<CaptureStatus> _captureStatus { CaptureIdle };
|
||||||
|
//
|
||||||
|
bool _capturing = false;
|
||||||
|
long _framesLeftToTrigger = 0;
|
||||||
|
long _framesLeftToRelease = 0;
|
||||||
|
std::unique_ptr<float[]> _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<jack_client_t, jack_client_delete> _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;
|
||||||
|
};
|
||||||
334
devtools/CaptureEG.ui
Normal file
334
devtools/CaptureEG.ui
Normal file
|
|
@ -0,0 +1,334 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>MainWindow</class>
|
||||||
|
<widget class="QMainWindow" name="MainWindow">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>833</width>
|
||||||
|
<height>337</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>MainWindow</string>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="centralwidget">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||||
|
<item>
|
||||||
|
<widget class="QWidget" name="" native="true">
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="groupBox">
|
||||||
|
<property name="title">
|
||||||
|
<string>Step 1</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_3">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<weight>75</weight>
|
||||||
|
<bold>true</bold>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Configure player</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_5">
|
||||||
|
<property name="text">
|
||||||
|
<string><html><head/><body><p>Connect audio and MIDI to Dimension synth.</p><p>Initialize the program.</p></body></html></string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QFrame" name="frame">
|
||||||
|
<property name="frameShape">
|
||||||
|
<enum>QFrame::Box</enum>
|
||||||
|
</property>
|
||||||
|
<property name="frameShadow">
|
||||||
|
<enum>QFrame::Raised</enum>
|
||||||
|
</property>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_6">
|
||||||
|
<property name="text">
|
||||||
|
<string>Synth internal gain</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDoubleSpinBox" name="internalGainVal">
|
||||||
|
<property name="decimals">
|
||||||
|
<number>3</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="verticalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="groupBox_6">
|
||||||
|
<property name="title">
|
||||||
|
<string>Step 2</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_4">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<weight>75</weight>
|
||||||
|
<bold>true</bold>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Enter the time of release</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDoubleSpinBox" name="releaseTimeVal"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="verticalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="groupBox_2">
|
||||||
|
<property name="title">
|
||||||
|
<string>Step 3</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<weight>75</weight>
|
||||||
|
<bold>true</bold>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Edit envelope opcodes</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPlainTextEdit" name="envelopeEdit">
|
||||||
|
<property name="plainText">
|
||||||
|
<string>ampeg_attack=1
|
||||||
|
ampeg_hold=0
|
||||||
|
ampeg_decay=1
|
||||||
|
ampeg_sustain=50
|
||||||
|
ampeg_release=1
|
||||||
|
</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QWidget" name="" native="true">
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="groupBox_3">
|
||||||
|
<property name="title">
|
||||||
|
<string>Step 4</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>58</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="DragFileLabel" name="dragFileLabel">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>100</width>
|
||||||
|
<height>100</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="frameShape">
|
||||||
|
<enum>QFrame::Panel</enum>
|
||||||
|
</property>
|
||||||
|
<property name="frameShadow">
|
||||||
|
<enum>QFrame::Raised</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="font">
|
||||||
|
<font>
|
||||||
|
<weight>75</weight>
|
||||||
|
<bold>true</bold>
|
||||||
|
</font>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Drag SFZ to player</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>57</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="groupBox_4">
|
||||||
|
<property name="title">
|
||||||
|
<string>Step 5</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_3">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="captureButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>Start capture</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_4">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="groupBox_5">
|
||||||
|
<property name="title">
|
||||||
|
<string>Step 6</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_5">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="saveButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>Save data</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="horizontalSpacer_6">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>40</width>
|
||||||
|
<height>20</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>DragFileLabel</class>
|
||||||
|
<extends>QLabel</extends>
|
||||||
|
<header>CaptureEG.h</header>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
||||||
|
|
@ -12,25 +12,39 @@
|
||||||
namespace sfz {
|
namespace sfz {
|
||||||
|
|
||||||
template <class Type>
|
template <class Type>
|
||||||
void ADSREnvelope<Type>::reset(const Region& region, const MidiState& state, int delay, float velocity, float sampleRate) noexcept
|
void ADSREnvelope<Type>::reset(const EGDescription& desc, const Region& region, const MidiState& state, int delay, float velocity, float sampleRate) noexcept
|
||||||
{
|
{
|
||||||
auto secondsToSamples = [sampleRate](Type timeInSeconds) {
|
auto secondsToSamples = [sampleRate](Type timeInSeconds) {
|
||||||
return static_cast<int>(timeInSeconds * sampleRate);
|
return static_cast<int>(timeInSeconds * sampleRate);
|
||||||
};
|
};
|
||||||
|
|
||||||
this->delay = delay + secondsToSamples(region.amplitudeEG.getDelay(state, velocity));
|
auto secondsToLinRate = [sampleRate](Type timeInSeconds) {
|
||||||
this->attack = secondsToSamples(region.amplitudeEG.getAttack(state, velocity));
|
timeInSeconds = std::max<Type>(timeInSeconds, config::virtuallyZero);
|
||||||
this->decay = secondsToSamples(region.amplitudeEG.getDecay(state, velocity));
|
return 1 / (sampleRate * timeInSeconds);
|
||||||
this->release = secondsToSamples(region.amplitudeEG.getRelease(state, velocity));
|
};
|
||||||
this->hold = secondsToSamples(region.amplitudeEG.getHold(state, velocity));
|
|
||||||
|
auto secondsToExpRate = [sampleRate](Type timeInSeconds) {
|
||||||
|
timeInSeconds = std::max<Type>(25e-3, timeInSeconds);
|
||||||
|
return std::exp(-8.0 / (timeInSeconds * sampleRate));
|
||||||
|
};
|
||||||
|
|
||||||
|
this->delay = delay + secondsToSamples(desc.getDelay(state, velocity));
|
||||||
|
this->attackStep = secondsToLinRate(desc.getAttack(state, velocity));
|
||||||
|
this->decayRate = secondsToExpRate(desc.getDecay(state, velocity));
|
||||||
|
this->releaseRate = secondsToExpRate(desc.getRelease(state, velocity));
|
||||||
|
this->hold = secondsToSamples(desc.getHold(state, velocity));
|
||||||
this->peak = 1.0;
|
this->peak = 1.0;
|
||||||
this->sustain = normalizePercents(region.amplitudeEG.getSustain(state, velocity));
|
this->sustain = normalizePercents(desc.getSustain(state, velocity));
|
||||||
this->start = this->peak * normalizePercents(region.amplitudeEG.getStart(state, velocity));
|
this->sustain = max(this->sustain, config::virtuallyZero);
|
||||||
|
this->start = this->peak * normalizePercents(desc.getStart(state, velocity));
|
||||||
|
|
||||||
releaseDelay = 0;
|
releaseDelay = 0;
|
||||||
shouldRelease = false;
|
shouldRelease = false;
|
||||||
freeRunning = ((region.trigger == SfzTrigger::release) || (region.trigger == SfzTrigger::release_key));
|
freeRunning = (
|
||||||
step = 0.0;
|
(region.trigger == SfzTrigger::release)
|
||||||
|
|| (region.trigger == SfzTrigger::release_key)
|
||||||
|
|| region.loopMode == SfzLoopMode::one_shot
|
||||||
|
);
|
||||||
currentValue = this->start;
|
currentValue = this->start;
|
||||||
currentState = State::Delay;
|
currentState = State::Delay;
|
||||||
}
|
}
|
||||||
|
|
@ -38,13 +52,8 @@ void ADSREnvelope<Type>::reset(const Region& region, const MidiState& state, int
|
||||||
template <class Type>
|
template <class Type>
|
||||||
Type ADSREnvelope<Type>::getNextValue() noexcept
|
Type ADSREnvelope<Type>::getNextValue() noexcept
|
||||||
{
|
{
|
||||||
if (shouldRelease && releaseDelay-- == 0) {
|
if (shouldRelease && releaseDelay-- == 0)
|
||||||
currentState = State::Release;
|
currentState = State::Release;
|
||||||
if (currentValue > config::virtuallyZero)
|
|
||||||
step = std::exp((std::log(config::virtuallyZero) - std::log(currentValue + config::virtuallyZero)) / (release > 0 ? release : 1));
|
|
||||||
else
|
|
||||||
step = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (currentState) {
|
switch (currentState) {
|
||||||
case State::Delay:
|
case State::Delay:
|
||||||
|
|
@ -52,13 +61,11 @@ Type ADSREnvelope<Type>::getNextValue() noexcept
|
||||||
return start;
|
return start;
|
||||||
|
|
||||||
currentState = State::Attack;
|
currentState = State::Attack;
|
||||||
step = (peak - currentValue) / (attack > 0 ? attack : 1);
|
|
||||||
// fallthrough
|
// fallthrough
|
||||||
case State::Attack:
|
case State::Attack:
|
||||||
if (attack-- > 0) {
|
currentValue += peak * attackStep;
|
||||||
currentValue += step;
|
if (currentValue < peak)
|
||||||
return currentValue;
|
return currentValue;
|
||||||
}
|
|
||||||
|
|
||||||
currentState = State::Hold;
|
currentState = State::Hold;
|
||||||
currentValue = peak;
|
currentValue = peak;
|
||||||
|
|
@ -67,14 +74,12 @@ Type ADSREnvelope<Type>::getNextValue() noexcept
|
||||||
if (hold-- > 0)
|
if (hold-- > 0)
|
||||||
return currentValue;
|
return currentValue;
|
||||||
|
|
||||||
step = std::exp(std::log(sustain + config::virtuallyZero) / (decay > 0 ? decay : 1));
|
|
||||||
currentState = State::Decay;
|
currentState = State::Decay;
|
||||||
// fallthrough
|
// fallthrough
|
||||||
case State::Decay:
|
case State::Decay:
|
||||||
if (decay-- > 0) {
|
currentValue *= decayRate;
|
||||||
currentValue *= step;
|
if (currentValue > sustain)
|
||||||
return currentValue;
|
return currentValue;
|
||||||
}
|
|
||||||
|
|
||||||
currentState = State::Sustain;
|
currentState = State::Sustain;
|
||||||
currentValue = sustain;
|
currentValue = sustain;
|
||||||
|
|
@ -84,10 +89,9 @@ Type ADSREnvelope<Type>::getNextValue() noexcept
|
||||||
shouldRelease = true;
|
shouldRelease = true;
|
||||||
return currentValue;
|
return currentValue;
|
||||||
case State::Release:
|
case State::Release:
|
||||||
if (release-- > 0) {
|
currentValue *= releaseRate;
|
||||||
currentValue *= step;
|
if (currentValue > config::virtuallyZero)
|
||||||
return currentValue;
|
return currentValue;
|
||||||
}
|
|
||||||
|
|
||||||
currentState = State::Done;
|
currentState = State::Done;
|
||||||
currentValue = 0.0;
|
currentValue = 0.0;
|
||||||
|
|
@ -100,110 +104,92 @@ Type ADSREnvelope<Type>::getNextValue() noexcept
|
||||||
template <class Type>
|
template <class Type>
|
||||||
void ADSREnvelope<Type>::getBlock(absl::Span<Type> output) noexcept
|
void ADSREnvelope<Type>::getBlock(absl::Span<Type> output) noexcept
|
||||||
{
|
{
|
||||||
auto originalSpan = output;
|
State currentState = this->currentState;
|
||||||
auto remainingSamples = static_cast<int>(output.size());
|
Type currentValue = this->currentValue;
|
||||||
int length;
|
bool shouldRelease = this->shouldRelease;
|
||||||
switch (currentState) {
|
int releaseDelay = this->releaseDelay;
|
||||||
case State::Delay:
|
|
||||||
length = min(remainingSamples, delay);
|
|
||||||
fill<Type>(output, currentValue);
|
|
||||||
output.remove_prefix(length);
|
|
||||||
remainingSamples -= length;
|
|
||||||
delay -= length;
|
|
||||||
if (remainingSamples == 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
currentState = State::Attack;
|
while (!output.empty()) {
|
||||||
step = (peak - start) / (attack > 0 ? attack : 1);
|
size_t count = 0;
|
||||||
// fallthrough
|
size_t size = output.size();
|
||||||
case State::Attack:
|
|
||||||
length = min(remainingSamples, attack);
|
|
||||||
currentValue = linearRamp<Type>(output, currentValue, step);
|
|
||||||
output.remove_prefix(length);
|
|
||||||
remainingSamples -= length;
|
|
||||||
attack -= length;
|
|
||||||
if (remainingSamples == 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
currentValue = peak;
|
if (shouldRelease && releaseDelay == 0) {
|
||||||
currentState = State::Hold;
|
// release takes effect this frame
|
||||||
// fallthrough
|
currentState = State::Release;
|
||||||
case State::Hold:
|
releaseDelay = -1;
|
||||||
length = min(remainingSamples, hold);
|
}
|
||||||
fill<Type>(output, currentValue);
|
else if (shouldRelease && releaseDelay > 0) {
|
||||||
output.remove_prefix(length);
|
// prevent computing the segment further than release point
|
||||||
remainingSamples -= length;
|
size = std::min<size_t>(size, releaseDelay);
|
||||||
hold -= length;
|
|
||||||
if (remainingSamples == 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
step = std::exp(std::log(sustain + config::virtuallyZero) / (decay > 0 ? decay : 1));
|
|
||||||
currentState = State::Decay;
|
|
||||||
// fallthrough
|
|
||||||
case State::Decay:
|
|
||||||
length = min(remainingSamples, decay);
|
|
||||||
currentValue = multiplicativeRamp<Type>(output, currentValue, step);
|
|
||||||
output.remove_prefix(length);
|
|
||||||
remainingSamples -= length;
|
|
||||||
decay -= length;
|
|
||||||
if (remainingSamples == 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
currentValue = sustain;
|
|
||||||
currentState = State::Sustain;
|
|
||||||
// fallthrough
|
|
||||||
case State::Sustain:
|
|
||||||
if (freeRunning)
|
|
||||||
shouldRelease = true;
|
|
||||||
break;
|
|
||||||
case State::Release:
|
|
||||||
length = min(remainingSamples, release);
|
|
||||||
currentValue = multiplicativeRamp<Type>(output, currentValue, step);
|
|
||||||
output.remove_prefix(length);
|
|
||||||
remainingSamples -= length;
|
|
||||||
release -= length;
|
|
||||||
if (remainingSamples == 0)
|
|
||||||
break;
|
|
||||||
|
|
||||||
currentValue = 0.0;
|
|
||||||
currentState = State::Done;
|
|
||||||
// fallthrough
|
|
||||||
case State::Done:
|
|
||||||
// fallthrough
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
fill<Type>(output, currentValue);
|
|
||||||
|
|
||||||
if (shouldRelease) {
|
|
||||||
remainingSamples = static_cast<int>(originalSpan.size());
|
|
||||||
if (releaseDelay > remainingSamples)
|
|
||||||
{
|
|
||||||
releaseDelay -= remainingSamples;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
originalSpan.remove_prefix(releaseDelay);
|
switch (currentState) {
|
||||||
if (originalSpan.size() > 0)
|
case State::Delay:
|
||||||
currentValue = originalSpan.front();
|
while (count < size && delay-- > 0) {
|
||||||
if (currentValue > config::virtuallyZero)
|
currentValue = start;
|
||||||
step = std::exp((std::log(config::virtuallyZero) - std::log(currentValue)) / (release > 0 ? release : 1));
|
output[count++] = currentValue;
|
||||||
else
|
}
|
||||||
step = 1;
|
if (delay <= 0)
|
||||||
remainingSamples -= releaseDelay;
|
currentState = State::Attack;
|
||||||
length = min(remainingSamples, release);
|
break;
|
||||||
currentState = State::Release;
|
case State::Attack:
|
||||||
currentValue = multiplicativeRamp<Type>(originalSpan, currentValue, step);
|
while (count < size && (currentValue += peak * attackStep) < peak)
|
||||||
originalSpan.remove_prefix(length);
|
output[count++] = currentValue;
|
||||||
release -= length;
|
if (currentValue >= peak) {
|
||||||
|
currentValue = peak;
|
||||||
if (release == 0) {
|
currentState = State::Hold;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case State::Hold:
|
||||||
|
while (count < size && hold-- > 0)
|
||||||
|
output[count++] = currentValue;
|
||||||
|
if (hold <= 0)
|
||||||
|
currentState = State::Decay;
|
||||||
|
break;
|
||||||
|
case State::Decay:
|
||||||
|
while (count < size && (currentValue *= decayRate) > sustain)
|
||||||
|
output[count++] = currentValue;
|
||||||
|
if (currentValue <= sustain) {
|
||||||
|
currentValue = sustain;
|
||||||
|
currentState = State::Sustain;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case State::Sustain:
|
||||||
|
if (!shouldRelease && freeRunning) {
|
||||||
|
shouldRelease = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
count = size;
|
||||||
|
currentValue = sustain;
|
||||||
|
sfz::fill(output.first(count), currentValue);
|
||||||
|
break;
|
||||||
|
case State::Release:
|
||||||
|
while (count < size && (currentValue *= releaseRate) > config::virtuallyZero)
|
||||||
|
output[count++] = currentValue;
|
||||||
|
if (currentValue <= config::virtuallyZero) {
|
||||||
|
currentValue = 0;
|
||||||
|
currentState = State::Done;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
count = size;
|
||||||
currentValue = 0.0;
|
currentValue = 0.0;
|
||||||
currentState = State::Done;
|
sfz::fill(output, currentValue);
|
||||||
fill<Type>(originalSpan, 0.0);
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldRelease)
|
||||||
|
releaseDelay = std::max(-1, releaseDelay - static_cast<int>(count));
|
||||||
|
|
||||||
|
output.remove_prefix(count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this->currentState = currentState;
|
||||||
|
this->currentValue = currentValue;
|
||||||
|
this->shouldRelease = shouldRelease;
|
||||||
|
this->releaseDelay = releaseDelay;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <class Type>
|
template <class Type>
|
||||||
bool ADSREnvelope<Type>::isSmoothing() const noexcept
|
bool ADSREnvelope<Type>::isSmoothing() const noexcept
|
||||||
{
|
{
|
||||||
|
|
@ -213,7 +199,7 @@ bool ADSREnvelope<Type>::isSmoothing() const noexcept
|
||||||
template <class Type>
|
template <class Type>
|
||||||
bool ADSREnvelope<Type>::isReleased() const noexcept
|
bool ADSREnvelope<Type>::isReleased() const noexcept
|
||||||
{
|
{
|
||||||
return (currentState == State::Release);
|
return (currentState == State::Release) || shouldRelease;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <class Type>
|
template <class Type>
|
||||||
|
|
@ -228,11 +214,8 @@ void ADSREnvelope<Type>::startRelease(int releaseDelay, bool fastRelease) noexce
|
||||||
shouldRelease = true;
|
shouldRelease = true;
|
||||||
this->releaseDelay = releaseDelay;
|
this->releaseDelay = releaseDelay;
|
||||||
|
|
||||||
if (releaseDelay == 0)
|
|
||||||
currentState = State::Release;
|
|
||||||
|
|
||||||
if (fastRelease)
|
if (fastRelease)
|
||||||
this->release = 0;
|
this->releaseRate = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,13 @@ public:
|
||||||
* @brief Resets the ADSR envelope given a Region, the current midi state, and a delay and
|
* @brief Resets the ADSR envelope given a Region, the current midi state, and a delay and
|
||||||
* trigger velocity
|
* trigger velocity
|
||||||
*
|
*
|
||||||
|
* @param desc
|
||||||
* @param region
|
* @param region
|
||||||
* @param state
|
* @param state
|
||||||
* @param delay
|
* @param delay
|
||||||
* @param velocity
|
* @param velocity
|
||||||
*/
|
*/
|
||||||
void reset(const Region& region, const MidiState& state, int delay, float velocity, float sampleRate) noexcept;
|
void reset(const EGDescription& desc, const Region& region, const MidiState& state, int delay, float velocity, float sampleRate) noexcept;
|
||||||
/**
|
/**
|
||||||
* @brief Get the next value for the envelope
|
* @brief Get the next value for the envelope
|
||||||
*
|
*
|
||||||
|
|
@ -85,11 +86,10 @@ private:
|
||||||
};
|
};
|
||||||
State currentState { State::Done };
|
State currentState { State::Done };
|
||||||
Type currentValue { 0.0 };
|
Type currentValue { 0.0 };
|
||||||
Type step { 0.0 };
|
|
||||||
int delay { 0 };
|
int delay { 0 };
|
||||||
int attack { 0 };
|
Type attackStep { 0 };
|
||||||
int decay { 0 };
|
Type decayRate { 0 };
|
||||||
int release { 0 };
|
Type releaseRate { 0 };
|
||||||
int hold { 0 };
|
int hold { 0 };
|
||||||
Type start { 0 };
|
Type start { 0 };
|
||||||
Type peak { 0 };
|
Type peak { 0 };
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ namespace config {
|
||||||
constexpr int omniOnCC { 125 };
|
constexpr int omniOnCC { 125 };
|
||||||
constexpr float halfCCThreshold { 0.5f };
|
constexpr float halfCCThreshold { 0.5f };
|
||||||
constexpr int centPerSemitone { 100 };
|
constexpr int centPerSemitone { 100 };
|
||||||
constexpr float virtuallyZero { 0.00005f };
|
constexpr float virtuallyZero { 0.001f };
|
||||||
constexpr float fastReleaseDuration { 0.01f };
|
constexpr float fastReleaseDuration { 0.01f };
|
||||||
constexpr char defineCharacter { '$' };
|
constexpr char defineCharacter { '$' };
|
||||||
constexpr Oversampling defaultOversamplingFactor { Oversampling::x1 };
|
constexpr Oversampling defaultOversamplingFactor { Oversampling::x1 };
|
||||||
|
|
@ -57,7 +57,6 @@ namespace config {
|
||||||
constexpr float voiceStealingThreshold { 0.00001f };
|
constexpr float voiceStealingThreshold { 0.00001f };
|
||||||
constexpr uint16_t numCCs { 512 };
|
constexpr uint16_t numCCs { 512 };
|
||||||
constexpr int chunkSize { 1024 };
|
constexpr int chunkSize { 1024 };
|
||||||
constexpr float defaultAmpEGRelease { 0.02f };
|
|
||||||
constexpr int filtersInPool { maxVoices * 2 };
|
constexpr int filtersInPool { maxVoices * 2 };
|
||||||
constexpr int filtersPerVoice { 2 };
|
constexpr int filtersPerVoice { 2 };
|
||||||
constexpr int eqsPerVoice { 3 };
|
constexpr int eqsPerVoice { 3 };
|
||||||
|
|
|
||||||
|
|
@ -201,7 +201,7 @@ namespace Default
|
||||||
constexpr float decay { 0 };
|
constexpr float decay { 0 };
|
||||||
constexpr float delayEG { 0 };
|
constexpr float delayEG { 0 };
|
||||||
constexpr float hold { 0 };
|
constexpr float hold { 0 };
|
||||||
constexpr float release { config::defaultAmpEGRelease };
|
constexpr float release { 0 };
|
||||||
constexpr float vel2release { 0.0f };
|
constexpr float vel2release { 0.0f };
|
||||||
constexpr float start { 0.0 };
|
constexpr float start { 0.0 };
|
||||||
constexpr float sustain { 100.0 };
|
constexpr float sustain { 100.0 };
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value,
|
||||||
initialDelay = delay + static_cast<int>(region->getDelay() * sampleRate);
|
initialDelay = delay + static_cast<int>(region->getDelay() * sampleRate);
|
||||||
baseFrequency = midiNoteFrequency(number);
|
baseFrequency = midiNoteFrequency(number);
|
||||||
bendStepFactor = centsFactor(region->bendStep);
|
bendStepFactor = centsFactor(region->bendStep);
|
||||||
egEnvelope.reset(*region, resources.midiState, delay, value, sampleRate);
|
egEnvelope.reset(region->amplitudeEG, *region, resources.midiState, delay, value, sampleRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool sfz::Voice::isFree() const noexcept
|
bool sfz::Voice::isFree() const noexcept
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,7 @@ TEST_CASE("[ADSREnvelope] Basic state")
|
||||||
sfz::ADSREnvelope<float> envelope;
|
sfz::ADSREnvelope<float> envelope;
|
||||||
std::array<float, 5> output;
|
std::array<float, 5> output;
|
||||||
std::array<float, 5> expected { 0.0, 0.0, 0.0, 0.0, 0.0 };
|
std::array<float, 5> expected { 0.0, 0.0, 0.0, 0.0, 0.0 };
|
||||||
for (auto& out : output)
|
envelope.getBlock(absl::MakeSpan(output));
|
||||||
out = envelope.getNextValue();
|
|
||||||
REQUIRE(approxEqual<float>(output, expected));
|
REQUIRE(approxEqual<float>(output, expected));
|
||||||
|
|
||||||
absl::c_fill(output, -1.0f);
|
absl::c_fill(output, -1.0f);
|
||||||
|
|
@ -49,14 +48,13 @@ TEST_CASE("[ADSREnvelope] Attack")
|
||||||
sfz::Region region { state };
|
sfz::Region region { state };
|
||||||
region.amplitudeEG.attack = 0.02f;
|
region.amplitudeEG.attack = 0.02f;
|
||||||
|
|
||||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f);
|
||||||
std::array<float, 5> output;
|
std::array<float, 5> output;
|
||||||
std::array<float, 5> expected { 0.0f, 0.5f, 1.0f, 1.0f, 1.0f };
|
std::array<float, 5> expected { 0.5f, 1.0f, 1.0f, 1.0f, 1.0f };
|
||||||
for (auto& out : output)
|
envelope.getBlock(absl::MakeSpan(output));
|
||||||
out = envelope.getNextValue();
|
|
||||||
REQUIRE(approxEqual<float>(output, expected));
|
REQUIRE(approxEqual<float>(output, expected));
|
||||||
|
|
||||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f);
|
||||||
absl::c_fill(output, -1.0f);
|
absl::c_fill(output, -1.0f);
|
||||||
envelope.getBlock(absl::MakeSpan(output));
|
envelope.getBlock(absl::MakeSpan(output));
|
||||||
REQUIRE(approxEqual<float>(output, expected));
|
REQUIRE(approxEqual<float>(output, expected));
|
||||||
|
|
@ -69,14 +67,13 @@ TEST_CASE("[ADSREnvelope] Attack again")
|
||||||
sfz::Region region { state };
|
sfz::Region region { state };
|
||||||
region.amplitudeEG.attack = 0.03f;
|
region.amplitudeEG.attack = 0.03f;
|
||||||
|
|
||||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f);
|
||||||
std::array<float, 5> output;
|
std::array<float, 5> output;
|
||||||
std::array<float, 5> expected { 0.0f, 0.33333f, 0.66667f, 1.0f, 1.0f };
|
std::array<float, 5> expected { 0.33333f, 0.66667f, 1.0f, 1.0f, 1.0f };
|
||||||
for (auto& out : output)
|
envelope.getBlock(absl::MakeSpan(output));
|
||||||
out = envelope.getNextValue();
|
|
||||||
REQUIRE(approxEqual<float>(output, expected));
|
REQUIRE(approxEqual<float>(output, expected));
|
||||||
|
|
||||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f);
|
||||||
absl::c_fill(output, -1.0f);
|
absl::c_fill(output, -1.0f);
|
||||||
envelope.getBlock(absl::MakeSpan(output));
|
envelope.getBlock(absl::MakeSpan(output));
|
||||||
REQUIRE(approxEqual<float>(output, expected));
|
REQUIRE(approxEqual<float>(output, expected));
|
||||||
|
|
@ -90,15 +87,14 @@ TEST_CASE("[ADSREnvelope] Release")
|
||||||
region.amplitudeEG.attack = 0.02f;
|
region.amplitudeEG.attack = 0.02f;
|
||||||
region.amplitudeEG.release = 0.04f;
|
region.amplitudeEG.release = 0.04f;
|
||||||
|
|
||||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f);
|
||||||
envelope.startRelease(2);
|
envelope.startRelease(2);
|
||||||
std::array<float, 9> output;
|
std::array<float, 8> output;
|
||||||
std::array<float, 9> expected { 0.0f, 0.5f, 1.0f, 0.08409f, 0.00707f, 0.000594604f, 0.00005f, 0.0f, 0.0f };
|
std::array<float, 8> expected { 0.5f, 1.0f, 0.13534f, 0.018f, 0.0024f, 0.0f, 0.0f, 0.0f };
|
||||||
for (auto& out : output)
|
envelope.getBlock(absl::MakeSpan(output));
|
||||||
out = envelope.getNextValue();
|
|
||||||
REQUIRE(approxEqual<float>(output, expected));
|
REQUIRE(approxEqual<float>(output, expected));
|
||||||
|
|
||||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f);
|
||||||
envelope.startRelease(2);
|
envelope.startRelease(2);
|
||||||
absl::c_fill(output, -1.0f);
|
absl::c_fill(output, -1.0f);
|
||||||
envelope.getBlock(absl::MakeSpan(output));
|
envelope.getBlock(absl::MakeSpan(output));
|
||||||
|
|
@ -113,15 +109,14 @@ TEST_CASE("[ADSREnvelope] Delay")
|
||||||
region.amplitudeEG.attack = 0.02f;
|
region.amplitudeEG.attack = 0.02f;
|
||||||
region.amplitudeEG.release = 0.04f;
|
region.amplitudeEG.release = 0.04f;
|
||||||
region.amplitudeEG.delay = 0.02f;
|
region.amplitudeEG.delay = 0.02f;
|
||||||
std::array<float, 11> output;
|
std::array<float, 10> output;
|
||||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f);
|
||||||
envelope.startRelease(4);
|
envelope.startRelease(4);
|
||||||
std::array<float, 11> expected { 0.0f, 0.0f, 0.0f, 0.5f, 1.0f, 0.08409f, 0.00707f, 0.000594604f, 0.00005f, 0.0f, 0.0f };
|
std::array<float, 10> expected { 0.0f, 0.0f, 0.5f, 1.0f, 0.13534f, 0.018f, 0.0024f, 0.0f, 0.0f, 0.0f };
|
||||||
for (auto& out : output)
|
envelope.getBlock(absl::MakeSpan(output));
|
||||||
out = envelope.getNextValue();
|
|
||||||
REQUIRE(approxEqual<float>(output, expected));
|
REQUIRE(approxEqual<float>(output, expected));
|
||||||
|
|
||||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f);
|
||||||
envelope.startRelease(4);
|
envelope.startRelease(4);
|
||||||
absl::c_fill(output, -1.0f);
|
absl::c_fill(output, -1.0f);
|
||||||
envelope.getBlock(absl::MakeSpan(output));
|
envelope.getBlock(absl::MakeSpan(output));
|
||||||
|
|
@ -137,14 +132,13 @@ TEST_CASE("[ADSREnvelope] Lower sustain")
|
||||||
region.amplitudeEG.release = 0.04f;
|
region.amplitudeEG.release = 0.04f;
|
||||||
region.amplitudeEG.delay = 0.02f;
|
region.amplitudeEG.delay = 0.02f;
|
||||||
region.amplitudeEG.sustain = 50.0f;
|
region.amplitudeEG.sustain = 50.0f;
|
||||||
std::array<float, 11> output;
|
std::array<float, 10> output;
|
||||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f);
|
||||||
std::array<float, 11> expected { 0.0f, 0.0f, 0.0f, 0.5f, 1.0f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f };
|
std::array<float, 10> expected { 0.0f, 0.0f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f };
|
||||||
for (auto& out : output)
|
envelope.getBlock(absl::MakeSpan(output));
|
||||||
out = envelope.getNextValue();
|
|
||||||
REQUIRE(approxEqual<float>(output, expected));
|
REQUIRE(approxEqual<float>(output, expected));
|
||||||
|
|
||||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f);
|
||||||
absl::c_fill(output, -1.0f);
|
absl::c_fill(output, -1.0f);
|
||||||
envelope.getBlock(absl::MakeSpan(output));
|
envelope.getBlock(absl::MakeSpan(output));
|
||||||
REQUIRE(approxEqual<float>(output, expected));
|
REQUIRE(approxEqual<float>(output, expected));
|
||||||
|
|
@ -160,14 +154,13 @@ TEST_CASE("[ADSREnvelope] Decay")
|
||||||
region.amplitudeEG.delay = 0.02f;
|
region.amplitudeEG.delay = 0.02f;
|
||||||
region.amplitudeEG.sustain = 50.0f;
|
region.amplitudeEG.sustain = 50.0f;
|
||||||
region.amplitudeEG.decay = 0.02f;
|
region.amplitudeEG.decay = 0.02f;
|
||||||
std::array<float, 11> output;
|
std::array<float, 10> output;
|
||||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f);
|
||||||
std::array<float, 11> expected { 0.0f, 0.0f, 0.0f, 0.5f, 1.0f, 0.707107f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5 };
|
std::array<float, 10> expected { 0.0f, 0.0f, 0.5f, 1.0f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5 };
|
||||||
for (auto& out : output)
|
envelope.getBlock(absl::MakeSpan(output));
|
||||||
out = envelope.getNextValue();
|
|
||||||
REQUIRE(approxEqual<float>(output, expected));
|
REQUIRE(approxEqual<float>(output, expected));
|
||||||
|
|
||||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f);
|
||||||
absl::c_fill(output, -1.0f);
|
absl::c_fill(output, -1.0f);
|
||||||
envelope.getBlock(absl::MakeSpan(output));
|
envelope.getBlock(absl::MakeSpan(output));
|
||||||
REQUIRE(approxEqual<float>(output, expected));
|
REQUIRE(approxEqual<float>(output, expected));
|
||||||
|
|
@ -184,14 +177,13 @@ TEST_CASE("[ADSREnvelope] Hold")
|
||||||
region.amplitudeEG.sustain = 50.0f;
|
region.amplitudeEG.sustain = 50.0f;
|
||||||
region.amplitudeEG.decay = 0.02f;
|
region.amplitudeEG.decay = 0.02f;
|
||||||
region.amplitudeEG.hold = 0.02f;
|
region.amplitudeEG.hold = 0.02f;
|
||||||
std::array<float, 13> output;
|
std::array<float, 12> output;
|
||||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f);
|
||||||
std::array<float, 13> expected { 0.0f, 0.0f, 0.0f, 0.5f, 1.0f, 1.0f, 1.0f, 0.707107f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f };
|
std::array<float, 12> expected { 0.0f, 0.0f, 0.5f, 1.0f, 1.0f, 1.0f, 0.707107f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f };
|
||||||
for (auto& out : output)
|
envelope.getBlock(absl::MakeSpan(output));
|
||||||
out = envelope.getNextValue();
|
|
||||||
REQUIRE(approxEqual<float>(output, expected));
|
REQUIRE(approxEqual<float>(output, expected));
|
||||||
|
|
||||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f);
|
||||||
absl::c_fill(output, -1.0f);
|
absl::c_fill(output, -1.0f);
|
||||||
envelope.getBlock(absl::MakeSpan(output));
|
envelope.getBlock(absl::MakeSpan(output));
|
||||||
REQUIRE(approxEqual<float>(output, expected));
|
REQUIRE(approxEqual<float>(output, expected));
|
||||||
|
|
@ -208,15 +200,14 @@ TEST_CASE("[ADSREnvelope] Hold with release")
|
||||||
region.amplitudeEG.sustain = 50.0f;
|
region.amplitudeEG.sustain = 50.0f;
|
||||||
region.amplitudeEG.decay = 0.02f;
|
region.amplitudeEG.decay = 0.02f;
|
||||||
region.amplitudeEG.hold = 0.02f;
|
region.amplitudeEG.hold = 0.02f;
|
||||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f);
|
||||||
envelope.startRelease(8);
|
envelope.startRelease(8);
|
||||||
std::array<float, 15> output;
|
std::array<float, 15> output;
|
||||||
std::array<float, 15> expected { 0.0f, 0.0f, 0.0f, 0.5f, 1.0f, 1.0f, 1.0f, 0.707107f, 0.5f, 0.05f, 0.005f, 0.0005f, 0.00005f, 0.0f, 0.0f };
|
std::array<float, 15> expected { 0.0f, 0.0f, 0.0f, 0.5f, 1.0f, 1.0f, 1.0f, 0.707107f, 0.5f, 0.05f, 0.005f, 0.0005f, 0.00005f, 0.0f, 0.0f };
|
||||||
for (auto& out : output)
|
envelope.getBlock(absl::MakeSpan(output));
|
||||||
out = envelope.getNextValue();
|
|
||||||
|
|
||||||
REQUIRE(approxEqual<float>(output, expected));
|
REQUIRE(approxEqual<float>(output, expected));
|
||||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f);
|
||||||
envelope.startRelease(8);
|
envelope.startRelease(8);
|
||||||
absl::c_fill(output, -1.0f);
|
absl::c_fill(output, -1.0f);
|
||||||
envelope.getBlock(absl::MakeSpan(output));
|
envelope.getBlock(absl::MakeSpan(output));
|
||||||
|
|
@ -234,14 +225,13 @@ TEST_CASE("[ADSREnvelope] Hold with release 2")
|
||||||
region.amplitudeEG.sustain = 50.0f;
|
region.amplitudeEG.sustain = 50.0f;
|
||||||
region.amplitudeEG.decay = 0.02f;
|
region.amplitudeEG.decay = 0.02f;
|
||||||
region.amplitudeEG.hold = 0.02f;
|
region.amplitudeEG.hold = 0.02f;
|
||||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f);
|
||||||
envelope.startRelease(4);
|
envelope.startRelease(4);
|
||||||
std::array<float, 15> output;
|
std::array<float, 15> output;
|
||||||
std::array<float, 15> expected { 0.0f, 0.0f, 0.0f, 0.5f, 1.0f, 0.08409f, 0.00707f, 0.000594604f, 0.00005f, 0.0f, 0.0f, 0.0f, 0.0 };
|
std::array<float, 15> expected { 0.0f, 0.0f, 0.5f, 1.0f, 0.08409f, 0.00707f, 0.000594604f, 0.00005f, 0.0f, 0.0f, 0.0f, 0.0 };
|
||||||
for (auto& out : output)
|
envelope.getBlock(absl::MakeSpan(output));
|
||||||
out = envelope.getNextValue();
|
|
||||||
REQUIRE(approxEqual<float>(output, expected));
|
REQUIRE(approxEqual<float>(output, expected));
|
||||||
envelope.reset(region, state, 0, 0.0f, 100.0f);
|
envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f);
|
||||||
envelope.startRelease(4);
|
envelope.startRelease(4);
|
||||||
absl::c_fill(output, -1.0f);
|
absl::c_fill(output, -1.0f);
|
||||||
envelope.getBlock(absl::MakeSpan(output));
|
envelope.getBlock(absl::MakeSpan(output));
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ set(SFIZZ_TEST_SOURCES
|
||||||
OnePoleFilterT.cpp
|
OnePoleFilterT.cpp
|
||||||
RegionActivationT.cpp
|
RegionActivationT.cpp
|
||||||
RegionValueComputationsT.cpp
|
RegionValueComputationsT.cpp
|
||||||
|
# If we're tweaking the curves this kind of tests does not make sense
|
||||||
|
# Use integration tests with comparison curves
|
||||||
# ADSREnvelopeT.cpp
|
# ADSREnvelopeT.cpp
|
||||||
EventEnvelopesT.cpp
|
EventEnvelopesT.cpp
|
||||||
MainT.cpp
|
MainT.cpp
|
||||||
|
|
|
||||||
|
|
@ -837,7 +837,7 @@ TEST_CASE("[Region] Parsing opcodes")
|
||||||
REQUIRE(region.amplitudeEG.decay == 0.0f);
|
REQUIRE(region.amplitudeEG.decay == 0.0f);
|
||||||
REQUIRE(region.amplitudeEG.delay == 0.0f);
|
REQUIRE(region.amplitudeEG.delay == 0.0f);
|
||||||
REQUIRE(region.amplitudeEG.hold == 0.0f);
|
REQUIRE(region.amplitudeEG.hold == 0.0f);
|
||||||
REQUIRE(region.amplitudeEG.release == 0.02f);
|
REQUIRE(region.amplitudeEG.release == 0.0f);
|
||||||
REQUIRE(region.amplitudeEG.start == 0.0f);
|
REQUIRE(region.amplitudeEG.start == 0.0f);
|
||||||
REQUIRE(region.amplitudeEG.sustain == 100.0f);
|
REQUIRE(region.amplitudeEG.sustain == 100.0f);
|
||||||
REQUIRE(region.amplitudeEG.depth == 0);
|
REQUIRE(region.amplitudeEG.depth == 0);
|
||||||
|
|
|
||||||
|
|
@ -224,6 +224,29 @@ TEST_CASE("[Synth] Trigger=release_key and an envelope properly kills the voice
|
||||||
REQUIRE( synth.getVoiceView(0)->isFree() );
|
REQUIRE( synth.getVoiceView(0)->isFree() );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("[Synth] loopmode=one_shot and an envelope properly kills the voice at the end of the envelope")
|
||||||
|
{
|
||||||
|
sfz::Synth synth;
|
||||||
|
synth.setSampleRate(48000);
|
||||||
|
synth.setSamplesPerBlock(480);
|
||||||
|
sfz::AudioBuffer<float> buffer(2, 480);
|
||||||
|
synth.setNumVoices(1);
|
||||||
|
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/envelope_one_shot.sfz");
|
||||||
|
synth.noteOn(0, 60, 63);
|
||||||
|
synth.noteOff(0, 60, 63);
|
||||||
|
REQUIRE( !synth.getVoiceView(0)->isFree() );
|
||||||
|
synth.renderBlock(buffer); // Attack (0.02)
|
||||||
|
synth.renderBlock(buffer);
|
||||||
|
synth.renderBlock(buffer); // Decay (0.02)
|
||||||
|
synth.renderBlock(buffer);
|
||||||
|
synth.renderBlock(buffer); // Release (0.1)
|
||||||
|
REQUIRE( synth.getVoiceView(0)->canBeStolen() );
|
||||||
|
// Release is 0.1s
|
||||||
|
for (int i = 0; i < 10; ++i)
|
||||||
|
synth.renderBlock(buffer);
|
||||||
|
REQUIRE( synth.getVoiceView(0)->isFree() );
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("[Synth] Number of effect buses and resetting behavior")
|
TEST_CASE("[Synth] Number of effect buses and resetting behavior")
|
||||||
{
|
{
|
||||||
sfz::Synth synth;
|
sfz::Synth synth;
|
||||||
|
|
|
||||||
12
tests/TestFiles/envelope_one_shot.sfz
Normal file
12
tests/TestFiles/envelope_one_shot.sfz
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
<group>
|
||||||
|
lovel=0
|
||||||
|
hivel=127
|
||||||
|
|
||||||
|
<region>
|
||||||
|
sample=*noise
|
||||||
|
loop_mode=one_shot
|
||||||
|
ampeg_attack=0.02
|
||||||
|
ampeg_decay=0.02
|
||||||
|
ampeg_release=0.1
|
||||||
|
ampeg_sustain=0
|
||||||
Loading…
Add table
Reference in a new issue