From 6e733e8c1170e025df21fc20437480417b68f5d3 Mon Sep 17 00:00:00 2001 From: Jean Pierre Cimalando Date: Fri, 14 Feb 2020 01:45:50 +0100 Subject: [PATCH] Add the wavetable oscillator and demo --- src/sfizz/Wavetables.cpp | 80 ++++++++++++++++ src/sfizz/Wavetables.h | 50 ++++++++++ tests/CMakeLists.txt | 5 + tests/DemoWavetables.cpp | 197 +++++++++++++++++++++++++++++++++++++++ tests/DemoWavetables.ui | 50 ++++++++++ 5 files changed, 382 insertions(+) create mode 100644 tests/DemoWavetables.cpp create mode 100644 tests/DemoWavetables.ui diff --git a/src/sfizz/Wavetables.cpp b/src/sfizz/Wavetables.cpp index 8f2251ee..4b0fccc0 100644 --- a/src/sfizz/Wavetables.cpp +++ b/src/sfizz/Wavetables.cpp @@ -11,6 +11,78 @@ namespace sfz { +static WavetableMulti silenceMulti = WavetableMulti::createSilence(); + +void WavetableOscillator::init(double sampleRate) +{ + _sampleInterval = 1.0 / sampleRate; + _multi = &silenceMulti; + clear(); +} + +void WavetableOscillator::clear() +{ + _phase = 0.0f; +} + +void WavetableOscillator::setWavetable(const WavetableMulti* wave) +{ + _multi = wave ? wave : &silenceMulti; +} + +void WavetableOscillator::process(float frequency, float* output, unsigned nframes) +{ + float phase = _phase; + float phaseInc = frequency * _sampleInterval; + + const WavetableMulti& multi = *_multi; + unsigned tableSize = multi.tableSize(); + absl::Span table = multi.getTableForFrequency(frequency); + + for (unsigned i = 0; i < nframes; ++i) { + float position = phase * tableSize; + unsigned index = static_cast(position); + float frac = position - index; + output[i] = interpolate(&table[index], frac); + + phase += phaseInc; + phase -= static_cast(phase); + } + + _phase = phase; +} + +void WavetableOscillator::processModulated(const float* frequencies, float* output, unsigned nframes) +{ + float phase = _phase; + float sampleInterval = _sampleInterval; + + const WavetableMulti& multi = *_multi; + unsigned tableSize = multi.tableSize(); + + for (unsigned i = 0; i < nframes; ++i) { + float frequency = frequencies[i]; + float phaseInc = frequency * sampleInterval; + absl::Span table = multi.getTableForFrequency(frequency); + + float position = phase * tableSize; + unsigned index = static_cast(position); + float frac = position - index; + output[i] = interpolate(&table[index], frac); + + phase += phaseInc; + phase -= static_cast(phase); + } + + _phase = phase; +} + +float WavetableOscillator::interpolate(const float* x, float delta) +{ + return x[0] + delta * (x[1] - x[0]); +} + +//------------------------------------------------------------------------------ void HarmonicProfile::generate( absl::Span table, double amplitude, double cutoff) const { @@ -177,6 +249,14 @@ WavetableMulti WavetableMulti::createForHarmonicProfile( return wm; } +WavetableMulti WavetableMulti::createSilence() +{ + WavetableMulti wm; + wm.allocateStorage(1); + wm.fillExtra(); + return wm; +} + void WavetableMulti::allocateStorage(unsigned tableSize) { _multiData.reset(new float[(tableSize + _tableExtra) * multiSize()]()); diff --git a/src/sfizz/Wavetables.h b/src/sfizz/Wavetables.h index 5accadf3..6e18978a 100644 --- a/src/sfizz/Wavetables.h +++ b/src/sfizz/Wavetables.h @@ -11,6 +11,53 @@ namespace sfz { +class WavetableMulti; + +/** + An oscillator based on wavetables + */ +class WavetableOscillator { +public: + /** + Initialize with the given sample rate. + Run it once after instantiating. + */ + void init(double sampleRate); + + /** + Reset the oscillation to the initial phase. + */ + void clear(); + + /** + Set the wavetable to generate with this oscillator. + */ + void setWavetable(const WavetableMulti* wave); + + /** + Compute a cycle of the oscillator, with constant frequency. + */ + void process(float frequency, float* output, unsigned nframes); + + /** + Compute a cycle of the oscillator, with varying frequency. + */ + void processModulated(const float* frequencies, float* output, unsigned nframes); + +private: + /** + Interpolate a value from a part of table, with delta in 0 to 1 excluded. + There are `TableExtra` elements available for reading. + (cf. WavetableMulti) + */ + static float interpolate(const float* x, float delta); + +private: + float _phase = 0.0f; + float _sampleInterval = 0.0f; + const WavetableMulti* _multi = nullptr; +}; + /** A description of the harmonics of a particular wave form */ @@ -101,6 +148,9 @@ public: static WavetableMulti createForHarmonicProfile( const HarmonicProfile& hp, unsigned tableSize, double refSampleRate = 44100.0); + // create the tiniest wavetable with null content for use with oscillators + static WavetableMulti createSilence(); + private: // get a pointer to the beginning of the N-th table const float* getTablePointer(unsigned index) const diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7218f2a7..598e58ea 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -50,6 +50,11 @@ if(JACK_FOUND AND TARGET Qt5::Widgets) target_include_directories(sfizz_demo_stereo PRIVATE ${JACK_INCLUDE_DIRS}) target_link_libraries(sfizz_demo_stereo PRIVATE sfizz::sfizz Qt5::Widgets ${JACK_LIBRARIES}) set_target_properties(sfizz_demo_stereo PROPERTIES AUTOUIC ON) + + add_executable(sfizz_demo_wavetables DemoWavetables.cpp) + target_include_directories(sfizz_demo_wavetables PRIVATE ${JACK_INCLUDE_DIRS}) + target_link_libraries(sfizz_demo_wavetables PRIVATE sfizz::sfizz Qt5::Widgets ${JACK_LIBRARIES}) + set_target_properties(sfizz_demo_wavetables PROPERTIES AUTOUIC ON) endif() add_executable(eq_apply EQ.cpp) diff --git a/tests/DemoWavetables.cpp b/tests/DemoWavetables.cpp new file mode 100644 index 00000000..2e8b77a8 --- /dev/null +++ b/tests/DemoWavetables.cpp @@ -0,0 +1,197 @@ +// 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/Wavetables.h" +#include "sfizz/MathHelpers.h" +#include "ui_DemoWavetables.h" +#include +#include +#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 valueChangedWave(int value); + void buttonClickedPlaySweep(); + +private: + QMainWindow* fWindow = nullptr; + Ui::DemoWavetablesWindow fUi; + + sfz::WavetableMulti fMulti[4]; + sfz::WavetableOscillator fOsc; + unsigned fWavePlaying = 0; + std::atomic fNewWavePending { -1 }; + std::atomic fStartNewSweep { false }; + + static constexpr float sweepMin = 0.0; + static constexpr float sweepMax = 136.0; + static constexpr float sweepDuration = 3.0; + + float fSweepCurrent = sweepMax; + float fSweepIncrement = 0.0; + + std::unique_ptr fTmpFrequency; + + jack_client_u fClient; + jack_port_t* fPorts[2] = {}; +}; + +DemoApp::DemoApp(int& argc, char** argv) + : QApplication(argc, argv) +{ + setApplicationName(tr("Sfizz Wavetables")); +} + +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; + } + + double sampleRate = jack_get_sample_rate(client); + fOsc.init(sampleRate); + fSweepIncrement = ((sweepMax - sweepMin) / (sweepDuration * sampleRate)); + + unsigned bufferSize = jack_get_buffer_size(client); + fTmpFrequency.reset(new float[bufferSize]); + + fMulti[0] = sfz::WavetableMulti::createForHarmonicProfile( + sfz::HarmonicProfile::getSine(), 2048); + fMulti[1] = sfz::WavetableMulti::createForHarmonicProfile( + sfz::HarmonicProfile::getTriangle(), 2048); + fMulti[2] = sfz::WavetableMulti::createForHarmonicProfile( + sfz::HarmonicProfile::getSaw(), 2048); + fMulti[3] = sfz::WavetableMulti::createForHarmonicProfile( + sfz::HarmonicProfile::getSquare(), 2048); + + fClient.reset(client); + + fPorts[0] = jack_port_register(client, "out_left", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); + fPorts[1] = jack_port_register(client, "out_right", JACK_DEFAULT_AUDIO_TYPE, JackPortIsOutput, 0); + + if (!(fPorts[0] && fPorts[1])) { + 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()); + + fUi.valWave->addItem(tr("1 - Sine")); + fUi.valWave->addItem(tr("2 - Triangle")); + fUi.valWave->addItem(tr("3 - Saw")); + fUi.valWave->addItem(tr("4 - Square")); + + connect( + fUi.valWave, QOverload::of(&QComboBox::currentIndexChanged), + this, [this](int index) { valueChangedWave(index); }); + + connect( + fUi.btnPlaySweep, &QPushButton::clicked, + this, [this]() { buttonClickedPlaySweep(); }); + + window->adjustSize(); + window->setFixedSize(window->size()); + + window->show(); +} + +int DemoApp::processAudio(jack_nframes_t nframes, void* cbdata) +{ + DemoApp* self = reinterpret_cast(cbdata); + + sfz::WavetableOscillator& osc = self->fOsc; + + int newWave = self->fNewWavePending.exchange(-1); + if (newWave != -1) + self->fWavePlaying = newWave; + + osc.setWavetable(&self->fMulti[self->fWavePlaying]); + + float* left = reinterpret_cast( + jack_port_get_buffer(self->fPorts[0], nframes)); + float* right = reinterpret_cast( + jack_port_get_buffer(self->fPorts[1], nframes)); + + // sweep the pitch of the oscillator + float* frequency = self->fTmpFrequency.get(); + float sweepCurrent = self->fSweepCurrent; + if (self->fStartNewSweep.exchange(false)) + sweepCurrent = sweepMin; + float sweepIncrement = self->fSweepIncrement; + for (unsigned i = 0; i < nframes; ++i) { + frequency[i] = 440.0f * std::pow(2.0f, (sweepCurrent - 69.0f) * (1.0f / 12.0f)); + sweepCurrent = std::min(sweepMax, sweepCurrent + sweepIncrement); + } + self->fSweepCurrent = sweepCurrent; + + // compute oscillator + osc.processModulated(frequency, left, nframes); + std::memcpy(right, left, nframes * sizeof(float)); + + return 0; +} + +void DemoApp::valueChangedWave(int value) +{ + fNewWavePending.store(value); +} + +void DemoApp::buttonClickedPlaySweep() +{ + fStartNewSweep.store(true); +} + +int main(int argc, char* argv[]) +{ + DemoApp app(argc, argv); + + if (!app.initSound()) + return 1; + + app.initWindow(); + + return app.exec(); +} diff --git a/tests/DemoWavetables.ui b/tests/DemoWavetables.ui new file mode 100644 index 00000000..6a5f5def --- /dev/null +++ b/tests/DemoWavetables.ui @@ -0,0 +1,50 @@ + + + DemoWavetablesWindow + + + + 0 + 0 + 170 + 103 + + + + + + + + Select wave + + + + + + + Play sweep + + + + + + + + + + + 64 + 64 + + + + + + + + + + + + +