Wind Synth Advice

Davidelvig

Well-known member
I'm using an off shelf Teensy 4.1 and the SGTL 5000 chip on a daughterboard. I'm using headphone out and line out in USB audio out. My synthesizer is a basic four wave synthesizer with a reverb effect. I also mix in sound from a separate general midi chip with the 128 standard general MIDI sound of varying quality.

Most wind synth sounds use a filter cut off adjustable in proportion to breath pressure on the wind controller.

I'm interested in replicating some of the top quality sound I've heard from Teensy-based projects in other posts.
I'm sharing the Teensy Audio GUI view of the synth configuration I'm using.

I imagine there are a number of things that I can do differently in the arrangement of objects or selection of audio objects from other Teensy-compatible audio libraries.

So my ask here:
  • comment on my use of standard objects here and let me know if you need more information such as the code that modifies the objects based on midi input
  • Recommend a reliable alternative audio library that does as well and better than the basic Teensy library
Thanks!

1782249087264.png

// GUItool: begin automatically generated code
AudioSynthWaveform waveform3; // xy=121.17647171020508,789.7479152679443
AudioSynthWaveform waveform0; // xy=123.6470947265625,682.8235549926758
AudioSynthWaveform waveform1; // xy=123.6470947265625,717.8235549926758
AudioSynthWaveform waveform2; // xy=123.6470947265625,752.8235549926758
AudioInputI2S i2sIn; // xy=124.6470947265625,590.8235549926758
AudioMixer4 mixerVS1053; // xy=336.6470947265625,602.8235549926758
AudioMixer4 mixer_Waves; // xy=338.6470947265625,692.8235549926758
AudioSynthWaveformDc dc_CutoffFreq; // xy=340.6470947265625,751.8235549926758
AudioSynthWaveformDc dc_Resonance; // xy=341.6470947265625,790.8235549926758
AudioFilterLadder ladder1; // xy=510.6470947265625,742.8235549926758
AudioMixer4 mixerSynths; // xy=692.5042152404785,614.2520980834961
AudioEffectFreeverbStereo freeverbs1; // xy=1305.0757331848145,597.5376958847046
AudioMixer4 mixerLeft; // xy=1335.0756072998047,235.39498710632324
AudioMixer4 mixerRight; // xy=1346.5042686462402,995.8235321044922
AudioOutputUSB usbOut; // xy=1629.6470642089844,657.8235473632812
AudioOutputI2S i2sOut; // xy=1630.6470642089844,607.8235473632812
AudioConnection patchCord1(waveform3, 0, mixer_Waves, 3);
AudioConnection patchCord2(waveform0, 0, mixer_Waves, 0);
AudioConnection patchCord3(waveform1, 0, mixer_Waves, 1);
AudioConnection patchCord4(waveform2, 0, mixer_Waves, 2);
AudioConnection patchCord5(i2sIn, 0, mixerVS1053, 0);
AudioConnection patchCord6(i2sIn, 1, mixerVS1053, 1);
AudioConnection patchCord7(mixerVS1053, 0, mixerSynths, 0);
AudioConnection patchCord8(mixer_Waves, 0, ladder1, 0);
AudioConnection patchCord9(dc_CutoffFreq, 0, ladder1, 1);
AudioConnection patchCord10(dc_Resonance, 0, ladder1, 2);
AudioConnection patchCord11(ladder1, 0, mixerSynths, 1);
AudioConnection patchCord12(mixerSynths, 0, mixerLeft, 0);
AudioConnection patchCord13(mixerSynths, 0, mixerRight, 0);
AudioConnection patchCord14(mixerSynths, freeverbs1);
AudioConnection patchCord15(freeverbs1, 0, mixerLeft, 1);
AudioConnection patchCord16(freeverbs1, 1, mixerRight, 1);
AudioConnection patchCord17(mixerLeft, 0, i2sOut, 0);
AudioConnection patchCord18(mixerLeft, 0, usbOut, 0);
AudioConnection patchCord19(mixerRight, 0, i2sOut, 1);
AudioConnection patchCord20(mixerRight, 0, usbOut, 1);
AudioControlSGTL5000 sgtl5000_1; // xy=117.6470947265625,469.8235549926758
// GUItool: end automatically generated code
 
You probably need more CCs than just breath for a truly expressive sound; a reed or bite sensor for pitch bend is “traditional“, but an accelerometer is probably easier to implement and gives more channels to play with.

Using modulated waveforms makes pitch bend easier, and lets you vary things like pulse duty cycle.

Using DC waveforms for control (as you have for the filter) is a very good plan, as it prevents “zipper noise“ when parameters are changed every 2.9ms on the audio update. If you need real-time amplitude changes, don’t use a mixer - use a DC and a multiplier, and the amplitude(level, milliseconds) method to smooth the changes.

You might want to experiment with an envelope and noise to emulate the effect of tonguing a reed at the start of a note.
 
Thanks a bunch. There are a number of cc's configurable and sendable on moving the instrument up-down, left-right, and rolling. There are accelerometers and gyroscopes in the instrument. Pitch bend messages are use for falls, doits (upward falls?) and vibrato.

Another key question I have is whether these audio objects will give me big and beautiful sound when connected to a suitable sound system. They are not yet big and beautiful in the headphones. I've seen references to other audio libraries on this forum that use floating point numbers and seem to have more choices. Can I get big and beautiful sound using the standard library objects? Is there a stable and more powerful alternative?
 
You can get big and massive with the standard library but right now it looks like you have a 4 osc mono synth.

No problem there but depending on how you are mixing, detuning each of those oscs and what wave forms you are using it’s hard to tell.

The ladder filter also has various options like drive which should be set to taste.

For everything I do on my synth I always strip back to basic one source wave, simple adsr in to filter.

Use the modulated waveforms that made it easier to add lfo to the pulse width etc.

Have fun. If you want to share code sure you’ll get plenty of pointers
 
Thanks, @bk40!
Here's the code (dbAudio.cpp).
It accepts MIDI messages and produced the sound.
There are no envelopes used (yet). The breath (CC2 and/or CC11) is the driver of perceived loads (through low pass filter cutoff opening)

I would welcome comment to make the sound richer.

Note that in addition to the 4 wave generators, I have a VS1053 chip as a mixable input. It could also use some down-stream "thickening" or "enriching" though I think we can focus just on the waves for now.

is it better to paste the code in a "quotes" tag? Attached here as a file.

Thanks!
 

Attachments

  • dbAudio.cpp
    18.5 KB · Views: 16
Thanks, @bk40!
Here's the code (dbAudio.cpp).
It accepts MIDI messages and produced the sound.
There are no envelopes used (yet). The breath (CC2 and/or CC11) is the driver of perceived loads (through low pass filter cutoff opening)

I would welcome comment to make the sound richer.

Note that in addition to the 4 wave generators, I have a VS1053 chip as a mixable input. It could also use some down-stream "thickening" or "enriching" though I think we can focus just on the waves for now.

is it better to paste the code in a "quotes" tag? Attached here as a file.

Thanks!
Easier for me if pasted using the code function as I can always view that.

Files don’t always work on my phone

Good idea too if you give an idea of a sound you want to produce
 
Code:
#include <Arduino.h>
#include <dbAudio.h>
#include <dbConfig.h>
#include <dbMIDICommands.h>
#include <dbUtility.h>

#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>

// GUItool: begin automatically generated code
AudioSynthWaveform waveform3;         // xy=121.17647171020508,789.7479152679443
AudioSynthWaveform waveform0;         // xy=123.6470947265625,682.8235549926758
AudioSynthWaveform waveform1;         // xy=123.6470947265625,717.8235549926758
AudioSynthWaveform waveform2;         // xy=123.6470947265625,752.8235549926758
AudioInputI2S i2sIn;                  // xy=124.6470947265625,590.8235549926758
AudioMixer4 mixerVS1053;              // xy=336.6470947265625,602.8235549926758
AudioMixer4 mixer_Waves;              // xy=338.6470947265625,692.8235549926758
AudioSynthWaveformDc dc_CutoffFreq;   // xy=340.6470947265625,751.8235549926758
AudioSynthWaveformDc dc_Resonance;    // xy=341.6470947265625,790.8235549926758
AudioFilterLadder ladder1;            // xy=510.6470947265625,742.8235549926758
AudioMixer4 mixerSynths;              // xy=692.5042152404785,614.2520980834961
AudioEffectFreeverbStereo freeverbs1; // xy=1305.0757331848145,597.5376958847046
AudioMixer4 mixerLeft;                // xy=1335.0756072998047,235.39498710632324
AudioMixer4 mixerRight;               // xy=1346.5042686462402,995.8235321044922
AudioOutputUSB usbOut;                // xy=1629.6470642089844,657.8235473632812
AudioOutputI2S i2sOut;                // xy=1630.6470642089844,607.8235473632812
AudioConnection patchCord1(waveform3, 0, mixer_Waves, 3);
AudioConnection patchCord2(waveform0, 0, mixer_Waves, 0);
AudioConnection patchCord3(waveform1, 0, mixer_Waves, 1);
AudioConnection patchCord4(waveform2, 0, mixer_Waves, 2);
AudioConnection patchCord5(i2sIn, 0, mixerVS1053, 0);
AudioConnection patchCord6(i2sIn, 1, mixerVS1053, 1);
AudioConnection patchCord7(mixerVS1053, 0, mixerSynths, 0);
AudioConnection patchCord8(mixer_Waves, 0, ladder1, 0);
AudioConnection patchCord9(dc_CutoffFreq, 0, ladder1, 1);
AudioConnection patchCord10(dc_Resonance, 0, ladder1, 2);
AudioConnection patchCord11(ladder1, 0, mixerSynths, 1);
AudioConnection patchCord12(mixerSynths, 0, mixerLeft, 0);
AudioConnection patchCord13(mixerSynths, 0, mixerRight, 0);
AudioConnection patchCord14(mixerSynths, freeverbs1);
AudioConnection patchCord15(freeverbs1, 0, mixerLeft, 1);
AudioConnection patchCord16(freeverbs1, 1, mixerRight, 1);
AudioConnection patchCord17(mixerLeft, 0, i2sOut, 0);
AudioConnection patchCord18(mixerLeft, 0, usbOut, 0);
AudioConnection patchCord19(mixerRight, 0, i2sOut, 1);
AudioConnection patchCord20(mixerRight, 0, usbOut, 1);
AudioControlSGTL5000 sgtl5000_1; // xy=117.6470947265625,469.8235549926758
// GUItool: end automatically generated code

SGTL5000Audio audio;

TeensyWaveformTypes getTeensyWaveformType(dbAudioWaveformTypes waveformType)
{
    switch (waveformType)
    {
    case dbWAVEFORM_BANDLIMIT_SAWTOOTH:
        return TEENSY_WAVEFORM_BANDLIMIT_SAWTOOTH;
    case dbWAVEFORM_BANDLIMIT_SAWTOOTH_REVERSE:
        return TEENSY_WAVEFORM_BANDLIMIT_SAWTOOTH_REVERSE;
    case dbWAVEFORM_BANDLIMIT_SQUARE:
        return TEENSY_WAVEFORM_BANDLIMIT_SQUARE;
    case dbWAVEFORM_TRIANGLE_VARIABLE:
        return TEENSY_WAVEFORM_TRIANGLE_VARIABLE;
    case dbWAVEFORM_SINE:
        return TEENSY_WAVEFORM_SINE;
    case dbWAVEFORM_BANDLIMIT_PULSE:
        return TEENSY_WAVEFORM_BANDLIMIT_PULSE;
    default:
        return TEENSY_WAVEFORM_UNKNOWN;
    }
}

////////////////////////////////////////////////////////////
// SGTL5000 codec (headphone / line output)
////////////////////////////////////////////////////////////
#define DEFAULT_AUDIO_LEVEL 0.5
void SGTL5000Audio::setup(void)
{
    for (size_t i = 0; i < 4; i++) {
        mixerLeft.gain(i, 0);
        mixerRight.gain(i, 0);
    }

    AudioMemory(50);
    sgtl5000_1.enable();
    setVolume();
    sgtl5000_1.inputSelect(AUDIO_INPUT_LINEIN);
    delay(1000); // this apears to avoid a click when the audio is enabled
}
void SGTL5000Audio::setVolume(void)
{
    sgtl5000_1.volume(headphoneVolume() / 127.0);
}

void setSoundOutput(bool on)
{
    for (size_t i = 0; i < 4; i++) {
        mixerLeft.gain(i, on ? DEFAULT_AUDIO_LEVEL : 0);
        mixerRight.gain(i, on ? DEFAULT_AUDIO_LEVEL : 0);
    }
}

////////////////////////////////////////////////////////////
// Synth voicing: ladder filter, GM/wave mix, reverb
////////////////////////////////////////////////////////////
float _filterResonance = 0.3;
float _filterFrequency = 800;
float _filterOctave = 2.6;
void setFilter(void)
{
    _filterOctave = filterSemitones() / 12.0;
    _filterResonance = filterResonancePercent() / 100.0;

    ladder1.resonance(_filterResonance);
    ladder1.frequency(_filterFrequency);
    ladder1.octaveControl(_filterOctave);
}

float _waveInputMixFloat = 0;
float _VS1053InputMixFloat = 0;
void setInputMix(uint8_t mixPercent)
{
    _waveInputMixFloat = (float)mixPercent / 100.0;
    _VS1053InputMixFloat = constrainFloat(1.0 - ((float)mixPercent / 100.0), 0.0, .5); // Constrain VS1053 to 50% volume to avoid hiss
    mixerSynths.gain(0, _VS1053InputMixFloat); // VS1053
    mixerSynths.gain(1, _waveInputMixFloat);         // Waves & Filter
}

float _audioReverbAmount = 0.0;
void setAudioReverbMix(void)
{
    freeverbs1.roomsize(_audioReverbAmount);
    freeverbs1.damping(_audioReverbAmount);
    mixerLeft.gain(0, 1 - _audioReverbAmount);
    mixerRight.gain(0, 1 - _audioReverbAmount);
    mixerLeft.gain(1, _audioReverbAmount);
    mixerRight.gain(1, _audioReverbAmount);
}

void setAudioReverbAmount(float audioReverbAmount)
{
    if (audioReverbAmount != _audioReverbAmount)
    {
        _audioReverbAmount = audioReverbAmount;
        setAudioReverbMix();
    }
}

void dbAudioSynth::setup(void)
{
    if (!isSetUp)
    {
        for (size_t i = 0; i < 4; i++) {
            mixerLeft.gain(i, 0);
            mixerRight.gain(i, 0);
        }   
        usbOut.begin();
        setPatchParams();
        setInputMix(presetSynthMixPercent());
        setAudioReverbAmount(0);
        isSetUp = true;
    }
}

float freq = 0;
int inboundPitchBend = 0;
int lastRPN_MSB = 0;
int lastRPN_LSB = 0;
int lastDataEntry_MSB = 0;
float currentPitchBendRange = 12;

void setWaves(void)
{
    waveform0.begin(getTeensyWaveformType((dbAudioWaveformTypes)wave1Form().getValue()));
    waveform1.begin(getTeensyWaveformType((dbAudioWaveformTypes)wave2Form().getValue()));
    waveform2.begin(getTeensyWaveformType((dbAudioWaveformTypes)wave3Form().getValue()));
    waveform3.begin(getTeensyWaveformType((dbAudioWaveformTypes)wave4Form().getValue()));

    waveform0.pulseWidth(wave1DutyCycle() / 100.0);
    waveform1.pulseWidth(wave2DutyCycle() / 100.0);
    waveform2.pulseWidth(wave3DutyCycle() / 100.0);
    waveform3.pulseWidth(wave4DutyCycle() / 100.0);

    // scale the volume of all waveformes such that the sum does not exceed 1.0
    float waveVolumeSum = wave1Volume() + wave2Volume() + wave3Volume() + wave4Volume();
    mixer_Waves.gain(0, (float)wave1Volume() / waveVolumeSum);
    mixer_Waves.gain(1, (float)wave2Volume() / waveVolumeSum);
    mixer_Waves.gain(2, (float)wave3Volume() / waveVolumeSum);
    mixer_Waves.gain(3, (float)wave4Volume() / waveVolumeSum);

    // consistent, moderate level for all waveforms
    waveform0.amplitude(.5);
    waveform1.amplitude(.5);
    waveform2.amplitude(.5);
    waveform3.amplitude(.5);
}

// Pushes every configurable IntValue into the synth objects. Called on any internal/external
// parameter change and on preset/patch change.
// TODO: update only the parameters that actually changed rather than re-pushing all of them.
void setPatchParams(void)
{
    setWaves();
    setFilter();
    setInputMix(presetSynthMixPercent());
    setAudioReverbAmount(0);
}

////////////////////////////////////////////////////////////
// MIDI consumer: note / CC / pitch-bend -> synth voices
////////////////////////////////////////////////////////////
void dbAudioSynth::consume(const dbMsg &msg)
{
    static int lastNote = 0;
    static int lastLevel = 0;
    if (!isSetUp)
    {
        setup();
    }

    static float ex = 0;
    switch (msg.type())
    {
    case CONTROL_CHANGE:
        switch (msg.value1())
        {
        case BREATH_CONTROLLER:
        case EXPRESSION_CONTROLLER:
            ex = (float)msg.value2() / 127.0;
            ex = ex * 2.0 - 1.0; // now a value between -1 and +1
            dc_CutoffFreq.amplitude(ex, 5);
            dc_Resonance.amplitude(ex / 20, 5);
            lastLevel = msg.value2();
            break;
        case EFFECT_DEPTH_1:
            setAudioReverbAmount((float)msg.value2() / 128.0);
            break;
        case RPN_MSB:
            lastRPN_MSB = msg.value2();
            break;
        case RPN_LSB:
            lastRPN_LSB = msg.value2();
            break;
        case DATA_ENTRY_MSB:
            lastDataEntry_MSB = msg.value2();
            if (lastRPN_MSB == 0 && lastRPN_LSB == 0)
            {
                currentPitchBendRange = lastDataEntry_MSB;
            }
            break;
        case DATA_ENTRY_LSB:
            // LSB fine-tunes the pitch-bend range; MSB resolution (whole semitones) is sufficient here.
            break;
        }
        break;
    case NOTE_ON:
        lastNote = msg.value1();
        freq = dbMIDI::getFrequency(lastNote);
        _filterFrequency = freq;
        setFilter();
        break;
    case NOTE_OFF:
        break;
    case PITCH_BEND:
        inboundPitchBend = ((msg.value2() * 128) + msg.value1()) - 8192; // MSB * 128 + LSB - 8192 to create a value between -8192 and 8191
        break;
    case PROGRAM_CHANGE:
        setPatchParams();
        break;
    }
    
    if (lastLevel > 0)
    {
        mixer_Waves.gain(0, wave1Volume() / 100.0);
        mixer_Waves.gain(1, wave2Volume() / 100.0);
        mixer_Waves.gain(2, wave3Volume() / 100.0);
        mixer_Waves.gain(3, wave4Volume() / 100.0);
        float transposedFreq = freq * pow(2,  ((float)wave1Semitones().getValue() + ((float)wave1Cents().getValue() / 100.0)) / 12.0  );
        float pitchBendSemitones = (inboundPitchBend / (16383.0f / 2.0f)) * currentPitchBendRange;
        float bentFreq = transposedFreq * pow(2, pitchBendSemitones / 12.0f);
        waveform0.frequency(bentFreq);
        waveform1.frequency(bentFreq * pow(2, (wave2Semitones() + (wave2Cents() / 100.0)) / 12.0));
        waveform2.frequency(bentFreq * pow(2, (wave3Semitones() + (wave3Cents() / 100.0)) / 12.0));
        waveform3.frequency(bentFreq * pow(2, (wave4Semitones() + (wave4Cents() / 100.0)) / 12.0));
    }
    else
    {
        mixer_Waves.gain(0, 0);
        mixer_Waves.gain(1, 0);
        mixer_Waves.gain(2, 0);
        mixer_Waves.gain(3, 0);
    }
}

static const char *dbAudioWaveformNames[] = {
    "Saw",
    "Reverse Saw",
    "Square",
    "Triangle",
    "Sine",
    "Pulse Mod",
};

char _badWaveFormString[20];
const char *getWaveformName(uint8_t waveformType)
{
    if (waveformType < sizeof(dbAudioWaveformNames) / sizeof(dbAudioWaveformNames[0]))
    {
        return dbAudioWaveformNames[waveformType];
    }
    else
    {
        snprintf(_badWaveFormString, sizeof(_badWaveFormString), "Unknown Wave %d", waveformType);
        return _badWaveFormString;
    }
}

////////////////////////////////////////////////////////////
// PRESET IntValues
////////////////////////////////////////////////////////////
static void _presetPatchNumberChanged(int newValue, Cause cause)
{
    if (cause == Cause::Edit)
    {
        loadCurrentPatch(newValue);
        setPatchParams();
    } else {
        #if defined(ERROR_TRACKING)
            Serial.printf("Unexpected cause: %d\n", cause);
        #endif
    }
}

static IntValue _presetPatchNumber = IntValue(0).range(0, 8).onChange(_presetPatchNumberChanged);
IntValue &presetPatchNumber(void) { return _presetPatchNumber; }

// Preset Synth Mix Percent
static void _describePresetSynthMixPercent(int amt, char *buffer, size_t capacity)
{
    String s = String(100 - amt) + " : " + String(amt);
    strncpy(buffer, s.c_str(), capacity);
}

static void _presetSynthMixPercentChanged(int newValue, Cause cause)
{
    if (cause == Cause::Edit)
    {
        setInputMix(newValue);
    }
}

static IntValue _presetSynthMixPercent = IntValue(100).range(0, 100).step(10).describeWith(_describePresetSynthMixPercent).onChange(_presetSynthMixPercentChanged);
IntValue &presetSynthMixPercent(void) { return _presetSynthMixPercent; }

////////////////////////////////////////////////////////////
// SYNTH PATCH IntValues
////////////////////////////////////////////////////////////

// Filter Semitones
static void _describeFilterSemitones(int amt, char *buffer, size_t capacity)
{
    String s = String(amt) + " semitones";
    strncpy(buffer, s.c_str(), capacity);
}

static IntValue _filterSemitones = IntValue(30).range(0, 36).describeWith(_describeFilterSemitones);
IntValue &filterSemitones(void) { return _filterSemitones; }

// Filter Resonance Tenths
static void _describeFilterResonancePercent(int amt, char *buffer, size_t capacity)
{
    String s = String(amt) + "%";
    strncpy(buffer, s.c_str(), capacity);
}

static IntValue _filterResonancePercent = IntValue(5).range(0, 100).describeWith(_describeFilterResonancePercent);
IntValue &filterResonancePercent(void) { return _filterResonancePercent; }

static IntValue _wave1Form = IntValue(0).range(0, 5).describeWith(dbAudioWaveformNames);
IntValue &wave1Form(void) { return _wave1Form; }

static IntValue _wave2Form = IntValue(0).range(0, 5).describeWith(dbAudioWaveformNames);
IntValue &wave2Form(void) { return _wave2Form; }

static IntValue _wave3Form = IntValue(0).range(0, 5).describeWith(dbAudioWaveformNames);
IntValue &wave3Form(void) { return _wave3Form; }

static IntValue _wave4Form = IntValue(0).range(0, 5).describeWith(dbAudioWaveformNames);
IntValue &wave4Form(void) { return _wave4Form; }

// Wave Volume
static void _describeWaveVolume(int amt, char *buffer, size_t capacity)
{
    String wave1VolumeString = String(amt) + "%";
    strncpy(buffer, wave1VolumeString.c_str(), capacity);
}

static IntValue _wave1Volume = IntValue(100).range(0, 100).describeWith(_describeWaveVolume);
IntValue &wave1Volume(void) { return _wave1Volume; }

static IntValue _wave2Volume = IntValue(0).range(0, 100).describeWith(_describeWaveVolume);
IntValue &wave2Volume(void) { return _wave2Volume; }

static IntValue _wave3Volume = IntValue(0).range(0, 100).describeWith(_describeWaveVolume);
IntValue &wave3Volume(void) { return _wave3Volume; }

static IntValue _wave4Volume = IntValue(0).range(0, 100).describeWith(_describeWaveVolume);
IntValue &wave4Volume(void) { return _wave4Volume; }

// Wave Semitone
static void _describeWaveSemitones(int amt, char *buffer, size_t capacity)
{
    String wave1SemitonesString = String(amt) + " semitones";
    strncpy(buffer, wave1SemitonesString.c_str(), capacity);
}

static IntValue _wave1Semitones = IntValue(0).range(-36, 36).describeWith(_describeWaveSemitones);
IntValue &wave1Semitones(void) { return _wave1Semitones; }

static IntValue _wave2Semitones = IntValue(0).range(-36, 36).describeWith(_describeWaveSemitones);
IntValue &wave2Semitones(void) { return _wave2Semitones; }

static IntValue _wave3Semitones = IntValue(0).range(-36, 36).describeWith(_describeWaveSemitones);
IntValue &wave3Semitones(void) { return _wave3Semitones; }

static IntValue _wave4Semitones = IntValue(0).range(-36, 36).describeWith(_describeWaveSemitones);
IntValue &wave4Semitones(void) { return _wave4Semitones; }

// Wave Cents
static void _describeWaveCents(int amt, char *buffer, size_t capacity)
{
    String wave1CentsString = String(amt) + " cents";
    strncpy(buffer, wave1CentsString.c_str(), capacity);
}

static IntValue _wave1Cents = IntValue(0).range(-99, 99).describeWith(_describeWaveCents);
IntValue &wave1Cents(void) { return _wave1Cents; }

static IntValue _wave2Cents = IntValue(0).range(-99, 99).describeWith(_describeWaveCents);
IntValue &wave2Cents(void) { return _wave2Cents; }

static IntValue _wave3Cents = IntValue(0).range(-99, 99).describeWith(_describeWaveCents);
IntValue &wave3Cents(void) { return _wave3Cents; }

static IntValue _wave4Cents = IntValue(0).range(-99, 99).describeWith(_describeWaveCents);
IntValue &wave4Cents(void) { return _wave4Cents; }

// Wave Duty Cycle
static void _describeWaveDutyCycle(int amt, char *buffer, size_t capacity)
{
    String wave1DutyCycleString = String(amt) + "%";
    strncpy(buffer, wave1DutyCycleString.c_str(), capacity);
}

static IntValue _wave1DutyCycle = IntValue(50).range(10, 90).step(10).describeWith(_describeWaveDutyCycle);
IntValue &wave1DutyCycle(void) { return _wave1DutyCycle; }

static IntValue _wave2DutyCycle = IntValue(50).range(10, 90).step(10).describeWith(_describeWaveDutyCycle);
IntValue &wave2DutyCycle(void) { return _wave2DutyCycle; }

static IntValue _wave3DutyCycle = IntValue(50).range(10, 90).step(10).describeWith(_describeWaveDutyCycle);
IntValue &wave3DutyCycle(void) { return _wave3DutyCycle; }

static IntValue _wave4DutyCycle = IntValue(50).range(10, 90).step(10).describeWith(_describeWaveDutyCycle);
IntValue &wave4DutyCycle(void) { return _wave4DutyCycle; }

void printAudioParams(void)
{
#if defined(UTILITY_TRACKING)
    Serial.printf("Patch: %d\n", presetPatchNumber().getValue());
    Serial.printf("Synth Mix: %d\n", presetSynthMixPercent().getValue());
    Serial.printf("\twave1Form: %d\twave1Volume: %d\twave1Semitones: %d\twave1Cents: %d\twave1DutyCycle: %d\n"
                  "\twave2Form: %d\twave2Volume: %d\twave2Semitones: %d\twave2Cents: %d\twave2DutyCycle: %d\n"
                  "\twave3Form: %d\twave3Volume: %d\twave3Semitones: %d\twave3Cents: %d\twave3DutyCycle: %d\n"
                  "\twave4Form: %d\twave4Volume: %d\twave4Semitones: %d\twave4Cents: %d\twave4DutyCycle: %d\n",
                  wave1Form().getValue(), wave1Volume().getValue(), wave1Semitones().getValue(), wave1Cents().getValue(), wave1DutyCycle().getValue(),
                  wave2Form().getValue(), wave2Volume().getValue(), wave2Semitones().getValue(), wave2Cents().getValue(), wave2DutyCycle().getValue(),
                  wave3Form().getValue(), wave3Volume().getValue(), wave3Semitones().getValue(), wave3Cents().getValue(), wave3DutyCycle().getValue(),
                  wave4Form().getValue(), wave4Volume().getValue(), wave4Semitones().getValue(), wave4Cents().getValue(), wave4DutyCycle().getValue());
    Serial.printf("\n\tfilterSemitones: %d\tfilterResonanceTenths: %d\n", filterSemitones().getValue(), filterResonancePercent().getValue());
    Serial.printf("\taudioReverbAmount: %1.2f (reverbAmount: %d)\n", _audioReverbAmount, reverbAmount().getValue());
    Serial.printf("\tfreq: %1.2f\tfilterFrequency: %1.2f\tfilterOctave: %1.2f\tfilterResonance: %1.2f\n", freq , _filterFrequency, _filterOctave, _filterResonance);
    Serial.printf("\n");
#endif
}
 
Code:
#include <Arduino.h>
#include <dbAudio.h>
#include <dbConfig.h>
#include <dbMIDICommands.h>
#include <dbUtility.h>

#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>

// GUItool: begin automatically generated code
AudioSynthWaveform waveform3;         // xy=121.17647171020508,789.7479152679443
AudioSynthWaveform waveform0;         // xy=123.6470947265625,682.8235549926758
AudioSynthWaveform waveform1;         // xy=123.6470947265625,717.8235549926758
AudioSynthWaveform waveform2;         // xy=123.6470947265625,752.8235549926758
AudioInputI2S i2sIn;                  // xy=124.6470947265625,590.8235549926758
AudioMixer4 mixerVS1053;              // xy=336.6470947265625,602.8235549926758
AudioMixer4 mixer_Waves;              // xy=338.6470947265625,692.8235549926758
AudioSynthWaveformDc dc_CutoffFreq;   // xy=340.6470947265625,751.8235549926758
AudioSynthWaveformDc dc_Resonance;    // xy=341.6470947265625,790.8235549926758
AudioFilterLadder ladder1;            // xy=510.6470947265625,742.8235549926758
AudioMixer4 mixerSynths;              // xy=692.5042152404785,614.2520980834961
AudioEffectFreeverbStereo freeverbs1; // xy=1305.0757331848145,597.5376958847046
AudioMixer4 mixerLeft;                // xy=1335.0756072998047,235.39498710632324
AudioMixer4 mixerRight;               // xy=1346.5042686462402,995.8235321044922
AudioOutputUSB usbOut;                // xy=1629.6470642089844,657.8235473632812
AudioOutputI2S i2sOut;                // xy=1630.6470642089844,607.8235473632812
AudioConnection patchCord1(waveform3, 0, mixer_Waves, 3);
AudioConnection patchCord2(waveform0, 0, mixer_Waves, 0);
AudioConnection patchCord3(waveform1, 0, mixer_Waves, 1);
AudioConnection patchCord4(waveform2, 0, mixer_Waves, 2);
AudioConnection patchCord5(i2sIn, 0, mixerVS1053, 0);
AudioConnection patchCord6(i2sIn, 1, mixerVS1053, 1);
AudioConnection patchCord7(mixerVS1053, 0, mixerSynths, 0);
AudioConnection patchCord8(mixer_Waves, 0, ladder1, 0);
AudioConnection patchCord9(dc_CutoffFreq, 0, ladder1, 1);
AudioConnection patchCord10(dc_Resonance, 0, ladder1, 2);
AudioConnection patchCord11(ladder1, 0, mixerSynths, 1);
AudioConnection patchCord12(mixerSynths, 0, mixerLeft, 0);
AudioConnection patchCord13(mixerSynths, 0, mixerRight, 0);
AudioConnection patchCord14(mixerSynths, freeverbs1);
AudioConnection patchCord15(freeverbs1, 0, mixerLeft, 1);
AudioConnection patchCord16(freeverbs1, 1, mixerRight, 1);
AudioConnection patchCord17(mixerLeft, 0, i2sOut, 0);
AudioConnection patchCord18(mixerLeft, 0, usbOut, 0);
AudioConnection patchCord19(mixerRight, 0, i2sOut, 1);
AudioConnection patchCord20(mixerRight, 0, usbOut, 1);
AudioControlSGTL5000 sgtl5000_1; // xy=117.6470947265625,469.8235549926758
// GUItool: end automatically generated code

SGTL5000Audio audio;

TeensyWaveformTypes getTeensyWaveformType(dbAudioWaveformTypes waveformType)
{
    switch (waveformType)
    {
    case dbWAVEFORM_BANDLIMIT_SAWTOOTH:
        return TEENSY_WAVEFORM_BANDLIMIT_SAWTOOTH;
    case dbWAVEFORM_BANDLIMIT_SAWTOOTH_REVERSE:
        return TEENSY_WAVEFORM_BANDLIMIT_SAWTOOTH_REVERSE;
    case dbWAVEFORM_BANDLIMIT_SQUARE:
        return TEENSY_WAVEFORM_BANDLIMIT_SQUARE;
    case dbWAVEFORM_TRIANGLE_VARIABLE:
        return TEENSY_WAVEFORM_TRIANGLE_VARIABLE;
    case dbWAVEFORM_SINE:
        return TEENSY_WAVEFORM_SINE;
    case dbWAVEFORM_BANDLIMIT_PULSE:
        return TEENSY_WAVEFORM_BANDLIMIT_PULSE;
    default:
        return TEENSY_WAVEFORM_UNKNOWN;
    }
}

////////////////////////////////////////////////////////////
// SGTL5000 codec (headphone / line output)
////////////////////////////////////////////////////////////
#define DEFAULT_AUDIO_LEVEL 0.5
void SGTL5000Audio::setup(void)
{
    for (size_t i = 0; i < 4; i++) {
        mixerLeft.gain(i, 0);
        mixerRight.gain(i, 0);
    }

    AudioMemory(50);
    sgtl5000_1.enable();
    setVolume();
    sgtl5000_1.inputSelect(AUDIO_INPUT_LINEIN);
    delay(1000); // this apears to avoid a click when the audio is enabled
}
void SGTL5000Audio::setVolume(void)
{
    sgtl5000_1.volume(headphoneVolume() / 127.0);
}

void setSoundOutput(bool on)
{
    for (size_t i = 0; i < 4; i++) {
        mixerLeft.gain(i, on ? DEFAULT_AUDIO_LEVEL : 0);
        mixerRight.gain(i, on ? DEFAULT_AUDIO_LEVEL : 0);
    }
}

////////////////////////////////////////////////////////////
// Synth voicing: ladder filter, GM/wave mix, reverb
////////////////////////////////////////////////////////////
float _filterResonance = 0.3;
float _filterFrequency = 800;
float _filterOctave = 2.6;
void setFilter(void)
{
    _filterOctave = filterSemitones() / 12.0;
    _filterResonance = filterResonancePercent() / 100.0;

    ladder1.resonance(_filterResonance);
    ladder1.frequency(_filterFrequency);
    ladder1.octaveControl(_filterOctave);
}

float _waveInputMixFloat = 0;
float _VS1053InputMixFloat = 0;
void setInputMix(uint8_t mixPercent)
{
    _waveInputMixFloat = (float)mixPercent / 100.0;
    _VS1053InputMixFloat = constrainFloat(1.0 - ((float)mixPercent / 100.0), 0.0, .5); // Constrain VS1053 to 50% volume to avoid hiss
    mixerSynths.gain(0, _VS1053InputMixFloat); // VS1053
    mixerSynths.gain(1, _waveInputMixFloat);         // Waves & Filter
}

float _audioReverbAmount = 0.0;
void setAudioReverbMix(void)
{
    freeverbs1.roomsize(_audioReverbAmount);
    freeverbs1.damping(_audioReverbAmount);
    mixerLeft.gain(0, 1 - _audioReverbAmount);
    mixerRight.gain(0, 1 - _audioReverbAmount);
    mixerLeft.gain(1, _audioReverbAmount);
    mixerRight.gain(1, _audioReverbAmount);
}

void setAudioReverbAmount(float audioReverbAmount)
{
    if (audioReverbAmount != _audioReverbAmount)
    {
        _audioReverbAmount = audioReverbAmount;
        setAudioReverbMix();
    }
}

void dbAudioSynth::setup(void)
{
    if (!isSetUp)
    {
        for (size_t i = 0; i < 4; i++) {
            mixerLeft.gain(i, 0);
            mixerRight.gain(i, 0);
        }  
        usbOut.begin();
        setPatchParams();
        setInputMix(presetSynthMixPercent());
        setAudioReverbAmount(0);
        isSetUp = true;
    }
}

float freq = 0;
int inboundPitchBend = 0;
int lastRPN_MSB = 0;
int lastRPN_LSB = 0;
int lastDataEntry_MSB = 0;
float currentPitchBendRange = 12;

void setWaves(void)
{
    waveform0.begin(getTeensyWaveformType((dbAudioWaveformTypes)wave1Form().getValue()));
    waveform1.begin(getTeensyWaveformType((dbAudioWaveformTypes)wave2Form().getValue()));
    waveform2.begin(getTeensyWaveformType((dbAudioWaveformTypes)wave3Form().getValue()));
    waveform3.begin(getTeensyWaveformType((dbAudioWaveformTypes)wave4Form().getValue()));

    waveform0.pulseWidth(wave1DutyCycle() / 100.0);
    waveform1.pulseWidth(wave2DutyCycle() / 100.0);
    waveform2.pulseWidth(wave3DutyCycle() / 100.0);
    waveform3.pulseWidth(wave4DutyCycle() / 100.0);

    // scale the volume of all waveformes such that the sum does not exceed 1.0
    float waveVolumeSum = wave1Volume() + wave2Volume() + wave3Volume() + wave4Volume();
    mixer_Waves.gain(0, (float)wave1Volume() / waveVolumeSum);
    mixer_Waves.gain(1, (float)wave2Volume() / waveVolumeSum);
    mixer_Waves.gain(2, (float)wave3Volume() / waveVolumeSum);
    mixer_Waves.gain(3, (float)wave4Volume() / waveVolumeSum);

    // consistent, moderate level for all waveforms
    waveform0.amplitude(.5);
    waveform1.amplitude(.5);
    waveform2.amplitude(.5);
    waveform3.amplitude(.5);
}

// Pushes every configurable IntValue into the synth objects. Called on any internal/external
// parameter change and on preset/patch change.
// TODO: update only the parameters that actually changed rather than re-pushing all of them.
void setPatchParams(void)
{
    setWaves();
    setFilter();
    setInputMix(presetSynthMixPercent());
    setAudioReverbAmount(0);
}

////////////////////////////////////////////////////////////
// MIDI consumer: note / CC / pitch-bend -> synth voices
////////////////////////////////////////////////////////////
void dbAudioSynth::consume(const dbMsg &msg)
{
    static int lastNote = 0;
    static int lastLevel = 0;
    if (!isSetUp)
    {
        setup();
    }

    static float ex = 0;
    switch (msg.type())
    {
    case CONTROL_CHANGE:
        switch (msg.value1())
        {
        case BREATH_CONTROLLER:
        case EXPRESSION_CONTROLLER:
            ex = (float)msg.value2() / 127.0;
            ex = ex * 2.0 - 1.0; // now a value between -1 and +1
            dc_CutoffFreq.amplitude(ex, 5);
            dc_Resonance.amplitude(ex / 20, 5);
            lastLevel = msg.value2();
            break;
        case EFFECT_DEPTH_1:
            setAudioReverbAmount((float)msg.value2() / 128.0);
            break;
        case RPN_MSB:
            lastRPN_MSB = msg.value2();
            break;
        case RPN_LSB:
            lastRPN_LSB = msg.value2();
            break;
        case DATA_ENTRY_MSB:
            lastDataEntry_MSB = msg.value2();
            if (lastRPN_MSB == 0 && lastRPN_LSB == 0)
            {
                currentPitchBendRange = lastDataEntry_MSB;
            }
            break;
        case DATA_ENTRY_LSB:
            // LSB fine-tunes the pitch-bend range; MSB resolution (whole semitones) is sufficient here.
            break;
        }
        break;
    case NOTE_ON:
        lastNote = msg.value1();
        freq = dbMIDI::getFrequency(lastNote);
        _filterFrequency = freq;
        setFilter();
        break;
    case NOTE_OFF:
        break;
    case PITCH_BEND:
        inboundPitchBend = ((msg.value2() * 128) + msg.value1()) - 8192; // MSB * 128 + LSB - 8192 to create a value between -8192 and 8191
        break;
    case PROGRAM_CHANGE:
        setPatchParams();
        break;
    }
   
    if (lastLevel > 0)
    {
        mixer_Waves.gain(0, wave1Volume() / 100.0);
        mixer_Waves.gain(1, wave2Volume() / 100.0);
        mixer_Waves.gain(2, wave3Volume() / 100.0);
        mixer_Waves.gain(3, wave4Volume() / 100.0);
        float transposedFreq = freq * pow(2,  ((float)wave1Semitones().getValue() + ((float)wave1Cents().getValue() / 100.0)) / 12.0  );
        float pitchBendSemitones = (inboundPitchBend / (16383.0f / 2.0f)) * currentPitchBendRange;
        float bentFreq = transposedFreq * pow(2, pitchBendSemitones / 12.0f);
        waveform0.frequency(bentFreq);
        waveform1.frequency(bentFreq * pow(2, (wave2Semitones() + (wave2Cents() / 100.0)) / 12.0));
        waveform2.frequency(bentFreq * pow(2, (wave3Semitones() + (wave3Cents() / 100.0)) / 12.0));
        waveform3.frequency(bentFreq * pow(2, (wave4Semitones() + (wave4Cents() / 100.0)) / 12.0));
    }
    else
    {
        mixer_Waves.gain(0, 0);
        mixer_Waves.gain(1, 0);
        mixer_Waves.gain(2, 0);
        mixer_Waves.gain(3, 0);
    }
}

static const char *dbAudioWaveformNames[] = {
    "Saw",
    "Reverse Saw",
    "Square",
    "Triangle",
    "Sine",
    "Pulse Mod",
};

char _badWaveFormString[20];
const char *getWaveformName(uint8_t waveformType)
{
    if (waveformType < sizeof(dbAudioWaveformNames) / sizeof(dbAudioWaveformNames[0]))
    {
        return dbAudioWaveformNames[waveformType];
    }
    else
    {
        snprintf(_badWaveFormString, sizeof(_badWaveFormString), "Unknown Wave %d", waveformType);
        return _badWaveFormString;
    }
}

////////////////////////////////////////////////////////////
// PRESET IntValues
////////////////////////////////////////////////////////////
static void _presetPatchNumberChanged(int newValue, Cause cause)
{
    if (cause == Cause::Edit)
    {
        loadCurrentPatch(newValue);
        setPatchParams();
    } else {
        #if defined(ERROR_TRACKING)
            Serial.printf("Unexpected cause: %d\n", cause);
        #endif
    }
}

static IntValue _presetPatchNumber = IntValue(0).range(0, 8).onChange(_presetPatchNumberChanged);
IntValue &presetPatchNumber(void) { return _presetPatchNumber; }

// Preset Synth Mix Percent
static void _describePresetSynthMixPercent(int amt, char *buffer, size_t capacity)
{
    String s = String(100 - amt) + " : " + String(amt);
    strncpy(buffer, s.c_str(), capacity);
}

static void _presetSynthMixPercentChanged(int newValue, Cause cause)
{
    if (cause == Cause::Edit)
    {
        setInputMix(newValue);
    }
}

static IntValue _presetSynthMixPercent = IntValue(100).range(0, 100).step(10).describeWith(_describePresetSynthMixPercent).onChange(_presetSynthMixPercentChanged);
IntValue &presetSynthMixPercent(void) { return _presetSynthMixPercent; }

////////////////////////////////////////////////////////////
// SYNTH PATCH IntValues
////////////////////////////////////////////////////////////

// Filter Semitones
static void _describeFilterSemitones(int amt, char *buffer, size_t capacity)
{
    String s = String(amt) + " semitones";
    strncpy(buffer, s.c_str(), capacity);
}

static IntValue _filterSemitones = IntValue(30).range(0, 36).describeWith(_describeFilterSemitones);
IntValue &filterSemitones(void) { return _filterSemitones; }

// Filter Resonance Tenths
static void _describeFilterResonancePercent(int amt, char *buffer, size_t capacity)
{
    String s = String(amt) + "%";
    strncpy(buffer, s.c_str(), capacity);
}

static IntValue _filterResonancePercent = IntValue(5).range(0, 100).describeWith(_describeFilterResonancePercent);
IntValue &filterResonancePercent(void) { return _filterResonancePercent; }

static IntValue _wave1Form = IntValue(0).range(0, 5).describeWith(dbAudioWaveformNames);
IntValue &wave1Form(void) { return _wave1Form; }

static IntValue _wave2Form = IntValue(0).range(0, 5).describeWith(dbAudioWaveformNames);
IntValue &wave2Form(void) { return _wave2Form; }

static IntValue _wave3Form = IntValue(0).range(0, 5).describeWith(dbAudioWaveformNames);
IntValue &wave3Form(void) { return _wave3Form; }

static IntValue _wave4Form = IntValue(0).range(0, 5).describeWith(dbAudioWaveformNames);
IntValue &wave4Form(void) { return _wave4Form; }

// Wave Volume
static void _describeWaveVolume(int amt, char *buffer, size_t capacity)
{
    String wave1VolumeString = String(amt) + "%";
    strncpy(buffer, wave1VolumeString.c_str(), capacity);
}

static IntValue _wave1Volume = IntValue(100).range(0, 100).describeWith(_describeWaveVolume);
IntValue &wave1Volume(void) { return _wave1Volume; }

static IntValue _wave2Volume = IntValue(0).range(0, 100).describeWith(_describeWaveVolume);
IntValue &wave2Volume(void) { return _wave2Volume; }

static IntValue _wave3Volume = IntValue(0).range(0, 100).describeWith(_describeWaveVolume);
IntValue &wave3Volume(void) { return _wave3Volume; }

static IntValue _wave4Volume = IntValue(0).range(0, 100).describeWith(_describeWaveVolume);
IntValue &wave4Volume(void) { return _wave4Volume; }

// Wave Semitone
static void _describeWaveSemitones(int amt, char *buffer, size_t capacity)
{
    String wave1SemitonesString = String(amt) + " semitones";
    strncpy(buffer, wave1SemitonesString.c_str(), capacity);
}

static IntValue _wave1Semitones = IntValue(0).range(-36, 36).describeWith(_describeWaveSemitones);
IntValue &wave1Semitones(void) { return _wave1Semitones; }

static IntValue _wave2Semitones = IntValue(0).range(-36, 36).describeWith(_describeWaveSemitones);
IntValue &wave2Semitones(void) { return _wave2Semitones; }

static IntValue _wave3Semitones = IntValue(0).range(-36, 36).describeWith(_describeWaveSemitones);
IntValue &wave3Semitones(void) { return _wave3Semitones; }

static IntValue _wave4Semitones = IntValue(0).range(-36, 36).describeWith(_describeWaveSemitones);
IntValue &wave4Semitones(void) { return _wave4Semitones; }

// Wave Cents
static void _describeWaveCents(int amt, char *buffer, size_t capacity)
{
    String wave1CentsString = String(amt) + " cents";
    strncpy(buffer, wave1CentsString.c_str(), capacity);
}

static IntValue _wave1Cents = IntValue(0).range(-99, 99).describeWith(_describeWaveCents);
IntValue &wave1Cents(void) { return _wave1Cents; }

static IntValue _wave2Cents = IntValue(0).range(-99, 99).describeWith(_describeWaveCents);
IntValue &wave2Cents(void) { return _wave2Cents; }

static IntValue _wave3Cents = IntValue(0).range(-99, 99).describeWith(_describeWaveCents);
IntValue &wave3Cents(void) { return _wave3Cents; }

static IntValue _wave4Cents = IntValue(0).range(-99, 99).describeWith(_describeWaveCents);
IntValue &wave4Cents(void) { return _wave4Cents; }

// Wave Duty Cycle
static void _describeWaveDutyCycle(int amt, char *buffer, size_t capacity)
{
    String wave1DutyCycleString = String(amt) + "%";
    strncpy(buffer, wave1DutyCycleString.c_str(), capacity);
}

static IntValue _wave1DutyCycle = IntValue(50).range(10, 90).step(10).describeWith(_describeWaveDutyCycle);
IntValue &wave1DutyCycle(void) { return _wave1DutyCycle; }

static IntValue _wave2DutyCycle = IntValue(50).range(10, 90).step(10).describeWith(_describeWaveDutyCycle);
IntValue &wave2DutyCycle(void) { return _wave2DutyCycle; }

static IntValue _wave3DutyCycle = IntValue(50).range(10, 90).step(10).describeWith(_describeWaveDutyCycle);
IntValue &wave3DutyCycle(void) { return _wave3DutyCycle; }

static IntValue _wave4DutyCycle = IntValue(50).range(10, 90).step(10).describeWith(_describeWaveDutyCycle);
IntValue &wave4DutyCycle(void) { return _wave4DutyCycle; }

void printAudioParams(void)
{
#if defined(UTILITY_TRACKING)
    Serial.printf("Patch: %d\n", presetPatchNumber().getValue());
    Serial.printf("Synth Mix: %d\n", presetSynthMixPercent().getValue());
    Serial.printf("\twave1Form: %d\twave1Volume: %d\twave1Semitones: %d\twave1Cents: %d\twave1DutyCycle: %d\n"
                  "\twave2Form: %d\twave2Volume: %d\twave2Semitones: %d\twave2Cents: %d\twave2DutyCycle: %d\n"
                  "\twave3Form: %d\twave3Volume: %d\twave3Semitones: %d\twave3Cents: %d\twave3DutyCycle: %d\n"
                  "\twave4Form: %d\twave4Volume: %d\twave4Semitones: %d\twave4Cents: %d\twave4DutyCycle: %d\n",
                  wave1Form().getValue(), wave1Volume().getValue(), wave1Semitones().getValue(), wave1Cents().getValue(), wave1DutyCycle().getValue(),
                  wave2Form().getValue(), wave2Volume().getValue(), wave2Semitones().getValue(), wave2Cents().getValue(), wave2DutyCycle().getValue(),
                  wave3Form().getValue(), wave3Volume().getValue(), wave3Semitones().getValue(), wave3Cents().getValue(), wave3DutyCycle().getValue(),
                  wave4Form().getValue(), wave4Volume().getValue(), wave4Semitones().getValue(), wave4Cents().getValue(), wave4DutyCycle().getValue());
    Serial.printf("\n\tfilterSemitones: %d\tfilterResonanceTenths: %d\n", filterSemitones().getValue(), filterResonancePercent().getValue());
    Serial.printf("\taudioReverbAmount: %1.2f (reverbAmount: %d)\n", _audioReverbAmount, reverbAmount().getValue());
    Serial.printf("\tfreq: %1.2f\tfilterFrequency: %1.2f\tfilterOctave: %1.2f\tfilterResonance: %1.2f\n", freq , _filterFrequency, _filterOctave, _filterResonance);
    Serial.printf("\n");
#endif
}
Maybe I’m missing some of the extra files that show how you actually set the desired waveform etc.

If you have a git hub or a share then I can compile at some point and test.
 
Right. There's a whole infrastructure for configuration. I'll try to create a simplified tool that can run in Arduino. For the wave forms on essentially selecting one of the available waveform enumerations. Sawtooth is the most common. If I choose pulse with modulation, I have a separate duty cycle setting.

Let me do some more preparation in creating a small app that can be compiled to standalone, perhaps with some Terminal based modifiers. I'm also trying to record what I'm looking for and struggling a bit. I'll work that through as well. I am modeling my wave base synthesis Korg's 4-wave MonoPoly software synthesizer.

Thanks for your help so far. I'll post back when I have more complete source code to review and some audio samples to play.
 
@Davidelvig: I have a few suggestions for you to try, which might help to broaden the resulting sound that you are getting. These ideas stem from my experience building & tinkering with my TeensyMIDIPolySynth (<here's> a link to the description of the capabilities that I built into my TMPS. It was created using the standard audio libraries, plus a few custom objects: reverb provided by @Pio, available <here>, & a ladder filter provided by @rvh, available <here>):

- add an additional waveform generator pair with each of your existing waveform generators, & run one additional generator an octave above and one an octave below . . . these summed oscillators will provide a fuller sound (you can play with the mix levels to make sure that your fundamental is heard the most)

- you can also try running one or both of these additional generators in the same octave, but detuning them slightly from the fundamental & that will also broaden the resulting sound mix

Good luck & have fun !!

Mark J Culross
KD5RXT
 
Thanks @kd5rxt-mark
I’ve been reluctant to add too many objects without understanding what each does. This is a nice thing to try… Stacking up a couple of extra oscillators around each of the four voices with a specific task in mind - thickening each voice with the octaves above and below. Maybe also my first LFO to try de-tuning those thickening waves a bit.

Of course, building a Mohr intricate synthesizer is one thing. Deciding what to make user configurable is another. I guess I’ll first try to achieve great sound… And then see what needs to be changeable by the end user.

I will also check out the alternate ladder filter and the reverb. I have not yet included any add-on libraries and it seems like there are plenty that could offer some value.

Do I assume correctly that I would be adding those manually at the top of the CPP file as opposed to somehow adding them through the visual design tool?
 
Do I assume correctly that I would be adding those manually at the top of the CPP file as opposed to somehow adding them through the visual design tool?
Yes, you are correct. Using the reverb as an example, on the standard library Audio Design Tool, leave a connection open (say, from the output of the next-to-last mixer, to the input of the following/final mixer/amp. Manually add the reverb device as an object in your list, then manually add the connection from the unconnected output of the next-to-last mixer to the input of the reverb, then add the remaining connection from the output of the reverb, to the unconnected input of the following/final mixer/amp. Personally, when I add these manual connections, I make the connections start at a number high enough (e.g. 1001) so that the automatic numbering of any future additions using the Audio Design Tool will not conflict with the numbering of my manual connections.

Hope that helps . . .

Mark J Culross
KD5RXT
 
Like many people I came here put a few modules together on the editor and went from there.

There are lots of examples through the forum, I’ve loaded some but for me my goal was to replicate the jp8000 so I limited myself to build that.

If you like the monopoly then you should start there.

Build with the items in the library. Check what else you want to do once complete and then add in the extra libraries.

The reverbs mentioned above are excellent.

But you can get the starting point of your synth going.

The video list below is old but fundamentally explained and a good example.


For the monopoly I’d start simple be able to layout the signal path that you are replicating.

4 osc into a mixer, adsr for amp and same for filter.

The moog filter is brilliant but be warned it is a different sound that the korg. (But as I said work with what you have then look to replace later)

Change your waveforms to the modulated library though as that makes life easier going forward for PWM
 
> I'm interested in replicating some of the top quality sound I've heard from Teensy-based projects in other posts.

Would you provide links to the posts you are referring to ?

As to "thickness" of the sound and "broadening" - these are typically used in the context of "analog sound".

The thing with digital synth that makes it thin is that it is too perfect.

Analog circuits produce noise, distortion, have non-linearity, instabilities, etc. All those artifacts make sound "thicker".

The Teensy audio library is great for beginners and super quick to get good results but to achieve "analog" feel you need to introduce imperfections.
The only component of Teensy audio library that is purposefully introducing imperfections is Ladder filter that adds tanh-like distortion.
This adds some character to the filter, although the implementation still is far from analog counterpart (perfect recreation is computationally costly due to need to solve non-linear differential equations in real time using numerical integration).

You can simulate imperfections by adding:
a) detune to multiple oscillators
b) modulate pitch with very small signal and small low-frequency (pink) noise simulating instability of oscillators
c) add distortion in signal path

But actually the easiest option to thicken the sound is to use effects.
A bit of chorusing and phasing effect does miracles to the sound.

A famous example is Solina String ensemble. Its oscillators were plain octave down dividers (purely digital) that sounded awfully and yet it derived all its lush string sound from famous 3-way 120degree phase shift chorus effect modulated by 2 independent LFOs.

FWIW: For my own Teensy synth I am not using Teensy audio library at all because of
a) I use floating point for majority of calculations (this saves a lot of worry about scaling)
b) I need per-sample modulation of everything, not block based
c) re-configurable mod matrix
d) better resource saving / management (so modules save aggressively CPU time when they are not used to modulate other parts)
e) filter and oscillator models not found in the lib (for example hard sync, poly blep)
 
Back
Top