Add the wavetable oscillator and demo

This commit is contained in:
Jean Pierre Cimalando 2020-02-14 01:45:50 +01:00 committed by Paul Fd
parent c57ab54c24
commit 6e733e8c11
5 changed files with 382 additions and 0 deletions

View file

@ -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<const float> table = multi.getTableForFrequency(frequency);
for (unsigned i = 0; i < nframes; ++i) {
float position = phase * tableSize;
unsigned index = static_cast<unsigned>(position);
float frac = position - index;
output[i] = interpolate(&table[index], frac);
phase += phaseInc;
phase -= static_cast<int>(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<const float> table = multi.getTableForFrequency(frequency);
float position = phase * tableSize;
unsigned index = static_cast<unsigned>(position);
float frac = position - index;
output[i] = interpolate(&table[index], frac);
phase += phaseInc;
phase -= static_cast<int>(phase);
}
_phase = phase;
}
float WavetableOscillator::interpolate(const float* x, float delta)
{
return x[0] + delta * (x[1] - x[0]);
}
//------------------------------------------------------------------------------
void HarmonicProfile::generate(
absl::Span<float> 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()]());

View file

@ -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

View file

@ -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)

197
tests/DemoWavetables.cpp Normal file
View file

@ -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 <QApplication>
#include <QMainWindow>
#include <QMessageBox>
#include <QButtonGroup>
#include <QDebug>
#include <jack/jack.h>
#include <atomic>
#include <memory>
#include <cstring>
///
struct jack_delete {
void operator()(jack_client_t* x) const noexcept { jack_client_close(x); }
};
typedef std::unique_ptr<jack_client_t, jack_delete> 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<int> fNewWavePending { -1 };
std::atomic<bool> 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<float[]> 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<int>::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<DemoApp*>(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<float*>(
jack_port_get_buffer(self->fPorts[0], nframes));
float* right = reinterpret_cast<float*>(
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();
}

50
tests/DemoWavetables.ui Normal file
View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DemoWavetablesWindow</class>
<widget class="QMainWindow" name="DemoWavetablesWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>170</width>
<height>103</height>
</rect>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Select wave</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Play sweep</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QComboBox" name="valWave"/>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="btnPlaySweep">
<property name="minimumSize">
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="icon">
<iconset theme="media-playback-start"/>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>