Add the wavetable oscillator and demo
This commit is contained in:
parent
c57ab54c24
commit
6e733e8c11
5 changed files with 382 additions and 0 deletions
|
|
@ -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()]());
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
197
tests/DemoWavetables.cpp
Normal 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
50
tests/DemoWavetables.ui
Normal 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>
|
||||
Loading…
Add table
Reference in a new issue