History

importing some org files

Paul Batchelor committed 6 years ago

Commit 5f348381 parent 8888fbf12 files · +3764 −1Browse files

Unified · Split

12 files
  1. README.md +2 −1
  2. bigverb.org +1035 −0
  3. dcblocker.org +118 −0
  4. fmpair.org +576 −0
  5. index.org +21 −0
  6. modalres.org +306 −0
  7. osc.org +409 −0
  8. peakeq.org +358 −0
  9. phasewarp.org +104 −0
  10. rline.org +403 −0
  11. scrubber.org +56 −0
  12. vardelay.org +376 −0
README.md+2−1
@@ -1 +1,2 @@
1# sndkit1# sndkit
2sndkit is a sonic toolkit for everywhere.
bigverb.org+1035−0
@@ -0,0 +1,1035 @@
1#+TITLE: Bigverb
2* Introduction
3Bigverb is an implementation of a classic waveguide
4reverberator. It is composed up
5of 8 feedback delay lines running in parallel, in what is
6known as a [[https://ccrma.stanford.edu/~jos/cfdn/Feedback_Delay_Networks.html][Feedback Delay Network]]. It is largely inspired by
7the works Julius O. Smith, and the work he did in using
8waveguide networks. In particular, this algorithm is based
9on ideas in a 1985 paper called "A New Approach to Digital
10Reverberation using Waveguide Networks".
11
12Bigverb, as the name implies, has a very nice warm sound
13when set to be a very large space, and is ideal for use in
14ambient style music. Bigverb is not designed to sound
15realistic, and is not an ideal choice for anything that
16needs room modelling.
17* Distinct Characterstics
18The "sound" of this algorithm comes from a few major
19things.
20** Use of Jitter Modulation
21Every delay line has a bit of jitter applied to
22the delay time, which causes a little of pitch modulation.
23Several schroeder and FDN-style reverb designs use this
24kind of modulation, such as the Mutable instruments
25clouds reverb, and the tank reverb described in the Dattorro
26paper "Effect design, part 1: reverberator and other filters".
27Such modulation creates the illusion of the reverb being
28a higher-order system than it actual (aka: a more complex
29reverb algorithm with more delay lines).
30While these designs tend to use a sinusoidal LFO on only a
31few delay lines, the Costello topology applies non-correlated
32modulation to every single delay line. As a result, you get
33a very dense-sounding reverb output.
34** Parameter Tunings
35The parameter tunings of this reverb add to the
36particular character of this sound. All units are set in
37such a way so that they can be stored as integer values.
38
39The tuning consists of 8 parameter sets, corresponding to
40each delay line "unit". A set has the following parameters: delay time
41(=delay=) (in units of samples with a samplerate of
4244.1kHz), the amount of variation in delay time to apply
43(=drift=) (in units of tenth milliseconds, in order to use
44integer values), the random variation frequency (=randfreq=)
45(in Hz multiplied by 1000 to avoid decimals),
46as well as a starting seed for the internal RNG.
47
48The credit for these parameter tunings (and this topology)
49go to Sean Costello, who also originally designed this
50reverb algorithm in 1999. This work would later pave the way
51for his famous commercial reverb plugins and algorithms
52published by his company ValhallaDSP.
53
54#+NAME: parameter_set
55#+BEGIN_SRC c
56struct bigverb_paramset {
57 int delay; /* in samples, 44.1 kHz */
58 int drift; /* 1/10 milliseconds */
59 int randfreq; /* Hertz * 1000 */
60 int seed;
61};
62
63static const struct bigverb_paramset params[8] = {
64 {0x09a9, 0x0a, 0xc1c, 0x07ae},
65 {0x0acf, 0x0b, 0xdac, 0x7333},
66 {0x0c91, 0x11, 0x456, 0x5999},
67 {0x0de5, 0x06, 0xf85, 0x2666},
68 {0x0f43, 0x0a, 0x925, 0x50a3},
69 {0x101f, 0x0b, 0x769, 0x5999},
70 {0x085f, 0x11, 0x37b, 0x7333},
71 {0x078d, 0x06, 0xc95, 0x3851}
72};
73#+END_SRC
74** Delay Line Configuration
75Normally, this family of reverbs is a combination of allpass
76filters and comb filters in various series/parallel
77combinations. This implementation has a far simpler
78topology: it is essentially (but not *quite*) 8 feedback delay
79lines in parallel with a 1-pole lowpass IIR filter for tone
80control. If it weren't for the massive amount delay
81modulation, this would be a pretty metallic sounding reverb!
82* Top Level Files and Structs
83** Files to Tangle To
84This Bigverb document tanlges to two files:
85=bigverb.c= and =bigverb.h=.
86
87=bigverb.h= contains typedefs, structs, and forward
88function declarations. To enable structs, define the
89macro =SK_BIGVERB_PRIV=.
90
91#+NAME: bigverb.c
92#+BEGIN_SRC c :tangle bigverb.c
93#include <stdlib.h>
94#include <stdio.h>
95#include <math.h>
96#define SK_BIGVERB_PRIV
97#include "bigverb.h"
98<<parameter_set>>
99<<fdelay_constants>>
100<<static_funcdefs>>
101<<funcs>>
102#+END_SRC
103
104#+NAME: bigverb.h
105#+BEGIN_SRC c :tangle bigverb.h
106#ifndef SK_BIGVERB_H
107#define SK_BIGVERB_H
108
109#ifndef SKFLT
110#define SKFLT float
111#endif
112<<typedefs>>
113<<funcdefs>>
114
115#ifdef SK_BIGVERB_PRIV
116<<main_struct>>
117#endif
118#endif
119#+END_SRC
120** The Bigverb Struct
121An instance of Bigverb is contained inside of a struct
122called =sk_bigverb=.
123#+NAME: typedefs
124#+BEGIN_SRC c
125typedef struct sk_bigverb sk_bigverb;
126#+END_SRC
127
128#+NAME: main_struct
129#+BEGIN_SRC c
130<<delay_struct>>
131struct sk_bigverb {
132 int sr;
133 <<sk_bigverb>>
134};
135#+END_SRC
136* Setup and Cleanup
137A new instance of bigverb is created with
138=sk_bigverb_new=. The only argument required is the sampling
139rate. If something goes wrong, this will return =NULL=.
140
141#+NAME: funcdefs
142#+BEGIN_SRC c
143sk_bigverb * sk_bigverb_new(int sr);
144#+END_SRC
145
146#+NAME: funcs
147#+BEGIN_SRC c
148sk_bigverb * sk_bigverb_new(int sr)
149{
150 sk_bigverb *bv;
151
152 bv = calloc(1, sizeof(sk_bigverb));
153
154 bv->sr = sr;
155 <<init_variables>>
156 <<setup_delay_lines>>
157
158 return bv;
159}
160#+END_SRC
161
162When it is done being used, bigverb must be cleanly freed
163with =sk_bigverb_del=.
164
165#+NAME: funcdefs
166#+BEGIN_SRC c
167void sk_bigverb_del(sk_bigverb *bv);
168#+END_SRC
169
170#+NAME: funcs
171#+BEGIN_SRC c
172void sk_bigverb_del(sk_bigverb *bv)
173{
174 <<cleanup>>
175 free(bv);
176 bv = NULL;
177}
178#+END_SRC
179* High level parameters
180High level parametric control of bigverb includes
181"size" and "cutoff". Set parameters before computing
182audio. These are are just floating point values contained
183in the struct that can be indirectly set with setters in
184situations where the struct is opaque.
185** Size
186Set the reverb size with =sk_bigverb_size=
187
188#+NAME: funcdefs
189#+BEGIN_SRC c
190void sk_bigverb_size(sk_bigverb *bv, SKFLT size);
191#+END_SRC
192
193Size is a variable between 0-1, which
194controls the feedback level for the delay line.
195
196The =size= parameter is stored as a variable called
197=size=, and is set to be a pretty sounding value of
198=0.93=.
199
200#+NAME: sk_bigverb
201#+BEGIN_SRC c
202SKFLT size;
203#+END_SRC
204
205#+NAME: init_variables
206#+BEGIN_SRC c
207sk_bigverb_size(bv, 0.93);
208#+END_SRC
209
210#+NAME: funcs
211#+BEGIN_SRC c
212void sk_bigverb_size(sk_bigverb *bv, SKFLT size)
213{
214 bv->size = size;
215}
216#+END_SRC
217** Cutoff
218The tone of bigverb can be set with =sk_bigverb_cutoff=.
219
220#+NAME: funcdefs
221#+BEGIN_SRC c
222void sk_bigverb_cutoff(sk_bigverb *bv, SKFLT cutoff);
223#+END_SRC
224
225=cutoff= is a parameter in Hz that determines the overall
226timbre of the reverb. This controls the cutoff frequency of
227the one pole lowpass filter applied to the reverb.
228
229It is set to be a default value of 10kHz, or 10,000 hz.
230
231#+NAME: init_variables
232#+BEGIN_SRC c
233sk_bigverb_cutoff(bv, 10000.0);
234#+END_SRC
235
236Cutoff uses caching in order to monitor if the parameter
237has changed. It does this in order to prevent needing to
238compute filter coefficients every sample. The main variable
239to be set is =cutoff=, and the cached variable is =pcutoff=.
240At the beginning, =pcutoff= is set to be a negative value,
241which will cause bigverb to calculate coefficients in the
242first call to the tick function after initialization.
243
244#+NAME: sk_bigverb
245#+BEGIN_SRC c
246SKFLT cutoff;
247SKFLT pcutoff;
248#+END_SRC
249
250#+NAME: init_variables
251#+BEGIN_SRC c
252bv->pcutoff = -1;
253#+END_SRC
254
255#+NAME: funcs
256#+BEGIN_SRC c
257void sk_bigverb_cutoff(sk_bigverb *bv, SKFLT cutoff)
258{
259 bv->cutoff = cutoff;
260}
261#+END_SRC
262* Filter
263State in a constant called =filt=.
264
265#+NAME: sk_bigverb
266#+BEGIN_SRC c
267SKFLT filt;
268#+END_SRC
269
270#+NAME: init_variables
271#+BEGIN_SRC c
272bv->filt = 1.0;
273#+END_SRC
274* Computing Audio
275After bigverb has been initialized, it is ready to
276process audio. This implementation uses what is known
277as a =tick= function, or a function that computes audio
278one sample at a time instead of one block at a time. This
279simplifies the implementation at the cost of a little bit
280of performance overhead, depending on the compiler and
281optimization settings.
282** Top-Level Tick Function
283The function to tick one sample unit of audio is done with
284=sk_bigverb_tick=. It takes in two stereo input
285values, and returns two stereo output values.
286
287#+NAME: funcdefs
288#+BEGIN_SRC c
289void sk_bigverb_tick(sk_bigverb *bv,
290 SKFLT inL, SKFLT inR,
291 SKFLT *outL, SKFLT *outR);
292#+END_SRC
293
294#+NAME: funcs
295#+BEGIN_SRC c
296void sk_bigverb_tick(sk_bigverb *bv,
297 SKFLT inL, SKFLT inR,
298 SKFLT *outL, SKFLT *outR)
299{
300 /* TODO: implement */
301 SKFLT lsum, rsum;
302
303 lsum = 0;
304 rsum = 0;
305
306 <<update_filter_coefficients>>
307 <<calculate_junction_pressure>>
308 <<compute_delay_bank>>
309
310 *outL = lsum;
311 *outR = rsum;
312}
313#+END_SRC
314** Updating filter coefficients
315Bigverb uses parameter caching for the =cutoff= parameter in
316order to save on computation time.
317
318Any time =cutoff= changes, the filter coefficients must be
319updated. This happens in the tick function, before any
320computation happens.
321
322The filter is a simple 1-pole IIR lowpass filter whose
323difference equation been reduced to only require a single
324parameter. This in turn then gets used in each filter delay
325line.
326
327#+NAME: update_filter_coefficients
328#+BEGIN_SRC c
329if (bv->pcutoff != bv->cutoff) {
330 bv->pcutoff = bv->cutoff;
331 bv->filt = 2.0 - cos(bv->pcutoff * 2 * M_PI / bv->sr);
332 bv->filt = bv->filt - sqrt(bv->filt * bv->filt - 1.0);
333}
334#+END_SRC
335** Calculating Resultant Junction Pressure Amount
336The resultant junction pressure amount is calculated from
337the delay bank, and then factored into the input signals.
338
339Sum of all the delay line signals, and scaled by 0.25, or
3402/N, where N is the number of delay lines (8).
341
342#+NAME: calculate_junction_pressure
343#+BEGIN_SRC c
344{
345 int i;
346 SKFLT jp;
347
348 jp = 0;
349
350 for (i = 0; i < 8; i++) {
351 jp += bv->delay[i].y;
352 }
353
354 jp *= 0.25;
355
356 inL = jp + inL;
357 inR = jp + inR;
358}
359#+END_SRC
360** Computing the delay bank
361The delay bank is then computed. Each delay line is computed
362and summed with either the left or right input signal, and
363then sent to a corresponding left or right channel.
364
365At the end, a final scaling out of the output happens. This
366is hard coded to be 35 percent.
367
368#+NAME: compute_delay_bank
369#+BEGIN_SRC c
370{
371 int i;
372 for (i = 0; i < 8; i++) {
373 if (i & 1) {
374 rsum += delay_compute(&bv->delay[i],
375 inR,
376 bv->size,
377 bv->filt,
378 bv->sr);
379 } else {
380 lsum += delay_compute(&bv->delay[i],
381 inL,
382 bv->size,
383 bv->filt,
384 bv->sr);
385 }
386 }
387}
388rsum *= 0.35f;
389lsum *= 0.35f;
390#+END_SRC
391* The Feedback Delay Line Bank
3928 delay units come together to make the delay line bank.
393Each is initialized using one of the parameter sets.
394** Memory Allocation + Setup
395#+NAME: sk_bigverb
396#+BEGIN_SRC c
397SKFLT *buf;
398#+END_SRC
399
400#+NAME: init_variables
401#+BEGIN_SRC c
402bv->buf = NULL;
403#+END_SRC
404
405#+NAME: setup_delay_lines
406#+BEGIN_SRC c
407{
408unsigned long total_size;
409int i;
410SKFLT *buf;
411
412total_size = 0;
413buf = NULL;
414<<calculate_pool_size>>
415<<allocate_memory>>
416<<initialize_delay_banks>>
417}
418#+END_SRC
419The delay bank is the abstraction in charge of properly
420allocating all the memory needed for the buffers.
421
422Memory is allocated in one giant chunk, and then divied up
423to each delay line.
424
425The total memory size is obtained by summing all the delay
426times. These times are stored as fixed delay times in units
427of samples. These parameters assume a sampling rate of
42844.1kHz. If this is not the case, this value must be scaled
429accordingly, and then truncated to be an integer. This value
430is used again to properly slice up the big memory chunk.
431
432#+NAME: static_funcdefs
433#+BEGIN_SRC c
434static int get_delay_size(const struct bigverb_paramset *p, int sr);
435#+END_SRC
436
437#+NAME: funcs
438#+BEGIN_SRC c
439static int get_delay_size(const struct bigverb_paramset *p, int sr)
440{
441 SKFLT sz;
442 sz = (SKFLT)p->delay/44100 + (p->drift * 0.0001) * 1.125;
443 return floor(16 + sz*sr);
444}
445#+END_SRC
446
447#+NAME: calculate_pool_size
448#+BEGIN_SRC c
449for (i = 0; i < 8; i++) {
450 total_size += get_delay_size(&params[i], sr);
451}
452#+END_SRC
453
454Allocation is done with =calloc=, which zeros out the memory
455as well. This memory will eventually be freed in
456=sk_bigverb_del=.
457
458#+NAME: allocate_memory
459#+BEGIN_SRC c
460buf = calloc(1, sizeof(SKFLT) * total_size);
461bv->buf = buf;
462#+END_SRC
463
464#+NAME: cleanup
465#+BEGIN_SRC c
466free(bv->buf);
467#+END_SRC
468
469#+NAME: sk_bigverb
470#+BEGIN_SRC c
471sk_bigverb_delay delay[8];
472#+END_SRC
473
474#+NAME: initialize_delay_banks
475#+BEGIN_SRC c
476{
477 unsigned long bufpos;
478 bufpos = 0;
479 for (i = 0; i < 8; i++) {
480 unsigned int sz;
481 sz = get_delay_size(&params[i], sr);
482
483 delay_init(&bv->delay[i], &params[i],
484 &buf[bufpos], sz, sr);
485 bufpos += sz;
486 }
487}
488#+END_SRC
489** A Single Delay Line Unit
490A delay unit in a bank consists of variable delay line with
491cubic interpolation with a 1 pole low-pass filter for tone
492control, whose frequency is determined using a master
493parameter, as well as a jitter generator. Feedback as well.
494*** Struct Declaration
495A delay unit is known as a struct called =sk_bigverb_delay=.
496
497#+NAME: typedefs
498#+BEGIN_SRC c
499typedef struct sk_bigverb_delay sk_bigverb_delay;
500#+END_SRC
501
502#+NAME: delay_struct
503#+BEGIN_SRC c
504struct sk_bigverb_delay {
505 <<bigverb_delay>>
506};
507#+END_SRC
508*** Initialization
509#+NAME: static_funcdefs
510#+BEGIN_SRC c
511static void delay_init(sk_bigverb_delay *d,
512 const struct bigverb_paramset *p,
513 SKFLT *buf,
514 size_t sz,
515 int sr);
516#+END_SRC
517
518#+NAME: funcs
519#+BEGIN_SRC c
520static void delay_init(sk_bigverb_delay *d,
521 const struct bigverb_paramset *p,
522 SKFLT *buf,
523 size_t sz,
524 int sr)
525{
526 SKFLT readpos;
527 <<delay_init>>
528}
529#+END_SRC
530
531Set up buffer + sz
532
533#+NAME: bigverb_delay
534#+BEGIN_SRC c
535SKFLT *buf;
536size_t sz;
537#+END_SRC
538
539#+NAME: delay_init
540#+BEGIN_SRC c
541d->buf = buf;
542d->sz = sz;
543#+END_SRC
544
545Initialize write position (0), abbreviated as =wpos=.
546
547#+NAME: bigverb_delay
548#+BEGIN_SRC c
549int wpos;
550#+END_SRC
551
552#+NAME: delay_init
553#+BEGIN_SRC c
554d->wpos = 0;
555#+END_SRC
556
557Initialize read position. Based on delay time, =drift= and
558initial seed. Read position has to components, an integer
559read position, and a floating point read position. These
560will be abbreviated =irpos= and =frpos=.
561
562#+NAME: bigverb_delay
563#+BEGIN_SRC c
564int irpos;
565int frpos;
566#+END_SRC
567
568Seed value is multiplied by the initial drift value, and then
569divided by 32767.
570
571#+NAME: bigverb_delay
572#+BEGIN_SRC c
573int rng;
574#+END_SRC
575
576#+NAME: delay_init
577#+BEGIN_SRC c
578d->rng = p->seed;
579<<setup_readpos>>
580#+END_SRC
581
582
583The initial time is added to this.
584
585=bufsize - (readpos * sr)= <-- this puts the read position at
586the end of the buffer.
587
588Truncate (using integer cast).'
589
590#+NAME: setup_readpos
591#+BEGIN_SRC c
592readpos = ((SKFLT)p->delay / 44100);
593readpos += d->rng * (p->drift * 0.0001) / 32768.0;
594readpos = sz - (readpos * sr);
595d->irpos = floor(readpos);
596d->frpos = floor((readpos - d->irpos) * FRACSCALE);
597#+END_SRC
598
599Create first random segments.
600
601#+NAME: delay_init
602#+BEGIN_SRC c
603<<init_jitter>>
604generate_next_line(d, sr);
605#+END_SRC
606*** Top-Level Compute
607The delay line computation is done in a tick function. It
608takes in an input sample, returns an output sample. In
609addition to delay, filtering, feedback, and jittering
610happens as well.
611
612Because feedback + filtering are global options, these
613are passed in as parameters on the stack. What is required
614is the feedback amount, and the calculated filter
615coeffecient used in the filter.
616
617#+NAME: static_funcdefs
618#+BEGIN_SRC c
619static SKFLT delay_compute(sk_bigverb_delay *d,
620 SKFLT in,
621 SKFLT fdbk,
622 SKFLT filt,
623 int sr);
624#+END_SRC
625
626#+NAME: funcs
627#+BEGIN_SRC c
628static SKFLT delay_compute(sk_bigverb_delay *del,
629 SKFLT in,
630 SKFLT fdbk,
631 SKFLT filt,
632 int sr)
633{
634 SKFLT out;
635 SKFLT frac_norm;
636 SKFLT a, b, c, d;
637 SKFLT s[4];
638 out = 0;
639 <<write_to_buffer>>
640 <<increment_write_position>>
641 <<update_fractional_read_position>>
642 <<update_integer_read_position>>
643 <<normalize_fractional_component>>
644 <<calculate_interpolation_coefficients>>
645 <<read_from_buffer>>
646 <<compute_interpolation>>
647 <<increment_fractional_read_position>>
648 <<apply_feedback_and_filter>>
649 <<update_jitter>>
650 return out;
651}
652#+END_SRC
653
654# since filter is the same cutoff... probably
655# only need to filter once to get same result.
656# this is the cummutative property I think?
657# May do this later.
658
659The following things happen:
660
661Write the to delay buffer and pre-filter the input by
662subtracting the filter state =y=.
663
664#+NAME: write_to_buffer
665#+BEGIN_SRC c
666del->buf[del->wpos] = in - del->y;
667#+END_SRC
668
669Increment the write position. If this is greater than the
670buffer size, wrap around.
671
672#+NAME: increment_write_position
673#+BEGIN_SRC c
674del->wpos++;
675if (del->wpos >= del->sz) del->wpos -= del->sz;
676#+END_SRC
677
678Update the fractional read position. If the read position
679exceeds the maximum fractional scale amount, it means it has
680bits that must carry over to the integer read position.
681After these bits have been carried over, mask out the upper
682bits to keep the range in bounds.
683
684#+NAME: update_fractional_read_position
685#+BEGIN_SRC c
686if (del->frpos >= FRACSCALE) {
687 del->irpos += del->frpos >> FRACNBITS;
688 del->frpos &= FRACMASK;
689}
690#+END_SRC
691
692If needed, update the read position with wrap-around.
693
694#+NAME: update_integer_read_position
695#+BEGIN_SRC c
696if (del->irpos >= del->sz) del->irpos -= del->sz;
697#+END_SRC
698
699Normalize the fractional component so that it is in range
7000 and 1. This is done by dividing the amount by fractional
701scaling factor =FRACSCALE=.
702
703#+NAME: normalize_fractional_component
704#+BEGIN_SRC c
705frac_norm = del->frpos / (SKFLT)FRACSCALE;
706#+END_SRC
707
708Calculate interpolation coefficients. These are 4
709pre-derived coefficents used to compute third-order
710lagrangian interpolation.
711Derivation of these is currently beyond the scope of this
712document. These will be called =a=, =b=, =c=, and =d=,
713respectively, and will correspond to =x(n - 1)=, =x(n)=,
714=x(n + 1)=, and =x(n + 2)=, respectively.
715
716#+NAME: calculate_interpolation_coefficients
717#+BEGIN_SRC c
718{
719 SKFLT tmp[2];
720 d = ((frac_norm * frac_norm) - 1) / 6.0;
721 tmp[0] = ((frac_norm + 1.0) * 0.5);
722 tmp[1] = 3.0 * d;
723 a = tmp[0] - 1.0 - d;
724 c = tmp[0] - tmp[1];
725 b = tmp[1] - frac_norm;
726}
727#+END_SRC
728
729Read the samples needed, based on the current playhead
730position. When the read position is in regular bounds, this
731means reading the previous, current, two next samples.
732Otherwise, this means the same thing, but with wrap-around
733and bounds checks.
734
735#+NAME: read_from_buffer
736#+BEGIN_SRC c
737{
738 int n;
739 SKFLT *x;
740 n = del->irpos;
741 x = del->buf;
742
743 if (n > 0 && n < (del->sz - 2)) {
744 s[0] = x[n - 1];
745 s[1] = x[n];
746 s[2] = x[n + 1];
747 s[3] = x[n + 2];
748 } else {
749 int k;
750 n--;
751 if (n < 0) n += del->sz;
752 s[0] = x[n];
753 for (k = 0; k < 3; k++) {
754 n++;
755 if (n >= del->sz) n -= del->sz;
756 s[k + 1] = x[n];
757 }
758 }
759}
760#+END_SRC
761
762Calculate interpolation. Using the coefficents
763described above and the fractional component =f=, one can compute
764cubic interpolation with the following expression:
765
766$$
767y(n) = (a x(n - 1) + b x(n) + c x(n + 1) + d x(n + 2)) \cdot f + x(n)
768$$
769
770#+NAME: compute_interpolation
771#+BEGIN_SRC c
772out = (a*s[0] + b*s[1] + c*s[2] + d*s[3]) * frac_norm + s[1];
773#+END_SRC
774
775Increment fractional read position, as determined by the
776jitter.
777
778#+NAME: increment_fractional_read_position
779#+BEGIN_SRC c
780del->frpos += del->inc;
781#+END_SRC
782
783Apply feedback and filter. The feedback will scale the delay
784output. The filtering is a difference equation, optimized
785and factored to use a minimum number of arithmetic
786operations.
787
788#+NAME: apply_feedback_and_filter
789#+BEGIN_SRC c
790out *= fdbk;
791out += (del->y - out) * filt;
792del->y = out;
793#+END_SRC
794
795# Original difference equation
796#
797# y(n) = c1 * x(n) + c2 * y(n - 1)
798#
799# c2 = b - sqrt(b^2 - 1.0)
800# c1 = (1 - c2)
801# b = 2 - cos(\omega))
802# \omega = freq * 2 * PI / sr
803#
804# can be derived from tone in the following way:
805# y(n) = c1 * x(n) + c2 * Y(n - 1)
806# = (1 - c2)*x(n) + c2 * y(n - 1)
807# = x(n) - c2*x(n) + c2*y(n-1)
808# = c2*y(n - 1) - c2*x(n) + x(n)
809# = c2*(y(n - 1) - x(n)) + x(n)
810
811Update jitter, if needed. When the counter zeros out
812(or worse), it is time to find a new random target to lerp
813to.
814
815#+NAME: update_jitter
816#+BEGIN_SRC c
817del->counter--;
818if (del->counter <= 0) {
819 generate_next_line(del, sr);
820}
821#+END_SRC
822*** Feedback Fractional Delay Line
823A delay line is initialized with a pre-allocated zeroed
824buffer its size. Memory will be managed outside of this
825abstraction.
826
827Being a fractional delay line means the read position has
828two components: an integer component and a fractional
829component. The integer component is the current position
830in the delay buffer. The fractional component tells how much
831it goes over into the next discrete sample position.
832In a way, interpolation can be thought of as the process of
833using these two values to make a really good guess of what
834lies in between the samples.
835
836The fractional delay component is maintained as a 28 bit
837integer. This is done to avoid some of the weirdness found
838in floating point operations. The remaining upper bits
839are "carry-over" samples, that get accumulated in integer
840component of the read position.
841
842A few constants are used to conveniently work with this
843fractional delay component.
844
845=FRACSCALE= is the fractional scaling amount,
846which is $1^28$, or =0x10000000=. Multiplied with a uniform
847scalar, this is used to calculate the increment.
848
849#+NAME: fdelay_constants
850#+BEGIN_SRC c
851#define FRACSCALE 0x10000000
852#+END_SRC
853
854=FRACMASK= is the bitmask used to keep the fractional
855position in 28-bit range. It is $1^28 - 1$, or =0xFFFFFFF=.
856This is in particular is used to filter out upper bits that
857get carried over to the integer read position.
858
859#+NAME: fdelay_constants
860#+BEGIN_SRC c
861#define FRACMASK 0xFFFFFFF
862#+END_SRC
863
864=FRACNBITS= is the number of bits in the number. set to be
86528.
866
867#+NAME: fdelay_constants
868#+BEGIN_SRC c
869#define FRACNBITS 28
870#+END_SRC
871*** Jitter
872Jitter in this context, is a random line segment
873generator. It linearly interpolates between random values in
874a given range, using random durations in a given range.
875
876A line generator stores a counter and increment amount.
877
878#+NAME: bigverb_delay
879#+BEGIN_SRC c
880int inc;
881int counter;
882#+END_SRC
883
884#+NAME: init_jitter
885#+BEGIN_SRC c
886d->inc = 0;
887d->counter = 0;
888#+END_SRC
889
890The most significant thing to happen in the jitter signal is
891calculating the next random segment. This is done in a
892static function called =generate_next_line=.
893
894#+NAME: static_funcdefs
895#+BEGIN_SRC c
896static void generate_next_line(sk_bigverb_delay *d, int sr);
897#+END_SRC
898
899To begin, another random value is created based on the
900previous random value.
901
902The RNG algorithm used is quite simple, and is used to
903produce a 16-bit value.
904
905$$x(n) = (5^{6}x(n - 1) + 1) \& 0xFFFF$$
906
907Before and after this equation, the value is balanced so that
908is a 16-bit bipolar signal.
909Before, it adds 0x10000 if the
910value is less than 0. After, it substracts 0x10000 if the
911seed value is greater than 0x8000.
912
913#+NAME: generate_random_number
914#+BEGIN_SRC c
915if (d->rng < 0) d->rng += 0x10000;
916/* 5^6 = 15625 */
917d->rng = (1 + d->rng * 0x3d09);
918d->rng &= 0xFFFF;
919if (d->rng >= 0x8000) d->rng -= 0x10000;
920#+END_SRC
921
922This new random value is used to produce the next random
923value in seconds.
924
925The line counter is reset. This value comes from the
926high-level parameter.
927
928#+NAME: bigverb_delay
929#+BEGIN_SRC c
930int maxcount;
931#+END_SRC
932
933#+NAME: init_jitter
934#+BEGIN_SRC c
935d->maxcount = round((sr / ((SKFLT)p->randfreq * 0.001)));
936#+END_SRC
937
938#+NAME: reset_counter
939#+BEGIN_SRC c
940d->counter = d->maxcount;
941#+END_SRC
942
943Compute delay time values. The current delay time,
944=curdel=, is obtained by subtracting the write + integer
945read positions, then adding in the fractional component.
946Wraparound is applied.
947
948#+NAME: compute_delay_values
949#+BEGIN_SRC c
950curdel = d->wpos -
951 (d->irpos + (d->frpos/(SKFLT)FRACSCALE));
952while (curdel < 0) curdel += d->sz;
953curdel /= sr;
954#+END_SRC
955
956The next delay time to lerp to is derived from the RNG
957and drift amount.
958
959#+NAME: compute_delay_values
960#+BEGIN_SRC c
961nxtdel = (d->rng * (d->drift * 0.0001) / 32768.0) + d->dels;
962#+END_SRC
963
964The delay time, in seconds (=dels=).
965
966#+NAME: bigverb_delay
967#+BEGIN_SRC c
968SKFLT dels;
969#+END_SRC
970
971#+NAME: init_jitter
972#+BEGIN_SRC c
973d->dels = p->delay / 44100.0;
974#+END_SRC
975
976#+NAME: bigverb_delay
977#+BEGIN_SRC c
978SKFLT drift;
979#+END_SRC
980
981#+NAME: init_jitter
982#+BEGIN_SRC c
983d->drift = p->drift;
984#+END_SRC
985
986The linear increment value is the difference between the current
987and next delay times, divided by the number of steps needed
988to draw a line between them (=counter=). This value is then
989converted into samples. An extra sample is tacked on to
990prevent nil values.
991
992#+NAME: compute_increment
993#+BEGIN_SRC c
994inc = ((curdel - nxtdel) / (SKFLT)d->counter)*sr;
995inc += 1;
996#+END_SRC
997
998This increment value is truncated and converted to the
999fractional read.
1000
1001#+NAME: set_fractional_read
1002#+BEGIN_SRC c
1003d->inc = floor(inc * FRACSCALE);
1004#+END_SRC
1005
1006#+NAME: funcs
1007#+BEGIN_SRC c
1008static void generate_next_line(sk_bigverb_delay *d, int sr)
1009{
1010 SKFLT curdel;
1011 SKFLT nxtdel;
1012 SKFLT inc;
1013 <<generate_random_number>>
1014 <<reset_counter>>
1015 <<compute_delay_values>>
1016 <<compute_increment>>
1017 <<set_fractional_read>>
1018}
1019#+END_SRC
1020*** Filter Memory
1021A one-pole lowpass filter such as the one used in the delay
1022line requires one sample of memory, which stores the output
1023of the previous filter. In a difference equation, this would
1024be known as $y(n - 1)$. In C code, we abbreviate this as
1025=y=.
1026
1027#+NAME: bigverb_delay
1028#+BEGIN_SRC c
1029SKFLT y;
1030#+END_SRC
1031
1032#+NAME: delay_init
1033#+BEGIN_SRC c
1034d->y = 0.0;
1035#+END_SRC
dcblocker.org+118−0
@@ -0,0 +1,118 @@
1#+TITLE: DC Blocker
2* Introduction
3This document outlines an implementation of a DC Blocker, a
4typical utility used to block out what is known as DC offset.
5DC Blockers are occasionally used to clean up recorded
6audio, but they are more often seen handling digital
7algorithms that tend to naturally introduce a lot of DC
8into their signal. This is common in the physical modelling
9family of DSP algorithms (Karplus-Strongs, waveguides,
10FDN reverberators, etc).
11
12Without knowing how a DC blocker works, it would seem to have
13magical characteristics. A DC blocker has the ability to
14"drag down" a waveform of audio with DC offset so that it is
15centered. But how does it know how much to drag?
16
17In truth, a DC blocker is typically nothing more than
18a recursive highpass filter whose cutoff set to be a
19sub-audio frequency. This has the effect of removing any
20slow moving waveforms that often are the culrpits of
21unwanted DC biasing.
22* Difference Equation
23The difference equation for a
24[[https://ccrma.stanford.edu/~jos/filters/DC_Blocker.html][DC blocker]] looks like this:
25
26$$
27y(n) = x(n) - x(n - 1) + Ry(n - 1)
28$$
29
30Where $R$ is usually a constant between 0.9 and 1.
31
32This difference equation outlines the whole darn algorithm!
33What will be needed is a function that takes in an input,
34and stores the previous input, output, as well as the
35constant $R$. And that's it.
36* Tangled Files
37This program outputs two files: =dcblocker.c= and
38=dcblocker.h=. Using the macro =SK_DCBLOCKER_PRIV= will
39expose the main struct. Otherwise, it is opaque.
40
41#+NAME: dcblocker.c
42#+BEGIN_SRC c :tangle dcblocker.c
43#define SK_DCBLOCKER_PRIV
44#include "dcblocker.h"
45<<funcs>>
46#+END_SRC
47
48#+NAME: dcblocker.h
49#+BEGIN_SRC c :tangle dcblocker.h
50#ifndef SK_DCBLOCKER_H
51#define SK_DCBLOCKER_H
52#ifndef SKFLT
53#define SKFLT float
54#endif
55<<typedefs>>
56#ifdef SK_DCBLOCKER_PRIV
57<<structs>>
58#endif
59<<funcdefs>>
60#endif
61#+END_SRC
62* Struct Definition
63The main struct for this DC blocker will be called
64=sk_dcblocker=. It only needs three parameters: an
65=x= value storing the previous input value, a =y= value
66storing the previous output value, and an =R= value
67storing the DC filter constant.
68
69#+NAME: typedefs
70#+BEGIN_SRC c
71typedef struct sk_dcblocker sk_dcblocker;
72#+END_SRC
73#+NAME: structs
74#+BEGIN_SRC c
75struct sk_dcblocker {
76 SKFLT x, y, R;
77};
78#+END_SRC
79* Initialization
80The =sk_dcblocker= can be initialized with
81=sk_dcblocker_init=. It zeros out =x= and =y=, and sets
82=R= to be a value of =0.99=, which is a reasonable DC
83coefficent value.
84
85#+NAME: funcdefs
86#+BEGIN_SRC c
87void sk_dcblocker_init(sk_dcblocker *dcblk);
88#+END_SRC
89
90#+NAME: funcs
91#+BEGIN_SRC c
92void sk_dcblocker_init(sk_dcblocker *dcblk)
93{
94 dcblk->x = 0;
95 dcblk->y = 0;
96 dcblk->R = 0.99; /* quite reasonable, indeed! */
97}
98#+END_SRC
99* Computation
100A single sample of audio is processed with the function
101=sk_dcblocker_tick=. It performs the arithmetic for the
102difference equation, updates the internal values, then
103returns the output.
104
105#+NAME: funcdefs
106#+BEGIN_SRC c
107SKFLT sk_dcblocker_tick(sk_dcblocker *dcblk, SKFLT in);
108#+END_SRC
109
110#+NAME: funcs
111#+BEGIN_SRC c
112SKFLT sk_dcblocker_tick(sk_dcblocker *dcblk, SKFLT in)
113{
114 dcblk->y = in - dcblk->x + dcblk->R*dcblk->y;
115 dcblk->x = in;
116 return dcblk->y;
117}
118#+END_SRC
fmpair.org+576−0
@@ -0,0 +1,576 @@
1#+TITLE: FM Pair Oscillator
2* Introduction
3This document outlines an implementation of a classic
4FM oscillator with frequency, C:M ratio, and modulation
5index control.
6
7There is quite a lot to be said about FM synthesis, but a
8classic FM oscillator implementation is poetic tribute to
9the technique. Requiring only two oscillators (usually sine
10waves), an FM operator pair is capable of generating very
11rich and dynamic spectrums. For how little computation is
12required, these suckers can pack a punch!
13
14The FM pair is the Bechamel sauce of the FM world. Creating
15one from scratch provides great sonic intution into the
16mostly unintuitive world of FM synthesis programming. It can
17be the building block for grokking more complex modulation
18configurations, such as those found on the DX7.
19* Some words on FM synthesis
20(I can't help but ramble a little bit about the topic of
21FM synthesis. Feel free to scroll/skip/page ahead.)
22
23FM synthesis, or Frequently Modulation synthesis, was first
24implemented by John Chowning while doing research at the
25Stanford Artificial Intelligence Laboratory (SAIL, whose
26spritual successor would become CCRMA, or the Center for
27Computer Research in Music and Acoustics). As the legend
28goes, Chowning stumbled upon the technique by accident.
29Experimenting with different vibrato speeds, he found that
30when vibrato got fast enough, it started changing timbre
31and spectrum.
32
33The humble beginings of FM synthesis started out with an FM
34pair configuration similar to this one. But can you imagine
35the surprise he got when he first heard that new FM sound?
36
37To paint a picture:
38
39At the time, computers were about the
40size of fridges. They were expensive devices owned by the
41University, and time spent using the computer was precious.
42Chowing no doubt had to fight to get his time slot. Being
43a composer amongst scientists and engineers, he probably
44could only work in late hours.
45
46Typing away at a mainframe 3am, Chowning would have to had
47to wait at least a day for the computer to run his FM
48synthesis program. Assuming it successfully ran without any
49errors, Chowning would then have to ship the output of that
50program off somewhere to get it converted to magnetic
51reel-to-reel tape. Back then,
52digital-to-analog converers (DACs) were things that you had
53to get in a car and drive to. Now-a-days, audio DACs are
54things found on pretty much every electronic device you own.
55
56So, if you're keeping track, that's at least a week from
57code to sound. That's an enormous amount of round-trip
58latency! To go that long, and to finally hear these new
59sounds must have been such a rewarding experience.
60
61Chowing's original FM synthesis algorithm would eventually
62become one of Stanford University's highest grossing patents
63of all time. To this day, the royalties of this patent help
64to sustain the CCRMA facilities.
65
66Famously, the patent was sold to Yamaha, and that gave us
67things like the DX7 synthesizer (and all the cheesy music
68that came with it). Due to the efficiency of
69the algorithm, it also found itself in a whole generation of
70sound cards at the time, used in video game consoles and
71arcade machines. The most famous of these chips was the
72[[https://en.wikipedia.org/wiki/Yamaha_YM2151][YM2151]], also known as the OPM chip.
73* Overview of Algorithm and Implementation
74An FM Pair uses two oscillators, sometimes referred two as
75=operators=. One of these operators synthesizes audble sound.
76This known as the =carrier=, and can be remembered because
77it *carries* the sound signal to the speaker. The other
78operator *modulates* the frequency of the carrier, and it
79is known as the =modulator=.
80
81If you set the modulator to any frequency that is audio rate
82(give or take 20hz or higher), you will technically get some
83kind of FM synthesis. However, depending on the frequency of
84the carrier, the spectrum can range from being very pitched
85with harmonically related overtones, to something quite
86clangorous and not at all something you could easily play
87"Hot Cross Buns" on. So, to make an FM pair easier to use,
88three unit-less parameters are used to control the
89FM characteristics of the sound that work evenly across
90the spectrum: a carrier-to-modulator frequency ratio known
91as the C:M ratio, and something known as the modulation
92index, which can kind of be thought of as a thing that
93controls the amount of modulation.
94
95The C and M parameters are values that primarily multiply
96the frequency value of the carrier and modulator operators,
97respectively.
98
99The modulation index can be defined as having the following
100relationship (taken from the Computer Music Tutorial's
101chapter on Modulation synthesis, page 229):
102
103$$ I = D/M $$
104
105Where $I$ is the modulation index, $M$ is the modulation
106frequency (which would be the oscillator frequency multiplied
107by the modulator ratio), and $D$ is the amount of modulation
108=depth=. The equation can be reworked in terms of $D$:
109
110$$ D = MI $$
111
112This depth value can be thought of as the "amplitude" amount
113the modulator operator (in Hz). This will produce a signal
114that modulates the carrier frequency up the depth amount,
115and then down that same amount.
116
117In this implemetation, two table-lookup oscillators will be
118used for the operators, using an algorithm similar to the
119@!(ref "osc" "table lookup oscillator")!@, which employ a
120fixed-point phasor with linear interpolation. Details for
121the gory details of these oscillators are beyond the scope
122of this article, but are described in the implementation
123referenced above.
124* Tangled Files
125=fmpair.c= contains the ANSI C code required for the
126implementation.
127
128#+NAME: fmpair.c
129#+BEGIN_SRC c :tangle fmpair.c
130#include <math.h>
131#define SK_FMPAIR_PRIV
132#include "fmpair.h"
133<<constants>>
134<<funcs>>
135#+END_SRC
136
137=fmpair.h= is the corresponding header file for the C file.
138It contains all the forward declarations. If
139=SK_FMPAIR_PRIV= is defined, it exposes the structs.
140
141#+NAME: fmpair.h
142#+BEGIN_SRC c :tangle fmpair.h
143#ifndef SK_FMPAIR_H
144#define SK_FMPAIR_H
145#ifndef SKFLT
146#define SKFLT float
147#endif
148<<typedefs>>
149<<funcdefs>>
150#ifdef SK_FMPAIR_PRIV
151<<structs>>
152#endif
153#endif
154#+END_SRC
155* Struct
156#+NAME: typedefs
157#+BEGIN_SRC c
158typedef struct sk_fmpair sk_fmpair;
159#+END_SRC
160
161#+NAME: structs
162#+BEGIN_SRC c
163struct sk_fmpair {
164 <<sk_fmpair>>
165};
166#+END_SRC
167* Table-lookup oscillator data and constants
168** Constants
169The constant =SK_FMPAIR_MAXLEN= is the maximum length a
170lookup table can be.
171
172The constant =SK_FMPAIR_PHASEMASK= is a bitmask used by
173the fixed point phasor.
174#+NAME: constants
175#+BEGIN_SRC c
176#define SK_FMPAIR_MAXLEN 0x1000000L
177#define SK_FMPAIR_PHASEMASK 0x0FFFFFFL
178#+END_SRC
179** Struct Data
180The FM oscillator implements two independent table-[ookup
181oscillators. Each one needs their own set of wavetables,
182incrementors, sizes, and cached phase positions.
183
184The convention used will this: any variables used for the
185carrier oscillator will begin with 'c':
186
187#+NAME: sk_fmpair
188#+BEGIN_SRC c
189SKFLT *ctab;
190int csz;
191int clphs;
192#+END_SRC
193
194#+NAME: init
195#+BEGIN_SRC c
196fmp->ctab = ctab;
197fmp->csz = msz;
198#+END_SRC
199
200Any variables used for the modulator oscillator will begin
201with 'm':
202
203#+NAME: sk_fmpair
204#+BEGIN_SRC c
205SKFLT *mtab;
206int msz;
207int mlphs;
208#+END_SRC
209
210#+NAME: init
211#+BEGIN_SRC c
212fmp->mtab = mtab;
213fmp->msz = msz;
214#+END_SRC
215
216In order to be used by the fixed point phasor,
217the initial phases need to be rescaled by =SK_FMPAIR_MAXLEN=
218and then converted to an integer value. These are
219stored in each respective variable keeping track
220of phase.
221
222#+NAME: init
223#+BEGIN_SRC c
224fmp->clphs = floor(ciphs * SK_FMPAIR_MAXLEN);
225fmp->mlphs = floor(miphs * SK_FMPAIR_MAXLEN);
226#+END_SRC
227
228A number of constants are used with these tables: =nlb=,
229=inlb=, =mask=, and =maxlens=. These are beyond the scope
230of this document, but are touched upon in @!(ref "osc")!@.
231Since these are all based on table size, two sets of
232constants are derived for the carrier and modulator.
233
234#+NAME: sk_fmpair
235#+BEGIN_SRC c
236/* carrier constants */
237int cnlb;
238SKFLT cinlb;
239unsigned long cmask;
240
241/* modulator constants */
242int mnlb;
243SKFLT minlb;
244unsigned long mmask;
245
246SKFLT maxlens;
247#+END_SRC
248
249#+NAME: init
250#+BEGIN_SRC c
251{
252 int tmp;
253
254 /* carrier */
255 tmp = SK_FMPAIR_MAXLEN / csz;
256 fmp->cnlb = 0;
257 while (tmp >>= 1) fmp->cnlb++;
258
259 /* modulator */
260 tmp = SK_FMPAIR_MAXLEN / msz;
261 fmp->mnlb = 0;
262 while (tmp >>= 1) fmp->mnlb++;
263}
264
265/* phase mask for dividing lower/upper bits */
266
267fmp->cmask = (1<<fmp->cnlb) - 1;
268fmp->mmask = (1<<fmp->mnlb) - 1;
269
270/* constant used to convert to floating point */
271
272fmp->cinlb = 1.0 / (1<<fmp->cnlb);
273fmp->minlb = 1.0 / (1<<fmp->mnlb);
274
275/* max table length in seconds */
276/* used to convert cycles-per-second units to cycles */
277
278fmp->maxlens = 1.0 * SK_FMPAIR_MAXLEN / sr;
279#+END_SRC
280* Initialization
281The FM oscillator is initialize with =sk_fmpair_init=.
282It needs the sampling rate =sr=, and two sets of
283wavetables (=cwt= and =mwt=),
284their sizes (=csz=) and (=msz=), and
285initial phases (=ciphs= and =miphs=) for the carrier (c)
286and modulator (m) oscillators.
287
288In a classic FM pair, these would be identical,
289and the wavetable would contain a sampled period of a
290sine wave.
291
292#+NAME: funcdefs
293#+BEGIN_SRC c
294void sk_fmpair_init(sk_fmpair *fmp, int sr,
295 SKFLT *ctab, int csz, SKFLT ciphs,
296 SKFLT *mtab, int msz, SKFLT miphs);
297#+END_SRC
298
299#+NAME: funcs
300#+BEGIN_SRC c
301void sk_fmpair_init(sk_fmpair *fmp, int sr,
302 SKFLT *ctab, int csz, SKFLT ciphs,
303 SKFLT *mtab, int msz, SKFLT miphs)
304{
305 <<init>>
306}
307#+END_SRC
308* Parameter Control
309** Frequency
310Set with =sk_fmpair_freq=.
311
312#+NAME: funcdefs
313#+BEGIN_SRC c
314void sk_fmpair_freq(sk_fmpair *fmp, SKFLT freq);
315#+END_SRC
316
317#+NAME: funcs
318#+BEGIN_SRC c
319void sk_fmpair_freq(sk_fmpair *fmp, SKFLT freq)
320{
321 fmp->freq = freq;
322}
323#+END_SRC
324
325#+NAME: sk_fmpair
326#+BEGIN_SRC c
327SKFLT freq;
328#+END_SRC
329
330A sensible default of A440.
331
332#+NAME: init
333#+BEGIN_SRC c
334sk_fmpair_freq(fmp, 440);
335#+END_SRC
336** Carrier/Modulator Ratios
337Set with =sk_fmpair_modulator= and =sk_fmpair_carrier=.
338
339#+NAME: funcdefs
340#+BEGIN_SRC c
341void sk_fmpair_modulator(sk_fmpair *fmp, SKFLT mod);
342void sk_fmpair_carrier(sk_fmpair *fmp, SKFLT car);
343#+END_SRC
344
345#+NAME: funcs
346#+BEGIN_SRC c
347void sk_fmpair_modulator(sk_fmpair *fmp, SKFLT mod)
348{
349 fmp->mod = mod;
350}
351
352void sk_fmpair_carrier(sk_fmpair *fmp, SKFLT car)
353{
354 fmp->car = car;
355}
356#+END_SRC
357
358#+NAME: sk_fmpair
359#+BEGIN_SRC c
360SKFLT car;
361SKFLT mod;
362#+END_SRC
363
364A 1:1 ratio is a good strong and sensible default.
365
366#+NAME: init
367#+BEGIN_SRC c
368sk_fmpair_carrier(fmp, 1);
369sk_fmpair_modulator(fmp, 1);
370#+END_SRC
371** Modulation Index
372Set with =sk_fmpair_modindex=.
373
374#+NAME: funcdefs
375#+BEGIN_SRC c
376void sk_fmpair_modindex(sk_fmpair *fmp, SKFLT index);
377#+END_SRC
378
379#+NAME: funcs
380#+BEGIN_SRC c
381void sk_fmpair_modindex(sk_fmpair *fmp, SKFLT index)
382{
383 fmp->index = index;
384}
385#+END_SRC
386
387#+NAME: sk_fmpair
388#+BEGIN_SRC c
389SKFLT index;
390#+END_SRC
391
392A modulation index of 1 on with 1:1 C-to-M ratio produces
393a very warm and versatile initial sound.
394
395#+NAME: init
396#+BEGIN_SRC c
397sk_fmpair_modindex(fmp, 1);
398#+END_SRC
399* Computing a Sample
400A single sample of audio is computed with =sk_fmpair_tick=.
401
402#+NAME: funcdefs
403#+BEGIN_SRC c
404SKFLT sk_fmpair_tick(sk_fmpair *fmp);
405#+END_SRC
406
407#+NAME: funcs
408#+BEGIN_SRC c
409SKFLT sk_fmpair_tick(sk_fmpair *fmp)
410{
411 SKFLT out;
412 SKFLT cfreq, mfreq;
413 SKFLT modout;
414 int ipos;
415 SKFLT frac;
416 SKFLT x[2];
417 out = 0;
418 <<calculate_frequencies>>
419 <<table_lookup_for_modulator>>
420 <<scale_modulator_output>>
421 <<modulate_carrier_frequency>>
422 <<table_lookup_for_carrier>>
423 <<update_phase_positions>>
424 return out;
425}
426#+END_SRC
427
428The carrier and modulator frequencies are calculated by
429multiplying their respective ratio values with
430the oscillator frequency.
431
432#+NAME: calculate_frequencies
433#+BEGIN_SRC c
434cfreq = fmp->freq * fmp->car;
435mfreq = fmp->freq * fmp->mod;
436#+END_SRC
437
438The modulator oscillator does its table-lookup and
439linear interpolation with some bitwise magic. The details of
440this can be found in @!(ref "osc")!@.
441
442#+NAME: table_lookup_for_modulator
443#+BEGIN_SRC c
444fmp->mlphs &= SK_FMPAIR_PHASEMASK;
445ipos = fmp->mlphs >> fmp->mnlb;
446x[0] = fmp->mtab[ipos];
447
448if (ipos == fmp->msz - 1) {
449 x[1] = fmp->mtab[0];
450} else {
451 x[1] = fmp->mtab[ipos + 1];
452}
453
454frac = (fmp->mlphs & fmp->mmask) * fmp->minlb;
455modout = (x[0] + (x[1] - x[0]) * frac);
456#+END_SRC
457
458Before it can modulate the carrier, the output of the
459modulator needs to be appropriately scaled. This amplitude
460amount is the modulator frequency and modulation
461index multiplied together.
462
463#+NAME: scale_modulator_output
464#+BEGIN_SRC c
465modout *= mfreq * fmp->index;
466#+END_SRC
467
468The frequency of the carrier oscillator is now ready to be
469modulated. Modulation is an addition operation; The output
470of the scaled modulator is tacked on the carrier frequency.
471
472#+NAME: modulate_carrier_frequency
473#+BEGIN_SRC c
474cfreq += modout;
475#+END_SRC
476
477A table-lookup operation computation happens for the
478carrier oscillator, similar to what happened with the
479modulator oscillator. This output is what gets returned.
480
481#+NAME: table_lookup_for_carrier
482#+BEGIN_SRC c
483fmp->clphs &= SK_FMPAIR_PHASEMASK;
484ipos = (fmp->clphs) >> fmp->cnlb;
485x[0] = fmp->ctab[ipos];
486
487if (ipos == fmp->csz - 1) {
488 x[1] = fmp->ctab[0];
489} else {
490 x[1] = fmp->ctab[ipos + 1];
491}
492
493frac = (fmp->clphs & fmp->cmask) * fmp->cinlb;
494out = (x[0] + (x[1] - x[0]) * frac);
495#+END_SRC
496
497To wrap things up, the phase increments and positions
498of both oscillators are updated, based on their respective
499frequencies.
500
501increment value is derived by multiplying oscillator
502frequency (in units of cycles-per-second) to the maximum
503length of the phasor (in units of seconds). The seconds unit
504cancels, and the resulting output is cycles. This value,
505truncated to an integer value, becomes the phase increment
506value.
507
508#+NAME: update_phase_positions
509#+BEGIN_SRC c
510fmp->clphs += floor(cfreq * fmp->maxlens);
511fmp->mlphs += floor(mfreq * fmp->maxlens);
512#+END_SRC
513* C:M Ratio tips
514Some suggestions to get started with picking out good C,M
515and I parameters. For those starting out, these should help
516build some sonic intuition in FM.
517
518Generally speaking, the more rational C:M ratios are
519(1:1, 1:2, 3:2, etc), the more harmonic and pitched the
520spectrum will be. The more irrational they are
521(1:1.6180339..., 1:3.1415926..., etc) the more clangorous
522and unpitched the spectrum will be.
523
524Fractional or strange looking ratios that look irrational
525can sometimes be simple ratios in disguise. For example,
5261:0.5 is harmonically similar to 2:1.
527
528The carrier ratio value determines what the pitch will be.
529Whole integer values correspond with the harmonic series.
530A value of 1 is the base frequency, 2 is an octave, 3 is
531an octave and a fifth, etc.
532
533The modulator ratio can be thought of as how spread-out
534the harmonic spectrum will be. A 1:1 ratio will give you
535a denser spectrum than a 1:7 ratio. These spread out
536spectrums were what made classic DX7 keyboard tine sounds
537work so well. Using ratios with high M values, they were
538able to cut through a mix while also leaving room for other
539instruments.
540
541Adding small fractional values to the M ratio (ex: 1:1.007) can
542add some very unique spectral beating. FM pairs can be
543layered together with different fractional amounts to create
544thicker sounds.
545
546In an FM pair, the behavior of the modulation index can be
547thought of as a kind of tone control, similar to a filter
548cutoff control that you'd find in a subtractive synthesizer.
549It's a gross simplification for all the amazing things
550happening to the spectrum, but it's close enough.
551
552Controlling the modulation index of a 1:1 FM pair kind of
553feels like controlling a subtractive synthesizer, especially
554for lower modulation index values.
555
556FM sounds can alias like crazy, especially when frequency,
557modulator, and modulation index values are high. When you
558crank up the modulation index crazy high, you can get some
559very interesting alias noises, but finding the sweet spots
560can be a bit of a treasure hunt. The plugin
561[[https://www.kvraudio.com/product/mr-alias-by-insert-piz-here][Mr.Alias]] is built around this concept.
562
563Keyboard/frequency scaling the modulation index is a Good
564Idea. As frequencies get lower, the modulation index can
565get larger without risk of aliasing. More modulation index
566means more harmonic content, means more sonic beef for
567lower notes.
568
569The modulation index spectrum behavior is determined by
570[[https://en.wikipedia.org/wiki/Bessel_function#Bessel_functions_of_the_first_kind][Bessel Functions of the First kind]].
571The wiki page on this has a good chart of this that
572plots the amplitudes first 3 harmonics. You can see how
573ahey dip in and out. One should at least be passively aware
574that this happens. Changing the modulation index on a bass
575sound, for example, can sometimes cause the fundamental
576to drop out, which can produces thin patches of sound.
index.org+21−0
@@ -0,0 +1,21 @@
1#+TITLE: sndkit
2sndkit is a sonic toolkit for everywhere.
3
4It is a collection of highly portable ANSI-C implementations
5of audio DSP algorithms commonly used in computer music, all
6wrapped up inside of a statically generated wiki.
7
8Each DSP algorithm is written in a literate programming
9style, a technique pioneered by Donald Knuth. Surrounding
10the implementation itself are words and equations used to
11help explain and contextualize what every single line of
12code is doing.
13
14The text in this wiki is licensed under the
15[[https://creativecommons.org/share-your-work/public-domain/cc0/][Creatives Commons Zero]] license, otherwise
16known as "CC0".
17
18The tangled output is dual-licensed under MIT or
19the [[https://unlicense.org/][Unlicense]].
20
21Happy Hacking Humans.
modalres.org+306−0
@@ -0,0 +1,306 @@
1#+TITLE: ModalRes
2* Overview
3ModalRes is an implementation of a 2nd-order resonator
4filter suitable for use in modal synthesis.
5
6This filter is particularly useful in constructing bell or
7other impact-based sounds. In order to do this, you'd need
8to know the modal ratios pre-measured of time. This is what
9is known as black-box modelling. A similar example of this
10is formant-based speech synthesis.
11
12Some good modal ratios and measurements can be found [[https://csound.com/docs/manual/MiscModalFreq.html][here]].
13* Tangled Files
14=modalres.c= and =modalres.h= are the two files that are
15tangled.
16
17#+NAME: modalres.c
18#+BEGIN_SRC c :tangle modalres.c
19#include <math.h>
20#define SK_MODALRES_PRIV
21#include "modalres.h"
22
23#ifndef M_PI
24#define M_PI 3.14159265358979323846
25#endif
26
27<<funcs>>
28#+END_SRC
29
30#+NAME: modalres.h
31#+BEGIN_SRC c :tangle modalres.h
32#ifndef SK_MODALRES_H
33#define SK_MODALRES_H
34
35#ifndef SKFLT
36#define SKFLT float
37#endif
38
39<<typedefs>>
40<<funcdefs>>
41
42#ifdef SK_MODALRES_PRIV
43<<structs>>
44#endif
45#endif
46#+END_SRC
47* Initiazation
48Initialized with =sk_modalres_init=. Only the sampling rate
49needs to be supplied.
50
51#+NAME: funcdefs
52#+BEGIN_SRC c
53void sk_modalres_init(sk_modalres *mr, int sr);
54#+END_SRC
55
56#+NAME: funcs
57#+BEGIN_SRC c
58void sk_modalres_init(sk_modalres *mr, int sr)
59{
60 <<init>>
61}
62#+END_SRC
63* Struct
64The encapsulating struct is called =sk_modalres=.
65
66#+NAME: typedefs
67#+BEGIN_SRC c
68typedef struct sk_modalres sk_modalres;
69#+END_SRC
70
71#+NAME: structs
72#+BEGIN_SRC c
73struct sk_modalres {
74 <<sk_modalres>>
75};
76#+END_SRC
77* Filter State Variables and Coefficients
78The filter state parameters are the typical things you'd
79need in a biquadratic filter difference equation.
80
81Variables are created to store the previous x value and
822 y values.
83
84#+NAME: sk_modalres
85#+BEGIN_SRC c
86SKFLT x;
87SKFLT y[2];
88#+END_SRC
89
90#+NAME: init
91#+BEGIN_SRC c
92mr->x = 0;
93mr->y[0] = 0;
94mr->y[1] = 0;
95#+END_SRC
96
97Coefficients for the biquad are =b1= for the x value, and
98=a1= and =a2= for the y values. These coefficents get
99computed in the computation section.
100
101#+NAME: sk_modalres
102#+BEGIN_SRC c
103SKFLT b1;
104SKFLT a1;
105SKFLT a2;
106#+END_SRC
107
108#+NAME: init
109#+BEGIN_SRC c
110mr->b1 = 0;
111mr->a1 = 0;
112mr->a2 = 0;
113#+END_SRC
114
115As per usual, a copy of the sampling rate is stored, as it
116is needed to calculate filter coefficients.
117
118#+NAME: sk_modalres
119#+BEGIN_SRC c
120int sr;
121#+END_SRC
122
123#+NAME: init
124#+BEGIN_SRC c
125mr->sr = sr;
126#+END_SRC
127
128An unusual part of this filter design is the scaler
129variable, which is used to scale the audio output, but
130is not part of the actual transer function. We'll call
131this one =s=.
132
133#+NAME: sk_modalres
134#+BEGIN_SRC c
135SKFLT s;
136#+END_SRC
137
138#+NAME: init
139#+BEGIN_SRC c
140mr->s = 0;
141#+END_SRC
142* Parameters
143** Frequency
144Set with =sk_modalres_freq=.
145
146#+NAME: funcdefs
147#+BEGIN_SRC c
148void sk_modalres_freq(sk_modalres *mr, SKFLT freq);
149#+END_SRC
150
151#+NAME: funcs
152#+BEGIN_SRC c
153void sk_modalres_freq(sk_modalres *mr, SKFLT freq)
154{
155 mr->freq = freq;
156}
157#+END_SRC
158
159#+NAME: sk_modalres
160#+BEGIN_SRC c
161SKFLT freq;
162SKFLT pfreq;
163#+END_SRC
164
165The frequency parameter uses caching to check if filter
166coefficients needs to be updated. The cached value is set
167to be negative in order to force computation on the first
168tick.
169
170#+NAME: init
171#+BEGIN_SRC c
172sk_modalres_freq(mr, 1000);
173mr->pfreq = -1;
174#+END_SRC
175** Q value
176Set with =sk_modalres_q=.
177
178#+NAME: funcdefs
179#+BEGIN_SRC c
180void sk_modalres_q(sk_modalres *mr, SKFLT q);
181#+END_SRC
182
183#+NAME: funcs
184#+BEGIN_SRC c
185void sk_modalres_q(sk_modalres *mr, SKFLT q)
186{
187 mr->q = q;
188}
189#+END_SRC
190
191The Q parameter uses caching to check if filter
192coefficients needs to be updated. The cached value
193is set to be negative in order to force computation on
194the initial tick.
195
196#+NAME: sk_modalres
197#+BEGIN_SRC c
198SKFLT q;
199SKFLT pq;
200#+END_SRC
201
202#+NAME: init
203#+BEGIN_SRC c
204sk_modalres_q(mr, 1);
205mr->pq = -1;
206#+END_SRC
207* Computation
208A single sample is computed with =sk_modalres_tick=.
209
210#+NAME: funcdefs
211#+BEGIN_SRC c
212SKFLT sk_modalres_tick(sk_modalres *mr, SKFLT in);
213#+END_SRC
214
215#+NAME: funcs
216#+BEGIN_SRC c
217SKFLT sk_modalres_tick(sk_modalres *mr, SKFLT in)
218{
219 SKFLT out;
220
221 out = 0;
222 <<update_coefficients>>
223 <<compute_difference_equation>>
224 <<update_filter_state>>
225 <<scale_output>>
226 return out;
227}
228#+END_SRC
229
230Update the coefficients, if needed.
231First, frequency is converted to radians.
232While traditionally represented as the
233lowercase $\omega$ in mathematical notation, the variable
234=w= will be used in C as crude approximation.
235
236From there the following coefficients are computed:
237
238#+BEGIN_SRC tex
239b1 = {1 \over \omega^2 + {\omega \over 2Q}}
240a1 = {1 - 2\omega^2}b1
241a2 = {\omega^2 - {\omega \over 2Q}}
242#+END_SRC
243
244#+NAME: update_coefficients
245#+BEGIN_SRC c
246if (mr->freq != mr->pfreq || mr->q != mr->pq) {
247 SKFLT w;
248 SKFLT a, b, d;
249
250 w = mr->freq * 2.0 * M_PI;
251
252 a = mr->sr / w;
253 b = a*a;
254 d = 0.5*a;
255
256 mr->pfreq = mr->freq;
257 mr->pq = mr->q;
258
259 mr->b1 = 1.0 / (b + d/mr->q);
260 mr->a1 = (1.0 - 2.0*b) * mr->b1;
261 mr->a2 = (b - d/mr->q) * mr->b1;
262 mr->s = d;
263}
264#+END_SRC
265
266Compute difference equation. This computes the difference
267equation:
268
269#+BEGIN_SRC tex
270$$
271y(n) = b0 x(n) + b1 x(n - 1) - a1 y(n - 1) - a2 y(n - 2)
272$$
273#+END_SRC
274
275Where $b0$ is 0, effectively cancelling out this term.
276
277What's interesting about this is that it adds an implicit
278one-sample delay to the filter.
279This is done because more accurately simulates the impulse
280response in an ideal mass-spring-damper system. The produces
281is, after all, a *response* to the impulse.
282
283#+NAME: compute_difference_equation
284#+BEGIN_SRC c
285out = mr->b1*mr->x - mr->a1*mr->y[0] - mr->a2*mr->y[1];
286#+END_SRC
287
288Update filter state. Shift things forward in time one
289sample. What was once =y[0]= is now =y[1]=. What is now
290the current output becomes =y[0]=, what is now the current
291input value because the previous input value =x=.
292
293#+NAME: update_filter_state
294#+BEGIN_SRC c
295mr->y[1] = mr->y[0];
296mr->y[0] = out;
297mr->x = in;
298#+END_SRC
299
300The output is scaled by the variable =s= before it is
301returned. It is not part of the recursive filter function.
302
303#+NAME: scale_output
304#+BEGIN_SRC c
305out *= mr->s;
306#+END_SRC
osc.org+409−0
@@ -0,0 +1,409 @@
1#+TITLE: The Oscillator
2* Introduction.
3This document will describe an implementation of a classic
4table-lookup oscillator with linear interpolation.
5
6The algorithm for this is an interesting mix of numerical
7processing, with the phasor being implemented in fixed
8point, and everything else being done in floating point.
9A big advantage to doing it this way is numerical stability:
10there is no risk of any phase acculumation or truncation
11like you'd get with floating-point. This
12is an important characteristic needed for phasor to avoid
13things like drift over time. As will be seen later, the
14fixed-point approach can be a little bit harder to
15understand, especially for those unfamiliar with fixed-point
16DSP. The author has put extra effort to explain these
17portions as clearly as possible.
18
19The implementation also has a famous limitation of only
20being able to take in tables with sizes that are a power of
212.
22* Generated Files
23Header:
24
25#+NAME: osc.h
26#+BEGIN_SRC c :tangle osc.h
27#ifndef SK_OSC_H
28#define SK_OSC_H
29#ifndef SKFLT
30#define SKFLT float
31#endif
32<<typedefs>>
33#ifdef SK_OSC_PRIV
34<<structs>>
35#endif
36<<funcdefs>>
37#endif
38#+END_SRC
39
40C file:
41
42#+NAME: osc.c
43#+BEGIN_SRC c :tangle osc.c
44#include <stdint.h>
45#include <stdlib.h>
46#include <math.h>
47#define SK_OSC_PRIV
48#include "osc.h"
49<<constants>>
50<<funcs>>
51#+END_SRC
52* Top-level functions
53The table-lookup oscillator is initialized with
54=sk_osc_init=. The following arguments must be provided:
55
56=osc= is a pre-allocated struct of =sk_osc=.
57
58=sr= is the sampling rate.
59
60=tab= is a pre-allocated wavetable, an array of =SKFLT=
61floating-point values.
62
63=sz= is the array size of the wavetable =wt=.
64
65=iphs= provides the initial phase of the oscillator. It is
66a value between 0-1.
67
68#+NAME: funcdefs
69#+BEGIN_SRC c
70void sk_osc_init(sk_osc *osc, int sr, SKFLT *wt, int sz, SKFLT iphs);
71#+END_SRC
72
73To compute a sample of audio, use =sk_osc_tick=.
74
75#+NAME: funcdefs
76#+BEGIN_SRC c
77SKFLT sk_osc_tick(sk_osc *osc);
78#+END_SRC
79
80This oscillator has 2 main parameters: frequency (freq),
81and amplitude (amp). They can be
82set with the following functions.
83
84#+NAME: funcdefs
85#+BEGIN_SRC c
86void sk_osc_freq(sk_osc *osc, SKFLT freq);
87void sk_osc_amp(sk_osc *osc, SKFLT amp);
88#+END_SRC
89
90#+NAME: funcs
91#+BEGIN_SRC c
92void sk_osc_freq(sk_osc *osc, SKFLT freq)
93{
94 osc->freq = freq;
95}
96
97void sk_osc_amp(sk_osc *osc, SKFLT amp)
98{
99 osc->amp = amp;
100}
101#+END_SRC
102* Constants
103#+NAME: constants
104#+BEGIN_SRC c
105#define SK_OSC_MAXLEN 0x1000000L
106#define SK_OSC_PHASEMASK 0x0FFFFFFL
107#+END_SRC
108* Struct and Constants
109The main struct of this oscillator is called =sk_osc=.
110#+NAME: typedefs
111#+BEGIN_SRC c
112typedef struct sk_osc sk_osc;
113#+END_SRC
114#+NAME: structs
115#+BEGIN_SRC c
116struct sk_osc {
117 <<sk_osc>>
118};
119#+END_SRC
120
121#+NAME: sk_osc
122#+BEGIN_SRC c
123SKFLT freq, amp;
124SKFLT *tab;
125int inc;
126size_t sz;
127uint32_t nlb;
128SKFLT inlb;
129uint32_t mask;
130SKFLT maxlens;
131int32_t lphs;
132#+END_SRC
133
134The oscillator stores it's main parameters =freq= and =amp=
135as floating point parameters. They are set to be values
136440 and 0.2 by default.
137
138#+NAME: osc_init
139#+BEGIN_SRC c
140osc->freq = 440;
141osc->amp = 0.2;
142#+END_SRC
143
144A reference to the table is stored in the variable =tab=,
145with its size =sz=.
146
147#+NAME: osc_init
148#+BEGIN_SRC c
149osc->tab = wt;
150osc->sz = sz;
151#+END_SRC
152
153A table lookup oscillator indexes through the table using
154the increment rate stored in the integer value =inc=. This
155value can be positive or negative. Is is zeroed out at
156init-time.
157
158#+NAME: osc_init
159#+BEGIN_SRC c
160osc->inc = 0;
161#+END_SRC
162
163The variable =lphs= stores the phase position of the
164previous sample. The initial phase value =iphs= is
165multiplied with the the maximum table value, and then
166masked to keep values in range.
167
168#+NAME: osc_init
169#+BEGIN_SRC c
170osc->lphs = ((int32_t)(iphs * SK_OSC_MAXLEN)) & SK_OSC_PHASEMASK;
171#+END_SRC
172
173For the fixed point table-lookup, some constants are derived
174and stored.
175
176Phasor position is stored by splitting the bits of an
177N-bit integer number into two parts. The upper bits store the
178integer portion, while the lower bits store fractional
179portion. The maximum number of bits is arbitrary, but the
180underlying architecture must be able to accomodate for the
181width. In this implementation, the phasor uses 28 bits
182inside of a 32-bit number. This implicitely means the largest
183value can be
184
185Split in the phasor position is measured by counting the
186number of lower bits. This value is stored in the variable
187=nlb=. This value is calculated with the equation
188
189@!
190#$$n = log2(M/s)$$
191(img "/res/wiki/osc/osc_log2.png")
192!@
193
194Where =n= is the number of lower bits, =M= is the maximum
195wavetable size, and =s= is the size of the wavetable.
196
197To calculate =nlb=, and hand-rolled =log2= function is
198created.
199
200Smaller values of =s= mean more bits in the fractional
201component of the number.
202
203#+NAME: osc_init
204#+BEGIN_SRC c
205{
206 uint32_t tmp;
207 tmp = SK_OSC_MAXLEN / sz;
208 osc->nlb = 0;
209 while (tmp >>= 1) osc->nlb++;
210}
211<<calculate_mask>>
212<<calculate_inlb>>
213<<calculate_maxlens>>
214#+END_SRC
215
216The =mask= is the lower-bits masking variable. When an AND
217operation is used against this constant, it filters out all
218the upper bits, so only the lower bits can pass through.
219This constant is necessary for being able to extract the
220lower bits from the fixed-point phase value representation.
221This sort of value is known in the bit-twiddling world as a
222=mask=. In binary, all the lower bits up to the number of
223lower bits are set to be on, with the remaining bits set
224to be 0.
225
226#+NAME: calculate_mask
227#+BEGIN_SRC c
228osc->mask = (1<<osc->nlb) - 1;
229#+END_SRC
230
231The inverse of max lower bits value, or $1/2^{nlb}$, is
232stored as a constant. This cached value is used to replace
233and divide operation with a multiply operation, which has
234traditionally been a cheaper operation to do on a computer.
235
236#+NAME: calculate_inlb
237#+BEGIN_SRC c
238osc->inlb = 1.0 / (1<<osc->nlb);
239#+END_SRC
240
241The constant =maxlens= is the maximum table length in units
242of seconds. This is a value used to efficiently convert the
243frequency parameter to sample increment value.
244
245#+NAME: calculate_maxlens
246#+BEGIN_SRC c
247osc->maxlens = 1.0 * SK_OSC_MAXLEN / sr;
248#+END_SRC
249* Initialization
250In addition to setting variables, the init function will
251also set the starting phase of oscillator.
252#+NAME: funcs
253#+BEGIN_SRC c
254void sk_osc_init(sk_osc *osc, int sr, SKFLT *wt, int sz, SKFLT iphs)
255{
256 <<osc_init>>
257}
258#+END_SRC
259* Computation
260The meat of the algorithm is here. Here outlines the
261=tick= function, where a single sample of an oscillator
262is computer.
263
264#+NAME: funcs
265#+BEGIN_SRC c
266SKFLT sk_osc_tick(sk_osc *osc)
267{
268 SKFLT out;
269 SKFLT fract;
270 SKFLT x1, x2;
271 int32_t phs;
272 int pos;
273
274 out = 0;
275 <<update_increment_amount>>
276 <<lookup_values>>
277 <<obtain_fractional_component>>
278 <<interpolate_values>>
279 <<update_the_state>>
280 return out;
281}
282#+END_SRC
283
284First, the increment amount =inc= is updated.
285The increment amount tells how much further to move the read
286pointer in the table. This increment amount is based on the
287current oscillator frequency =freq= and the variable
288=maxlens=. =lrintf= is used to round to the nearest integer.
289
290This is kind of a baffling operation. How could multiplying
291the frequency by some arbitrary duration yield a phasor
292increment amount? And where is the sampling rate in all
293this?
294
295The thing that throws everything off is this fixed point
296business. Things make a lot more sense if you wanted to
297do this the floating-point way.
298
299Recall that a phasor is a repeating line that ramps from
3000 to 1 over a given period of time. If we call this period
301of time =t=, the increment value =I= needs to work so that
302=t * sr * I = 1=. In other words, it's the slope of a line
303discretised.
304
305Linear slope is found using good-ol rise over run, change in
306value over change in time. Inverting the frequency =1/F=
307gives it's period length in seconds. Multiplying by the
308sampling rate =sr= converts that value to samples. This
309gives us =sr/f=, our change in time. Because of the
310normalized range, change in value is just 1. Putting it all
311together This gives us a slope of =1/(sr/f)=, or =f/sr=.
312That is the normalized increment value.
313
314If we wanted this to work with our fixed point phasor,
315we'd need to scale it by the maximum length of the phasor.
316That looks like =(f/sr)*maxlen)=, or =(f * maxlen)/sr)=.
317The frequency =f= can be pulled out of the numerator to
318get =f * (maxlen/sr)=, which can be reduced to the operation
319similarly seen below of =f * maxlens=.
320
321Before frequency units were measured in Hertz, they were
322measured in cycles-per-second. If you ever took a highschool
323chemistry or physics class, you may recall that units can
324"cancel out" one another like a fraction. When
325cycles-per-second (cycles / seconds) gets multiplied by
326a value in seconds, the seconds cancel. What you are left
327with is a unit called *cycles*.
328
329#+NAME: update_increment_amount
330#+BEGIN_SRC c
331osc->inc = floor(lrintf(osc->freq * osc->maxlens));
332#+END_SRC
333
334Look up values A =n= and B =n + 1= samples from wavetable.
335Perform table lookup. Both the current position, and it's
336neighor are needed.
337This position is found by looking at the upper bits of
338the current phase.
339
340#+NAME: lookup_values
341#+BEGIN_SRC c
342phs = osc->lphs;
343pos = phs >> osc->nlb;
344x1 = osc->tab[pos];
345x2 = osc->tab[(pos + 1) % osc->sz];
346#+END_SRC
347
348Now, it's time to interpolate between points A and B. This
349oscillator uses linear interpolation, which can be thought
350of as a crossfade between two values. The equation for
351linear interpolation is commonly shown as:
352
353@!
354#$$
355#y = (1 - \alpha) x_1 + \alpha x_2
356#$$
357(img "/res/wiki/osc/osc_lerp.png")
358!@
359
360Where $x_1$ and $x_2$ are two values, and $\alpha$ is a
361fractional value between 0 and 1. The $\alpha$ values
362determines the distribution balance of
363the two values. When $\alpha = 0$, it is entirely $x_1$, and
364when $\alpha = 1$, the value is $x_2$.
365
366The fractional (alpha) value is obtained by taking the lower
367bits portion of the current fixed-point phase position, and
368normalizing it to be a floating-point value between 0 and 1.
369
370#+NAME: obtain_fractional_component
371#+BEGIN_SRC c
372fract = (phs & osc->mask) * osc->inlb;
373#+END_SRC
374
375There are now all the parts to do the interpolation. It
376turns out the equation above can be simplified further to
377shave off a multiply operation.
378
379@!
380# $$
381# \eqalign{
382# y &= (1 - \alpha)x_1 + \alpha x_2 \cr
383# &= x_1 - \alpha x_1 + \alpha x_2 \cr
384# &= x_1 + \alpha x_2 - \alpha x_1 \cr
385# &= x_1 + (x_2 - x_1)\alpha
386# }
387# $$
388(img "/res/wiki/osc/osc_lerp_simp.png")
389!@
390
391Which then gets translated to the following C code below.
392In this step, the output is also being scaled by the
393amplitude =amp=.
394
395#+NAME: interpolate_values
396#+BEGIN_SRC c
397out = (x1 + (x2 - x1) * fract) * osc->amp;
398#+END_SRC
399
400And now the sample has been computed! To wrap up, the
401internal phase amount =lphs= is updated and masked to
402prevent overflow.
403
404#+NAME: update_the_state
405#+BEGIN_SRC c
406phs += osc->inc;
407phs &= SK_OSC_PHASEMASK;
408osc->lphs = phs;
409#+END_SRC
peakeq.org+358−0
@@ -0,0 +1,358 @@
1#+TITLE: PeakEQ
2* Overview
3PeakEQ is an implementation of a single
4[[https://ccrma.stanford.edu/~jos//filters/Peaking_Equalizers.html][peaking equalizer filter]], with sweepable
5frequency, bandwidth, and gain.
6When used in series, the PeakEQ filter can be used
7to construct parametric and graphic equalizers commonly
8found in DAWs.
9* Algorithm Details
10The filter design based on the 2nd-order equalization filter
11design by Reglia and Mitra, from the paper
12"Tunable Digital Frequency Response Equalization Filters",
13IEEE Trans. on Ac., Sp. and Sig Proc., 35 (1), 1987
14
15This implementation takes the original filter design and
16converts it to a direct-form 2 filter configuration, which
17has better numerical stability.
18* Conversion to Direct Form 2
19We begin with the transfer function, described in the 1987
20paper:
21
22#+BEGIN_SRC tex
23$$
24{
25\alpha + \beta(1 + \alpha)z^{-1} + z^{-2}
26\over
271 + \beta(1 + \alpha)z^{-1} + \alpha z^{-2}
28}
29$$
30#+END_SRC
31
32This has the following difference equation:
33
34#+BEGIN_SRC tex
35$$
36y(n) = \alpha x(n) + \beta(1 + \alpha)x(n - 1) + x(n - 2)
37- \beta(1 + \alpha)z^{-1} y(n - 1) - \alpha y(n - 2)
38$$
39#+END_SRC
40
41The
42[[https://ccrma.stanford.edu/~jos/filters/Direct_Form_II.html][Direct Form II Structure]]. can be
43represented in this generalized difference equation:
44
45#+BEGIN_SRC tex
46$$
47v(n) = x(n) - a_1 v(n - 1) - a_2 v(n - 2)
48
49y(n) = b_0 v(n) + b_1 v(n - 1) + b_2 v(n - 2)
50$$
51#+END_SRC
52
53Converting to direct form II is a matter of putting
54coefficients in their correct places. Placing in the
55coefficients from the Reglia and Mitra design yields the
56following difference equation:
57
58#+BEGIN_SRC tex
59$$
60v(n) = x(n) - \beta(1 + \alpha)v(n - 1) - \alpha v(n - 2)
61
62y(n) = \alpha v(n) + \beta(1 + \alpha) v(n - 1) + v(n - 2)
63$$
64#+END_SRC
65
66Which can be translated to C code.
67* Tangled Files
68Tangles to files =peakeq.c= and =peakeq.h=.
69
70#+NAME: peakeq.c
71#+BEGIN_SRC c :tangle peakeq.c
72#include <math.h>
73#define SK_PEAKEQ_PRIV
74#include "peakeq.h"
75
76#ifndef M_PI
77#define M_PI 3.14159265358979323846
78#endif
79
80<<funcs>>
81#+END_SRC
82
83When =SK_PEAKEQ_PRIV= is defined, it exposes the main
84struct.
85
86#+NAME: peakeq.h
87#+BEGIN_SRC c :tangle peakeq.h
88#ifndef SK_PEAKEQ_H
89#define SK_PEAKEQ_H
90
91#ifndef SKFLT
92#define SKFLT float
93#endif
94
95<<typedefs>>
96<<funcdefs>>
97
98#ifdef SK_PEAKEQ_PRIV
99<<structs>>
100#endif
101#endif
102#+END_SRC
103* Struct
104Everything is stored in a struct called =sk_peakeq=.
105
106#+NAME: typedefs
107#+BEGIN_SRC c
108typedef struct sk_peakeq sk_peakeq;
109#+END_SRC
110
111#+NAME: structs
112#+BEGIN_SRC c
113struct sk_peakeq {
114 <<sk_peakeq>>
115};
116#+END_SRC
117* Init
118PeakEQ is initialized with =sk_peakeq_init=. The sampling
119rate is required here.
120
121#+NAME: funcdefs
122#+BEGIN_SRC c
123void sk_peakeq_init(sk_peakeq *eq, int sr);
124#+END_SRC
125
126#+NAME: funcs
127#+BEGIN_SRC c
128void sk_peakeq_init(sk_peakeq *eq, int sr)
129{
130 <<init>>
131}
132#+END_SRC
133* Filter State Memory and Constants
134The direct-form 2 form can be thought of as a 2-pole filter,
135followed by a 2-zero filter.
136
137The memory for the 2-pole filter is stored in a variable
138called =v=.
139
140#+NAME: sk_peakeq
141#+BEGIN_SRC c
142SKFLT v[2];
143#+END_SRC
144
145#+NAME: init
146#+BEGIN_SRC c
147eq->v[0] = 0;
148eq->v[1] = 0;
149#+END_SRC
150
151The alpha and beta coefficients are stored as variables
152called =a= and =b=.
153
154#+NAME: sk_peakeq
155#+BEGIN_SRC c
156SKFLT a;
157SKFLT b;
158#+END_SRC
159
160#+NAME: init
161#+BEGIN_SRC c
162eq->a = 0;
163eq->b = 0;
164#+END_SRC
165
166The sampling rate is required to calculate new coefficients
167on-the-fly. A copy is stored in this struct.
168
169#+NAME: sk_peakeq
170#+BEGIN_SRC c
171int sr;
172#+END_SRC
173
174#+NAME: init
175#+BEGIN_SRC c
176eq->sr = sr;
177#+END_SRC
178* Parameters
179** Frequency
180Set with =sk_peakeq_freq=. Used to set the center frequency
181of the filter, in units of Hz.
182
183#+NAME: funcdefs
184#+BEGIN_SRC c
185void sk_peakeq_freq(sk_peakeq *eq, SKFLT freq);
186#+END_SRC
187
188#+NAME: funcs
189#+BEGIN_SRC c
190void sk_peakeq_freq(sk_peakeq *eq, SKFLT freq)
191{
192 eq->freq = freq;
193}
194#+END_SRC
195
196This parameter uses caching to detect if the parameter
197changes.
198
199#+NAME: sk_peakeq
200#+BEGIN_SRC c
201SKFLT freq;
202SKFLT pfreq;
203#+END_SRC
204
205=pfreq= is set to be a negative value to force initial
206coefficient calculation.
207
208#+NAME: init
209#+BEGIN_SRC c
210sk_peakeq_freq(eq, 1000);
211eq->pfreq = -1;
212#+END_SRC
213** Bandwidth
214Set with =sk_peakeq_bandwidth=. This parameter sets the
215bandwidth of the EQ filter, in units of Hz.
216
217#+NAME: funcdefs
218#+BEGIN_SRC c
219void sk_peakeq_bandwidth(sk_peakeq *eq, SKFLT bw);
220#+END_SRC
221
222#+NAME: funcs
223#+BEGIN_SRC c
224void sk_peakeq_bandwidth(sk_peakeq *eq, SKFLT bw)
225{
226 eq->bw = bw;
227}
228#+END_SRC
229
230This parameter uses caching in order to detect if the
231parameter changes.
232
233#+NAME: sk_peakeq
234#+BEGIN_SRC c
235SKFLT bw;
236SKFLT pbw;
237#+END_SRC
238
239Like with freq, =pbw= is set to be a negative value to force
240coefficient calculation at the initial computation.
241
242#+NAME: init
243#+BEGIN_SRC c
244sk_peakeq_bandwidth(eq, 1000);
245eq->pbw = -1;
246#+END_SRC
247** Gain
248This sets the gain of the filter. Positive values will cause
249a boost. Negative values will create a cut.
250
251#+NAME: funcdefs
252#+BEGIN_SRC c
253void sk_peakeq_gain(sk_peakeq *eq, SKFLT gain);
254#+END_SRC
255
256#+NAME: funcs
257#+BEGIN_SRC c
258void sk_peakeq_gain(sk_peakeq *eq, SKFLT gain)
259{
260 eq->gain = gain;
261}
262#+END_SRC
263
264#+NAME: sk_peakeq
265#+BEGIN_SRC c
266SKFLT gain;
267#+END_SRC
268
269#+NAME: init
270#+BEGIN_SRC c
271sk_peakeq_gain(eq, 1.0);
272#+END_SRC
273* Compute
274=sk_peakeq_tick=.
275
276#+NAME: funcdefs
277#+BEGIN_SRC c
278SKFLT sk_peakeq_tick(sk_peakeq *eq, SKFLT in);
279#+END_SRC
280
281#+NAME: funcs
282#+BEGIN_SRC c
283SKFLT sk_peakeq_tick(sk_peakeq *eq, SKFLT in)
284{
285 SKFLT out;
286 SKFLT v;
287 SKFLT y;
288 out = 0;
289
290 <<update_coefficients>>
291 <<compute_difference_equations>>
292 <<compute_gain>>
293 <<update_filter_state>>
294
295 return out;
296}
297#+END_SRC
298
299Update coefficients, if needed. This happens at init time
300or when any of the parameters change.
301
302The coefficents computed are alpha and beta. The beta
303coefficient is the negative cosine of the center frequency
304in units of radians. The alpha coefficient is the expression
305=(1 - c) / (1 + c)=, where =c= is the tangent of the
306bandwidth, times =PI=, divided by the sampling rate. More
307details on the both coefficient derivations can be found in
308the original Reglia and Mitra paper.
309
310#+NAME: update_coefficients
311#+BEGIN_SRC c
312if (eq->bw != eq->pbw || eq->freq != eq->pfreq) {
313 SKFLT c;
314 eq->b = -cos(2 * M_PI * eq->freq / eq->sr);
315 c = tan(M_PI * eq->bw / eq->sr);
316 eq->a = (1.0 - c) / (1.0 + c);
317
318 eq->pbw = eq->bw;
319 eq->pfreq = eq->freq;
320}
321#+END_SRC
322
323Compute difference equations. First the 2-pole filter,
324followed by the 2-zero filter. The output of the 2-pole
325filter goes into the 2-zero filter. See the previous section
326on conversion to direct-form 2 for more information.
327
328#+NAME: compute_difference_equations
329#+BEGIN_SRC c
330v = in - eq->b*(1.0 + eq->a)*eq->v[0] - eq->a*eq->v[1];
331y = eq->a*v + eq->b*(1.0 + eq->a)*eq->v[0] + eq->v[1];
332#+END_SRC
333
334Compute the gain. This is done with the following equation:
335
336#+BEGIN_SRC tex
337$$
338y = {((x + f) + g (x - f) \over 2}
339$$
340#+END_SRC
341
342#+NAME: compute_gain
343#+BEGIN_SRC c
344out = ((in + y) + eq->gain*(in - y)) * 0.5;
345#+END_SRC
346
347Where $y$ is the output, $x$ is the input signal, $g$ is
348the gain amount, and =f= is the filtered version of =x=.
349
350Update filter state. The output of the 2-pole filter becomes
351=v[0]=, or $v(n - 1)$, and the previous =v[0]= becomes
352=v[1]=, or $v(n - 2)$.
353
354#+NAME: update_filter_state
355#+BEGIN_SRC c
356eq->v[1] = eq->v[0];
357eq->v[0] = v;
358#+END_SRC
phasewarp.org+104−0
@@ -0,0 +1,104 @@
1#+TITLE: PhaseWarp
2* Overview
3PhaseWarp can be thought of as a special kind of filter for
4normalized phasor signals. When used with a phasor and a
5table-lookup oscillator this produces what is known as
6[[https://en.wikipedia.org/wiki/Phase_distortion_synthesis][phase distortion synthesis]], famously used on Casio
7synthesizers like the VZ-1.
8* Algorithm
9The algorithm for PhaseWarp works by adjusting the midpoint
10position where the phasor reaches 0.5. The adjustment amount
11is usually a normalized bipolar signal in the range -1 to 1.
12When the position value is positive, it warps the midpoint
13towards the upper bounds, causing a greater slope in the
14upper half, while decreasing the slope in the lower half.
15A negative position does opposite, creating a higher slope
16in the lower half, and a lower slope in the upper half.
17
18(The whole thing makes a lot more sense when you graph it.
19Someday, I'll do that here. For now, you'll just have to
20take my word for it.)
21* Tangled Files
22=phasewarp.c= and =phasewarp.h=.
23
24#+NAME: phasewarp.c
25#+BEGIN_SRC c :tangle phasewarp.c
26#include "phasewarp.h"
27<<funcs>>
28#+END_SRC
29
30#+NAME: phasewarp.h
31#+BEGIN_SRC c :tangle phasewarp.h
32#ifndef SK_PHASEWARP_H
33#define SK_PHASEWARP_H
34
35#ifndef SKFLT
36#define SKFLT float
37#endif
38
39<<funcdefs>>
40#endif
41#+END_SRC
42* Compute
43This algorithm is so simple, it can be done in a stateless
44function called =sk_phasewarp_tick=. Its arguments are
45the input phasor signal and the warp factor.
46
47#+NAME: funcdefs
48#+BEGIN_SRC c
49SKFLT sk_phasewarp_tick(SKFLT in, SKFLT warp);
50#+END_SRC
51
52#+NAME: funcs
53#+BEGIN_SRC c
54SKFLT sk_phasewarp_tick(SKFLT in, SKFLT warp)
55{
56 SKFLT out;
57 SKFLT wmp;
58
59 out = 0;
60
61 <<calculate_warped_midpoint>>
62 <<determine_side_and_warp>>
63 return out;
64}
65#+END_SRC
66
67Calculate warped midpoint. Call it =wmp=.
68
69#+NAME: calculate_warped_midpoint
70#+BEGIN_SRC c
71wmp = (warp + 1.0) * 0.5;
72#+END_SRC
73
74Determine side and compute. If the phasor position is
75less than the warped midpoint, than compute
76with lefthand slope. Otherwise, compute righthand slope.
77
78#+NAME: determine_side_and_warp
79#+BEGIN_SRC c
80if (in < wmp) {
81 <<compute_with_lefthand_slope>>
82} else {
83 <<compute_with_righthand_slope>>
84}
85#+END_SRC
86
87Compute With Lefthand Slope. This slope is equal to
88=0.5/wmp=. This value scales the phasor input =slope * in=.
89
90#+NAME: compute_with_lefthand_slope
91#+BEGIN_SRC c
92if (wmp != 0) out = ((SKFLT)0.5 / wmp) * in;
93#+END_SRC
94
95Compute Righthand Slope. The righthand slope is computed as
96=0.5/(1 - wmp)=. The output is computed as
97=slope * (in - wmp) + 0.5=.
98
99#+NAME: compute_with_righthand_slope
100#+BEGIN_SRC c
101if (wmp != 1.0) {
102 out = ((SKFLT)0.5 / (SKFLT)(1.0 - wmp)) * (in - wmp) + 0.5;
103}
104#+END_SRC
rline.org+403−0
@@ -0,0 +1,403 @@
1#+TITLE: R-Line
2* Introduction
3R-Line is a random line segment generator. It
4produces random values at regular rate, and then
5linearly interpolates between them. The algorithm has
6audio-rate parametric control of the range of values, as
7well as the frequency that values change.
8
9This algorithm is a great little control signal generator
10that can add a bit of zest to a otherwise
11static parameter in a patch. It's a very efficient way to
12add a warm organic feel to a patch.
13* Notable Implementation Details
14This particular algorithm uses a fixed-point phasor similar
15to the ones seen in @!(ref "osc")!@ and @!(ref "fmpair")!@.
16Fixed point has very good numerical stability, which makes
17it ideal for a phasor algorithm.
18
19To keep things more portable, R-Line implements its own
20pseudo-Random Number Generator (pRNG), using a common
21algorithm generating random values known as
22[[https://en.wikipedia.org/wiki/Linear_congruential_generator][Linear Congruential Generator]], or an LCG. This is
23what most =rand= function
24implementations use under the hood. The modulus, multiplier,
25and and increment values come from the Microsoft
26Visual/Quick C/C++ library (as found on Wikipedia).
27* Tangled Files
28R-Line tangles to a C file and a header file.
29
30The C file =rline.c= contains the core implementation
31code.
32
33#+NAME: rline.c
34#+BEGIN_SRC c :tangle rline.c
35#include <math.h>
36#define SK_RLINE_PRIV
37#include "rline.h"
38<<macros>>
39<<funcs>>
40#+END_SRC
41
42The header file =rline.h= contains forward function
43declarations. When =SK_RLINE_PRIV= is enabled, the structs
44are exposed. Otherwise, they are left as opaque.
45
46#+NAME: rline.h
47#+BEGIN_SRC c :tangle rline.h
48#ifndef SK_RLINE_H
49#define SK_RLINE_H
50
51#ifndef SKFLT
52#define SKFLT float
53#endif
54
55<<typedefs>>
56<<funcdefs>>
57
58#ifdef SK_RLINE_PRIV
59<<structs>>
60#endif
61#endif
62#+END_SRC
63* Core Struct
64The core struct is called =sk_rline=.
65
66#+NAME: typedefs
67#+BEGIN_SRC c
68typedef struct sk_rline sk_rline;
69#+END_SRC
70
71#+NAME: structs
72#+BEGIN_SRC c
73struct sk_rline {
74 <<sk_rline>>
75};
76#+END_SRC
77* Constants and Variables
78The value from the LCG is often scaled to be normalized
79between 0 and 1. The constant required for this is
80$1/(2^31 - 1$, or =1/((1L<<31) - 1)=. This value is
81stored in a constant.
82
83#+NAME: sk_rline
84#+BEGIN_SRC c
85SKFLT rngscale;
86#+END_SRC
87
88#+NAME: set_random_scaler
89#+BEGIN_SRC c
90rl->rngscale = 1.0 / ((1L<<31) - 1);
91#+END_SRC
92
93The current state of the random number generator is stored
94as an integer =rng=. It is initialized to be =seed=.
95
96#+NAME: sk_rline
97#+BEGIN_SRC c
98int rng;
99#+END_SRC
100
101#+NAME: init
102#+BEGIN_SRC c
103rl->rng = seed;
104<<set_random_scaler>>
105<<generate_initial_values>>
106#+END_SRC
107
108The fixed point phasor has two major constants: the maximum
109length, as well as the masking value. Detailed explanation
110of these are beyond the scope of this document.
111
112#+NAME: macros
113#+BEGIN_SRC c
114#define SK_RLINE_PHSLEN 0x1000000L
115#define SK_RLINE_PHSMSK 0xFFFFFFL
116#+END_SRC
117
118The variable =maxlens= is maximum phasor length converted to
119units of seconds by dviding it by the sampling rate.
120Multipyling this value with a frequency will return an
121increment value in units of phasor, which is used
122as the phasor increment value.
123
124#+NAME: sk_rline
125#+BEGIN_SRC c
126SKFLT maxlens;
127#+END_SRC
128
129#+NAME: init
130#+BEGIN_SRC c
131rl->maxlens = (SKFLT)SK_RLINE_PHSLEN / sr;
132#+END_SRC
133
134Phase position is stored in an unsigned long called
135=phspos=. When it needs to be converted for use in floating
136point operations , the constant =scale= is used.
137
138#+NAME: sk_rline
139#+BEGIN_SRC c
140unsigned long phasepos;
141SKFLT scale;
142#+END_SRC
143
144#+NAME: init
145#+BEGIN_SRC c
146rl->phasepos = 0;
147#+END_SRC
148
149The scale value works by normalizing to be in the range 0-1
150by dividing by the max phase length =SK_RLINE_PHSLEN=, then
151scaling it to be the difference between the start/end point
152values. Doing this shaves off a divide operation later.
153
154#+NAME: calculate_initial_scale
155#+BEGIN_SRC c
156rl->scale = (rl->end - rl->start) / SK_RLINE_PHSLEN;
157#+END_SRC
158
159A line has two points: a start point, and an end point.
160These are stored as normalized floating point variables
161=start= and =end=, and then are dynamically scaled to the
162=min= and =max= values during computation.
163
164#+NAME: sk_rline
165#+BEGIN_SRC c
166SKFLT start;
167SKFLT end;
168#+END_SRC
169
170#+NAME: generate_initial_values
171#+BEGIN_SRC c
172rl->rng = LCG(rl->rng);
173rl->start = RNG(rl->rng) * rl->rngscale;
174rl->rng = LCG(rl->rng);
175rl->end = RNG(rl->rng) * rl->rngscale;
176<<calculate_initial_scale>>
177#+END_SRC
178* Linear Congruential Generator
179An internal
180[[https://en.wikipedia.org/wiki/Linear_congruential_generator][Linear Congruential Generator]] is
181used to generate sequences of pseudo-random numbers.
182
183It is the following equation
184
185$$
186y(n) = ((y(n - 1) * a + c) \gg 1)
187\& m
188$$
189
190Where $m$ is the masking value =0x7ffffff=, $a$ is a
191=multiplier=, and $c$ is the =increment=. In this
192implementation, $a$ will be =0x343fd=, and $c$ will be
193=0x2693ec3=. These constants come from the wikipedia page
194on LCGs, and originate the Microsoft Visual C/C++ standard
195library.
196
197The LCG here can be implemented as a stateless function or
198macro. In this case, we will go with the macro.
199
200#+NAME: macros
201#+BEGIN_SRC c
202#define LCG(y) (y * 0x343fd + 0x269ec3)
203#+END_SRC
204
205The =LCG= operation only computes the next state state of
206the random-number generator. To actually get it within the
207correct bounds for this sytem, it has to be right-shifted
208to knock it down 1 bit, then masked by =0x7ffffff= as a kind
209of modulo operation.
210
211This macro operation =RNG= assumes that =y= is the current
212state of the LCG.
213#+NAME: macros
214#+BEGIN_SRC c
215#define RNG(y) ((y >> 1) & 0x7fffffff)
216#+END_SRC
217* Initialization
218Initialization is done with =sk_rline_init=.
219
220The main things needed for initialization are the sampling
221rate =sr=, as well as the initial seed value for the random
222number generator.
223
224#+NAME: funcdefs
225#+BEGIN_SRC c
226void sk_rline_init(sk_rline *rl, int sr, int seed);
227#+END_SRC
228
229#+NAME: funcs
230#+BEGIN_SRC c
231void sk_rline_init(sk_rline *rl, int sr, int seed)
232{
233 <<init>>
234}
235#+END_SRC
236* Parameters
237** Minimum value
238Set with =sk_rline_min=.
239
240#+NAME: funcdefs
241#+BEGIN_SRC c
242void sk_rline_min(sk_rline *rl, SKFLT min);
243#+END_SRC
244
245#+NAME: funcs
246#+BEGIN_SRC c
247void sk_rline_min(sk_rline *rl, SKFLT min)
248{
249 rl->min = min;
250}
251#+END_SRC
252
253#+NAME: sk_rline
254#+BEGIN_SRC c
255SKFLT min;
256#+END_SRC
257
258Initialized to be 0.
259
260#+NAME: init
261#+BEGIN_SRC c
262sk_rline_min(rl, 0);
263#+END_SRC
264** Maximum value
265Set with =sk_rline_max=.
266
267#+NAME: funcdefs
268#+BEGIN_SRC c
269void sk_rline_max(sk_rline *rl, SKFLT max);
270#+END_SRC
271
272#+NAME: funcs
273#+BEGIN_SRC c
274void sk_rline_max(sk_rline *rl, SKFLT max)
275{
276 rl->max= max;
277}
278#+END_SRC
279
280#+NAME: sk_rline
281#+BEGIN_SRC c
282SKFLT max;
283#+END_SRC
284
285Initialized to be 1.
286
287#+NAME: init
288#+BEGIN_SRC c
289sk_rline_max(rl, 1);
290#+END_SRC
291** Rate
292Set with =sk_rline_rate=.
293
294#+NAME: funcdefs
295#+BEGIN_SRC c
296void sk_rline_rate(sk_rline *rl, SKFLT rate);
297#+END_SRC
298
299#+NAME: funcs
300#+BEGIN_SRC c
301void sk_rline_rate(sk_rline *rl, SKFLT rate)
302{
303 rl->rate= rate;
304}
305#+END_SRC
306
307#+NAME: sk_rline
308#+BEGIN_SRC c
309SKFLT rate;
310#+END_SRC
311
312Initialized to be 1.
313
314#+NAME: init
315#+BEGIN_SRC c
316sk_rline_rate(rl, 1);
317#+END_SRC
318* Computing a sample
319A single sample is computed with =sk_rline_tick=.
320
321#+NAME: funcdefs
322#+BEGIN_SRC c
323SKFLT sk_rline_tick(sk_rline *rl);
324#+END_SRC
325
326#+NAME: funcs
327#+BEGIN_SRC c
328SKFLT sk_rline_tick(sk_rline *rl)
329{
330 SKFLT out;
331
332 out = 0;
333
334 <<compute_current_sample>>
335 <<update_phase>>
336 <<generate_next_line_segment>>
337
338 return out;
339}
340#+END_SRC
341
342Compute the current sample. The line interpolation is
343calculated in a normalized space, then scaled to be
344in the min/max range. Doing it this way allows the min/max
345values to be dynamically changed over time without having
346to wait for the next line.
347
348The normalized output can be computed with the expression:
349
350$$
351y = x_1 + pc
352$$
353
354Where $x_1$ is the starting point of the line, $p$ is
355the current phase increment, represented in fixed point, and
356$c$ is the constant that normalizes and scales the phase to
357be the amount of progress to value $x_2$.
358
359#+NAME: compute_current_sample
360#+BEGIN_SRC c
361out = rl->start + rl->phasepos*rl->scale;
362out = out * (rl->max - rl->min) + rl->min;
363#+END_SRC
364
365
366Update phase position. The phase is updated by incrementing
367it by a amount obtained by multilying the frequency by the
368maximum phase length in units of seconds. How this works is
369beyond the scope of this document, but is explained in @!(ref
370"osc")!@.
371
372#+NAME: update_phase
373#+BEGIN_SRC c
374rl->phasepos += floor(rl->rate * rl->maxlens);
375#+END_SRC
376
377Generate next line segment. Preparation for a new line
378segment happens when the phase of the phasor reaches the
379end, and is greater than or equal to the max length.
380The phasor is masked in order to filter out upper
381bits and allow the lower bits to roll over. The the starting
382value is set to be the current end value, and a new end
383value is obtained using the random number generator.
384
385After the new points have been obtained, the constant used
386to normalize + scale the phasor value is computed. Dividing
387by the maximum phasor length normalizes the phasor to be in
388range 0 and 1. Multiplying by the difference of the two
389segment values scales this value to be in the correct range.
390This constant is useful because it shaves off a divide
391operation, which has traditionally been costly arithmetic
392computatoin compared to a multiply.
393
394#+NAME: generate_next_line_segment
395#+BEGIN_SRC c
396if (rl->phasepos >= SK_RLINE_PHSLEN) {
397 rl->phasepos &= SK_RLINE_PHSMSK;
398 rl->start = rl->end;
399 rl->rng = LCG(rl->rng);
400 rl->end = RNG(rl->rng) * rl->rngscale;
401 rl->scale = (rl->end - rl->start) / SK_RLINE_PHSLEN;
402}
403#+END_SRC
scrubber.org+56−0
@@ -0,0 +1,56 @@
1#+TITLE: Scrubber
2* Overview
3An FFT based in-memory sample scrubber with pitch control.
4* Tangled Files
5* FFT procedures, and Soundpipe
6One could spend a whole lifetime building FFT algorithms,
7so an existing FFT implementation is used. For FFT and
8inverse-FFT operations, the Soundpipe fft utilities are
9used. So yes, Soundpipe is a hard dependency for this
10algorithm. Or at least, bits of Soundpipe are. The FFT
11portions can easily be extracted from the Soundpipe
12codebase.
13
14The soundpipe FFT utility is a wrapper around a public
15domain FFT implementation by John Green.
16* Initialization
17Done with =sk_scrubber_init=.
18* Cleanup
19Done with =sk_scrubber_cleanup=. Mainly done for the sake
20of the =sp_fft=. Future iterations will try to remove any
21mandatory use of =malloc= and =free=.
22* Constants
23Scrubber uses a fixed FFT size, which greatly simplifies
24things. The FFT size chosen is 2048, or =2^11=. It's a good
25value that equal parts effeciency and fidelity.
26
27The LOG2 of this FFT size is 11, and this is what is passed
28into the soundpipe FFT interface.
29
30The hopsize determines how quickly to slide along the FFT
31window. In this case it's 512, or the FFT size divided up
32into 4 bits.
33
34This divisor is a significant constant that will be
35stored as a constant as well.
36* Parameters
37** Position
38Sets the current read position of the buffer, in samples.
39** Playback
40Sets the relative playback speed. 1 is normal, 2 is 2x
41speed, and 0.5 is 2x slower.
42* Computation
43Read interpolated samples from buffer forward/backward
44frames.
45
46Take FFT of frames
47
48Phase Vocode.
49
50Inverse FFT operation.
51
52Overlap-add to output frames.
53
54Write to output and scale;
55
56Update counter and frame position.
vardelay.org+376−0
@@ -0,0 +1,376 @@
1#+TITLE: Vardelay
2* Overview
3Vardelay is an implementation of a variable-length delay
4line, using third order [[https://ccrma.stanford.edu/~jos/pasp/Lagrange_Interpolation.html][Langrange Interpolation]].
5This delay time of this delay line can be adjusted over
6time. Because it uses interpolation, any adjustments can
7cause pitch shifting to occur. This itself is often a
8desirable artifact, and is the basis for audio effects
9like flanging and chorusing.
10
11In addition to delay time, a feedback parameter will also be
12supplied, as it is so often used with delay lines.
13* Tangled Files
14=vardelay.c= and =vardelay.h= are the tangled files.
15
16#+NAME: vardelay.c
17#+BEGIN_SRC c :tangle vardelay.c
18#include <math.h>
19#include <stdlib.h>
20#define SK_VARDELAY_PRIV
21#include "vardelay.h"
22<<funcs>>
23#+END_SRC
24
25If =SK_VARDELAY_PRIV= is defined, private structs are made
26public.
27
28#+NAME: vardelay.h
29#+BEGIN_SRC c :tangle vardelay.h
30#ifndef SK_VARDELAY_H
31#define SK_VARDELAY_H
32
33#ifndef SKFLT
34#define SKFLT float
35#endif
36
37<<typedefs>>
38<<funcdefs>>
39#ifdef SK_VARDELAY_PRIV
40<<structs>>
41#endif
42#endif
43#+END_SRC
44* Initialization
45Initialized with =sk_vardelay_init=. must supply your own
46buffer, with buffer size. This way, no internal memory
47allocation is required.
48
49#+NAME: funcdefs
50#+BEGIN_SRC c
51void sk_vardelay_init(sk_vardelay *vd, int sr,
52 SKFLT *buf, unsigned long sz);
53#+END_SRC
54
55#+NAME: funcs
56#+BEGIN_SRC c
57void sk_vardelay_init(sk_vardelay *vd, int sr,
58 SKFLT *buf, unsigned long sz)
59{
60 <<init>>
61}
62#+END_SRC
63
64Anything buffer size less than 4 samples is not enough
65to do third-order interpolation, so it returns an error.
66* Struct
67Called =sk_vardelay=.
68
69#+NAME: typedefs
70#+BEGIN_SRC c
71typedef struct sk_vardelay sk_vardelay;
72#+END_SRC
73
74#+NAME: structs
75#+BEGIN_SRC c
76struct sk_vardelay {
77 <<sk_vardelay>>
78};
79#+END_SRC
80* Constants and Variables
81** Sample rate
82a copy of the sampling rate is stored in the struct.
83It is used to convert delay time seconds to samples.
84
85#+NAME: sk_vardelay
86#+BEGIN_SRC c
87int sr;
88#+END_SRC
89
90#+NAME: init
91#+BEGIN_SRC c
92vd->sr = sr;
93#+END_SRC
94** Buffer, Buffer Size
95The delay buffer is the chunk of memory where samples will
96be read/written. It is assumed to be neatly pre-allocated
97and zeroed out externally. The size is also assumed to be
98known ahead of time and correct.
99
100If the buffer size is less than 4, the delay line is
101disabled, as there aren't enough samples to do the
102interpolation.
103
104#+NAME: sk_vardelay
105#+BEGIN_SRC c
106SKFLT *buf;
107unsigned long sz;
108#+END_SRC
109
110#+NAME: init
111#+BEGIN_SRC c
112if (sz < 4) {
113 vd->buf = NULL;
114 vd->buf = 0;
115} else {
116 vd->buf = buf;
117 vd->sz = sz;
118}
119#+END_SRC
120** Previous output
121A variable is used to store the output of the previous delay
122in what is known as a single-sample delay or a unit delay.
123This delay is used to introduce feedback into the system.
124In discrete digital systems like this one, unit delays are
125inevitable for feedback, which is why they are referred to
126as *implicit* delays.
127
128#+NAME: sk_vardelay
129#+BEGIN_SRC c
130SKFLT prev;
131#+END_SRC
132
133#+NAME: init
134#+BEGIN_SRC c
135vd->prev = 0;
136#+END_SRC
137** Write Position
138The write position is used to store the current index
139position in the buffer being written to. The write position
140starts at zero, moves forward until it reaches the end of
141the buffer, and then goes back to the beginning again. The
142delayed samples start the write position, and move backwards
143in time. If that position is a negative, it wraps around.
144
145Notably, the write position is a signed =long= to compensate
146for negative values. This is needed for the delay to read
147stuff back in time.
148
149#+NAME: sk_vardelay
150#+BEGIN_SRC c
151long writepos;
152#+END_SRC
153
154#+NAME: init
155#+BEGIN_SRC c
156vd->writepos = 0;
157#+END_SRC
158* Parameters
159** Delay Time
160Set with =sk_vardelay_delay=, In units of seconds.
161
162#+NAME: funcdefs
163#+BEGIN_SRC c
164void sk_vardelay_delay(sk_vardelay *vd, SKFLT delay);
165#+END_SRC
166
167#+NAME: funcs
168#+BEGIN_SRC c
169void sk_vardelay_delay(sk_vardelay *vd, SKFLT delay)
170{
171 vd->delay = delay;
172}
173#+END_SRC
174
175#+NAME: sk_vardelay
176#+BEGIN_SRC c
177SKFLT delay;
178#+END_SRC
179
180The initial delay time is set to be half the delay time, in
181units of seconds.
182
183#+NAME: init
184#+BEGIN_SRC c
185sk_vardelay_delay(vd, ((SKFLT)sz / sr) * 0.5);
186#+END_SRC
187** Feedback
188Set with =sk_vardelay_feedback=. Should be a value between
1890 and 1.
190
191#+NAME: funcdefs
192#+BEGIN_SRC c
193void sk_vardelay_feedback(sk_vardelay *vd, SKFLT feedback);
194#+END_SRC
195
196#+NAME: funcs
197#+BEGIN_SRC c
198void sk_vardelay_feedback(sk_vardelay *vd, SKFLT feedback)
199{
200 vd->feedback = feedback;
201}
202#+END_SRC
203
204#+NAME: sk_vardelay
205#+BEGIN_SRC c
206SKFLT feedback;
207#+END_SRC
208
209No feedback by default.
210
211#+NAME: init
212#+BEGIN_SRC c
213sk_vardelay_feedback(vd, 0);
214#+END_SRC
215* Computation
216Done with =sk_vardelay_tick=.
217
218#+NAME: funcdefs
219#+BEGIN_SRC c
220SKFLT sk_vardelay_tick(sk_vardelay *vd, SKFLT in);
221#+END_SRC
222
223#+NAME: funcs
224#+BEGIN_SRC c
225SKFLT sk_vardelay_tick(sk_vardelay *vd, SKFLT in)
226{
227 SKFLT out;
228 SKFLT dels;
229 SKFLT f;
230 long i;
231 SKFLT s[4];
232 unsigned long n[4];
233 SKFLT a, b, c, d;
234
235 out = 0;
236 <<return_if_empty>>
237 <<write_to_buffer>>
238 <<calculate_read_position>>
239 <<wrap_and_flip>>
240 <<read_samples>>
241 <<calculate_interpolation_coefficients>>
242 <<interpolate>>
243 <<update_position>>
244 <<update_feedback>>
245
246 return out;
247}
248#+END_SRC
249
250If buffer is NULL or size is 0, return 0.
251
252#+NAME: return_if_empty
253#+BEGIN_SRC c
254if (vd->buf == NULL || vd->sz == 0) return 0;
255#+END_SRC
256
257Write to buffer with feedback.
258
259#+NAME: write_to_buffer
260#+BEGIN_SRC c
261vd->buf[vd->writepos] = in + vd->prev * vd->feedback;
262#+END_SRC
263
264Calculate the read position. This is two parts: a fractional
265component, and an integer component.
266
267Get floating point + integer components of delay time.
268The delay time position is set to be the write position
269=dels= samples in the past, where =dels= is the delay time
270in samples.
271
272#+NAME: calculate_read_position
273#+BEGIN_SRC c
274dels = vd->delay * vd->sr;
275i = floor(dels);
276f = i - dels;
277i = vd->writepos - i;
278#+END_SRC
279
280Because we're looking backwards, the fractional component is
281backwards too. A fractional value here is set to be
282negative (reaching back in time).
283
284If there is a fractional component that is negative, or the
285integer component is negative, these must be corrected.
286
287The fractional value is flipped to be positive by adding 1
288to itself. The integer position is set back in time one
289sample. This sets the interpolation up so that instead of
290taking a sample and interpolating backwards, you start with
291the previous sample and move forwards. The integer position
292is then set to be in bounds.
293
294By default, the position is wrapped around using addition
295until the sample is in bounds.
296
297#+NAME: wrap_and_flip
298#+BEGIN_SRC c
299if ((f < 0.0) || (i < 0)) {
300 /* flip fractional component */
301 f = f + 1.0;
302 /* go backwards one sample */
303 i = i - 1;
304 while (i < 0) i += vd->sz;
305} else while(i >= vd->sz) i -= vd->sz;
306#+END_SRC
307
308Read samples. This includes the current sample, the previous
309sample, and two samples in the future.
310
311#+NAME: read_samples
312#+BEGIN_SRC c
313/* x(n) */
314n[1] = i;
315
316/* x(n + 1) */
317if (i == (vd->sz - 1)) n[2] = 0;
318else n[2] = n[1] + 1;
319
320/* x(n - 1) */
321if (i == 0) n[0] = vd->sz - 1;
322else n[0] = i - 1;
323
324if (n[2] == vd->sz - 1) n[3] = 0;
325else n[3] = n[2] + 1;
326
327{
328 int j;
329 for (j = 0; j < 4; j++) s[j] = vd->buf[n[j]];
330}
331#+END_SRC
332
333Calculate interpolation coefficients. These four
334coefficients correspond with the four samples read.
335
336#+NAME: calculate_interpolation_coefficients
337#+BEGIN_SRC c
338{
339 SKFLT tmp[2];
340
341 d = ((f * f) - 1) * 0.1666666667;
342 tmp[0] = (f + 1.0) * 0.5;
343 tmp[1] = 3.0 * d;
344 a = tmp[0] - 1.0 - d;
345 c = tmp[0] - tmp[1];
346 b = tmp[1] - f;
347}
348#+END_SRC
349
350Interpolate. This follows the following equation:
351
352$$
353y(n) = (a x(n - 1) + b x(n) + c x(n + 1) + d x(n + 2)) \cdot f + x(n)
354$$
355
356#+NAME: interpolate
357#+BEGIN_SRC c
358out = (a*s[0] + b*s[1] + c*s[2] + d*s[3]) * f + s[1];
359#+END_SRC
360
361Update position. Increment the write position, and wrap
362back to zero if it reaches the end of the delay buffer.
363
364#+NAME: update_position
365#+BEGIN_SRC c
366vd->writepos++;
367if (vd->writepos == vd->sz) vd->writepos = 0;
368#+END_SRC
369
370Update feedback. The current output is set to be the =prev=
371value in the delay line.
372
373#+NAME: update_feedback
374#+BEGIN_SRC c
375vd->prev = out;
376#+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