New Teensy 4.1 DIY Synthesizer

I actually wanted to go for a bike ride. But I hate the rain ;) So I asked my new friend Fred (Google AI) if he could quickly draw a notch filter curve for Jeannie 😉

20260705_151541.jpg


This is how it all sounds... The VCF envelope controls the notch.
MP3 file is to long. This is a link to my MP3 https://www.sequencer.de/synthesize...honic-synthesizer-sampler.178440/post-3108215
 
Hi there 🖐️
There was a minor issue with the PCM5102A DAC being used—the same one found in the Jeannie_1. When a sound decayed during the "Note Off" phase, a faint click could be heard at the very end (see the first image). The root cause lies in the automatic muting function built into the DAC chip. If the PCM5102A receives nothing but zero data at its digital input, it activates an analog mute after a few milliseconds. This results in a faint clicking sound at its output. I was able to resolve this issue via software by mixing in a small DC offset within the digital output mixer. As a result, the DAC continuously receives data and does not trigger its mute function.

Image 1: Clicking sound at the end of the release phase
View attachment 39399

Image 2: DC offset of (-1.0f/32786.0f) in the output mixer
View attachment 39400

Principle of the Digital Output Mixer with DC Offset
View attachment 39401
Thanks for that, Rolf. You fixed my problem!
 
Wavetable sound from Jeannie 2

In principle, the wavetable engine in Jeannie 2 already works quite well. It's still missing modulation options, and I need to work on the wavetable control (WT). It's a bit temperamental 😬 The "Soft" control smooths out waveform jumps.

 
The next steps...

Next up are more wavetable synthesis, selectable via the 'WT' menu button. First, there's the classic 8-bit PPG wavetable, which I introduced in my last post. The second synthesizer is the WTX4 from Mutable Instruments' Braids synthesizer. The WTX4 is a four-voice wavetable synthesis where four oscillators with adjustable detuning sound simultaneously. Next is the WTLIN synthesis, similar to the WT. Finally, there's the WTMAP synthesis, which allows you to navigate through waveforms in two dimensions. All of this is, of course, 12-voice polyphonic thanks to the powerful Teensy MCU with an 812MHz clock speed.

The user interface will look something like this...

20260717_101743.jpg
 
Last edited:
Not went into the wavetables too much yet. Sounds really smooth. Are you using the features from the library or have you added smoothing?
 
I'm using a modified wavetable function from the Braids Mutable Instruments synthesizer. The "Soft" control determines the quality of the interpolation. For modulation with envelope or LFO, I programmed an additional control input on the AudioSynthWaveformModulated.
 
synth_waveform.cpp
C:
void AudioSynthWaveformModulated::update(void)
{
    audio_block_t *block, *moddata, *shapedata, * posblock;
    int16_t *bp, *end;
    int32_t val1, val2;
    int16_t magnitude15;
    uint32_t i, ph, index, index2, scale, priorphase;
    const uint32_t inc = phase_increment;

    moddata = receiveReadOnly(0);
    shapedata = receiveReadOnly(1);
    posblock = receiveReadOnly(2); // zusätzlicher Modulationseingang

    // Pre-compute the phase angle for every output sample of this update
    ph = phase_accumulator;
    priorphase = phasedata[AUDIO_BLOCK_SAMPLES-1];
    if (moddata && modulation_type == 0) {
        // Frequency Modulation
        bp = moddata->data;
        for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
            int32_t n = (*bp++) * modulation_factor; // n is # of octaves to mod
            int32_t ipart = n >> 27; // 4 integer bits
            n &= 0x7FFFFFF;          // 27 fractional bits
            #ifdef IMPROVE_EXPONENTIAL_ACCURACY
            // exp2 polynomial suggested by Stefan Stenzel on "music-dsp"
            // mail list, Wed, 3 Sep 2014 10:08:55 +0200
            int32_t x = n << 3;
            n = multiply_accumulate_32x32_rshift32_rounded(536870912, x, 1494202713);
            int32_t sq = multiply_32x32_rshift32_rounded(x, x);
            n = multiply_accumulate_32x32_rshift32_rounded(n, sq, 1934101615);
            n = n + (multiply_32x32_rshift32_rounded(sq,
                multiply_32x32_rshift32_rounded(x, 1358044250)) << 1);
            n = n << 1;
            #else
            // exp2 algorithm by Laurent de Soras
            // https://www.musicdsp.org/en/latest/Other/106-fast-exp2-approximation.html
            n = (n + 134217728) << 3;

            n = multiply_32x32_rshift32_rounded(n, n);
            n = multiply_32x32_rshift32_rounded(n, 715827883) << 3;
            n = n + 715827882;
            #endif
            uint32_t scale = n >> (14 - ipart);
            uint64_t phstep = (uint64_t)inc * scale;
            uint32_t phstep_msw = phstep >> 32;
            if (phstep_msw < 0x7FFE) {
                ph += phstep >> 16;
            } else {
                ph += 0x7FFE0000;
            }
            phasedata[i] = ph;
        }
        release(moddata);
    } else if (moddata) {
        // Phase Modulation
        bp = moddata->data;
        for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
            // more than +/- 180 deg shift by 32 bit overflow of "n"
                uint32_t n = ((uint32_t)(*bp++)) * modulation_factor;
            phasedata[i] = ph + n;
            ph += inc;
        }
        release(moddata);
    } else {
        // No Modulation Input
        for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
            phasedata[i] = ph;
            ph += inc;
        }
    }
    phase_accumulator = ph;

    // If the amplitude is zero, no output, but phase still increments properly
    if (magnitude == 0) {
        if (shapedata) release(shapedata);
        return;
    }
    block = allocate();
    if (!block) {
        if (shapedata) release(shapedata);
        return;
    }
    bp = block->data;

    // Now generate the output samples using the pre-computed phase angles
    switch(tone_type) {
    case WAVEFORM_SINE:
        for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
            ph = phasedata[i];
            index = ph >> 24;
            val1 = AudioWaveformSine[index];
            val2 = AudioWaveformSine[index+1];
            scale = (ph >> 8) & 0xFFFF;
            val2 *= scale;
            val1 *= 0x10000 - scale;
            *bp++ = multiply_32x32_rshift32(val1 + val2, magnitude);
        }
        break;

    case WAVEFORM_ARBITRARY:
        if (!arbdata) {
            release(block);
            if (shapedata) release(shapedata);
            return;
        }
        // len = 256
        for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
            ph = phasedata[i];
            index = ph >> 24;
            index2 = index + 1;
            if (index2 >= 256) index2 = 0;
            val1 = *(arbdata + index);
            val2 = *(arbdata + index2);
            scale = (ph >> 8) & 0xFFFF;
            val2 *= scale;
            val1 *= 0x10000 - scale;
            *bp++ = multiply_32x32_rshift32(val1 + val2, magnitude);
        }
        break;

    case WAVEFORM_PULSE:
        if (shapedata) {
            magnitude15 = signed_saturate_rshift(magnitude, 16, 1);
            for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
                uint32_t width = ((shapedata->data[i] + 0x8000) & 0xFFFF) << 16;
                if (phasedata[i] < width) {
                    *bp++ = magnitude15;
                } else {
                    *bp++ = -magnitude15;
                }
            }
            break;
        } // else fall through to orginary square without shape modulation

    case WAVEFORM_SQUARE:
        magnitude15 = signed_saturate_rshift(magnitude, 16, 1);
        for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
            if (phasedata[i] & 0x80000000) {
                *bp++ = -magnitude15;
            } else {
                *bp++ = magnitude15;
            }
        }
        break;

    case WAVEFORM_BANDLIMIT_PULSE:
        if (shapedata)
        {
          for (i=0; i < AUDIO_BLOCK_SAMPLES; i++)
          {
            uint32_t width = ((shapedata->data[i] + 0x8000) & 0xFFFF) << 16;
            int32_t val = band_limit_waveform.generate_pulse (phasedata[i], width, i) ;
            *bp++ = (int16_t) ((val * magnitude) >> 16) ;
          }
          break;
        } // else fall through to orginary square without shape modulation

    case WAVEFORM_BANDLIMIT_SQUARE:
        for (i = 0 ; i < AUDIO_BLOCK_SAMPLES ; i++)
        {
          int32_t val = band_limit_waveform.generate_square (phasedata[i], i) ;
          *bp++ = (int16_t) ((val * magnitude) >> 16) ;
        }
        break;

    case WAVEFORM_SAWTOOTH:
        for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
            *bp++ = signed_multiply_32x16t(magnitude, phasedata[i]);
        }
        break;

    case WAVEFORM_SAWTOOTH_REVERSE:
        for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
            *bp++ = signed_multiply_32x16t(0xFFFFFFFFu - magnitude, phasedata[i]);
        }
        break;

    case WAVEFORM_BANDLIMIT_SAWTOOTH:
    case WAVEFORM_BANDLIMIT_SAWTOOTH_REVERSE:
        for (i = 0 ; i < AUDIO_BLOCK_SAMPLES ; i++)
        {
          int16_t val = band_limit_waveform.generate_sawtooth (phasedata[i], i) ;
          val = (int16_t) ((val * magnitude) >> 16) ;
          *bp++ = tone_type == WAVEFORM_BANDLIMIT_SAWTOOTH_REVERSE ? (int16_t) -val : (int16_t) +val ;
        }
        break;

    case WAVEFORM_TRIANGLE_VARIABLE:
        if (shapedata) {
            for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
                uint32_t width = (shapedata->data[i] + 0x8000) & 0xFFFF;
                uint32_t rise = 0xFFFFFFFF / width;
                uint32_t fall = 0xFFFFFFFF / (0xFFFF - width);
                uint32_t halfwidth = width << 15;
                uint32_t n;
                ph = phasedata[i];
                if (ph < halfwidth) {
                    n = (ph >> 16) * rise;
                    *bp++ = ((n >> 16) * magnitude) >> 16;
                } else if (ph < 0xFFFFFFFF - halfwidth) {
                    n = 0x7FFFFFFF - (((ph - halfwidth) >> 16) * fall);
                    *bp++ = (((int32_t)n >> 16) * magnitude) >> 16;
                } else {
                    n = ((ph + halfwidth) >> 16) * rise + 0x80000000;
                    *bp++ = (((int32_t)n >> 16) * magnitude) >> 16;
                }
                ph += inc;
            }
            break;
        } // else fall through to orginary triangle without shape modulation

    case WAVEFORM_TRIANGLE:
        for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
            ph = phasedata[i];
            uint32_t phtop = ph >> 30;
            if (phtop == 1 || phtop == 2) {
                *bp++ = ((0xFFFF - (ph >> 15)) * magnitude) >> 16;
            } else {
                *bp++ = (((int32_t)ph >> 15) * magnitude) >> 16;
            }
        }
        break;
    case WAVEFORM_SAMPLE_HOLD:
        for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
            ph = phasedata[i];
            if (ph < priorphase) { // does not work for phase modulation
                sample = random(magnitude) - (magnitude >> 1);
            }
            priorphase = ph;
            *bp++ = sample;
        }
        break;

    // WaveformModulated with user wavetables ----------------
    case WAVEFORM_USER_WAVETABLE:
    {
        // smooth parameter value
        smooth_parameter_0();
        smooth_parameter_1();

        // --- NEU: Positions-Modulation einrechnen ---
        int16_t position_modulation = 0;
        if (posblock && posblock->data)
        {
            // Wir lesen das erste Sample des Blocks.
            // Wichtig: posblock->data ist ein int16_t Array, kein einzelner Wert!
            position_modulation = posblock->data[0];
        }

        // Achtung: Wenn Ihr posblock Werte von -32768 bis 32767 liefert,
        // müssen wir den Wert eventuell skalieren, damit er den Parameter nicht sprengt
        parameter_[0] = previous_smooth_parameter_[0] + par_a_mod_ + position_modulation;
        parameter_[1] = previous_smooth_parameter_[1] + par_b_mod_;

        position_modulation_out = position_modulation;

        if (parameter_[0] <= 0)
        {
            parameter_[0] = 0;
        }

        // clip max value
        parameter_[0] = saturate16(parameter_[0]);
        parameter_[1] = saturate16(parameter_[1]);

        smoothed_parameter_ = (3 * smoothed_parameter_ + (parameter_[0] << 1)) >> 2;

        uint16_t scan = smoothed_parameter_;
        uint8_t tabIndex = user_wt_index;
        uint32_t tab_offset = ((129 * 64) * tabIndex);
        const uint8_t *wave_0 = user_waves + tab_offset + userwave_line[previous_parameter_[0] >> 9] * 129;
        const uint8_t *wave_1 = user_waves + tab_offset + userwave_line[scan >> 10] * 129;
        const uint8_t *wave_2 = user_waves + tab_offset + userwave_line[(scan >> 10) + 1] * 129;

        uint16_t smooth_xfade = scan << 6;
        uint16_t rough_xfade = 0;
        uint16_t rough_xfade_increment = 32768 / 128;
        uint32_t balance = parameter_[1] << 3;

        uint32_t phase = phase_;
        uint32_t phase_increment1 = phase_increment >> 1;

        int16_t rough, smooth;

        if (parameter_[1] < 8192)
        {
            for (uint8_t i = 0; i < AUDIO_BLOCK_SAMPLES; i++)
            {
                int32_t sample = 0;
                rough = Crossfade(wave_0, wave_1, (phase >> 1) & 0xfe000000, rough_xfade);
                smooth = Crossfade(wave_0, wave_1, phase >> 1, rough_xfade);
                sample += Mix(rough, smooth, balance);
                phase = phasedata[i];
                rough_xfade += rough_xfade_increment;
                rough = Crossfade(wave_0, wave_1, (phase >> 1) & 0xfe000000, rough_xfade);
                smooth = Crossfade(wave_0, wave_1, phase >> 1, rough_xfade);
                sample += Mix(rough, smooth, balance);
                phase += phase_increment1;
                rough_xfade += rough_xfade_increment;
                *bp++ = ~sample >> 1;
            }
        }
        else if (parameter_[1] < 16384)
        {
            for (uint8_t i = 0; i < AUDIO_BLOCK_SAMPLES; i++)
            {
                int32_t sample = 0;
                rough = Crossfade(wave_0, wave_1, phase >> 1, rough_xfade);
                smooth = Crossfade(wave_1, wave_2, phase >> 1, smooth_xfade);
                sample += Mix(rough, smooth, balance);
                phase = phasedata[i];
                rough_xfade += rough_xfade_increment;
                rough = Crossfade(wave_0, wave_1, phase >> 1, rough_xfade);
                smooth = Crossfade(wave_1, wave_2, phase >> 1, smooth_xfade);
                sample += Mix(rough, smooth, balance);
                phase += phase_increment1;
                rough_xfade += rough_xfade_increment;
                *bp++ = ~sample >> 1;
            }
        }
        else if (parameter_[1] < 24576)
        {
            for (uint8_t i = 0; i < AUDIO_BLOCK_SAMPLES; i++)
            {
                int32_t sample = 0;
                smooth = Crossfade(wave_1, wave_2, phase >> 1, smooth_xfade);
                rough = Crossfade(wave_1, wave_2, (phase >> 1) & 0xfe000000, smooth_xfade);
                sample += Mix(smooth, rough, balance);
                phase = phasedata[i];
                smooth = Crossfade(wave_1, wave_2, phase >> 1, smooth_xfade);
                rough = Crossfade(wave_1, wave_2, (phase >> 1) & 0xfe000000, smooth_xfade);
                sample += Mix(smooth, rough, balance);
                phase += phase_increment1;
                *bp++ = ~sample >> 1;
            }
        }
        else
        {
            for (uint8_t i = 0; i < AUDIO_BLOCK_SAMPLES; i++)
            {
                int32_t sample = 0;
                smooth = Crossfade(wave_1, wave_2, (phase >> 1) & 0xfe000000, smooth_xfade);
                rough = Crossfade(wave_1, wave_2, (phase >> 1) & 0xf8000000, smooth_xfade);
                sample += Mix(smooth, rough, balance);
                phase = phasedata[i];
                smooth = Crossfade(wave_1, wave_2, (phase >> 1) & 0xfe000000, smooth_xfade);
                rough = Crossfade(wave_1, wave_2, (phase >> 1) & 0xf8000000, smooth_xfade);
                sample += Mix(smooth, rough, balance);
                phase += phase_increment1;
                *bp++ = ~sample >> 1;
            }
        }

        phase_ = phase;
        previous_parameter_[0] = smoothed_parameter_ >> 1;
    }
    break;
    }

    if (tone_offset) {
        bp = block->data;
        end = bp + AUDIO_BLOCK_SAMPLES;
        do {
            val1 = *bp;
            *bp++ = signed_saturate_rshift(val1 + tone_offset, 16, 0);
        } while (bp < end);
    }
    if (shapedata) release(shapedata);
    if (posblock) release(posblock); // <-- WICHTIG: Speicherleck verhindern!
    transmit(block, 0);
    release(block);
}

synth_waveform.h
C:
class AudioSynthWaveformModulated : public AudioStream
{
public:
    AudioSynthWaveformModulated(void) : AudioStream(3, inputQueueArray),
        phase_accumulator(0), phase_increment(0), modulation_factor(32768),
        magnitude(0), arbdata(NULL), sample(0), tone_offset(0),
        tone_type(WAVEFORM_SINE), modulation_type(0), par_a_mod_(0), par_b_mod_(0){}
        
        int16_t getPositionModulation() { return position_modulation_out;}

    void frequency(float freq) {
        if (freq < 0.0f) {
            freq = 0.0;
        } else if (freq > AUDIO_SAMPLE_RATE_EXACT / 2.0f) {
            freq = AUDIO_SAMPLE_RATE_EXACT / 2.0f;
        }
        phase_increment = freq * (4294967296.0f / AUDIO_SAMPLE_RATE_EXACT);
        if (phase_increment > 0x7FFE0000u) phase_increment = 0x7FFE0000;
    }
    void amplitude(float n) {    // 0 to 1.0
        if (n < 0) {
            n = 0;
        } else if (n > 1.0f) {
            n = 1.0f;
        }
        magnitude = n * 65536.0f;
    }
    void offset(float n) {
        if (n < -1.0f) {
            n = -1.0f;
        } else if (n > 1.0f) {
            n = 1.0f;
        }
        tone_offset = n * 32767.0f;
    }
    void begin(short t_type) {
        tone_type = t_type;
        if (t_type == WAVEFORM_BANDLIMIT_SQUARE)
          band_limit_waveform.init_square (phase_increment) ;
        else if (t_type == WAVEFORM_BANDLIMIT_PULSE)
          band_limit_waveform.init_pulse (phase_increment, 0x80000000u) ;
        else if (t_type == WAVEFORM_BANDLIMIT_SAWTOOTH || t_type == WAVEFORM_BANDLIMIT_SAWTOOTH_REVERSE)
          band_limit_waveform.init_sawtooth (phase_increment) ;
    }
    void begin(float t_amp, float t_freq, short t_type) {
        amplitude(t_amp);
        frequency(t_freq);
        begin (t_type) ;
    }
    void arbitraryWaveform(const int16_t *data, float maxFreq) {
        arbdata = data;
    }

    void user_wavetable_index (uint8_t wt)
  {
    user_wt_index = wt;
  }

    void frequencyModulation(float octaves) {
        if (octaves > 12.0f) {
            octaves = 12.0f;
        } else if (octaves < 0.1f) {
            octaves = 0.1f;
        }
        modulation_factor = octaves * 4096.0f;
        modulation_type = 0;
    }
    void phaseModulation(float degrees) {
        if (degrees > 9000.0f) {
            degrees = 9000.0f;
        } else if (degrees < 30.0f) {
            degrees = 30.0f;
        }
        modulation_factor = degrees * (float)(65536.0 / 180.0);
        modulation_type = 1;
    }

    void smooth_parameter_0()
  {
    uint16_t parameter_increment;
    int32_t difference = abs(parameter_0 - previous_smooth_parameter_[0]);

    if (difference > 0)
    {
      parameter_increment = (difference * 0.031f) + 1;

      if (previous_smooth_parameter_[0] < parameter_0)
      {
        previous_smooth_parameter_[0] += parameter_increment;
      }
      else if (previous_smooth_parameter_[0] > parameter_0)
      {
        previous_smooth_parameter_[0] -= parameter_increment;
      }
      else
      {
        previous_smooth_parameter_[0] = parameter_[0];
      }
    }
  }

  void smooth_parameter_1()
  {
    uint16_t parameter_increment;
    int32_t difference = abs(parameter_1 - previous_smooth_parameter_[1]);

    if (difference > 0)
    {
      parameter_increment = (difference * 0.031f) + 1;

      if (previous_smooth_parameter_[1] < parameter_1)
      {
        previous_smooth_parameter_[1] += parameter_increment;
      }
      else if (previous_smooth_parameter_[1] > parameter_1)
      {
        previous_smooth_parameter_[1] -= parameter_increment;
      }
      else
      {
        previous_smooth_parameter_[1] = parameter_[1];
      }
    }
  }

  void parameter_a(uint16_t Osc_par_a)
  { // parameter_a from Osc Menu
    osc_par_a = Osc_par_a;
    parameter_0 = (osc_par_a << 5);
  }

  void parameter_b(uint16_t Osc_par_b)
  { // parameter_b from Osc Menu
    osc_par_b = Osc_par_b;
    parameter_1 = (osc_par_b << 5);
  }

    int16_t Interpolate824_8(const uint8_t* table, uint32_t phase)
  {
    int32_t a = table[phase >> 24];
    int32_t b = table[(phase >> 24) + 1];
    return (a << 8) + ((b - a) * static_cast<int32_t>(phase & 0xffffff) >> 16) - 32768;
}

    int16_t Crossfade(const uint8_t* table_a, const uint8_t* table_b,
    uint32_t phase, uint16_t balance)
    {
      int32_t a = Interpolate824_8(table_a, phase);
      int32_t b = Interpolate824_8(table_b, phase);
      return a + ((b - a) * static_cast<int32_t>(balance) >> 16);
  }

  int16_t Mix(int16_t a, int16_t b, uint16_t balance)
  {
    return (a * (65535 - balance) + b * balance) >> 16;
  }

    virtual void update(void);

private:
    audio_block_t *inputQueueArray[3];
    uint32_t phase_accumulator;
    uint32_t phase_increment;
    uint32_t modulation_factor;
    int32_t  magnitude;
    const int16_t *arbdata;
    uint32_t phasedata[AUDIO_BLOCK_SAMPLES];
    int16_t  sample; // for WAVEFORM_SAMPLE_HOLD
    int16_t  tone_offset;
    uint8_t  tone_type;
    uint8_t  modulation_type;
    BandLimitedWaveform band_limit_waveform;
    // ab hier user_wavetables
    int32_t parameter_0;
      int32_t parameter_1;
    uint16_t osc_par_a = 0;
      uint16_t osc_par_b = 0;
      uint8_t user_wt_index;
    int32_t parameter_[2];
    int32_t previous_smooth_parameter_[2] = {0,0};
    int16_t par_a_mod_ = 0; // parameter_A
      int16_t par_b_mod_ = 0; // parameter_B
    int32_t smoothed_parameter_;
    int32_t previous_parameter_[2] = {0,0};
    uint32_t phase_;
    int16_t position_modulation_out = 0;
};
#endif
 
I must admit that I was inspired by the Iridium and the Microwave plugin from Waldorf. I've further improved the view in Jeannie 2 by drawing the selected waveform in the foreground. This makes it appear more vibrant and less pixelated.
To conserve processor resources for visualization, the GUI CPU (Teensy 4.0) contains the same waveform tables as the audio processor (Teensy 4.1). This means the audio processor only needs to transmit the number of the modulated waveform to the GUI CPU. Thanks to the framebuffer, the GUI CPU then draws a new 3D view in just a few milliseconds.

20260731_185501.jpg
 
Next step... is wavetable map
The wavetable map consists of a 2D matrix of 16 x 16 waveforms.

Wavetable.jpg


XMov and YMov, along with the touchscreen, allow for smooth crossfading between waveforms. A blue frame indicates the current position on the display. I'm curious to see if I can program it... :unsure:
 
Back
Top