Higher quality wavetable mipmaps

This commit is contained in:
Jean Pierre Cimalando 2020-08-09 05:19:16 +02:00
parent 1d3346012f
commit 13752d8043
4 changed files with 115 additions and 66 deletions

View file

@ -283,65 +283,70 @@ 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) {
double r = i * (1.0 / (table.size() - 1));
double f = F1 + r * (FN - F1);
double t = std::log(K * f) / LogB;
table[i] = clamp<float>(t, 0, N - 1);
}
// 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)
const std::array<float, MipmapRange::N + 1> MipmapRange::IndexToStartFrequency = []()
{
WavetableRange range;
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;
Fraction<uint64_t> mant = fp_mantissa(0.0f);
float k = 1.0f / frequencyScaleFactor;
return table;
}();
range.minFrequency = k * fp_from_parts<float>(0, o, 0);
range.maxFrequency = k * fp_from_parts<float>(0, o, mant.den - 1);
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 +361,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,41 @@ 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 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 +170,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 +181,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 +197,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);
}

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>