Tutorial 32. Wavetable interpolation and Vital tables
Working with pre-computed wavetables. |
|
In this tutorial, we will be looking at a wavetable synths proper. As we've discussed, the mOscil object is technically a wavetable synth; it generates a signal by repeatedly reading from a single-cycle wavetable stored in memory. If you've used a wavetable synthesizer plugin like Serum, Vital, Ableton's wavetable instrument or any others, you may find that our mOscil is lacking in the wavetable department. Isn't the point of a wavetable synthesizer to be able to interpolate between waveforms, giving us the ability to create complex timbres without worrying about filter design?
Let's implement just that!
In our first approach, we can just manually interpolate between two waveforms. It turns out this process is straightforward.
Imagine we have two oscillators: oscA and oscB, and an interpolation value x. x will be a value between 0 and 1. When x is zero we only hear the output of oscA and when x is one, we only hear the output of oscB. When x is between zero and one, we will hear a proportion of oscA and oscB according to its value; for example if x = 0.4 , we will hear 60% of oscA and 40% of oscB. The result is a linear interpolation between to two waveforms spectra. The following equation describes this relation
output = (1.0 - x) * oscA + x * oscB
Implementing this on meap, we'll scale x to be between 0 and 255 to avoid decimals.
out_sample = ((255 - x) * oscA.next() + x * oscB.next()) >> 8;
We could now scale one of our pots to the range 0-255 and be able to easily interpolate between two spectra at will!
This process works and sounds great, but most of the wavetables supplied by Mozzi and MEAP are basic waveforms( sines, saws, squares and triangles) so your results will be limited to their spectra. You could write some code to generate some new and exciting wavetables to apply this process to but it turns out there are lots of great wavetables in the world that we can bring into the MEAP environment.
The rest of this tutorial will utilizise the free wavetable synth plugin Vital. If you want to follow along and explore wavetables on your own, go ahead and download it.
From here, we will outline a process for grabbing a wavetable from Vital and playing it in MEAP using an mWavetable object.
Similar to how we implement our oscillators by precomputing a wavetable, we can also precompute interpolations between waveforms, saving a bit of computation time. For example, we could take a sine wave and a sawtooth wave and precompute 256 interpolations gradually morphing from a sine to a saw. We would then have a series of 256 wavetables we could swap between quickly get the same effect as the interpolation process outlined above. This is how many modern wavetable synthesizers implement their oscillators. Vital allows you to export waveforms in this format. What we will get is a .wav file consisting of 256 wavetables of 2048 samples each. Rather than splitting these in 256 different tables to be imported using an mOscil, the mWavetable object lets you easily access them as a single array as long as you tell it how many tables there are and how big they are.
We can get a wavetable from Vital as follows:
- Design/choose a wavetable in vital
- Export as a .wav file. (From the main vital window, click the pencil symbol next to your wavetable. In the wavetable window, choose "Export as a .wav File" from the hamburger menu button)
- Convert to a compatible format using the vital tool here:
- Move to sketch folder and import
This is a wavetable I obtained from Vital using the above process.
The wav file exported from vital consists of 256 frames of 2048 samples each. In the vital tool from step #3, you are able to decrease the frame size or specify the step size which will only keep every nth frame. Both of these options allow you to decrease the file size at the expense of some fidelity. Leaving them at 2048 and 1 respectively will preserve full wavetable fidelity.
mWavetable functions exactly as mOscil, with the exception of the setFrame function. This allows you to choose which of the wavetable frames will be read. It's really just a quick and convenient way of swapping between tables as you might do manually with mOscil. By default, these vital wavetables have 256 frames so you can easily connect the output of an 8-bit oscillator or envelope to the setFrame function to sweep through frames.
Arguments for the mWavetable template can be found in the wavetable's ".h" file. Also note how the setFrame() method is used in updateControl(), in this example we are using pot 0 to choose wavetable position.
Copy the following code to a new sketch and move the "solar.h" wavetable into the sketch's folder and upload!
Note that when we include a wavetable from the sketch folder rather than the library, we place quotation marks around the filepath rather than triangle brackets.
FULL CODE BELOW
/*
Wavetable from vital.
pot #0 controls wavetable position
*/
#define CONTROL_RATE 128 // Hz, powers of 2 are most reliable
#include <Meap.h> // MEAP library, includes all dependent libraries, including all Mozzi modules
Meap meap; // creates MEAP object to handle inputs and other MEAP library functions
MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, MIDI); // defines MIDI in/out ports
// ---------- YOUR GLOBAL VARIABLES BELOW ----------
#include "solar.h"
mWavetable<solar_FRAME_SIZE, solar_NUM_FRAMES, AUDIO_RATE> my_wavetable(solar_DATA);
int frame_num = 0; // when 0, all sine; when 255, all saw
void setup() {
Serial.begin(115200); // begins Serial communication with computer
Serial1.begin(31250, SERIAL_8N1, 43, 44); // sets up MIDI: baud rate, serial mode, rx pin, tx pin
startMozzi(CONTROL_RATE); // starts Mozzi engine with control rate defined above
meap.begin(); // sets up MEAP object
// ---------- YOUR SETUP CODE BELOW ----------
my_wavetable.setFreq(220);
}
void loop() {
audioHook(); // handles Mozzi audio generation behind the scenes
}
/** Called automatically at rate specified by CONTROL_RATE macro, most of your mode should live in here
*/
void updateControl() {
meap.readInputs();
// ---------- YOUR updateControl CODE BELOW ----------
my_wavetable.setFrame(map(meap.pot_vals[0], 0, 4095, 0, 255));
}
/** Called automatically at rate specified by AUDIO_RATE macro, for calculating samples sent to DAC, too much code in here can disrupt your output
*/
AudioOutput_t updateAudio() {
int64_t out_sample = my_wavetable.next();
return StereoOutput::fromNBit(16, (out_sample * meap.volume_val) >> 12, (out_sample * meap.volume_val) >> 12);
}
/**
* Runs whenever a touch pad is pressed or released
*
* int number: the number (0-7) of the pad that was pressed
* bool pressed: true indicates pad was pressed, false indicates it was released
*/
void updateTouch(int number, bool pressed) {
if (pressed) { // Any pad pressed
} else { // Any pad released
}
switch (number) {
case 0:
if (pressed) { // Pad 0 pressed
Serial.println("t0 pressed ");
} else { // Pad 0 released
Serial.println("t0 released");
}
break;
case 1:
if (pressed) { // Pad 1 pressed
Serial.println("t1 pressed");
} else { // Pad 1 released
Serial.println("t1 released");
}
break;
case 2:
if (pressed) { // Pad 2 pressed
Serial.println("t2 pressed");
} else { // Pad 2 released
Serial.println("t2 released");
}
break;
case 3:
if (pressed) { // Pad 3 pressed
Serial.println("t3 pressed");
} else { // Pad 3 released
Serial.println("t3 released");
}
break;
case 4:
if (pressed) { // Pad 4 pressed
Serial.println("t4 pressed");
} else { // Pad 4 released
Serial.println("t4 released");
}
break;
case 5:
if (pressed) { // Pad 5 pressed
Serial.println("t5 pressed");
} else { // Pad 5 released
Serial.println("t5 released");
}
break;
case 6:
if (pressed) { // Pad 6 pressed
Serial.println("t6 pressed");
} else { // Pad 6 released
Serial.println("t6 released");
}
break;
case 7:
if (pressed) { // Pad 7 pressed
Serial.println("t7 pressed");
} else { // Pad 7 released
Serial.println("t7 released");
}
break;
}
}
/**
* Runs whenever a DIP switch is toggled
*
* int number: the number (0-7) of the switch that was toggled
* bool up: true indicated switch was toggled up, false indicates switch was toggled
*/
void updateDip(int number, bool up) {
if (up) { // Any DIP toggled up
} else { //Any DIP toggled down
}
switch (number) {
case 0:
if (up) { // DIP 0 up
Serial.println("d0 up");
} else { // DIP 0 down
Serial.println("d0 down");
}
break;
case 1:
if (up) { // DIP 1 up
Serial.println("d1 up");
} else { // DIP 1 down
Serial.println("d1 down");
}
break;
case 2:
if (up) { // DIP 2 up
Serial.println("d2 up");
} else { // DIP 2 down
Serial.println("d2 down");
}
break;
case 3:
if (up) { // DIP 3 up
Serial.println("d3 up");
} else { // DIP 3 down
Serial.println("d3 down");
}
break;
case 4:
if (up) { // DIP 4 up
Serial.println("d4 up");
} else { // DIP 4 down
Serial.println("d4 down");
}
break;
case 5:
if (up) { // DIP 5 up
Serial.println("d5 up");
} else { // DIP 5 down
Serial.println("d5 down");
}
break;
case 6:
if (up) { // DIP 6 up
Serial.println("d6 up");
} else { // DIP 6 down
Serial.println("d6 down");
}
break;
case 7:
if (up) { // DIP 7 up
Serial.println("d7 up");
} else { // DIP 7 down
Serial.println("d7 down");
}
break;
}
}