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/benchmarks/BM_ADSR.cpp b/benchmarks/BM_ADSR.cpp index 4b781070..e2d077c0 100644 --- a/benchmarks/BM_ADSR.cpp +++ b/benchmarks/BM_ADSR.cpp @@ -43,7 +43,7 @@ public: BENCHMARK_DEFINE_F(EnvelopeFixture, Scalar)(benchmark::State& state) { for (auto _ : state) { - envelope.reset(region, midiState, 0, 0, sampleRate); + envelope.reset(region.amplitudeEG, region, midiState, 0, 0, sampleRate); envelope.startRelease(releaseTime); for (int offset = 0; offset < envelopeSize; offset += static_cast(state.range(0))) for (auto& out: output) @@ -56,7 +56,7 @@ BENCHMARK_DEFINE_F(EnvelopeFixture, Scalar)(benchmark::State& state) BENCHMARK_DEFINE_F(EnvelopeFixture, Block)(benchmark::State& state) { for (auto _ : state) { - envelope.reset(region, midiState, 0, 0, sampleRate); + envelope.reset(region.amplitudeEG, region, midiState, 0, 0, sampleRate); envelope.startRelease(releaseTime); for (int offset = 0; offset < envelopeSize; offset += static_cast(state.range(0))) envelope.getBlock(absl::MakeSpan(output)); 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..558b0d14 --- /dev/null +++ b/devtools/CaptureEG.cpp @@ -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 +#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("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 += "\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..37b1b403 --- /dev/null +++ b/devtools/CaptureEG.h @@ -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 +#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 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 { 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
+
+
+ + +
diff --git a/src/sfizz/ADSREnvelope.cpp b/src/sfizz/ADSREnvelope.cpp index 351b1267..9581ab7a 100644 --- a/src/sfizz/ADSREnvelope.cpp +++ b/src/sfizz/ADSREnvelope.cpp @@ -12,25 +12,39 @@ namespace sfz { template -void ADSREnvelope::reset(const Region& region, const MidiState& state, int delay, float velocity, float sampleRate) noexcept +void ADSREnvelope::reset(const EGDescription& desc, const Region& region, const MidiState& state, int delay, float velocity, float sampleRate) noexcept { auto secondsToSamples = [sampleRate](Type timeInSeconds) { return static_cast(timeInSeconds * sampleRate); }; - this->delay = delay + secondsToSamples(region.amplitudeEG.getDelay(state, velocity)); - this->attack = secondsToSamples(region.amplitudeEG.getAttack(state, velocity)); - this->decay = secondsToSamples(region.amplitudeEG.getDecay(state, velocity)); - this->release = secondsToSamples(region.amplitudeEG.getRelease(state, velocity)); - this->hold = secondsToSamples(region.amplitudeEG.getHold(state, velocity)); + auto secondsToLinRate = [sampleRate](Type timeInSeconds) { + timeInSeconds = std::max(timeInSeconds, config::virtuallyZero); + return 1 / (sampleRate * timeInSeconds); + }; + + auto secondsToExpRate = [sampleRate](Type timeInSeconds) { + timeInSeconds = std::max(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->sustain = normalizePercents(region.amplitudeEG.getSustain(state, velocity)); - this->start = this->peak * normalizePercents(region.amplitudeEG.getStart(state, velocity)); + this->sustain = normalizePercents(desc.getSustain(state, velocity)); + this->sustain = max(this->sustain, config::virtuallyZero); + this->start = this->peak * normalizePercents(desc.getStart(state, velocity)); releaseDelay = 0; shouldRelease = false; - freeRunning = ((region.trigger == SfzTrigger::release) || (region.trigger == SfzTrigger::release_key)); - step = 0.0; + freeRunning = ( + (region.trigger == SfzTrigger::release) + || (region.trigger == SfzTrigger::release_key) + || region.loopMode == SfzLoopMode::one_shot + ); currentValue = this->start; currentState = State::Delay; } @@ -38,13 +52,8 @@ void ADSREnvelope::reset(const Region& region, const MidiState& state, int template Type ADSREnvelope::getNextValue() noexcept { - if (shouldRelease && releaseDelay-- == 0) { + if (shouldRelease && releaseDelay-- == 0) 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) { case State::Delay: @@ -52,13 +61,11 @@ Type ADSREnvelope::getNextValue() noexcept return start; currentState = State::Attack; - step = (peak - currentValue) / (attack > 0 ? attack : 1); // fallthrough case State::Attack: - if (attack-- > 0) { - currentValue += step; + currentValue += peak * attackStep; + if (currentValue < peak) return currentValue; - } currentState = State::Hold; currentValue = peak; @@ -67,14 +74,12 @@ Type ADSREnvelope::getNextValue() noexcept if (hold-- > 0) return currentValue; - step = std::exp(std::log(sustain + config::virtuallyZero) / (decay > 0 ? decay : 1)); currentState = State::Decay; // fallthrough case State::Decay: - if (decay-- > 0) { - currentValue *= step; + currentValue *= decayRate; + if (currentValue > sustain) return currentValue; - } currentState = State::Sustain; currentValue = sustain; @@ -84,10 +89,9 @@ Type ADSREnvelope::getNextValue() noexcept shouldRelease = true; return currentValue; case State::Release: - if (release-- > 0) { - currentValue *= step; + currentValue *= releaseRate; + if (currentValue > config::virtuallyZero) return currentValue; - } currentState = State::Done; currentValue = 0.0; @@ -100,110 +104,92 @@ Type ADSREnvelope::getNextValue() noexcept template void ADSREnvelope::getBlock(absl::Span output) noexcept { - auto originalSpan = output; - auto remainingSamples = static_cast(output.size()); - int length; - switch (currentState) { - case State::Delay: - length = min(remainingSamples, delay); - fill(output, currentValue); - output.remove_prefix(length); - remainingSamples -= length; - delay -= length; - if (remainingSamples == 0) - break; + State currentState = this->currentState; + Type currentValue = this->currentValue; + bool shouldRelease = this->shouldRelease; + int releaseDelay = this->releaseDelay; - currentState = State::Attack; - step = (peak - start) / (attack > 0 ? attack : 1); - // fallthrough - case State::Attack: - length = min(remainingSamples, attack); - currentValue = linearRamp(output, currentValue, step); - output.remove_prefix(length); - remainingSamples -= length; - attack -= length; - if (remainingSamples == 0) - break; + while (!output.empty()) { + size_t count = 0; + size_t size = output.size(); - currentValue = peak; - currentState = State::Hold; - // fallthrough - case State::Hold: - length = min(remainingSamples, hold); - fill(output, currentValue); - output.remove_prefix(length); - remainingSamples -= length; - 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(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(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(output, currentValue); - - if (shouldRelease) { - remainingSamples = static_cast(originalSpan.size()); - if (releaseDelay > remainingSamples) - { - releaseDelay -= remainingSamples; - return; + if (shouldRelease && releaseDelay == 0) { + // release takes effect this frame + currentState = State::Release; + releaseDelay = -1; + } + else if (shouldRelease && releaseDelay > 0) { + // prevent computing the segment further than release point + size = std::min(size, releaseDelay); } - originalSpan.remove_prefix(releaseDelay); - if (originalSpan.size() > 0) - currentValue = originalSpan.front(); - if (currentValue > config::virtuallyZero) - step = std::exp((std::log(config::virtuallyZero) - std::log(currentValue)) / (release > 0 ? release : 1)); - else - step = 1; - remainingSamples -= releaseDelay; - length = min(remainingSamples, release); - currentState = State::Release; - currentValue = multiplicativeRamp(originalSpan, currentValue, step); - originalSpan.remove_prefix(length); - release -= length; - - if (release == 0) { + switch (currentState) { + case State::Delay: + while (count < size && delay-- > 0) { + currentValue = start; + output[count++] = currentValue; + } + if (delay <= 0) + currentState = State::Attack; + break; + case State::Attack: + while (count < size && (currentValue += peak * attackStep) < peak) + output[count++] = currentValue; + if (currentValue >= peak) { + currentValue = peak; + 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; - currentState = State::Done; - fill(originalSpan, 0.0); + sfz::fill(output, currentValue); + break; } + + if (shouldRelease) + releaseDelay = std::max(-1, releaseDelay - static_cast(count)); + + output.remove_prefix(count); } + + this->currentState = currentState; + this->currentValue = currentValue; + this->shouldRelease = shouldRelease; + this->releaseDelay = releaseDelay; } + template bool ADSREnvelope::isSmoothing() const noexcept { @@ -213,7 +199,7 @@ bool ADSREnvelope::isSmoothing() const noexcept template bool ADSREnvelope::isReleased() const noexcept { - return (currentState == State::Release); + return (currentState == State::Release) || shouldRelease; } template @@ -228,11 +214,8 @@ void ADSREnvelope::startRelease(int releaseDelay, bool fastRelease) noexce shouldRelease = true; this->releaseDelay = releaseDelay; - if (releaseDelay == 0) - currentState = State::Release; - if (fastRelease) - this->release = 0; + this->releaseRate = 0; } } diff --git a/src/sfizz/ADSREnvelope.h b/src/sfizz/ADSREnvelope.h index 6a647d03..608567c6 100644 --- a/src/sfizz/ADSREnvelope.h +++ b/src/sfizz/ADSREnvelope.h @@ -24,12 +24,13 @@ public: * @brief Resets the ADSR envelope given a Region, the current midi state, and a delay and * trigger velocity * + * @param desc * @param region * @param state * @param delay * @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 * @@ -85,11 +86,10 @@ private: }; State currentState { State::Done }; Type currentValue { 0.0 }; - Type step { 0.0 }; int delay { 0 }; - int attack { 0 }; - int decay { 0 }; - int release { 0 }; + Type attackStep { 0 }; + Type decayRate { 0 }; + Type releaseRate { 0 }; int hold { 0 }; Type start { 0 }; Type peak { 0 }; diff --git a/src/sfizz/Config.h b/src/sfizz/Config.h index a7c67223..a71ad3db 100644 --- a/src/sfizz/Config.h +++ b/src/sfizz/Config.h @@ -48,7 +48,7 @@ namespace config { constexpr int omniOnCC { 125 }; constexpr float halfCCThreshold { 0.5f }; constexpr int centPerSemitone { 100 }; - constexpr float virtuallyZero { 0.00005f }; + constexpr float virtuallyZero { 0.001f }; constexpr float fastReleaseDuration { 0.01f }; constexpr char defineCharacter { '$' }; constexpr Oversampling defaultOversamplingFactor { Oversampling::x1 }; @@ -57,7 +57,6 @@ namespace config { constexpr float voiceStealingThreshold { 0.00001f }; constexpr uint16_t numCCs { 512 }; constexpr int chunkSize { 1024 }; - constexpr float defaultAmpEGRelease { 0.02f }; constexpr int filtersInPool { maxVoices * 2 }; constexpr int filtersPerVoice { 2 }; constexpr int eqsPerVoice { 3 }; diff --git a/src/sfizz/Defaults.h b/src/sfizz/Defaults.h index ebef0041..0e35ca49 100644 --- a/src/sfizz/Defaults.h +++ b/src/sfizz/Defaults.h @@ -201,7 +201,7 @@ namespace Default constexpr float decay { 0 }; constexpr float delayEG { 0 }; constexpr float hold { 0 }; - constexpr float release { config::defaultAmpEGRelease }; + constexpr float release { 0 }; constexpr float vel2release { 0.0f }; constexpr float start { 0.0 }; constexpr float sustain { 100.0 }; diff --git a/src/sfizz/Voice.cpp b/src/sfizz/Voice.cpp index 5da0f909..1bd5dbc5 100644 --- a/src/sfizz/Voice.cpp +++ b/src/sfizz/Voice.cpp @@ -101,7 +101,7 @@ void sfz::Voice::startVoice(Region* region, int delay, int number, float value, initialDelay = delay + static_cast(region->getDelay() * sampleRate); baseFrequency = midiNoteFrequency(number); 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 diff --git a/tests/ADSREnvelopeT.cpp b/tests/ADSREnvelopeT.cpp index 0d344096..df104407 100644 --- a/tests/ADSREnvelopeT.cpp +++ b/tests/ADSREnvelopeT.cpp @@ -33,8 +33,7 @@ TEST_CASE("[ADSREnvelope] Basic state") sfz::ADSREnvelope envelope; std::array output; std::array expected { 0.0, 0.0, 0.0, 0.0, 0.0 }; - for (auto& out : output) - out = envelope.getNextValue(); + envelope.getBlock(absl::MakeSpan(output)); REQUIRE(approxEqual(output, expected)); absl::c_fill(output, -1.0f); @@ -49,14 +48,13 @@ TEST_CASE("[ADSREnvelope] Attack") sfz::Region region { state }; 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 output; - std::array expected { 0.0f, 0.5f, 1.0f, 1.0f, 1.0f }; - for (auto& out : output) - out = envelope.getNextValue(); + std::array expected { 0.5f, 1.0f, 1.0f, 1.0f, 1.0f }; + envelope.getBlock(absl::MakeSpan(output)); REQUIRE(approxEqual(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); envelope.getBlock(absl::MakeSpan(output)); REQUIRE(approxEqual(output, expected)); @@ -69,14 +67,13 @@ TEST_CASE("[ADSREnvelope] Attack again") sfz::Region region { state }; 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 output; - std::array expected { 0.0f, 0.33333f, 0.66667f, 1.0f, 1.0f }; - for (auto& out : output) - out = envelope.getNextValue(); + std::array expected { 0.33333f, 0.66667f, 1.0f, 1.0f, 1.0f }; + envelope.getBlock(absl::MakeSpan(output)); REQUIRE(approxEqual(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); envelope.getBlock(absl::MakeSpan(output)); REQUIRE(approxEqual(output, expected)); @@ -90,15 +87,14 @@ TEST_CASE("[ADSREnvelope] Release") region.amplitudeEG.attack = 0.02f; 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); - std::array output; - std::array expected { 0.0f, 0.5f, 1.0f, 0.08409f, 0.00707f, 0.000594604f, 0.00005f, 0.0f, 0.0f }; - for (auto& out : output) - out = envelope.getNextValue(); + std::array output; + std::array expected { 0.5f, 1.0f, 0.13534f, 0.018f, 0.0024f, 0.0f, 0.0f, 0.0f }; + envelope.getBlock(absl::MakeSpan(output)); REQUIRE(approxEqual(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); absl::c_fill(output, -1.0f); envelope.getBlock(absl::MakeSpan(output)); @@ -113,15 +109,14 @@ TEST_CASE("[ADSREnvelope] Delay") region.amplitudeEG.attack = 0.02f; region.amplitudeEG.release = 0.04f; region.amplitudeEG.delay = 0.02f; - std::array output; - envelope.reset(region, state, 0, 0.0f, 100.0f); + std::array output; + envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f); envelope.startRelease(4); - std::array expected { 0.0f, 0.0f, 0.0f, 0.5f, 1.0f, 0.08409f, 0.00707f, 0.000594604f, 0.00005f, 0.0f, 0.0f }; - for (auto& out : output) - out = envelope.getNextValue(); + std::array expected { 0.0f, 0.0f, 0.5f, 1.0f, 0.13534f, 0.018f, 0.0024f, 0.0f, 0.0f, 0.0f }; + envelope.getBlock(absl::MakeSpan(output)); REQUIRE(approxEqual(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); absl::c_fill(output, -1.0f); envelope.getBlock(absl::MakeSpan(output)); @@ -137,14 +132,13 @@ TEST_CASE("[ADSREnvelope] Lower sustain") region.amplitudeEG.release = 0.04f; region.amplitudeEG.delay = 0.02f; region.amplitudeEG.sustain = 50.0f; - std::array output; - envelope.reset(region, state, 0, 0.0f, 100.0f); - std::array expected { 0.0f, 0.0f, 0.0f, 0.5f, 1.0f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f }; - for (auto& out : output) - out = envelope.getNextValue(); + std::array output; + envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f); + std::array expected { 0.0f, 0.0f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f }; + envelope.getBlock(absl::MakeSpan(output)); REQUIRE(approxEqual(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); envelope.getBlock(absl::MakeSpan(output)); REQUIRE(approxEqual(output, expected)); @@ -160,14 +154,13 @@ TEST_CASE("[ADSREnvelope] Decay") region.amplitudeEG.delay = 0.02f; region.amplitudeEG.sustain = 50.0f; region.amplitudeEG.decay = 0.02f; - std::array output; - envelope.reset(region, state, 0, 0.0f, 100.0f); - std::array expected { 0.0f, 0.0f, 0.0f, 0.5f, 1.0f, 0.707107f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5 }; - for (auto& out : output) - out = envelope.getNextValue(); + std::array output; + envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f); + std::array expected { 0.0f, 0.0f, 0.5f, 1.0f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5 }; + envelope.getBlock(absl::MakeSpan(output)); REQUIRE(approxEqual(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); envelope.getBlock(absl::MakeSpan(output)); REQUIRE(approxEqual(output, expected)); @@ -184,14 +177,13 @@ TEST_CASE("[ADSREnvelope] Hold") region.amplitudeEG.sustain = 50.0f; region.amplitudeEG.decay = 0.02f; region.amplitudeEG.hold = 0.02f; - std::array output; - envelope.reset(region, state, 0, 0.0f, 100.0f); - std::array 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 }; - for (auto& out : output) - out = envelope.getNextValue(); + std::array output; + envelope.reset(region.amplitudeEG, region, state, 0, 0.0f, 100.0f); + std::array 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 }; + envelope.getBlock(absl::MakeSpan(output)); REQUIRE(approxEqual(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); envelope.getBlock(absl::MakeSpan(output)); REQUIRE(approxEqual(output, expected)); @@ -208,15 +200,14 @@ TEST_CASE("[ADSREnvelope] Hold with release") region.amplitudeEG.sustain = 50.0f; region.amplitudeEG.decay = 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); std::array output; std::array 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) - out = envelope.getNextValue(); + envelope.getBlock(absl::MakeSpan(output)); REQUIRE(approxEqual(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); absl::c_fill(output, -1.0f); envelope.getBlock(absl::MakeSpan(output)); @@ -234,14 +225,13 @@ TEST_CASE("[ADSREnvelope] Hold with release 2") region.amplitudeEG.sustain = 50.0f; region.amplitudeEG.decay = 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); std::array output; - std::array 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 }; - for (auto& out : output) - out = envelope.getNextValue(); + std::array 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 }; + envelope.getBlock(absl::MakeSpan(output)); REQUIRE(approxEqual(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); absl::c_fill(output, -1.0f); envelope.getBlock(absl::MakeSpan(output)); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 408deb52..423c867e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -19,6 +19,8 @@ set(SFIZZ_TEST_SOURCES OnePoleFilterT.cpp RegionActivationT.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 EventEnvelopesT.cpp MainT.cpp diff --git a/tests/RegionT.cpp b/tests/RegionT.cpp index 0ebe0cef..86fde09c 100644 --- a/tests/RegionT.cpp +++ b/tests/RegionT.cpp @@ -837,7 +837,7 @@ TEST_CASE("[Region] Parsing opcodes") REQUIRE(region.amplitudeEG.decay == 0.0f); REQUIRE(region.amplitudeEG.delay == 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.sustain == 100.0f); REQUIRE(region.amplitudeEG.depth == 0); diff --git a/tests/SynthT.cpp b/tests/SynthT.cpp index c911c0c7..0434d9ce 100644 --- a/tests/SynthT.cpp +++ b/tests/SynthT.cpp @@ -224,6 +224,29 @@ TEST_CASE("[Synth] Trigger=release_key and an envelope properly kills the voice 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 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") { sfz::Synth synth; diff --git a/tests/TestFiles/envelope_one_shot.sfz b/tests/TestFiles/envelope_one_shot.sfz new file mode 100644 index 00000000..6d820825 --- /dev/null +++ b/tests/TestFiles/envelope_one_shot.sfz @@ -0,0 +1,12 @@ + + +lovel=0 +hivel=127 + + +sample=*noise +loop_mode=one_shot +ampeg_attack=0.02 +ampeg_decay=0.02 +ampeg_release=0.1 +ampeg_sustain=0