This is a template for using Vult, a transcompiler well suited to write high-performance DSP code, on the Teensy 3.1/3.2 as a base for:
- Musical Instruments (e.g. Synthesizers)
- Audio Effect Processors
- Sound Generators
- Other Audio DSP tasks
Vult itself describes as:
Vult is a simple and powerful language to program high-performance algorithms
that may run in small microprocessors or microcontrollers. Vult is specially
useful when programming Digital Signal Processing (DSP) algorithms like
audio effects or synthesizers.
The core being used as "host" is the Teensy Audio library. Core generated by Vult in our setup can be used as an "Audio"-Object in Teensyduino.
This is our src/main.cpp
, which contains the C++ code base:
#include <ArduinoGimmicks.h>
#include <Audio.h>
#include <SPI.h>
#include <Wire.h>
#include "VultGen.h"
AudioOutputI2S i2s1; //xy=565,241
AudioControlSGTL5000 audioShield; //xy=586,175
VultGen voice;
AudioConnection patchCord1(voice, 0, i2s1, 0);
AudioConnection patchCord2(voice, 0, i2s1, 1);
// Handles the ontrol change
void OnControlChange(byte channel, byte control, byte value){
voice.controlChange(control,value);
}
// Handles note on events
void OnNoteOn(byte channel, byte note, byte velocity){
voice.noteOn(note,velocity);
}
// Handles note on events
void OnNoteOff(byte channel, byte note, byte velocity){
voice.noteOff(note,velocity);
}
void setup() {
AudioMemory(8);
audioShield.enable();
audioShield.volume(0.7);
voice.begin();
usbMIDI.setHandleControlChange(OnControlChange);
usbMIDI.setHandleNoteOn(OnNoteOn);
usbMIDI.setHandleNoteOff(OnNoteOff);
}
void loop() {
usbMIDI.read();
}
and in src/phasedist.vult
lies your Vult DSP code:
/*
= A simple synthesizer with one LFO and a Delay effect
CC30 - Volume
CC31 - Detune/Resonance
CC32 - LFO rate
CC33 - LFO amount (bipolar)
CC34 - Delay time
CC35 - Delay feedback
*/
// Used to soften the transitions of controls
fun smooth(input:real) : real {
mem x;
x = x+(input-x)*0.005;
return x;
}
// Returns true every time the input value changes
fun change(x:real):bool {
mem pre_x;
val v:bool = pre_x<>x;
pre_x = x;
return v;
}
// Returns true if the value changes from 0 to anything
fun edge(x:bool):bool {
mem pre_x;
val v:bool = (pre_x<>x) && (pre_x==false);
pre_x = x;
return v;
}
// Returns true every 'n' calls
fun each(n:int) : bool {
mem count;
val ret = (count == 0);
count = (count + 1) % n;
return ret;
}
// Converts the MIDI note to increment rate at a 44100 sample rate
fun pitchToRate(d) return 8.1758*exp(0.0577623*d)/44100.0;
fun phasor(pitch:real,reset:bool){
mem rate,phase;
if(change(pitch))
rate = pitchToRate(pitch);
phase = if reset then 0.0 else (phase + rate) % 1.0;
return phase;
}
// A simple LFO with reset signal
fun lfo(f:real,gate:bool) : real {
mem phase;
val rate = f * 10.0/44100.0;
if(edge(gate)) phase = 0.0;
phase = phase + rate;
if(phase>1.0) phase = phase-1.0;
return sin(phase*2.0*3.141592653589793)-0.5;
}
// Implements the resonant filter simulation as shown in
// http://en.wikipedia.org/wiki/Phase_distortion_synthesis
fun phd_osc(pitch:real,detune:real) : real {
mem pre_phase1; // used to detect when the phase wrapps from 1 to 0
val phase1 = phasor(pitch,false);
val comp = 1.0 - phase1;
val reset = (pre_phase1 - phase1) > 0.5;
pre_phase1 = phase1;
val phase2 = phasor(pitch+smooth(detune)*32.0,reset);
val sine = sin(2.0*3.14159265359*phase2);
return sine*comp;
}
// Simple delay.
fun delay(x:real, time:real, feedback:real) : real {
mem buffer : array(real,1500);
mem write_pos;
// Constraints the parameter values
time = clip(time,0.0,1.0);
feedback = clip(feedback,0.0,1.0);
// Gets the position in the buffer to read
val index_r = real(size(buffer)) * time;
val index_i = int(floor(index_r));
val delta = write_pos - index_i;
val read_pos = if delta < 0 then size(buffer)+delta else delta;
// Gets the decimal part of the position
val decimal = index_r - real(index_i);
// Reads the values in the buffer
val x1 = get(buffer,read_pos);
val x2 = get(buffer,(read_pos+1) % size(buffer));
// Interpolates the value
val ret = (x2-x1)*decimal + x1;
// Write the data to the buffer
write_pos = (write_pos+1) % size(buffer);
_ = set(buffer,write_pos,clip(x+feedback*ret,-1.0,1.0));
return ret;
}
/* These three functions handle midi on/off events in order to behave
* like a monophonic sinthesizer that can hold 4 notes */
// Activates a note and returns the current note value
fun mono_noteOn(n:int){
mem count,pre;
mem notes : array(int,4);
// Do not add more that the size of array
if(count < size(notes)) {
_ = set(notes,count,n);
pre = n;
if(count < size(notes)) count = count + 1;
}
return pre;
}
// Deactivates a note and returns the following note value;
and mono_noteOff(n:int){
mem count,pre;
mem notes : array(int,4);
val found = false;
val pos;
val i = 0;
// if there are no notes, no dot do anything
if(count == 0)
return pre;
// Finds the location of the note
while(i < size(notes) && not(found)){
if(get(notes,i) == n) {
pos = i;
found = true;
}
i = i + 1;
}
// if the note was found moves all the notes one location
if(found) {
val k = pos + 1;
while(k < size(notes)) {
_ = set(notes,k-1,get(notes,k));
k = k + 1;
}
// If found, decrease the number of active notes
if(found && count>0) {
count = count - 1;
pre = get(notes,count - 1);
}
}
return pre;
}
// Returns 1 if any note is active
and mono_isGateOn() {
mem count;
return count > 0;
}
// Main processing function
fun process(input:real){
mem volume,detune; // values set in 'controlChange'
mem pitch;
mem lfo_rate,lfo_amt;
mem time, feedback;
val gate = notes:mono_isGateOn();
// creates one LFO
val lfo_val = lfo(lfo_rate,gate)*lfo_amt;
// creates one oscillator
val o1 = phd_osc(pitch,detune+lfo_val);
// gets the amplification by using a low-pass on the gate
val amp = smooth(if gate then 1.0 else 0.0);
val osc_out = o1 * amp;
val delay_out = delay(osc_out,smooth(time),smooth(feedback));
return volume * (osc_out+delay_out) /2.0;
}
// Called when a note On is received
and noteOn(note:int,velocity:int){
mem pitch = real(notes:mono_noteOn(note));
}
// Called when a note Off is received
and noteOff(note){
mem pitch = real(notes:mono_noteOff(note));
}
// Called when a control changes
and controlChange(control,value){
mem volume;
mem detune;
mem lfo_rate, lfo_amt;
mem time, feedback;
// Control 30 defines the volume
if(control==30) volume = value/127.0;
if(control==31) detune = value/127.0;
if(control==32) lfo_rate = value/127.0;
if(control==33) lfo_amt = 2.0*((real(value)/127.)-0.5);
if(control==34) time = value/127.0;
if(control==35) feedback = value/127.0;
}
// Called on initialization to define initial values
and default(){
mem volume = 1.0;
mem pitch = 45.0;
mem detune = 0.8;
mem lfo_rate = 0.07;
mem lfo_amt = -0.8;
mem time = 0.5;
mem feedback = 0.5;
}
This will generate the files VultGen.cpp and VultGen.h containing the class VultGen.
This example requires a Teensy board 3.1 or 3.2 in addition to the Audio Adaptor Board. You have to configure the Teensy speed to 96 Mz for better performance. The USB type should be set to MIDI in order to receive messages. The available messages are:
MIDI note on/off in any channel to control the pitch Control Change (CC31) to control the detune parameter of the oscillator The source of the oscillator is the file phasedist.vult. In order to modify or regenerate the code you need to have the development version of Vult (https://github.com/modlfo/vult).