oscf
Overview
oscf
is a table-lookup oscillator using floating point
precision and linear interpolation. This is nearly identical
to osc, except that the internal phasor used to
keep track of table position is a floating point value
SKFLT
instead of a fixed point integer. What this enables
is oscillator sizes that aren't power-of-two. This allows
usage of waveforms like the Adventure Kid Waveforms.
Compared to fixed point, floating point numerical representation is quite complicated and has some gotchas. The author will attempt to navigate the known gotchas as best as possible.
Files
oscf
tanlges to two files: oscf.h
and oscf.c
. If
SK_OSCF_PRIV
is defined, the struct is exposed.
#ifndef SK_OSCF_H
#define SK_OSCF_H
#ifndef SKFLT
#define SKFLT float
#endif
<<typedefs>>
<<funcdefs>>
#ifdef SK_OSCF_PRIV
<<structs>>
#endif
#endif
#include <math.h>
#define SK_OSCF_PRIV
#include "oscf.h"
<<funcs>>
Struct and Contents
Declaration
Everything is wrapped inside of a struct called sk_osc
.
typedef struct sk_oscf sk_oscf;
struct sk_oscf {
<<sk_oscf>>
};
Phasor
The heartbeat of an oscillator is what is known as an
phasor
: a ramp signal that goes from 0-1 at a specified
frequency.
Phasor state is stored in a value called phs
.
SKFLT phs;
oscf->phs = iphs;
The phs
value gets incremented by a floating point value
inc
every sample.
SKFLT inc;
oscf->inc = 0;
Table
The table is stored in a value called tab
with a size
sz
.
SKFLT *tab;
unsigned long sz;
oscf->tab = tab;
oscf->sz = sz;
Sampling Rate
A copy of the sampling rate is stored. It is needed in order to compute the incrementor component in the phasor.
unsigned int sr;
oscf->sr = sr;
Initialization
oscf is initialized with sk_oscf_init
. It expects a
pre-allocated table tab
and known size sz
, as well
as an initial phase iphs
.
void sk_oscf_init(sk_oscf *oscf,
unsigned int sr,
SKFLT *tab,
unsigned long sz,
SKFLT iphs);
void sk_oscf_init(sk_oscf *oscf,
unsigned int sr,
SKFLT *tab,
unsigned long sz,
SKFLT iphs)
{
<<init>>
}
Frequency
The frequency of oscf can be set with sk_oscf_freq
.
void sk_oscf_freq(sk_oscf *oscf, SKFLT freq);
void sk_oscf_freq(sk_oscf *oscf, SKFLT freq)
{
oscf->freq = freq;
}
freq uses parameter caching to update internal values when the frequency is changed.
SKFLT freq;
SKFLT lfreq;
sk_oscf_freq(oscf, 440);
oscf->lfreq = -1;
Computation
A single sample of audio is computed with sk_oscf_tick
.
SKFLT sk_oscf_tick(sk_oscf *oscf);
SKFLT sk_oscf_tick(sk_oscf *oscf)
{
SKFLT out;
unsigned long ipos;
SKFLT fpos;
SKFLT x[2];
SKFLT phs;
phs = oscf->phs;
<<update_freq>>
<<get_position>>
<<get_values>>
<<interpolate>>
<<update_phase>>
<<bounds_checking>>
oscf->phs = phs;
return out;
}
To begin, the frequency parameter is checked to see if the incrementor needs to be updated.
This is one way to think about computing the increment amount:
The phasor needs to move at a certain rate, which means it needs to go from 0 to 1 over a certain period of time.
Inverting the frequency value gives the cycle time in seconds.
Multiplying by the sampling rate converts it into units of samples.
Inverting it again divides it into equal chunks that add up 1: our incrementor value.
But wait! That's a lot of operations. Back-tracking, this
whole operation can be simplified to: f/sr
. where f
is
the frequency, and sr
is the sampling rate.
if (oscf->lfreq != oscf->freq) {
oscf->lfreq = oscf->freq;
oscf->inc = oscf->freq / (SKFLT)oscf->sr;
}
Next up is to get the table lookup position. There
are two parts. The fractional component fpos
and
the integer component ipos
. The position is obtained
by scaling the phasor value.
Fun fact: keeping the phasor in a unit range and scaling this way is numerically ideal for floating point.
fpos = phs * oscf->sz;
ipos = floor(fpos);
fpos = fpos - ipos;
Table positions at ipos
and ipos + 1
are retrieved
from tab
. If ipos
is at the end of the table, it wraps
around.
The phasor is set to be in range 0 and 1 exactly by the time it is here, so there is no reason to normally expect out-of-bounds samples here.
x[0] = oscf->tab[ipos];
if (ipos >= (oscf->sz - 1)) {
x[1] = oscf->tab[0];
} else {
x[1] = oscf->tab[ipos + 1];
}
This is your run-of-the-mill linear interpolation. A crossfade in some circles.
out = fpos * x[1] + (1 - fpos) * x[0];
Update the internal phase state using the incrementor.
phs += oscf->inc;
Care must be done to ensure
that the phase is within the range 0 and 1. while loops
are used in place of things like fmod
because they are
less expensive.
A note that the phase cannot be exactly 1, hence the greather than or equal to. (This mistake was figured out the hard way by the author).
while (phs >= 1) phs--;
while (phs < 0) phs++;
Variation: external phase control
The function sk_oscf_tick_extphs
can be used in place
of sk_oscf_tick
to compute a sample of of oscf with an
external phasor. This can be used to implement things like
hard sync and phase distortion synthesis. Note that internal
frequency control is disabled in this mode.
SKFLT sk_oscf_tick_extphs(sk_oscf *oscf, SKFLT phs);
The function re-uses many of the same codeblocks used
in sk_oscf_tick
. The big change is that bounds checking
happens before the computation, rather than after. This
allows intentional wrap-around for things like hard sync
oscillation.
SKFLT sk_oscf_tick_extphs(sk_oscf *oscf, SKFLT phs)
{
SKFLT out;
unsigned long ipos;
SKFLT fpos;
SKFLT x[2];
out = 0;
<<bounds_checking>>
<<update_freq>>
<<get_position>>
<<get_values>>
<<interpolate>>
return out;
}