Merge pull request #347 from jpcima/hq-wavetables

Higher quality wavetable mipmaps
This commit is contained in:
JP Cimalando 2020-08-12 01:16:05 +02:00 committed by GitHub
commit b914e19491
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 148 additions and 97 deletions

View file

@ -95,6 +95,7 @@ namespace config {
constexpr int maxEffectBuses { 256 };
// Wavetable constants; amplitude values are matched to reference
static constexpr unsigned tableSize = 1024;
static constexpr double tableRefSampleRate = 44100.0 * 1.1; // +10% aliasing permissivity
static constexpr double amplitudeSine = 0.625;
static constexpr double amplitudeTriangle = 0.625;
static constexpr double amplitudeSaw = 0.515;

View file

@ -283,65 +283,75 @@ const HarmonicProfile& HarmonicProfile::getSquare()
}
//------------------------------------------------------------------------------
constexpr unsigned WavetableRange::countOctaves;
constexpr float WavetableRange::frequencyScaleFactor;
constexpr unsigned MipmapRange::N;
constexpr float MipmapRange::F1;
constexpr float MipmapRange::FN;
unsigned WavetableRange::getOctaveForFrequency(float f)
const float MipmapRange::K = 1.0 / F1;
const float MipmapRange::LogB = std::log(FN / F1) / (N - 1);
const std::array<float, 1024> MipmapRange::FrequencyToIndex = []()
{
int oct = fp_exponent(frequencyScaleFactor * f);
return clamp<int>(oct, 0, countOctaves - 1);
}
std::array<float, 1024> table;
static const auto octaveForFrequencyTable = []()
{
static constexpr unsigned N = 1024;
std::array<float, N> table;
constexpr double fmin = 1 / WavetableRange::frequencyScaleFactor;
constexpr double fmax = (1 << (WavetableRange::countOctaves - 1)) / WavetableRange::frequencyScaleFactor;
for (unsigned i = 0; i < N; ++i) {
double f = fmin + (i * (1.0 / (N - 1))) * (fmax - fmin);
table[i] = std::log2(f * WavetableRange::frequencyScaleFactor);
for (unsigned i = 0; i < table.size() - 1; ++i) {
float r = i * (1.0f / (table.size() - 1));
float f = F1 + r * (FN - F1);
table[i] = getExactIndexForFrequency(f);
}
// ensure the last element to be exact
table[table.size() - 1] = N - 1;
return table;
}();
float WavetableRange::getFractionalOctaveForFrequency(float f)
float MipmapRange::getIndexForFrequency(float f)
{
static constexpr unsigned N = octaveForFrequencyTable.size();
static constexpr unsigned tableSize = FrequencyToIndex.size();
constexpr double fmin = 1 / WavetableRange::frequencyScaleFactor;
constexpr double fmax = (1 << (WavetableRange::countOctaves - 1)) / WavetableRange::frequencyScaleFactor;
float pos = (f - F1) * ((tableSize - 1) / static_cast<float>(FN - F1));
pos = clamp<float>(pos, 0, tableSize - 1);
float pos = (f - fmin) * ((N - 1) / static_cast<float>(fmax - fmin));
int index1 = static_cast<int>(pos);
int index2 = std::min<int>(index1 + 1, tableSize - 1);
float frac = pos - index1;
index1 = clamp<int>(index1, 0, N - 1);
int index2 = std::min<int>(index1 + 1, N - 1);
return (1.0f - frac) * octaveForFrequencyTable[index1] +
frac * octaveForFrequencyTable[index2];
return (1.0f - frac) * FrequencyToIndex[index1] +
frac * FrequencyToIndex[index2];
}
WavetableRange WavetableRange::getRangeForOctave(int o)
float MipmapRange::getExactIndexForFrequency(float f)
{
WavetableRange range;
float t = (f < F1) ? 0.0f : (std::log(K * f) / LogB);
return clamp<float>(t, 0, N - 1);
}
Fraction<uint64_t> mant = fp_mantissa(0.0f);
float k = 1.0f / frequencyScaleFactor;
const std::array<float, MipmapRange::N + 1> MipmapRange::IndexToStartFrequency = []()
{
std::array<float, N + 1> table;
for (unsigned t = 0; t < N; ++t)
table[t] = std::exp(t * LogB) / K;
// end value for final table
table[N] = 22050.0;
range.minFrequency = k * fp_from_parts<float>(0, o, 0);
range.maxFrequency = k * fp_from_parts<float>(0, o, mant.den - 1);
return table;
}();
MipmapRange MipmapRange::getRangeForIndex(int o)
{
o = clamp<int>(o, 0, N - 1);
MipmapRange range;
range.minFrequency = IndexToStartFrequency[o];
range.maxFrequency = IndexToStartFrequency[o + 1];
return range;
}
WavetableRange WavetableRange::getRangeForFrequency(float f)
MipmapRange MipmapRange::getRangeForFrequency(float f)
{
int oct = getOctaveForFrequency(f);
return getRangeForOctave(oct);
int index = static_cast<int>(getIndexForFrequency(f));
return getRangeForIndex(index);
}
//------------------------------------------------------------------------------
@ -356,7 +366,7 @@ WavetableMulti WavetableMulti::createForHarmonicProfile(
wm.allocateStorage(tableSize);
for (unsigned m = 0; m < numTables; ++m) {
WavetableRange range = WavetableRange::getRangeForOctave(m);
MipmapRange range = MipmapRange::getRangeForIndex(m);
double freq = range.maxFrequency;

View file

@ -11,6 +11,7 @@
#include "MathHelpers.h"
#include <absl/types/span.h>
#include <absl/container/flat_hash_map.h>
#include <array>
#include <memory>
#include <complex>
@ -57,6 +58,11 @@ public:
*/
void setQuality(int q) { _quality = q; }
/**
Get the quality of this oscillator. (cf. `oscillator_quality`)
*/
int quality() const { return _quality; }
/**
Compute a cycle of the oscillator, with constant frequency.
*/
@ -117,36 +123,42 @@ public:
};
/**
A helper to select ranges of a multi-sampled oscillator, according to the
A helper to select ranges of a mip-mapped wave, according to the
frequency of an oscillator.
The ranges are identified by octave numbers; not octaves in a musical sense,
but as logarithmic divisions of the frequency range.
*/
class WavetableRange {
class MipmapRange {
public:
float minFrequency = 0;
float maxFrequency = 0;
static constexpr unsigned countOctaves = 10;
static constexpr float frequencyScaleFactor = 0.05;
// number of tables in the mipmap
static constexpr unsigned N = 24;
// start frequency of the first table in the mipmap
static constexpr float F1 = 20.0;
// start frequency of the last table in the mipmap
static constexpr float FN = 12000.0;
static unsigned getOctaveForFrequency(float f);
static float getFractionalOctaveForFrequency(float f);
static WavetableRange getRangeForOctave(int o);
static WavetableRange getRangeForFrequency(float f);
static float getIndexForFrequency(float f);
static float getExactIndexForFrequency(float f);
static MipmapRange getRangeForIndex(int o);
static MipmapRange getRangeForFrequency(float f);
// Note: using the frequency factor 0.05, octaves are as follows:
// octave 0: 20 Hz - 40 Hz
// octave 1: 40 Hz - 80 Hz
// octave 2: 80 Hz - 160 Hz
// octave 3: 160 Hz - 320 Hz
// octave 4: 320 Hz - 640 Hz
// octave 5: 640 Hz - 1280 Hz
// octave 6: 1280 Hz - 2560 Hz
// octave 7: 2560 Hz - 5120 Hz
// octave 8: 5120 Hz - 10240 Hz
// octave 9: 10240 Hz - 20480 Hz
// the frequency mapping of the mipmap is defined by formula:
// T(f) = log(k*f)/log(b)
// - T is the table number, converted to index by rounding down
// - f is the oscillation frequency
// - k and b are adjustment parameters according to constant parameters
// k = 1/F1
// b = exp(log(FN/F1)/(N-1))
static const float K;
static const float LogB;
static const std::array<float, 1024> FrequencyToIndex;
static const std::array<float, N + 1> IndexToStartFrequency;
};
/**
@ -159,7 +171,7 @@ public:
unsigned tableSize() const { return _tableSize; }
// number of tables in the multisample
static constexpr unsigned numTables() { return WavetableRange::countOctaves; }
static constexpr unsigned numTables() { return MipmapRange::N; }
// get the N-th table in the multisample
absl::Span<const float> getTable(unsigned index) const
@ -170,7 +182,7 @@ public:
// get the table which is adequate for a given playback frequency
absl::Span<const float> getTableForFrequency(float freq) const
{
return getTable(WavetableRange::getOctaveForFrequency(freq));
return getTable(MipmapRange::getIndexForFrequency(freq));
}
// adjacent tables with interpolation factor between them
@ -186,15 +198,15 @@ public:
DualTable dt;
int index = static_cast<int>(position);
dt.delta = position - index;
dt.table1 = getTablePointer(clamp<int>(index, 0, WavetableRange::countOctaves - 1));
dt.table2 = getTablePointer(clamp<int>(index + 1, 0, WavetableRange::countOctaves - 1));
dt.table1 = getTablePointer(clamp<int>(index, 0, MipmapRange::N - 1));
dt.table2 = getTablePointer(clamp<int>(index + 1, 0, MipmapRange::N - 1));
return dt;
}
// get the pair of tables for the given playback frequency (range checked)
DualTable getInterpolationPairForFrequency(float freq) const
{
float position = WavetableRange::getFractionalOctaveForFrequency(freq);
float position = MipmapRange::getIndexForFrequency(freq);
return getInterpolationPair(position);
}
@ -202,7 +214,9 @@ public:
// the reference sample rate is the minimum value accepted by the DSP
// system (most defavorable wrt. aliasing)
static WavetableMulti createForHarmonicProfile(
const HarmonicProfile& hp, double amplitude, unsigned tableSize = config::tableSize, double refSampleRate = 44100.0);
const HarmonicProfile& hp, double amplitude,
unsigned tableSize = config::tableSize,
double refSampleRate = config::tableRefSampleRate);
// get a tiny silent wavetable with null content for use with oscillators
static const WavetableMulti* getSilenceWavetable();

View file

@ -36,6 +36,7 @@ private:
private:
void valueChangedWave(int value);
void valueChangedQuality(int value);
void buttonClickedPlaySweep();
private:
@ -46,6 +47,7 @@ private:
sfz::WavetableOscillator fOsc;
unsigned fWavePlaying = 0;
std::atomic<int> fNewWavePending { -1 };
std::atomic<int> fNewQualityPending { -1 };
std::atomic<bool> fStartNewSweep { false };
static constexpr float sweepMin = 0.0;
@ -88,13 +90,13 @@ bool DemoApp::initSound()
fTmpFrequency.reset(new float[bufferSize]);
fMulti[0] = sfz::WavetableMulti::createForHarmonicProfile(
sfz::HarmonicProfile::getSine(), 1.0, 2048);
sfz::HarmonicProfile::getSine(), sfz::config::amplitudeSine, 2048);
fMulti[1] = sfz::WavetableMulti::createForHarmonicProfile(
sfz::HarmonicProfile::getTriangle(), 1.0, 2048);
sfz::HarmonicProfile::getTriangle(), sfz::config::amplitudeTriangle, 2048);
fMulti[2] = sfz::WavetableMulti::createForHarmonicProfile(
sfz::HarmonicProfile::getSaw(), 1.0, 2048);
sfz::HarmonicProfile::getSaw(), sfz::config::amplitudeSaw, 2048);
fMulti[3] = sfz::WavetableMulti::createForHarmonicProfile(
sfz::HarmonicProfile::getSquare(), 1.0, 2048);
sfz::HarmonicProfile::getSquare(), sfz::config::amplitudeSquare, 2048);
fClient.reset(client);
@ -128,10 +130,21 @@ void DemoApp::initWindow()
fUi.valWave->addItem(tr("3 - Saw"));
fUi.valWave->addItem(tr("4 - Square"));
fUi.valQuality->addItem(tr("1 - Nearest"));
fUi.valQuality->addItem(tr("2 - Linear"));
fUi.valQuality->addItem(tr("3 - High"));
fUi.valQuality->addItem(tr("4 - Dual-High"));
fUi.valQuality->setCurrentIndex(fOsc.quality());
connect(
fUi.valWave, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, [this](int index) { valueChangedWave(index); });
connect(
fUi.valQuality, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, [this](int index) { valueChangedQuality(index); });
connect(
fUi.btnPlaySweep, &QPushButton::clicked,
this, [this]() { buttonClickedPlaySweep(); });
@ -152,6 +165,10 @@ int DemoApp::processAudio(jack_nframes_t nframes, void* cbdata)
if (newWave != -1)
self->fWavePlaying = newWave;
int newQuality = self->fNewQualityPending.exchange(-1);
if (newQuality != -1)
osc.setQuality(newQuality);
osc.setWavetable(&self->fMulti[self->fWavePlaying]);
float* left = reinterpret_cast<float*>(
@ -183,6 +200,11 @@ void DemoApp::valueChangedWave(int value)
fNewWavePending.store(value);
}
void DemoApp::valueChangedQuality(int value)
{
fNewQualityPending.store(value);
}
void DemoApp::buttonClickedPlaySweep()
{
fStartNewSweep.store(true);

View file

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>170</width>
<width>255</width>
<height>103</height>
</rect>
</property>
@ -20,6 +20,13 @@
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Select quality</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Play sweep</string>
@ -30,6 +37,9 @@
<widget class="QComboBox" name="valWave"/>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="valQuality"/>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="btnPlaySweep">
<property name="minimumSize">
<size>
@ -38,7 +48,8 @@
</size>
</property>
<property name="icon">
<iconset theme="media-playback-start"/>
<iconset theme="media-playback-start">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>

View file

@ -13,47 +13,40 @@
TEST_CASE("[Wavetables] Frequency ranges")
{
int cur_oct = std::numeric_limits<int>::min();
int min_oct = std::numeric_limits<int>::max();
int max_oct = std::numeric_limits<int>::min();
int cur_index = std::numeric_limits<int>::min();
int min_index = std::numeric_limits<int>::max();
int max_index = std::numeric_limits<int>::min();
for (int note = 0; note < 128; ++note) {
double f = midiNoteFrequency(note);
int oct = sfz::WavetableRange::getOctaveForFrequency(f);
float fractionalIndex = sfz::MipmapRange::getExactIndexForFrequency(f);
int index = static_cast<int>(fractionalIndex);
REQUIRE(oct >= 0);
REQUIRE(oct < sfz::WavetableRange::countOctaves);
REQUIRE(index >= 0);
REQUIRE(static_cast<unsigned>(index) < sfz::MipmapRange::N);
REQUIRE(oct >= cur_oct);
cur_oct = oct;
float lerpFractionalIndex = sfz::MipmapRange::getIndexForFrequency(f);
int lerpIndex = static_cast<int>(lerpFractionalIndex);
min_oct = std::min(min_oct, oct);
max_oct = std::max(max_oct, oct);
// approximation should be equal or off by 1 table in worst cases
bool lerpIndexValid = (lerpIndex - index) == 0 || (lerpIndex - index) == -1;
REQUIRE(lerpIndexValid);
sfz::WavetableRange range = sfz::WavetableRange::getRangeForOctave(oct);
REQUIRE((f >= range.minFrequency || oct == 0));
REQUIRE((f <= range.maxFrequency || oct == sfz::WavetableRange::countOctaves - 1));
REQUIRE(index >= cur_index);
cur_index = index;
min_index = std::min(min_index, index);
max_index = std::max(max_index, index);
sfz::MipmapRange range = sfz::MipmapRange::getRangeForIndex(index);
REQUIRE((f >= range.minFrequency || index == 0));
REQUIRE((f <= range.maxFrequency || index == sfz::MipmapRange::N - 1));
}
// check ranges to be decently adjusted to the MIDI frequency range
REQUIRE(min_oct == 0);
REQUIRE(max_oct == sfz::WavetableRange::countOctaves - 1);
}
TEST_CASE("[Wavetables] Octave number lookup")
{
for (int note = 0; note < 128; ++note) {
double f = midiNoteFrequency(note);
float ref = std::log2(f * sfz::WavetableRange::frequencyScaleFactor);
float oct = sfz::WavetableRange::getFractionalOctaveForFrequency(f);
ref = clamp<float>(ref, 0, sfz::WavetableRange::countOctaves - 1);
oct = clamp<float>(oct, 0, sfz::WavetableRange::countOctaves - 1);
REQUIRE(oct == Approx(ref).margin(0.03f));
}
REQUIRE(min_index == 0);
REQUIRE(max_index == sfz::MipmapRange::N - 1);
}
TEST_CASE("[Wavetables] Wavetable sound files: Surge")