333 lines · 7.1 KB · Text
RawDownload
#+TITLE: oscf* Overview=oscf= is a table-lookup oscillator using floating pointprecision and linear interpolation. This is nearly identicalto @!(ref "osc")!@, except that the internal phasor used tokeep track of table position is a floating point value=SKFLT= instead of a fixed point integer. What this enablesis oscillator sizes that aren't power-of-two. This allowsusage of waveforms like the [[https://www.adventurekid.se/akrt/waveforms/adventure-kid-waveforms/][Adventure Kid Waveforms]]. Compared to fixed point, floating point numericalrepresentation is quite complicated and has some gotchas.The author will attempt to navigate the known gotchas asbest as possible.* Files=oscf= tanlges to two files: =oscf.h= and =oscf.c=. If=SK_OSCF_PRIV= is defined, the struct is exposed. #+NAME: oscf.h#+BEGIN_SRC c :tangle oscf.h#ifndef SK_OSCF_H#define SK_OSCF_H#ifndef SKFLT#define SKFLT float#endif<<typedefs>><<funcdefs>>#ifdef SK_OSCF_PRIV<<structs>>#endif#endif#+END_SRC #+NAME: oscf.c#+BEGIN_SRC c :tangle oscf.c#include <math.h>#define SK_OSCF_PRIV#include "oscf.h"<<funcs>>#+END_SRC* Struct and Contents** DeclarationEverything is wrapped inside of a struct called =sk_osc=.#+NAME: typedefs#+BEGIN_SRC ctypedef struct sk_oscf sk_oscf;#+END_SRC #+NAME: structs#+BEGIN_SRC cstruct sk_oscf {    <<sk_oscf>>};#+END_SRC** PhasorThe heartbeat of an oscillator is what is known as an=phasor=: a ramp signal that goes from 0-1 at a specifiedfrequency. Phasor state is stored in a value called =phs=. #+NAME: sk_oscf#+BEGIN_SRC cSKFLT phs;#+END_SRC #+NAME: init#+BEGIN_SRC coscf->phs = iphs;#+END_SRC The =phs= value gets incremented by a floating point value=inc= every sample. #+NAME: sk_oscf#+BEGIN_SRC cSKFLT inc;#+END_SRC #+NAME: init#+BEGIN_SRC coscf->inc = 0;#+END_SRC** TableThe table is stored in a value called =tab= with a size=sz=. #+NAME: sk_oscf#+BEGIN_SRC cSKFLT *tab;unsigned long sz;#+END_SRC #+NAME: init#+BEGIN_SRC coscf->tab = tab;oscf->sz = sz;#+END_SRC** Sampling RateA copy of the sampling rate is stored. It is needed in orderto compute the incrementor component in the phasor. #+NAME: sk_oscf#+BEGIN_SRC cunsigned int sr;#+END_SRC #+NAME: init#+BEGIN_SRC coscf->sr = sr;#+END_SRC* Initializationoscf is initialized with =sk_oscf_init=. It expects apre-allocated table =tab= and known size =sz=, as wellas an initial phase =iphs=. #+NAME: funcdefs#+BEGIN_SRC cvoid sk_oscf_init(sk_oscf *oscf,                  unsigned int sr,                  SKFLT *tab,                  unsigned long sz,                  SKFLT iphs);#+END_SRC #+NAME: funcs#+BEGIN_SRC cvoid sk_oscf_init(sk_oscf *oscf,                  unsigned int sr,                  SKFLT *tab,                  unsigned long sz,                  SKFLT iphs){    <<init>>}#+END_SRC* FrequencyThe frequency of oscf can be set with =sk_oscf_freq=. #+NAME: funcdefs#+BEGIN_SRC cvoid sk_oscf_freq(sk_oscf *oscf, SKFLT freq);#+END_SRC #+NAME: funcs#+BEGIN_SRC cvoid sk_oscf_freq(sk_oscf *oscf, SKFLT freq){    oscf->freq = freq;}#+END_SRC freq uses parameter caching to update internal valueswhen the frequency is changed. #+NAME: sk_oscf#+BEGIN_SRC cSKFLT freq;SKFLT lfreq;#+END_SRC #+NAME: init#+BEGIN_SRC csk_oscf_freq(oscf, 440);oscf->lfreq = -1;#+END_SRC* ComputationA single sample of audio is computed with =sk_oscf_tick=. #+NAME: funcdefs#+BEGIN_SRC cSKFLT sk_oscf_tick(sk_oscf *oscf);#+END_SRC #+NAME: funcs#+BEGIN_SRC cSKFLT 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;}#+END_SRC To begin, the frequency parameter is checked to see if theincrementor needs to be updated. This is one way to think about computing the incrementamount: The phasor needs to move at a certain rate, which means itneeds to go from 0 to 1 over a certain period of time. Inverting the frequency value gives the cycle time inseconds. Multiplying by the sampling rate converts it into units ofsamples. Inverting it again divides it into equal chunks that add up1: our incrementor value. But wait! That's a lot of operations. Back-tracking, thiswhole operation can be simplified to: =f/sr=. where =f= isthe frequency, and =sr= is the sampling rate. #+NAME: update_freq#+BEGIN_SRC cif (oscf->lfreq != oscf->freq) {    oscf->lfreq = oscf->freq;     oscf->inc = oscf->freq / (SKFLT)oscf->sr;}#+END_SRC Next up is to get the table lookup position. Thereare two parts. The fractional component =fpos= andthe integer component =ipos=. The position is obtainedby scaling the phasor value. Fun fact: keeping the phasor in a unit range and scalingthis way is numerically ideal for floating point. #+NAME: get_position#+BEGIN_SRC cfpos = phs * oscf->sz;ipos = floor(fpos);fpos = fpos - ipos;#+END_SRC Table positions at =ipos= and =ipos + 1= are retrievedfrom =tab=. If =ipos= is at the end of the table, it wrapsaround. The phasor is set to be in range 0 and 1 exactly by the timeit is here, so there is no reason to normally expectout-of-bounds samples here. #+NAME: get_values#+BEGIN_SRC cx[0] = oscf->tab[ipos]; if (ipos >= (oscf->sz - 1)) {    x[1] = oscf->tab[0];} else {    x[1] = oscf->tab[ipos + 1];}#+END_SRC This is your run-of-the-mill linear interpolation. Acrossfade in some circles. #+NAME: interpolate#+BEGIN_SRC cout = fpos * x[1] + (1 - fpos) * x[0];#+END_SRC Update the internal phase state using the incrementor. #+NAME: update_phase#+BEGIN_SRC cphs += oscf->inc;#+END_SRC Care must be done to ensurethat the phase is within the range 0 and 1. while loopsare used in place of things like =fmod= because they areless expensive. A note that the phase cannot be *exactly* 1, hence thegreather than *or* equal to. (This mistake was figured outthe hard way by the author). #+NAME: bounds_checking#+BEGIN_SRC cwhile (phs >= 1) phs--;while (phs < 0) phs++;#+END_SRC* Variation: external phase control@!(marker "external_phase_control")!@The function =sk_oscf_tick_extphs= can be used in placeof =sk_oscf_tick= to compute a sample of of oscf with anexternal phasor. This can be used to implement things likehard sync and @!(ref "phasewarp""phase distortion synthesis")!@. Note that internalfrequency control is disabled in this mode. #+NAME: funcdefs#+BEGIN_SRC cSKFLT sk_oscf_tick_extphs(sk_oscf *oscf, SKFLT phs);#+END_SRC The function re-uses many of the same codeblocks usedin =sk_oscf_tick=. The big change is that bounds checkinghappens *before* the computation, rather than after. Thisallows intentional wrap-around for things like hard syncoscillation. #+NAME: funcs#+BEGIN_SRC cSKFLT 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;}#+END_SRC

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t