AudioSynthKarplusStrong object with modulation/tuning input

kd5rxt-mark

Well-known member
SUMMARY:
- I need help creating a functionally correct AudioSynthKarplusStrongModulated class that allows for a tuning control input

CHALLENGE:
- the standard AudioSynthKarplusStrong object does not have a tuning input

PROBLEMS ENCOUNTERED:
- my attempt at creating an AudioSynthKarplusStrongModulated object does not have a consistently clean "pluck" at the beginning like the standard AudioSynthKarplusStrong object does
- the sound from my attempt at creating an AudioSynthKarplusStrongModulated object sounds "pinched" in comparison to the sound from the standard AudioSynthKarplusStrong object

MY DESIRE:
- where/what should I be looking at to address the two problems identified above ??

REQUIRED HARDWARE:
- the test code listed below runs on the T4.x with the Audio Adapter connected (the pot connected to the A2 input needs to be installed/connected, as it is used for adjusting the tuning of the generated sound for the modulated objects)

IMPLEMENTATION DETAILS:
In my TeensyMIDIPolySynth (TMPS) project, the three voices each have individual tuning control inputs. In addition, there is an overall tuning control input that affects each of the three voices as well. The individual controls allow me to improve the width of the sound (by de-tuning the individual voices with respect to each other). The overall tuning control allows me to tune my TMPS to match physical/analog instruments (reed instruments, horns, keyboards, other synths, etc.).

In my TMPS, as part of the sound generating capabilities, each of the voices currently includes the use of the AudioSynthKarplusStrong audio object (implemented in the standard audio library, specifically in synth_karplusstrong.cpp) in addition to waveform & noise objects (other synth_*.cpp & effect_*.cpp files).

MAIN OBJECTIVE:
Unfortunately, the AudioSynthKarplusStrong implementation has no equivalent "tuning" capability like the AudioSynthWaveformModulated object, so I have undertaken the task of adding a tuneable string object (attempting to add an AudioSynthKarplusStrongModulated class). Unfortunately, I am in well over my head.

MY ATTEMPT AT CREATION (using the SynthWaveformModulated object as my guide):
So, here's my first stab at adding a AudioSynthKarplusStrongModulated class to synth_karplusstrong.h & synth_karplusstrong.cpp:

C++:
/* Audio Library for Teensy 3.X
 * Copyright (c) 2016, Paul Stoffregen, paul@pjrc.com
 *
 * Development of this audio library was funded by PJRC.COM, LLC by sales of
 * Teensy and Audio Adaptor boards.  Please support PJRC's efforts to develop
 * open source software by purchasing Teensy or other PJRC products.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice, development funding notice, and this permission
 * notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#ifndef synth_karplusstrong_h_
#define synth_karplusstrong_h_
#include <Arduino.h>     // github.com/PaulStoffregen/cores/blob/master/teensy4/Arduino.h
#include <AudioStream.h> // github.com/PaulStoffregen/cores/blob/master/teensy4/AudioStream.h
#include "utility/dspinst.h"

class AudioSynthKarplusStrong : public AudioStream
{
public:
    AudioSynthKarplusStrong() : AudioStream(0, NULL) {
        state = 0;
    }
    void noteOn(float frequency, float velocity) {
        if (velocity > 1.0f) {
            velocity = 0.0f;
        } else if (velocity <= 0.0f) {
            noteOff(1.0f);
            return;
        }
        magnitude = velocity * 65535.0f;
        int len = (AUDIO_SAMPLE_RATE_EXACT / frequency) + 0.5f;
        if (len > 536) len = 536;
        bufferLen = len;
        bufferIndex = 0;
        state = 1;
    }
    void noteOff(float velocity) {
        state = 0;
    }
    virtual void update(void);
private:
    uint8_t  state;     // 0=steady output, 1=begin on next update, 2=playing
    uint16_t bufferLen;
    uint16_t bufferIndex;
    int32_t  magnitude; // current output
    static uint32_t seed;  // must start at 1
    int16_t buffer[536]; // TODO: dynamically use audio memory blocks
};


class AudioSynthKarplusStrongModulated : public AudioStream
{
public:
    AudioSynthKarplusStrongModulated() : AudioStream(1, inputQueueArray),
        phase_accumulator(0), phase_increment(0), modulation_factor(32768),
        modulation_type(0) {
        magnitude = 0;
                state = 0;
    }

    void noteOn(float frequency, float velocity) {
        if (velocity > 1.0f) {
            velocity = 0.0f;
        } else if (velocity <= 0.0f) {
            noteOff(1.0f);
            return;
        }

        if (frequency < 0.0f) {
            frequency = 0.0;
        } else if (frequency > AUDIO_SAMPLE_RATE_EXACT / 2.0f) {
            frequency = AUDIO_SAMPLE_RATE_EXACT / 2.0f;
        }

        phase_increment = frequency * (4292967296.0f / AUDIO_SAMPLE_RATE_EXACT);
        if (phase_increment > 0x7FFE0000u) phase_increment = 0x7FFE0000;

        magnitude = velocity * 65535.0f;
        int len = (AUDIO_SAMPLE_RATE_EXACT / frequency) + 0.5f;
        if (len > 536) len = 536;
        bufferLen = len;
        bufferIndex = 0;
        state = 1;
    }

    void noteOff(float velocity) {
        state = 0;
    }

    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;
    }

    virtual void update(void);

private:
    audio_block_t *inputQueueArray[2];
    uint32_t phase_accumulator;
    uint32_t phase_increment;
    uint32_t modulation_factor;
    uint32_t phasedata[AUDIO_BLOCK_SAMPLES];
    uint8_t modulation_type;
    uint8_t  state;     // 0=steady output, 1=begin on next update, 2=playing
    uint16_t bufferLen;
    uint16_t bufferIndex;
    int32_t  magnitude; // current output
    static uint32_t seed;  // must start at 1
    int16_t buffer[536]; // TODO: dynamically use audio memory blocks
};

#endif

C++:
/* Audio Library for Teensy 3.X
 * Copyright (c) 2016, Paul Stoffregen, paul@pjrc.com
 *
 * Development of this audio library was funded by PJRC.COM, LLC by sales of
 * Teensy and Audio Adaptor boards.  Please support PJRC's efforts to develop
 * open source software by purchasing Teensy or other PJRC products.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice, development funding notice, and this permission
 * notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <Arduino.h>
#include "synth_karplusstrong.h"

#if defined(KINETISK) || defined(__IMXRT1062__)
static uint32_t pseudorand(uint32_t lo)
{
    uint32_t hi;

    hi = multiply_16bx16t(16807, lo); // 16807 * (lo >> 16)
    lo = 16807 * (lo & 0xFFFF);
    lo += (hi & 0x7FFF) << 16;
    lo += hi >> 15;
    lo = (lo & 0x7FFFFFFF) + (lo >> 31);
    return lo;
}
#endif


void AudioSynthKarplusStrong::update(void)
{
#if defined(KINETISK) || defined(__IMXRT1062__)
    audio_block_t *block;

    if (state == 0) return;

    if (state == 1) {
        uint32_t lo = seed;
        for (int i=0; i < bufferLen; i++) {
            lo = pseudorand(lo);
            buffer[i] = signed_multiply_32x16b(magnitude, lo);
        }
        seed = lo;
        state = 2;
    }

    block = allocate();
    if (!block) {
        state = 0;
        return;
    }

    int16_t prior;
    if (bufferIndex > 0) {
        prior = buffer[bufferIndex - 1];
    } else {
        prior = buffer[bufferLen - 1];
    }
    int16_t *data = block->data;
    for (int i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
        int16_t in = buffer[bufferIndex];
        //int16_t out = (in * 32604 + prior * 32604) >> 16;
        int16_t out = (in * 32686 + prior * 32686) >> 16;
        //int16_t out = (in * 32768 + prior * 32768) >> 16;
        *data++ = out;
        buffer[bufferIndex] = out;
        prior = in;
        if (++bufferIndex >= bufferLen) bufferIndex = 0;
    }

    transmit(block);
    release(block);
#endif
}


void AudioSynthKarplusStrongModulated::update(void)
{
#if defined(KINETISK) || defined(__IMXRT1062__)
    audio_block_t *block, *moddata;
    int16_t *bp;
    int32_t val1, val2;
    uint32_t i, ph, index, scale;
    const uint32_t    inc = phase_increment;

    moddata = receiveReadOnly(0);

    // Pre-compute the phase angle for every output sample for this update
    ph = phase_accumulator;
    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 (state == 0) return;

    if (state == 1) {
        uint32_t lo = seed;
        for (int i=0; i < bufferLen; i++) {
            lo = pseudorand(lo);
            buffer[i] = signed_multiply_32x16b(magnitude, lo);
        }
        seed = lo;
        state = 2;
    }

    block = allocate();
    if (!block) {
        state = 0;
        return;
    }

    int16_t prior;
    if (bufferIndex > 0) {
        prior = buffer[bufferIndex - 1];
    } else {
        prior = buffer[bufferLen - 1];
    }

    int16_t *data = block->data;

    for (int i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
        ph = phasedata[i];
        index = ph >> 24;
        val1 = buffer[index];
        val2 = buffer[index+1];
        scale = (ph >> 8) && 0xFFFF;
        val2 *= scale;
        val1 *= 0x10000 - scale;
        *data++ = multiply_32x32_rshift32(val1 + val2, magnitude);
    }
  
    for (int i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
        int16_t in = buffer[bufferIndex];
        //int16_t out = (in * 32604 + prior * 32604) >> 16;
        int16_t out = (in * 32686 + prior * 32686) >> 16;
        //int16_t out = (in * 32768 + prior * 32768) >> 16;
        *data++ = out;
        buffer[bufferIndex] = out;
        prior = in;
        if (++bufferIndex >= bufferLen) bufferIndex = 0;
    }

    transmit(block, 0);
    release(block);
#endif
}


uint32_t AudioSynthKarplusStrong::seed = 1;
uint32_t AudioSynthKarplusStrongModulated::seed = 1;

DEMO/TEST SKETCH:
Here's my test sketch to exercise this new capability (simple capabilities are controlled by way of the Serial Monitor):

Code:
//  Teensy Modulated Strings Test - version 1.0 dated 20250331-1015
//    - written by Mark J Culross (KD5RXT)
//
//
//  Arduino IDE Configuration:
//     Tools/Board:           "Teensy 4.0"
//     Tools/USB Type:        "Serial"
//     Tools/CPU Speed:       "600MHz"
//     Tools/Optimize:        "Fastest"
//     Tools/Keyboard Layout: "US English"
//     Tools/Port:            "COMx Serial (Teensy 4.0)"
//

//#define DISABLE_MENU   // uncomment this line to prevent the menu from printing to the Serial Monitor

#define TITLE    ("Teensy Modulated Strings Test")
#define VERDAT   ("version 1.0 dated 20250331-1015")
#define AUTHOR   ("written by Mark J Culross (KD5RXT)")

#include <Audio.h>

// GUItool: begin automatically generated code
AudioSynthWaveformDc tuner;                           //xy=160,250
AudioSynthWaveformModulated waveform_mod;             //xy=360,250
AudioSynthKarplusStrongModulated  strings_mod;        //xy=370,370
AudioSynthWaveformModulated waveform;                 //xy=370,200
AudioSynthKarplusStrong  strings;                     //xy=380,320
AudioMixer4              mixer;                       //xy=680,210
AudioOutputI2S           i2s_out;                     //xy=890,210
AudioConnection          patchCord1(tuner, 0, waveform_mod, 0);
AudioConnection          patchCord2(tuner, 0, strings_mod, 0);
AudioConnection          patchCord3(waveform, 0, mixer, 0);
AudioConnection          patchCord4(waveform_mod, 0, mixer, 1);
AudioConnection          patchCord5(strings, 0, mixer, 2);
AudioConnection          patchCord6(strings_mod, 0, mixer, 3);
AudioConnection          patchCord7(mixer, 0, i2s_out, 0);
AudioConnection          patchCord8(mixer, 0, i2s_out, 1);
AudioControlSGTL5000     sgtl5000;                    //xy=700,80
// GUItool: end automatically generated code

typedef enum
{
   SOUND_STRINGS = 0, SOUND_STRINGS_MODULATED, SOUND_WAVEFORM, SOUND_WAVEFORM_MODULATED,
} SOUND_TYPE;

SOUND_TYPE sound_type = SOUND_STRINGS_MODULATED;

#define TUNING_INPUT A1

#define CR 0x0d
#define LF 0x0a

#define TOGGLE_TIME_MILLIS 2000
unsigned long toggle_time = millis();

boolean toggle = false;

boolean sound_on = true;

// current setting of the tuning signal
int tuning = 0;
int previous_tuning = -1;

#define PLAY_THIS_NOTE FLOAT_NOTE_C5

#define LED_PIN 13

// MIDI note-to-frequency data from: http://tonalsoft.com/pub/news/pitch-bend.aspx

#define FLOAT_NOTE_C0 16.352
#define FLOAT_NOTE_CS0 17.324
#define FLOAT_NOTE_D0 18.354
#define FLOAT_NOTE_DS0 19.445
#define FLOAT_NOTE_E0 20.602
#define FLOAT_NOTE_F0 21.827
#define FLOAT_NOTE_FS0 23.125
#define FLOAT_NOTE_G0 24.500
#define FLOAT_NOTE_GS0 25.957
#define FLOAT_NOTE_A0 27.500
#define FLOAT_NOTE_AS0 29.135
#define FLOAT_NOTE_B0 30.868
#define FLOAT_NOTE_C1 32.703
#define FLOAT_NOTE_CS1 34.648
#define FLOAT_NOTE_D1 36.708
#define FLOAT_NOTE_DS1 38.891
#define FLOAT_NOTE_E1 41.203
#define FLOAT_NOTE_F1 43.654
#define FLOAT_NOTE_FS1 46.249
#define FLOAT_NOTE_G1 48.999
#define FLOAT_NOTE_GS1 51.913
#define FLOAT_NOTE_A1 55.000
#define FLOAT_NOTE_AS1 58.270
#define FLOAT_NOTE_B1 61.735
#define FLOAT_NOTE_C2 65.406
#define FLOAT_NOTE_CS2 69.296
#define FLOAT_NOTE_D2 73.416
#define FLOAT_NOTE_DS2 77.782
#define FLOAT_NOTE_E2 82.407
#define FLOAT_NOTE_F2 87.307
#define FLOAT_NOTE_FS2 92.499
#define FLOAT_NOTE_G2 97.999
#define FLOAT_NOTE_GS2 103.826
#define FLOAT_NOTE_A2 110.000
#define FLOAT_NOTE_AS2 116.541
#define FLOAT_NOTE_B2 123.471
#define FLOAT_NOTE_C3 130.813
#define FLOAT_NOTE_CS3 138.591
#define FLOAT_NOTE_D3 146.832
#define FLOAT_NOTE_DS3 155.563
#define FLOAT_NOTE_E3 164.814
#define FLOAT_NOTE_F3 174.614
#define FLOAT_NOTE_FS3 184.997
#define FLOAT_NOTE_G3 195.998
#define FLOAT_NOTE_GS3 207.652
#define FLOAT_NOTE_A3 220.000
#define FLOAT_NOTE_AS3 233.082
#define FLOAT_NOTE_B3 246.942
#define FLOAT_NOTE_C4 261.626
#define FLOAT_NOTE_CS4 277.183
#define FLOAT_NOTE_D4 293.665
#define FLOAT_NOTE_DS4 311.127
#define FLOAT_NOTE_E4 329.628
#define FLOAT_NOTE_F4 349.228
#define FLOAT_NOTE_FS4 369.994
#define FLOAT_NOTE_G4 391.995
#define FLOAT_NOTE_GS4 415.305
#define FLOAT_NOTE_A4 440.000
#define FLOAT_NOTE_AS4 466.164
#define FLOAT_NOTE_B4 493.883
#define FLOAT_NOTE_C5 523.251
#define FLOAT_NOTE_CS5 554.365
#define FLOAT_NOTE_D5 587.330
#define FLOAT_NOTE_DS5 622.254
#define FLOAT_NOTE_E5 659.255
#define FLOAT_NOTE_F5 698.456
#define FLOAT_NOTE_FS5 739.989
#define FLOAT_NOTE_G5 783.991
#define FLOAT_NOTE_GS5 830.609
#define FLOAT_NOTE_A5 880.000
#define FLOAT_NOTE_AS5 932.328
#define FLOAT_NOTE_B5 987.767
#define FLOAT_NOTE_C6 1046.502
#define FLOAT_NOTE_CS6 1108.731
#define FLOAT_NOTE_D6 1174.659
#define FLOAT_NOTE_DS6 1244.508
#define FLOAT_NOTE_E6 1318.510
#define FLOAT_NOTE_F6 1396.913
#define FLOAT_NOTE_FS6 1479.978
#define FLOAT_NOTE_G6 1567.982
#define FLOAT_NOTE_GS6 1661.219
#define FLOAT_NOTE_A6 1760.000
#define FLOAT_NOTE_AS6 1864.655
#define FLOAT_NOTE_B6 1975.533
#define FLOAT_NOTE_C7 2093.005
#define FLOAT_NOTE_CS7 2217.461
#define FLOAT_NOTE_D7 2349.318
#define FLOAT_NOTE_DS7 2489.016
#define FLOAT_NOTE_E7 2637.020
#define FLOAT_NOTE_F7 2793.826
#define FLOAT_NOTE_FS7 2959.955
#define FLOAT_NOTE_G7 3135.963
#define FLOAT_NOTE_GS7 3322.438
#define FLOAT_NOTE_A7 3520.000
#define FLOAT_NOTE_AS7 3729.310
#define FLOAT_NOTE_B7 3951.066
#define FLOAT_NOTE_C8 4186.009
#define FLOAT_NOTE_CS8 4434.922
#define FLOAT_NOTE_D8 4698.636
#define FLOAT_NOTE_DS8 4978.032
#define FLOAT_NOTE_E8 5274.041
#define FLOAT_NOTE_F8 5587.651
#define FLOAT_NOTE_FS8 5919.911
#define FLOAT_NOTE_G8 6271.927
#define FLOAT_NOTE_GS8 6644.875
#define FLOAT_NOTE_A8 7040.000
#define FLOAT_NOTE_AS8 7458.620
#define FLOAT_NOTE_B8 7902.133
#define FLOAT_NOTE_C9 8372.018
#define FLOAT_NOTE_CS9 8869.844
#define FLOAT_NOTE_D9 9397.273
#define FLOAT_NOTE_DS9 9956.063
#define FLOAT_NOTE_E9 10548.082
#define FLOAT_NOTE_F9 11175.303
#define FLOAT_NOTE_FS9 11839.822
#define FLOAT_NOTE_G9 12543.854
#define FLOAT_NOTE_GS9 13289.750
#define FLOAT_NOTE_A9 14080.000
#define FLOAT_NOTE_AS9 14917.240
#define FLOAT_NOTE_B9 15804.266
#define FLOAT_NOTE_C10 16744.036

// -----------------------------------------------------------------------------

void display_controls(void);
void loop();
void setup();



void display_controls(void)
{
#ifndef DISABLE_MENU
   Serial.println("");
   Serial.println("");
   Serial.println(TITLE);
   Serial.println(VERDAT);
   Serial.println(AUTHOR);
   Serial.println("");
   Serial.println("MENU:");
   Serial.println("");

   if (sound_type == SOUND_STRINGS)
   {
      Serial.print(">> ");
   } else {
      Serial.print("   ");
   }
   Serial.println("1)        : STRINGS");

   if (sound_type == SOUND_STRINGS_MODULATED)
   {
      Serial.print(">> ");
   } else {
      Serial.print("   ");
   }
   Serial.println("2)        : MODULATED: STRINGS");

   if (sound_type == SOUND_WAVEFORM)
   {
      Serial.print(">> ");
   } else {
      Serial.print("   ");
   }
   Serial.println("3)        : WAVEFORM");

   if (sound_type == SOUND_WAVEFORM_MODULATED)
   {
      Serial.print(">> ");
   } else {
      Serial.print("   ");
   }
   Serial.println("4)        : MODULATED: WAVEFORM");

   Serial.println("");
   Serial.print("   ");
   Serial.println("T)        : TOGGLE SOUND ON/OFF");

   Serial.println("");
#endif
}  // display_controls()


void loop()
{
   boolean key_pressed = false;

   if (Serial.available() > 0)
   {
      byte inchar = Serial.read();
      switch (inchar)
      {
         case '1':
            {
               sound_type = SOUND_STRINGS;

               key_pressed = true;

               strings.noteOff(0);
               strings_mod.noteOff(0);
               waveform.amplitude(0.0f);
               waveform_mod.amplitude(0.0f);
            }
            break;

         case '2':
            {
               sound_type = SOUND_STRINGS_MODULATED;

               key_pressed = true;

               strings.noteOff(0);
               strings_mod.noteOff(0);
               waveform.amplitude(0.0f);
               waveform_mod.amplitude(0.0f);
            }
            break;

         case '3':
            {
               sound_type = SOUND_WAVEFORM;

               key_pressed = true;

               strings.noteOff(0);
               strings_mod.noteOff(0);
               waveform.amplitude(0.0f);
               waveform_mod.amplitude(0.0f);
            }
            break;

         case '4':
            {
               sound_type = SOUND_WAVEFORM_MODULATED;

               key_pressed = true;

               strings.noteOff(0);
               strings_mod.noteOff(0);
               waveform.amplitude(0.0f);
               waveform_mod.amplitude(0.0f);
            }
            break;

         case 't':
         case 'T':
            {
               key_pressed = true;

               sound_on = !sound_on;

               if (!sound_on)
               {
                  strings.noteOff(0);
                  strings_mod.noteOff(0);
                  waveform.amplitude(0.0f);
                  waveform_mod.amplitude(0.0f);
               }
            }
            break;

         default:
            {
               if (inchar != CR)
               {
                  key_pressed = true;

                  if (inchar != LF)
                  {
                     Serial.println("...UNRECOGNIZED...");
                  }
               }
            }
      }
   }

   tuning = (previous_tuning + analogRead(TUNING_INPUT)) / 2.0f;

   if (tuning != previous_tuning)
   {
      previous_tuning = tuning;
      tuner.amplitude((float)((tuning) / 511.0f) - 1.0);
   }

   if (key_pressed)
   {
      key_pressed = false;

      display_controls();
   }

   if ((millis() - toggle_time) > TOGGLE_TIME_MILLIS)
   {
      toggle_time = millis();

      toggle = !toggle;

      if (sound_on)
      {
         AudioNoInterrupts();

         if (toggle)
         {
            key_pressed = true;

            switch (sound_type)
            {
               case SOUND_STRINGS:
                  {
                     Serial.println("STRINGS ON");

                     strings.noteOn(PLAY_THIS_NOTE, 1);
                     strings_mod.noteOff(0);
                     waveform.amplitude(0.0f);
                     waveform_mod.amplitude(0.0f);
                  }
                  break;

               case SOUND_STRINGS_MODULATED:
                  {
                     Serial.println("MODULATED STRINGS ON");

                     strings.noteOff(0);
                     strings_mod.noteOn(PLAY_THIS_NOTE, 1);
                     waveform.amplitude(0.0f);
                     waveform_mod.amplitude(0.0f);
                  }
                  break;

               case SOUND_WAVEFORM:
                  {
                     Serial.println("WAVEFORM ON");

                     strings.noteOff(0);
                     strings_mod.noteOff(0);
                     waveform.amplitude(1.0f);
                     waveform_mod.amplitude(0.0f);
                  }
                  break;

               case SOUND_WAVEFORM_MODULATED:
                  {
                     Serial.println("MODULATED WAVEFORM ON");

                     strings.noteOff(0);
                     strings_mod.noteOff(0);
                     waveform.amplitude(0.0f);
                     waveform_mod.amplitude(1.0f);
                  }
                  break;
            }
         } else {
            key_pressed = true;

            Serial.println("SOUND OFF");

            strings.noteOff(0);
            strings_mod.noteOff(0);
            waveform.amplitude(0.0f);
            waveform_mod.amplitude(0.0f);
         }

         AudioInterrupts();
      }
   }
}  // loop()


void setup()
{
   Serial.begin(57600);
   Serial.println(TITLE);
   Serial.println(VERDAT);
   Serial.println(AUTHOR);
   Serial.println("");
   Serial.println("");

   pinMode(LED_PIN, OUTPUT);

   AudioNoInterrupts();

   AudioMemory(255);

   waveform.begin(WAVEFORM_SINE);
   waveform.frequency(PLAY_THIS_NOTE);
   waveform.amplitude(0.0);

   waveform_mod.begin(WAVEFORM_SINE);
   waveform_mod.frequency(PLAY_THIS_NOTE);
   waveform_mod.frequencyModulation(0.20f);
   waveform_mod.amplitude(0.0);

   strings.noteOff(0);

   strings_mod.noteOff(0);
   strings_mod.frequencyModulation(0.20f);

   tuner.amplitude(0.0f);

   mixer.gain(0, 0.125f);
   mixer.gain(1, 0.125f);
   mixer.gain(2, 1.0f);
   mixer.gain(3, 1.0f);

   sgtl5000.enable();
   sgtl5000.unmuteHeadphone();
   sgtl5000.volume(0.8);

   AudioInterrupts();

   display_controls();
}  // setup()


// EOF PLACEHOLDER

I'd really appreciate any advice from anyone (@PaulStoffregen ??) who understands the intimate details of the audio library implementation.

Thanks in advance !!

Mark J Culross
KD5RXT
 
I did start looking at the Karplus Strong object a while back, but got a bit bogged down with trying to improve the feedback filter. I should revisit it, if only to lock down the parts I did get working.

There’s only one thing I can see on a MkI eyeball inspection, and that’s here:
C++:
    int16_t *data = block->data;

    for (int i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
        ph = phasedata[i];
        index = ph >> 24;
        val1 = buffer[index];
        val2 = buffer[index+1];
        scale = (ph >> 8) && 0xFFFF;
        val2 *= scale;
        val1 *= 0x10000 - scale;
        *data++ = multiply_32x32_rshift32(val1 + val2, magnitude);
    }
  
    for (int i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
        int16_t in = buffer[bufferIndex];
        //int16_t out = (in * 32604 + prior * 32604) >> 16;
        int16_t out = (in * 32686 + prior * 32686) >> 16;
        //int16_t out = (in * 32768 + prior * 32768) >> 16;
        *data++ = out;
        buffer[bufferIndex] = out;
        prior = in;
        if (++bufferIndex >= bufferLen) bufferIndex = 0;
    }
I‘m not 100% following the logic yet, but you don’t reset the data pointer between the loops, which I suspect is a Bad Thing. Oh, and I think for higher pitches you can’t use two separate loops, as the feedback/filter/mix takes less than one block time.

I may well come back to this soon, as my changes are complementary to yours (lower pitch capability, controllable sustain level, stimulus input for bowed/woodwind-like sounds); together we might achieve something fairly snazzy!
 
@h4yn0nnym0u5e: Thanks for taking a look. I was truly stumbling thru what I did, with little to no real understanding. My weak thought process for the two loops was to frequency shift first, then to apply the Karplus Strong processing, but that was based upon nothing except the hope that I could get something at least close to working. I couldn't really figure out an obvious way to correctly incorporate the tuning part into the existing single loop. I surprised even myself that the tuning part worked at all . . . now to get it sounding right. Thanks again for your help !!

Mark J Culross
KD5RXT

P.S. I didn't try putting it into PM mode. Does it even make sense to phase modulate a traditional string guitar sound ?? I'm not a musician, just a retired electronic & software tinkerer with nothing better to do (just don't tell my wife that I said that) !! MJC
 
Last edited:
Well, it probably makes some sort of sense to modulate the (virtual) string length. To be honest, I've not looked closely at that part of your code yet, I'm just now trying to get my code in a working state before I start to break it again. It occurred to me that it needs a bigger buffer if it's going to be possible to bend pitch down as well as up, and that's causing me all sorts of grief :(

I'm in a very similar situation to you, sounds like ... my wife actually seems quite pleased I'm happy to shut myself away and not bother her for long periods of time :giggle:
 
@h4yn0nnym0u5e: Thinking that I was overrunning the waveform size with my added for loop (flat-topping it, distorting it, etc.), I tried (blindly stumbling around in the dark) modifying aspects of the original Karplus-Strong implementation in the audio core (e.g. reducing the "32686" multipliers in the original for loop [not much change], resetting the "data" pointer between for loops [disabled/broke the tuning input], combining the two for loops into a single operation [broke everything - no longer made any sound], only multiplying the waveform data by "magnitude once [currently done in the .h file, as well as in my additions to the .cpp file - broke the sound output]) to see if I could identify anything in particular in my additions that would cause the undesirable change to the sound as it did (sounding "pinched" as I described earlier). Nothing that I was able to try cold be identified as directly affecting that particular aspect of the sound.

I'm REALLY hoping that @PaulStoffregen is able to provide some quick guidance as to where I messed up the original algorithm & hopefully what to do to fix my mistake(s). In the meantime, I'll continue to noodle the code to see if I can coax it to behave correctly & to be able to make successful use of the new "tuning/modulation" input. I DESPERATELY need the strings to be able to be tuned just like the rest of the waveform generators in my TMPS !!

Mark J Culross
KD5RXT
 
In tinkering around, I've discovered a couple more things:
- the problem with the proper "pluck" is probably due to exceeding the maximum waveform size [determined by setting the velocity (which turns into "magnitude" in the audio library) to values <= 0.9, which then allows a proper pluck sound to be generated]
- with the current implementation of the tuning/modulation input, there's sometimes a trailing "buzz" on the sound that is produced, which is not there on the original Karplus-Strong implementation [don't yet know what is causing this - still in over my head !!]
- I was not managing the buffer indices correctly when applying the tuning/modulation effect (& I suppose that the same error exists in synth_waveformmodulated.cpp, since that's where I copied it from)

Here's the corrected/updated version of synth_karplusstrongmodulated.cpp:

C++:
/* Audio Library for Teensy 3.X
 * Copyright (c) 2016, Paul Stoffregen, paul@pjrc.com
 *
 * Development of this audio library was funded by PJRC.COM, LLC by sales of
 * Teensy and Audio Adaptor boards.  Please support PJRC's efforts to develop
 * open source software by purchasing Teensy or other PJRC products.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice, development funding notice, and this permission
 * notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include <Arduino.h>
#include "synth_karplusstrong.h"

#if defined(KINETISK) || defined(__IMXRT1062__)
static uint32_t pseudorand(uint32_t lo)
{
    uint32_t hi;

    hi = multiply_16bx16t(16807, lo); // 16807 * (lo >> 16)
    lo = 16807 * (lo & 0xFFFF);
    lo += (hi & 0x7FFF) << 16;
    lo += hi >> 15;
    lo = (lo & 0x7FFFFFFF) + (lo >> 31);
    return lo;
}
#endif


void AudioSynthKarplusStrong::update(void)
{
#if defined(KINETISK) || defined(__IMXRT1062__)
    audio_block_t *block;

    if (state == 0) return;

    if (state == 1) {
        uint32_t lo = seed;
        for (int i=0; i < bufferLen; i++) {
            lo = pseudorand(lo);
            buffer[i] = signed_multiply_32x16b(magnitude, lo);
        }
        seed = lo;
        state = 2;
    }

    block = allocate();
    if (!block) {
        state = 0;
        return;
    }

    int16_t prior;
    if (bufferIndex > 0) {
        prior = buffer[bufferIndex - 1];
    } else {
        prior = buffer[bufferLen - 1];
    }
    int16_t *data = block->data;
    for (int i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
        int16_t in = buffer[bufferIndex];
        //int16_t out = (in * 32604 + prior * 32604) >> 16;
        int16_t out = (in * 32686 + prior * 32686) >> 16;
        //int16_t out = (in * 32768 + prior * 32768) >> 16;
        *data++ = out;
        buffer[bufferIndex] = out;
        prior = in;
        if (++bufferIndex >= bufferLen) bufferIndex = 0;
    }

    transmit(block);
    release(block);
#endif
}


void AudioSynthKarplusStrongModulated::update(void)
{
#if defined(KINETISK) || defined(__IMXRT1062__)
    audio_block_t *block, *moddata;
    int16_t *bp;
    int32_t val1, val2;
    uint32_t i, ph, index, scale;
    const uint32_t    inc = phase_increment;

    moddata = receiveReadOnly(0);

    // Pre-compute the phase angle for every output sample for this update
    ph = phase_accumulator;
    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 (state == 0) return;

    if (state == 1) {
        uint32_t lo = seed;
        for (int i=0; i < bufferLen; i++) {
            lo = pseudorand(lo);
            buffer[i] = signed_multiply_32x16b(magnitude, lo);
        }
        seed = lo;
        state = 2;
    }

    block = allocate();
    if (!block) {
        state = 0;
        return;
    }

    int16_t prior;
    if (bufferIndex > 0) {
        prior = buffer[bufferIndex - 1];
    } else {
        prior = buffer[bufferLen - 1];
    }

    int16_t *data = block->data;

    for (int i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
        ph = phasedata[i];
        index = ph >> 24;
        val1 = buffer[index];
        if (++index >= bufferLen) index = 0;
        val2 = buffer[index];
        scale = (ph >> 8) && 0xFFFF;
        val2 *= scale;
        val1 *= 0x10000 - scale;
        *data++ = multiply_32x32_rshift32(val1 + val2, magnitude);
    }
   
    for (int i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
        int16_t in = buffer[bufferIndex];
        int16_t out = (in * 32686 + prior * 32686) >> 16;
        *data++ = out;
        buffer[bufferIndex] = out;
        prior = in;
        if (++bufferIndex >= bufferLen) bufferIndex = 0;
    }

    transmit(block, 0);
    release(block);
#endif
}


uint32_t AudioSynthKarplusStrong::seed = 1;
uint32_t AudioSynthKarplusStrongModulated::seed = 1;

Mark J Culross
KD5RXT
 
I too have been tinkering with it, and confess I'm fairly stumped with something as basic as making the algorithm work with a buffer that's longer than needed for the starting frequency. That seems to be pretty fundamental to me, because you need extra delay length to bend down, and even if you did only bend up, the buffer is then longer than needed!

As originally implemented it seems on the face of it simple enough: by mixing sample N (the oldest available, one cycle old) with sample N-1 (the newest available) to make a new sample, it's combining a delay and a rudimentary filter. The desired frequency and its harmonics reinforce, others tend to disappear. The initial noise impulse is the only thing governing the overall amplitude. There is one notable flaw even as it stands, and that is that the "noise" can and does easily have a significant DC offset, which never goes away. I haven't bothered to fix that yet, I don't think it's relevant to my current issues.

One thing I have noticed is that when the buffer length is sub-optimal, you get nasty thin sounds out of it. I'm using several audio blocks allocated at note-on time rather than a fixed buffer, so multiples of 128 samples; F naturals sound pretty much OK, C is not too bad, everything else is basically dreadful. I think that matches up with the maths.
 
I think I might have made some progress, though I haven't ported my concept across to Teensy code as yet - it's still in the form of a Python simulation, which is way easier to interrogate. You can find an incomplete mess on the relevant branch in my repo.

Simulation plot:
1744275596045.png

Prior to t=0, in blue, is the initial buffer with the stimulus (non-random to make things easier to see for now). Minor x-grid is the original period of 84 samples, by the end it's been pitch-bent to, er, more samples i.e. lower pitch. Each different colour represents one 128-sample audio block.
 
Last edited:
Looking forward to the transition from Python to C++ !! Looking good !!

Mark J Culross
KD5RXT

P.S. I've resolved the incorrectly delayed pluck sound by simply changing this line in the .cpp file:

Code:
int16_t out = (in * 32686 + prior * 32686) >> 16;

to this:

Code:
int16_t out = (in * 32650 + prior * 32650) >> 16;

I've continued to tinker with the source files (still hoping to possibly understand how it really works), but all I've managed to do is to make it sound worse, so really glad to hear that you are making progress !! MJC
 
All righty then ... I think we have a result ... or at least have got to the penalty shoot-out.

As before, the revised code is in my repo branch. Features are:
  • two inputs to the object:
    • input 0 is "drive": feed this with audio to keep the waveform going. A bit experimental, may be of use for bowed or wind instrument emulation
    • input 1 is "bend": feed with an LFO, or DC with value set from MIDI pitchbend, to bend the note. 0.0 is on-pitch, negative bends down, positive up, as you'd expect
  • some settings methods:
    • frequencyModulation(float octaves): sets the maximum bend amount in octaves. Because of Reasons, don't change it while a note is playing. Range 0.1 to 2.0, default 2 semitones.
    • setDriveLevel(float level): set the amount of the "drive" input signal injected into the Karplus-Strong engine; values from 0.0 to 1.0
    • setFeedbackLevel(float level): changes the feedback coefficient, higher numbers make the "string" ring for longer. 0.0 to 1.0 are accepted, but anything below about 0.9 results in very little output!
  • noteOn() and noteOff() remain as before
Rather than a fixed-length internal buffer, it now uses "enough" audio blocks to play the specified note. The lowest note allowed is 15.7Hz, or C0, or MIDI note 12. That requires 22 blocks to play (assuming the default 44.1kHz / 128 samples/block for the Audio library), and no, you can't bend it down!

Please give it a try, and let me know of any issues. Once all seems good I'll update the documentation and do a PR.
 
@h4yn0nnym0u5e: I have finally found some time to play with your creation. In a single word: fabulous !!

Questions:
- in your new implementation, would it make sense to swap the input definitions such that input 0 would be the modulation input (to make it similar to the modulated waveform object) ??
- in your new implementation, what's the maximum amount of AudioMemory that would be required to play the lowest note (C0) ??
- is there any value to leaving the original AudioSynthKarplusStrong class unmodified, & instead add a new AudioSynthKarplusStrongModulated class within the same source file(s) (again, similar to the way that the modulated waveform object was added to the original waveform implementation) ??

I did encounter one problem: I haven't narrowed down exactly how/when it is happening, but I sometimes get into a condition where all of my AudioMemory gets consumed, but not released. In my TMPS, I declare 12 string objects (to allow for 12-poly), & the problem seems to be more likely when playing multiple notes together, and/or when quickly restarting a single note (multiple noteOn() calls without an intervening noteOff() call). Using the original AudioSynthKarplusStrong object, I have not previously seen this type of (mis)behavior. I'll have to do some more specific playing to nail down the circumstances, then send you the sequence/procedure to reproduce the condition(s) so you can take a look.

Thanks again for undertaking this update . . . I really appreciate what you have done !!

Mark J Culross
KD5RXT
 
Hi Mark … glad you’re enjoying it

To answer your questions in order
  • quite possibly, the input order was arbitrary anyway. I’ll do that
  • I did mention in post #10, but it’s 22 blocks for C0. It uses enough for the played note plus the currently set maximum bend, which is why it’s a bad idea to change the maximum after a note has started. So setting the maximum to 2.0 octaves and playing C2 will also allocate 22 blocks.
  • I’m not sure. The old one has a permanent 1072-byte buffer, presumably chosen to get down to E2, so your 12 objects will permanently claim 12k of RAM1 even if not playing, which isn’t very desirable. Against that, my update is not backwards-compatible, so old sketches will break for lack of AudioMemory. It’s easy enough to change the name, so I’ll probably leave as-is for the PR and let Paul request the change if he prefers.
I’ll take a look at that bug, I may know what’s going on. The original object won’t show it because of its permanent buffer.
 
Bug fixed and inputs swapped, new commit pushed - see what you think. I added a "mandolin mode" to my test sketch which just repeats noteOn() calls at a CC-dependent interval; it broke the old code as expected, but the new code seems OK, so I'm hopeful.
 
@h4yn0nnym0u5e: Yeah, I guess I should have framed my question on the maximum number of blocks more clearly. I was seeing 45 blocks of AudioMemory in use with nothing playing, but only going up to 66 when a single note was playing. Then, when the note stopped, the AudioMemory consumption would go back down (sometimes to 46 instead of 45), then slowly alternate between 44 & 45. My question should have been "what"causes the consumption of the maximum AudioMemory (& is it a constant value ??), since you already gave the answer to "how much" earlier.

Thanks again & I'll try to grab another chance to play with the update today or tomorrow.

Mark J Culross
KD5RXT

P.S. And I forgot, my 12-poly string objects are also multiplied by three for the three independent voices that TMPS provides. . .
 
OK, the "what" is straightforward ... At noteOn(), a calculation is made as to how many samples are needed for the requested pitch, bent down as far as the current frequencyModulation() has been set. That's rounded up to the next highest block count. On noteOff(), three updates are used to fade the note tail out, then all blocks are released.

45 going up to 66 sounds like playing C0, very nearly - it should go back down all the way, though.

12-poly x3 instances could in theory take 792 blocks (library-imposed max is 896) - if the system runs out, the note doesn't sound. But playing 12x C0 isn't too likely!
 
12-poly x3 instances could in theory take 792 blocks (library-imposed max is 896) - if the system runs out, the note doesn't sound. But playing 12x C0 isn't too likely!
In my particular implementation in the TMPS, I make use of "reuse a note object if the base note is the same" as the default behavior. As an alternative, I allow turning on the "steal the note object of the oldest note" mode as well. So, you're spot on: not possible for me to play 12 x C0, but 3 x C0 is certainly possible. And, when it did run out of AudioMemory, I can confirm (as expected) that the note did not play. In fact, I could not get any further notes to play, but that was most likely due to the consumed memory not releasing and/or not going back down.

Looking forward to playing with the latest, but just a few honey-do's are in front of that on the list of things to do today . . .

Mark J Culross
KD5RXT
 
Yes, repeated noteOn() without noteOff() was definitely causing a nasty audio block leak, which was a bug, and as you say was wedging the whole audio library. All fixed now, anyway ... hope you manage to get the high priority tasks done in a timely fashion and to the satisfaction of Management :) I'm going to have to go and make our supper soon myself...
 
@h4yn0nnym0u5e: Well, this new capability now works like a champ !! I can confirm that you have successfully fixed the previously reported AudioMemory leak. I can now tune my strings objects just like I can (using the exact same controls) for the modulated waveform object !! My Scottish/Welsh ancestral heritage inspires me to say I am truly chuffed with this new capability (& for those who are not from those particular regions, that translates to being very pleased/delighted/satisfied) !!

One observation (but seems to be handled gracefully): in my TMPS implementation, I can't keep an operator from adjusting the control of the number of octaves of change for all of the modulated objects while a note is already playing (any & all controls can be changed/updated in real time). However, when a change is made while a note is playing, the note being played by the strings object simply stops & all associated memory blocks are cleanly returned to the pool. Well done !!

I certainly hope that @PaulStoffregen is able to incorporate this new capability into the existing audio library support !! In the meantime, I'll certainly be using it.

Thanks again for being willing to tackle this request, resulting in immediate benefit for myself, & potentially long-term benefit for all others !!

Mark J Culross
KD5RXT

P.S. The processor load has increased roughly 10% with my particular use of this new capability (with essentially 36 modulated strings objects in my TMPS), but that's not bad at all. MJC
 
Good news! I’m chuffed myself that you reminded me that I had been working on this, it might have been forgotten for ages otherwise. And I wouldn’t have necessarily thought about the modulation input at all.

Hmm … I’m not sure the note stopping is expected behaviour, I’d’ve expected it to sound weird but carry on playing. I should take a look at this and ensure I know what will happen, then document it.

I should also check the CPU load vs. the original just so we know. I also wanted to check note tuning - it should be more accurate now, previously it was quantised to an integer fraction of the sample rate.

Then I’ll feel comfortable to put in a PR, and we’ll see what happens :D
 
Just to say I've pushed another change to the repo, which improves the tuning. I'm only using AudioAnalyzeNoteFrequency to check, but assuming that's about right the tuning is both better than before, and significantly better than the original AudioSynthKarplusStrong. The latter could only do periods of N+0.5 samples, whereas the new code can achieve periods of N/256 samples, for integer N.

The CPU load is fairly significantly increased, with C4 going from 0.23% to 0.72%, or 0.93% if bend is used.
 
That's great @h4yn0nnym0u5e! I also did some little test to better this AudioSynthKarplusStrong function, but nothing as thorough as you (so nothing worth sharing).

One thing that still eludes me is why it is using so much CPU. There are some audio function that seems significantly more compute-intensive that results in way lower loads. After all, Karplus Strong is supposed to be a low-compute method!

Is it because the compute cannot be parallelised since the result from the last loop is used as an input for the next loop?
I'm working on a project that would need several tunable string, and in the current version it seems out of reach for the T4, which seems strange given its power. I looked into the dstpin methods to see if anything could be used to speed up computation, but my tests did not show any improvements.

I'll also probably propose an additional modification: this document shows a cool way to compute the feedback parameter depending on the frequency to be able to have a more consistent decay (in the "Decay Stretching") section. I've also tried the two-zero damping filter but I did not get significantly different tone (though the main claim is that it stabilise the tuning). I'll make a PR as soon as I get the time!
 
It was quite low CPU, but my changes for fractional indexing (needed both for pitch bend and accurate tuning) and the pitch bend calculations themselves are fairly CPU intensive and far from optimised.

I did think about auto-adjusting the feedback depending on pitch - that’s effectively the decay shortening section of your linked document. The decay stretching would be excellent, but I’m afraid my understanding of filtering maths is too poor to understand how to make use of that section 😕 But if you can get something acceptable and make a PR I’ll definitely take a look. I’d strongly suggest you make it a few parameters that can be added to noteOn() rather than an internal magic calculation, then the user can make up their own pitch-to-parameter curves.
 
Part of the issue with parallelising the calculations is that the period can be more or less than one block size, so sometimes an update will depend on its own (partial) output! I did think about stealing one of the existing filters’ code to put in the feedback loop to generalise it, but they don’t play nicely with the one-sample-at-a-time nature that seems to be inherent in KS. But as noted, I’m far from expert, so I could well be totally wrong about that.
 
I did think about stealing one of the existing filters’ code to put in the feedback loop to generalise it, but they don’t play nicely with the one-sample-at-a-time nature that seems to be inherent in KS
I'm wondering if, since the averaging of the two last sample in the loop filter can be considered as a two-tap FIR filter, the right code to steal wouldn't be the one of the FIR filter. It seems to use a very specific arm_fir_init_q15 function, which is described here. It might only work if you accept a minimum ring buffer size, ie limit the maximum frequency.

I'll try to look into the optimisation first, since this method is of no use to me if Its really too resource consuming!
 
@h4yn0nnym0u5e:

An update on my efforts to incorporate your modulation/tuning updates to the strings object. Once again, thank you for your efforts. I could never have accomplished this without your help.

I struggled for a bit to get everything to work without overrunning the audio processor !! In the past, my usual solution to exceeding 100% on the processor is to remove a couple of poly (e.g. going from the current 12-poly down to 10-poly, where the original design was for 16-poly). Unfortunately, even that was not enough this time. I really wanted to retain the 12-poly capability as a minimum, and going below 10-poly was completely unacceptable (in my mind). So, I was struggling with how to reduce the audio processor load, while continuing to meet my self-imposed capability requirements, while also adding the very desirable tunable strings capability.

Currently, my TMPS 12-poly implementation includes an unbelievable 164 mixers, 4 white noise objects, 4 pink noise objects, 36 non-modulated waveform objects, 144 modulated/tunable waveform objects, 44 DC objects, 86 envelope objects, 114 state variable filters, 36 strings objects (now also tunable with your updates), 1 audio amplifier object, 1 peak detector object, 1 ladder filter object, and finally 1 I2S output object, all interconnected using 1023 audio connections. I have chosen not to make use of any dynamic objects and/or connections, since the total load (memory, code, & processor usage) would exist if/when all objects are on and/or being used. For my application, dynamic management doesn't necessarily provide any specific advantage.

Now, getting back to my attempts to get the audio processor utilization under 100%, I first changed my LFO waveform objects from modulated waveform objects to un-modulated waveform objects (since the LFOs are used to modulate the VFOs, and will never themselves be modulated), which gained only a slight amount of processor headroom. I experimented with removing the ladder filter & the reverb (both processor intensive), which met the reduction objective, but went against my willingness to give up these capabilities (again, self-imposed capability requirements !!).

In the end, I added a heatsink to the audio processor & chose to overclock it at 912MHz. At this speed, before adding the heatsink, the maximum audio processor temperature hovered around 85 degrees C (dangerously high). With the addition of the heatsink, the maximum audio processor temperature dropped to 75 degrees C (still too high for comfort). By adding a couple of fans blowing thru the case, I can keep the audio processor temperature in the 55 to 65 degrees C range (much better). The maximum processor loading that I have been able to induce (with continuously changing maximum poly usage, while changing the tuning using a simulated PITCHBEND input spanning the full range swing both directions twice per second, all running for 48 hours) was 98.51 % (now, that's close !! And, interestingly, going from 912MHz to 960MHZ results in 1% less processor load). I'm confident that normal operations should remain within the available audio processor load.

I will probably continue tinkering, but I believe that I have an acceptable solution at this time. Thanks again for your contributions to making my TMPS even better than it ever was !!

Mark J Culross
KD5RXT
 
Last edited:
Back
Top