diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a5de550f..d14ea8f1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -31,4 +31,17 @@ target_link_libraries(sfizz_tests PRIVATE sfizz::sfizz) sfizz_enable_lto_if_needed(sfizz_tests) # target_link_libraries(sfizz_tests PRIVATE absl::strings absl::str_format absl::flat_hash_map cnpy absl::span absl::algorithm) +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_demo_filters DemoFilters.cpp) + target_include_directories(sfizz_demo_filters PRIVATE ${JACK_INCLUDE_DIRS}) + target_link_libraries(sfizz_demo_filters PRIVATE sfizz::sfizz Qt5::Widgets ${JACK_LIBRARIES}) + set_target_properties(sfizz_demo_filters PROPERTIES AUTOUIC ON) +endif() + file(COPY "." DESTINATION ${CMAKE_BINARY_DIR}/tests) diff --git a/tests/DemoFilters.cpp b/tests/DemoFilters.cpp new file mode 100644 index 00000000..9bbf5900 --- /dev/null +++ b/tests/DemoFilters.cpp @@ -0,0 +1,325 @@ +// 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 "sfizz/SfzFilter.h" +#include "ui_DemoFilters.h" +#include +#include +#include +#include +#include +#include +#include + +/// +struct jack_delete { + void operator()(jack_client_t *x) const noexcept { jack_client_close(x); } +}; + +typedef std::unique_ptr jack_client_u; + +/// +class DemoApp : public QApplication { +public: + DemoApp(int &argc, char **argv); + bool initSound(); + void initWindow(); + +private: + static int processAudio(jack_nframes_t nframes, void *cbdata); + +private: + void valueChangedType(int value); + void valueChangedCutoff(int value); + void valueChangedResonance(int value); + void valueChangedPkShGain(int value); + void valueChangedBandwidth(int value); + void valueChangedFilterMode(int value); + +private: + QMainWindow *fWindow = nullptr; + Ui::DemoFiltersWindow fUi; + + static constexpr int cutoffMin = 10.0; + static constexpr int cutoffMax = 20000.0; + + static constexpr int resoMin = 0.0; + static constexpr int resoMax = 40.0; + + static constexpr int pkshMin = -40.0; + static constexpr int pkshMax = 40.0; + + static constexpr int bwMin = 1.0; + static constexpr int bwMax = 10.0; + + int fType = sfz::kFilterNone; + int fCutoff = 500.0; + int fReso = 0.0; + int fPksh = 20.0; + int fBw = 1.0; + + sfz::Filter fFilter; + sfz::FilterEq fFilterEq; + + enum FilterMode { + kFilterModeMulti, + kFilterModeEq, + }; + FilterMode fFilterMode = kFilterModeMulti; + + jack_client_u fClient; + jack_port_t *fPorts[4] = {}; +}; + +DemoApp::DemoApp(int &argc, char **argv) + : QApplication(argc, argv) +{ + setApplicationName(tr("Sfizz Filters")); +} + +bool DemoApp::initSound() +{ + jack_client_t *client = jack_client_open( + applicationName().toUtf8().data(), JackNoStartServer, nullptr); + if (!client) { + QMessageBox::critical(nullptr, tr("Error"), tr("Cannot open JACK audio.")); + return false; + } + + fClient.reset(client); + + double sampleRate = jack_get_sample_rate(client); + + fFilter.init(sampleRate); + fFilter.setChannels(2); + + fFilterEq.init(sampleRate); + fFilterEq.setChannels(2); + + fPorts[0] = jack_port_register(client, "in_left", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); + fPorts[1] = jack_port_register(client, "in_right", JACK_DEFAULT_AUDIO_TYPE, JackPortIsInput, 0); + fPorts[2] = jack_port_register(client, "out_left", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); + fPorts[3] = jack_port_register(client, "out_right", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); + + if (!(fPorts[0] && fPorts[1] && fPorts[2] && fPorts[3])) { + QMessageBox::critical(nullptr, tr("Error"), tr("Cannot register JACK ports.")); + return false; + } + + jack_set_process_callback(client, &processAudio, this); + + if (jack_activate(client) != 0) { + QMessageBox::critical(nullptr, tr("Error"), tr("Cannot activate JACK client.")); + return false; + } + + return true; +} + +void DemoApp::initWindow() +{ + QMainWindow *window = new QMainWindow; + fWindow = window; + fUi.setupUi(window); + window->setWindowTitle(applicationDisplayName()); + + QComboBox *cbTypes = fUi.comboBox; + cbTypes->addItem("None", static_cast(sfz::kFilterNone)); + cbTypes->addItem("Apf1p", static_cast(sfz::kFilterApf1p)); + cbTypes->addItem("Bpf1p", static_cast(sfz::kFilterBpf1p)); + cbTypes->addItem("Bpf2p", static_cast(sfz::kFilterBpf2p)); + cbTypes->addItem("Bpf4p", static_cast(sfz::kFilterBpf4p)); + cbTypes->addItem("Bpf6p", static_cast(sfz::kFilterBpf6p)); + cbTypes->addItem("Brf1p", static_cast(sfz::kFilterBrf1p)); + cbTypes->addItem("Brf2p", static_cast(sfz::kFilterBrf2p)); + cbTypes->addItem("Hpf1p", static_cast(sfz::kFilterHpf1p)); + cbTypes->addItem("Hpf2p", static_cast(sfz::kFilterHpf2p)); + cbTypes->addItem("Hpf4p", static_cast(sfz::kFilterHpf4p)); + cbTypes->addItem("Hpf6p", static_cast(sfz::kFilterHpf6p)); + cbTypes->addItem("Lpf1p", static_cast(sfz::kFilterLpf1p)); + cbTypes->addItem("Lpf2p", static_cast(sfz::kFilterLpf2p)); + cbTypes->addItem("Lpf4p", static_cast(sfz::kFilterLpf4p)); + cbTypes->addItem("Lpf6p", static_cast(sfz::kFilterLpf6p)); + cbTypes->addItem("Pink", static_cast(sfz::kFilterPink)); + cbTypes->addItem("Lpf2pSv", static_cast(sfz::kFilterLpf2pSv)); + cbTypes->addItem("Hpf2pSv", static_cast(sfz::kFilterHpf2pSv)); + cbTypes->addItem("Bpf2pSv", static_cast(sfz::kFilterBpf2pSv)); + cbTypes->addItem("Brf2pSv", static_cast(sfz::kFilterBrf2pSv)); + cbTypes->addItem("Lsh", static_cast(sfz::kFilterLsh)); + cbTypes->addItem("Hsh", static_cast(sfz::kFilterHsh)); + cbTypes->addItem("Peq", static_cast(sfz::kFilterPeq)); + + cbTypes->setCurrentIndex(cbTypes->findData(fType)); + fUi.lcdType->display(fType); + + connect( + cbTypes, QOverload::of(&QComboBox::currentIndexChanged), + this, [this, cbTypes](int index) { valueChangedType(cbTypes->itemData(index).toInt()); }); + + fUi.dialCutoff->setRange(cutoffMin, cutoffMax); + fUi.dialResonance->setRange(resoMin, resoMax); + fUi.dialPkShGain->setRange(pkshMin, pkshMax); + fUi.dialBandwidth->setRange(bwMin, bwMax); + fUi.spinCutoff->setRange(cutoffMin, cutoffMax); + fUi.spinResonance->setRange(resoMin, resoMax); + fUi.spinPkShGain->setRange(pkshMin, pkshMax); + fUi.spinBandwidth->setRange(bwMin, bwMax); + + fUi.dialCutoff->setValue(fCutoff); + fUi.dialResonance->setValue(fReso); + fUi.dialPkShGain->setValue(fPksh); + fUi.dialBandwidth->setValue(fBw); + fUi.spinCutoff->setValue(fCutoff); + fUi.spinResonance->setValue(fReso); + fUi.spinPkShGain->setValue(fPksh); + fUi.spinBandwidth->setValue(fBw); + + connect( + fUi.dialCutoff, &QDial::valueChanged, + this, [this](int value) { valueChangedCutoff(value); }); + connect( + fUi.spinCutoff, QOverload::of(&QSpinBox::valueChanged), + this, [this](int value) { valueChangedCutoff(value); }); + connect( + fUi.dialResonance, &QDial::valueChanged, + this, [this](int value) { valueChangedResonance(value); }); + connect( + fUi.spinResonance, QOverload::of(&QSpinBox::valueChanged), + this, [this](int value) { valueChangedResonance(value); }); + connect( + fUi.dialPkShGain, &QDial::valueChanged, + this, [this](int value) { valueChangedPkShGain(value); }); + connect( + fUi.spinPkShGain, QOverload::of(&QSpinBox::valueChanged), + this, [this](int value) { valueChangedPkShGain(value); }); + connect( + fUi.dialBandwidth, &QDial::valueChanged, + this, [this](int value) { valueChangedBandwidth(value); }); + connect( + fUi.spinBandwidth, QOverload::of(&QSpinBox::valueChanged), + this, [this](int value) { valueChangedBandwidth(value); }); + + QButtonGroup *grpMode = new QButtonGroup(this); + grpMode->addButton(fUi.btnMultiMode, kFilterModeMulti); + grpMode->addButton(fUi.btnEqMode, kFilterModeEq); + fUi.btnMultiMode->setChecked(true); + grpMode->setExclusive(true); + + connect( + grpMode, QOverload::of(&QButtonGroup::buttonToggled), this, + [this](int id, bool toggled) { + if (toggled) + valueChangedFilterMode(id); + }); + + window->adjustSize(); + window->setFixedSize(window->size()); + + window->show(); +} + +int DemoApp::processAudio(jack_nframes_t nframes, void *cbdata) +{ + DemoApp *self = reinterpret_cast(cbdata); + + const float *ins[2]; + float *outs[2]; + + ins[0] = reinterpret_cast(jack_port_get_buffer(self->fPorts[0], nframes)); + ins[1] = reinterpret_cast(jack_port_get_buffer(self->fPorts[1], nframes)); + outs[0] = reinterpret_cast(jack_port_get_buffer(self->fPorts[2], nframes)); + outs[1] = reinterpret_cast(jack_port_get_buffer(self->fPorts[3], nframes)); + + switch (self->fFilterMode) { + default: + case kFilterModeMulti: + self->fFilter.setType(static_cast(self->fType)); + self->fFilter.process(ins, outs, self->fCutoff, self->fReso, self->fPksh, nframes); + break; + case kFilterModeEq: + self->fFilterEq.process(ins, outs, self->fCutoff, self->fBw, self->fPksh, nframes); + break; + } + + return 0; +} + +void DemoApp::valueChangedType(int value) +{ + fType = value; + fUi.lcdType->display(value); +} + +void DemoApp::valueChangedCutoff(int value) +{ + fUi.dialCutoff->blockSignals(true); + fUi.dialCutoff->setValue(value); + fUi.dialCutoff->blockSignals(false); + + fUi.spinCutoff->blockSignals(true); + fUi.spinCutoff->setValue(value); + fUi.spinCutoff->blockSignals(false); + + fCutoff = value; +} + +void DemoApp::valueChangedResonance(int value) +{ + fUi.dialResonance->blockSignals(true); + fUi.dialResonance->setValue(value); + fUi.dialResonance->blockSignals(false); + + fUi.spinResonance->blockSignals(true); + fUi.spinResonance->setValue(value); + fUi.spinResonance->blockSignals(false); + + fReso = value; +} + +void DemoApp::valueChangedPkShGain(int value) +{ + fUi.dialPkShGain->blockSignals(true); + fUi.dialPkShGain->setValue(value); + fUi.dialPkShGain->blockSignals(false); + + fUi.spinPkShGain->blockSignals(true); + fUi.spinPkShGain->setValue(value); + fUi.spinPkShGain->blockSignals(false); + + fPksh = value; +} + +void DemoApp::valueChangedBandwidth(int value) +{ + fUi.dialBandwidth->blockSignals(true); + fUi.dialBandwidth->setValue(value); + fUi.dialBandwidth->blockSignals(false); + + fUi.spinBandwidth->blockSignals(true); + fUi.spinBandwidth->setValue(value); + fUi.spinBandwidth->blockSignals(false); + + fBw = value; +} + +void DemoApp::valueChangedFilterMode(int value) +{ + fUi.stackedWidget->setCurrentIndex(value); + + fFilterMode = static_cast(value); +} + +int main(int argc, char *argv[]) +{ + DemoApp app(argc, argv); + + if (!app.initSound()) + return 1; + + app.initWindow(); + + return app.exec(); +} diff --git a/tests/DemoFilters.ui b/tests/DemoFilters.ui new file mode 100644 index 00000000..36281d1f --- /dev/null +++ b/tests/DemoFilters.ui @@ -0,0 +1,239 @@ + + + DemoFiltersWindow + + + + 0 + 0 + 415 + 216 + + + + + + + + + + + + + + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + 2 + + + + + + + Qt::AlignCenter + + + dB + + + + + + + + + + Resonance + + + Qt::AlignCenter + + + + + + + Type + + + Qt::AlignCenter + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Bandwidth + + + Qt::AlignCenter + + + + + + + Qt::AlignCenter + + + oct + + + + + + + + + + + + + + Qt::AlignCenter + + + Hz + + + + + + + Qt::AlignCenter + + + dB + + + + + + + Cutoff + + + Qt::AlignCenter + + + + + + + Peak/shelf gain + + + Qt::AlignCenter + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Multi-mode + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Equalizer + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + +