Scaling wavetables

I've used my earlier notes to generate band limited wavetables for my synthesizer. Some professional applications will dedicate a table for each MIDI note, but I'd like to strike a balance between audio quality/storage by interpolating between 12 wavetables spaced across the octaves.

┌─────┬─────────┬─────────┬─────────────┐
│index│MIDI note│frequency│max harmonics│
├─────┼─────────┼─────────┼─────────────┤
│0  ★ │0        │0.348834 │2935         │
├─────┼─────────┼─────────┼─────────────┤
│1  ★ │6        │0.493326 │2075         │
├─────┼─────────┼─────────┼─────────────┤
│2    │18       │0.986652 │1037         │
├─────┼─────────┼─────────┼─────────────┤
│3    │30       │1.9733   │518          │
├─────┼─────────┼─────────┼─────────────┤
│4    │42       │3.94661  │259          │
├─────┼─────────┼─────────┼─────────────┤
│5    │54       │7.89321  │129          │
├─────┼─────────┼─────────┼─────────────┤
│6    │66       │15.7864  │64           │
├─────┼─────────┼─────────┼─────────────┤
│7    │78       │31.5729  │32           │
├─────┼─────────┼─────────┼─────────────┤
│8    │90       │63.1457  │16           │
├─────┼─────────┼─────────┼─────────────┤
│9    │102      │126.291  │8            │
├─────┼─────────┼─────────┼─────────────┤
│10   │114      │252.583  │4            │
├─────┼─────────┼─────────┼─────────────┤
│11 ☆ │126      │505.166  │2            │
└─────┴─────────┴─────────┴─────────────┘

★ These octaves are technically inaudible; one of these can probably be left out of the final implementation.

☆ The scale ends at note 126 because note 127's table would have one harmonic, making it a pure sine.

If I were to play note 56, the synthesizer would read from the 6th table containing 129 harmonics and the 7th containing 64 harmonics, and then weight these two samples according to the distance of 56 from 54 and 66. In this instance, the sample from the wavetable centered on note 54 would be far more prominent due to its proximity to 56. It's a simple process that makes for a boring read, but I wanted to jot some thoughts down using J before implementing it in C.

Choosing

A wave's 12 band limited tables would be stored in a master array. Reproducing a non-aliased signal at frequency f would be a matter of finding a corresponding array index n, then interpolating between the waves at n and n+1.

At first glance it seems quite simple to hard code the value of n for each MIDI note. Note 56 can read between the 6th and 7th index of the tables array and call it a day; there's no reason to calculate anything on the fly. But that would make for boring, static sounds, so it never plays out that way. In frequency modulation synthesis, we can't assume that an oscillator is locked to a static pitch. Its frequency can fluctuate wildly from sample to sample, so the program must be able to orient it towards an appropriate wavetable in a dynamic manner.

Let's say I'm playing note 56 (frequency increment 8.85983) but some powerful modulation has its pitch for one sample at an increment of 21.0724. I now have to read from a different wavetable with less harmonics, but how can I index a completely arbitrary frequency against the array of tables?

The logarithmic nature of music scales comes in handy here. Since every octave is twice the frequency of the one that precedes it, it is almost enough to call log2 over the frequency increment values of the MIDI notes, truncate them, and receive a consecutive integer sequence. The only problem is the lower octaves.

   lowestFrequency =: 8.1757989156
   pitch =: lowestFrequency * 2^%&12
   wavetableIncrement =: (2048 % 48000)&*@pitch

   <.2^.wavetableIncrement 0 6 18 30 42 54 66 78 90 102 114 126

_2 _2 _1 0 1 2 3 4 5 6 7 8

Scaling helps.

   <.2^. 4.5*wavetableIncrement 0 6 18 30 42 54 66 78 90 102 114 126
0 1 2 3 4 5 6 7 8 9 10 11

Using this simple math shows that a pitch of 21.0724 should read between the 7th and 8th wavetables.

   selectWavetable=:<.@(2^.4.5&*)

   selectWavetable 21.0724

6

Needs to be bound checked of course.

Interpolating

The corresponding frequency at the wavetable index n can be found by multiplying the frequency at index 1 (0.493326) by 2n-1.

   toFrequency=:0.493326*(2^-&1)

   toFrequency 6

15.7864

   toFrequency 0

0.246663

This will return an inaccurate increment for index 0, but it shouldn't be a huge problem considering how we can't even hear anything at this range. Ask a whale if it sounds bad.

The frequencies for n and n+1 will be the bounds a,b respectively when interpolating. For frequency f = 21.0724, a = 15.7864 and b = 31.5729. The position of f between [a,b] can be found with the following equation.

f-a / b-a

In J.

   distance =: monad define
     'f a b'=.y
     (f-a)%(b-a)
   )

   distance 21.0724 15.7864 31.5729

0.334843

This value can be used as the fractional part in any standard linear interpolation function.