Forum Rule: Always post complete source code & details to reproduce any issue!
Results 1 to 7 of 7

Thread: Teensy V/OCT to VCO module

  1. #1

    Teensy V/OCT to VCO module

    Hi,
    I would like to evaluate usage of Teensy with DAC on board to send a buffer of Voltages to my VCO oscillator to emulate Plucking string with Kraplus Strong Algorithm.
    I was limited using Arduino and I2C MCP4725 by I2C max frequency of 100 hz.
    I wish to have a good quality tuned string sound from my VCO.
    What is the max frequency of analogWrite to DAC pin on Teensy 3.6?
    Thanks
    Happy new year!!

  2. #2
    Member fdaniels's Avatar
    Join Date
    Oct 2020
    Location
    Ostwestfalen, Germany
    Posts
    94
    Why dont you just use the Teensy Audio Systems AudioSynthKarplusStrong Object to directly synthezide Plucked Sounds?
    The onboard DAC is capable of at least 44,1 kHz @ 12 Bits.

  3. #3
    Quote Originally Posted by fdaniels View Post
    Why dont you just use the Teensy Audio Systems AudioSynthKarplusStrong Object to directly synthezide Plucked Sounds?
    The onboard DAC is capable of at least 44,1 kHz @ 12 Bits.
    Thank you for your question, fdaniels.
    I would like to use my analog VCO of my Eurorack Modular System.
    I'm trying to realize a Pluck Karplus Strong Module using:
    - Arduino with I2C DAC: unsuccessfull because of I2C slow timing
    - Arduino with Wired DAC: may be.... Dirty signal
    - Raspberry with I2C DAC: too much latency and still slow I2C
    - Teensy with DAC output: does it work ? Could analogWrite of a packet of Voltage values work?
    - STM32F or L with DAC on board: same as Teensy? Some example are present within th CUBEIDE.
    I would prefer Teensy.....
    I know that if I have a wav file composed using Karplus Strong code the output works fine.
    I want to have CV control on a possible KS Teensy Module. (Mutable Instruments source code ...?)....
    Thanks ,

  4. #4
    Member fdaniels's Avatar
    Join Date
    Oct 2020
    Location
    Ostwestfalen, Germany
    Posts
    94
    - Teensy with DAC output: does it work ? Could analogWrite of a packet of Voltage values work?
    Yes.

  5. #5
    Senior Member+ MichaelMeissner's Avatar
    Join Date
    Nov 2012
    Location
    Ayer Massachussetts
    Posts
    3,962
    Quote Originally Posted by fdaniels View Post
    - Teensy with DAC output: does it work ? Could analogWrite of a packet of Voltage values work?
    Yes.
    The Teensy 3.1/3.2 has a single pin (A14) that can do direct analog writes of the voltage. The function analogWriteResolution controls the number of bits used for the resolution in analogWrite. The 3.2 product page has a little example of its use. There is a function to change the reference output of analogWrite, but I don't remember what it is off hand. You can change it between 3.3v, 1.2v, and the voltage on AREF which must be 3.3v or less.

    The Teensy LC has a similar DAC, but it is on pin A12 instead of A14. I recall the LC may be more limited in changing the reference voltage for analog write.

    The Teensy 3.5 and 3.6 have two DAC's (A21 and A22).

    The Teensy 2.x, 3.0, 4.0, and 4.1 do not have DAC support.

  6. #6
    Senior Member PaulStoffregen's Avatar
    Join Date
    Nov 2012
    Posts
    23,729
    Yes, analogWrite has very low latency for writing to the DAC.

    But I believe you should consider your overall approach, regardless of which board you use. With a direct write function like analogWrite(), the actual performance will depend on the timing of the rest of your code.

    To explain with a concrete example, one of the earliest examples for the DAC (many years ago, when Teensy 3.1 was just released and the audio library was still far from usable) generated a sine wave by calling the ordinary sinf() function. Simple right? Well, the result was a distorted waveform with overtones, not a pure sine. The reason why is the sinf() function takes a different length of time to compute its result. Since the code just ran a loop, if the speed changed in parts of the numerical range, the DAC wouldn't update as fast and that section of the waveform got stretched.

    Of course there are ways to deal with those problems, but you have to be careful with timing if you craft your code around a function like analogWrite().

    The audio library takes the other approach, where waveforms are synthesized at a precise sample rate (44.1 kHz) and DMA-based code streams the buffers of samples to the hardware at precisely the correct speed. This way of programming is less like "do this thing right now" and more like how you would use a modular synth... you connect audio objects together and the library makes it all run automatically.

    We have a 31 page tutorial and 45 minute walkthrough video of the entire tutorial, so you can see how this audio stuff works on Teensy. Here's the link.

    https://www.pjrc.com/store/audio_tutorial_kit.html

    While not covered in the tutorial, the audio library does have an efficient Karplus Strong synth object. You can access it in the design tool (design tool is covered in part 2 of the tutorial), like all the other audio library features. Admittedly the library's Karplus Strong doesn't have a lot of tunable parameters, but you could give into the library code and tweak it as you see (hear) fit.

    Or if you really want an analog (or otherwise external) VCO to generate the sound, you might find the various waveform and noise synth features useful, maybe with DC synth and envelop effect to create smoothly time-varying control signals for the variable filter and multiplier effect so you can modulate the gain and bandwidth of each component of your CV signal.

    Before you build the control signal code all from scratch, in a direct way where timing variation of updates to analogWrite can spoil its output, I believe you really should take a serious look at the audio library. If you're into modular synth building, the way it works should seem very familiar... once you get used to the idea you can have software running on a microcontroller work that way!

  7. #7
    Quote Originally Posted by PaulStoffregen View Post
    Yes, analogWrite has very low latency for writing to the DAC.
    But I believe you should consider your overall approach, regardless of which board you use. With a direct write function like analogWrite(), the actual performance will depend on the timing of the rest of your code.
    That's was I expected.
    Of course it's a matter of code execution timing in between.

    Quote Originally Posted by PaulStoffregen View Post
    We have a 31 page tutorial and 45 minute walkthrough video of the entire tutorial, so you can see how this audio stuff works on Teensy. Here's the link.
    I'm learning your huge Library, using PDF and Video Tutorial and web site examples and projects.
    I'm still studying Teensy and AudioLib.

    Quote Originally Posted by PaulStoffregen View Post
    Or if you really want an analog (or otherwise external) VCO to generate the sound, you might find the various waveform and noise synth features useful, maybe with DC synth and envelop effect to create smoothly time-varying control signals for the variable filter and multiplier effect so you can modulate the gain and bandwidth of each component of your CV signal.
    Yes, I use patching Karplus & Strong Pluck using VCO/NOISE/DELAY/ENVELOPE/LFO with good results and big satisfaction as well as it's patched.

    Quote Originally Posted by PaulStoffregen View Post
    Before you build the control signal code all from scratch, in a direct way where timing variation of updates to analogWrite can spoil its output, I believe you really should take a serious look at the audio library. If you're into modular synth building, the way it works should seem very familiar... once you get used to the idea you can have software running on a microcontroller work that way!
    It's very easy to build up as I already made Eurorack Synth Modules using Microcontrollers (Arduino, or Teensy or others...):
    - triggering and clocking is very easy;
    - gate and CV generators are easy, too, 0-5Volts, a little bit more complex to have a 0-10Volts signal or inverted;
    - Multi channel Drum sequencers, Euclide Rhythms, Sequencing pattern of Tuned Notes, scales by Arduino (or Teensy) programmed modules are easy, too, even with pots and buttons for regulation and selection;

    Using an Arduino+I2C MCP4725 the Karplus Strong plucking string or percussion sound is well working but the quality is not so good and the pitch is not controllable as desired.
    I saw the code of the AudioLibrary and I'm sure the waveform sound is very very good.

    My last question:
    - here following is the code for experiments with Arduino and Karplus Strong:
    I would like to use analogWrite to Teensy x.x DAC pin instead of I2C instruction
    Code:
    dac60.setVoltage(DC_Value, false);
    I have to compose the Voltage buffer using:
    Code:
    SAMPLE_T ks_iter(size_t wl)
    but it's critical to calculate the length of the buffer
    Code:
    wl = (round(volt2freq(volt)) % (BUF_SIZE - MIN_WL)) + MIN_WL;
    according to the note frequency I like to play, that's the critical point.
    For the calculation of the lenght wl it's important to calibrate the number of samples / seconds of the output buffer.
    Overwise the approach is to have a grain of few ms to repeat (cloud or grain approach to sound generation), but it's a percussive sound not a pure guitar plucked string synth of a tuned note like E,A,D or others.

    Code:
    #include <Wire.h>
    #include <Adafruit_MCP4725.h>
    #include <SimpleTimer.h>
    
    Adafruit_MCP4725          dac60; // OUT JACK
    const byte  DAC_address =  0x60;  // OUT JACK
    
    #define     VOCT_IN_PIN        A0   // IN POT
    #define     TUNE_IN_POT        A1   // IN POT
    #define     LOWER_IN_POT       A2   // IN POT
    #define     SCALE_IN_PIN       A3   // IN POT //1C mag, , 2C min, 3 min mel, 4 7th, 5 jazz, 6 Maj Pentatonic, 7 Min Pentatonic
    #define     CLOCK_BMP          A4   // IN POT
    #define     CLOCK_PERCENTAGE   A5   // IN POT
    #define     TUNE_FROM_MCP4725  A6
    #define     TRIGGER_IN_PIN      7   // IN JACK
    #define     CLOCK_LED          13   // OUT JACK
    
    int         CLOCK_INOUT; 
    
    // CONFIGURATION
    int         usescale=1;       // 1=use scale selecting by pot SCALE_IN_PIN 0 = use pots -1= use input from MCP4725
    int         random_mysound=0; // 1=random in scale; 0=use pots VOCT_IN_PIN & TUNE_IN_POT
    int         internal_clock=2; // 1=external trigger 2=internal clock by pots 3=random clock
    
    #define     MIN_BPM            50    
    #define     MAX_BPM           440
    #define     MIN_PERCENTAGE      0
    #define     MAX_PERCENTAGE     90
    
    #define     CALIBRATE_MAX_POT 890
    
    #define     OCT_RANGE          10
    
    #define     VOCT_SCALE        1.0
    #define     KNOB_SCALE        1.0
    #define     BASE_OCTAVE         8
    #define     NB_SMOOTHING        5
    
    int         myscale;
    int         old_myscale;
    int         mysound;
    int         old_mysound;
    int         pot_clockINOUT;
    
    SimpleTimer timer;
    int         count = 0;
    int         input_BMP;
    int         input_PERCENTAGE;
    
    float       readings[NB_SMOOTHING];
    int         readIndex = 0;
    float       total = 0;
    float       average = 0;
    
    float       freq;
    float       volt;
    bool        is_sustaining = false;
    int         sustain_length = 4000;
    int         sustain_count = 0;
    bool        trigger_value = false;
    bool        last_trigger_value = false;
    int         last_led_value = 0;
    int         mynote=0;
    bool        trigger = false;
    
    int         numnotes;
    int         myscalenotes[7];
    
    bool        started = false;
    int         priority = 0;
    int         tapX = 0;
    int         tapactual;
    int         tap_time;
    int         time_actual;
    int         input1X = 0;
    float       BPM; 
    int         max_BPM = 500;
    int         min_BPM = 10;
    int         max_time = ((1/(min_BPM/60)) * 1000);
    int         min_time = ((1/(max_BPM/60)) * 1000);
    int         c;
    
    static const uint32_t DAC_CAL = 6826;
    
    
    #define   SAMPLE_T  int16_t
    #define   ZERO  2047 
    #define   BUF_SIZE  64 // 64
    SAMPLE_T  wav[BUF_SIZE];
    #define   MIN_WL    5
    #define   NOISE_L   24
    
    size_t    ks_wptr; // position in wavetable
    SAMPLE_T  ks_prev; // previous sample
    size_t    wl;
    
    void updateCV(uint32_t DC_Value) {
      dac60.setVoltage(DC_Value, false); 
    }
    
    void ks_init(size_t wl) {
      // initialize wavetable to short noise burst
      size_t i;
      for(i = 0; i < wl; i++) {
        if(i < NOISE_L) 
        { // short burst of noise
          wav[i] = (SAMPLE_T)(rand() & 0x0FFF) - ZERO;
        } 
        else 
        {
          wav[i] = 0;
        }
      }
      // initialize wavetable pointer
      ks_wptr = 0;
      // initialize filter
      SAMPLE_T ks_prev = wav[wl-1];
    }
    
    SAMPLE_T ks_iter(size_t wl) {
      // retrieve sample value from buffer
      SAMPLE_T v = wav[ks_wptr];
      // now filter the sample
      wav[ks_wptr] = (ks_prev / 2) + (v / 2);
      ks_prev = v;
      // recur
      ks_wptr = (ks_wptr + 1) % wl;
      // return output sample
      return v;
    }
    
    float smooth(float value_to_smooth){
      total = total - readings[readIndex];
      readings[readIndex] = value_to_smooth;
      total = total + readings[readIndex];
      readIndex = readIndex + 1;
      if (readIndex >= NB_SMOOTHING) {
        readIndex = 0;
      }
      average = total / NB_SMOOTHING;
      return average;
    }
    
    float volt2freq(float volt){
      return 440 / pow(2, 4.75) * pow(2, min(volt, OCT_RANGE) + BASE_OCTAVE);  // 440/2^4.75 * 2^(min(volt e 10)+8)
    }
    
    float quantize(float volt){
      return round(volt * 12) / 12.0; // perchŔ le mie note sono giÓ calibrate 
    }
    void change_scale(int myscale) //1C mag, , 2C min, 3 min mel, 4 7th, 5 jazz, 6 Maj Pentatonic, 7 Min Pentatonic
    {
      if (myscale==1)
      {
        numnotes=7;
        myscalenotes[0]=0;    // C3
        myscalenotes[1]=150;  // D3
        myscalenotes[2]=310;  // E3
        myscalenotes[3]=390;  // F3
        myscalenotes[4]=560;  // G3
        myscalenotes[5]=720;  // A3
        myscalenotes[6]=890;  // B3
        myscalenotes[7]=965;  // C4
      }  
      else if (myscale==2)
      {
        numnotes=7;
    ...... 
      }
      else if (myscale==3)
      {
        numnotes=7;
    .....   
      }  
      else if (myscale==4)
      {
        numnotes=7;
    ....
      }
      else if (myscale==5)
      {
        numnotes=6;
    ....
      } 
      else if (myscale==6)
      {
        numnotes=5;
    ....
      }
      else if (myscale==7)
      {
        numnotes=5;
    ....
      } 
    }
    void input_pots()
    {
      if (internal_clock==1)
      {
      }
      else if (internal_clock==2)
      {
        input_BMP = analogRead(CLOCK_BMP);
        input_PERCENTAGE = analogRead(CLOCK_PERCENTAGE);  
      }
      else if (internal_clock==3)
      {
        input_BMP=random(MIN_BPM,MAX_BPM);
        input_PERCENTAGE = random(MIN_PERCENTAGE,MAX_PERCENTAGE);
      }
    }
    void cycle_off() 
    {
       trigger = false;
       digitalWrite(CLOCK_LED, LOW);
    }
    void cycle_on() {  
      trigger = true;
      digitalWrite(CLOCK_LED,HIGH);
    
      input_pots();
      BPM = map(input_BMP, 0, CALIBRATE_MAX_POT, MIN_BPM, MAX_BPM);
    
      float duration_percentage =  map(input_PERCENTAGE, 0, CALIBRATE_MAX_POT, 1, 90);
      int cycletime = (60000/BPM);
      float cycle_start = cycletime;
      float cycle_stop = (cycletime * (duration_percentage/100));
      timer.setTimeout(cycle_start, cycle_on);
      timer.setTimeout(cycle_stop, cycle_off);
    }
    
    void setup() {
      pinMode(CLOCK_LED, OUTPUT);
      pinMode(TRIGGER_IN_PIN, INPUT);
      pinMode(LOWER_IN_POT, INPUT);
      pinMode(TUNE_IN_POT, INPUT);
      pinMode(VOCT_IN_PIN,INPUT);
      pinMode(CLOCK_LED,OUTPUT);
      Serial.begin(9600);
      dac60.begin(0x60);
      change_scale(5);
      old_myscale=5;
      mynote=0;
      input_BMP=random(MIN_BPM,MAX_BPM);
      input_PERCENTAGE = random(MIN_PERCENTAGE,MAX_PERCENTAGE);
      wl=1;
    }
    
    void loop() {
      int pot_scale=analogRead(SCALE_IN_PIN);
      int myscale = map(pot_scale, 0, CALIBRATE_MAX_POT, 1, 7); 
      if (myscale!=old_myscale)
      {
        change_scale(myscale);
        old_myscale=myscale;
      } 
    
      if (internal_clock==1)
      {
        trigger_value = digitalRead(TRIGGER_IN_PIN) == HIGH;
        sustain_length = (4095.0 * analogRead(LOWER_IN_POT)/CALIBRATE_MAX_POT*KNOB_SCALE);
        if (is_sustaining && sustain_count < sustain_length) 
        {
          updateCV(ks_iter(wl) + ZERO); // ZERO=2047
          sustain_count = (sustain_count + 1) % 100000; // 5%7=2 7%5=5
        } 
        else 
        {
          is_sustaining = false;
        }
        if (last_trigger_value == trigger_value) 
        {
          if (trigger_value) 
          {
            last_led_value -= 1;
            if (last_led_value <= 0) 
            {
              digitalWrite(CLOCK_LED, LOW);
            }
          }
          return;
        }
        last_trigger_value = trigger_value;
        if (!trigger_value) 
        {
          last_led_value = 0;
          digitalWrite(CLOCK_LED, LOW);
          return;
        }    
      }
      else if (internal_clock==2)
      {    
        if (!started) 
        {
            cycle_on();
            started = true;
        }
        timer.run();
        time_actual=millis();    
    
        trigger_value = trigger;
        sustain_length = (4095.0 * analogRead(LOWER_IN_POT)/CALIBRATE_MAX_POT*KNOB_SCALE);
        if (is_sustaining && sustain_count < sustain_length) 
        {
          updateCV(ks_iter(wl) + ZERO); // ZERO=2047
          sustain_count = (sustain_count + 1) % 100000; // 5%7=2 7%5=5
        } 
        else 
        {
          is_sustaining = false;
        }
        if (last_trigger_value == trigger_value) 
        {
          if (trigger_value) 
          {
            last_led_value -= 1;
            if (last_led_value <= 0) 
            {
              digitalWrite(CLOCK_LED, LOW);
            }
          }
          return;
        }
        last_trigger_value = trigger_value;
        if (!trigger_value) 
        {
          last_led_value = 0;
          digitalWrite(CLOCK_LED, LOW);
          return;
        }
      }
      else if (internal_clock==3)
      {
        if (!started) 
        {
            cycle_on();
            started = true;
        }
        timer.run();
        time_actual=millis();        
    
        trigger_value = trigger;
        sustain_length = (4095.0 * analogRead(LOWER_IN_POT)/CALIBRATE_MAX_POT*KNOB_SCALE);
        if (is_sustaining && sustain_count < sustain_length) 
        {
          updateCV(ks_iter(wl) + ZERO); // ZERO=2047
          sustain_count = (sustain_count + 1) % 100000; // 5%7=2 7%5=5
        } 
        else 
        {
          is_sustaining = false;
        }
        if (last_trigger_value == trigger_value) 
        {
          if (trigger_value) 
          {
            last_led_value -= 1;
            if (last_led_value <= 0) 
            {
              digitalWrite(CLOCK_LED, LOW);
            }
          }
          return;
        }
        last_trigger_value = trigger_value;
        if (!trigger_value) 
        {
          last_led_value = 0;
          digitalWrite(CLOCK_LED, LOW);
          return;
        }    
      }
      
      if (usescale==1)
      {
        if (random_mysound==1)
        {
          mysound=random(0,numnotes);      
        }
        else
        {
          mysound=map(analogRead(VOCT_IN_PIN),0,CALIBRATE_MAX_POT,0,numnotes);
        }
        if (mysound!=old_mysound)
        {
          if (mysound>numnotes)
          {
            mysound=0;
          }
          float myvolt=myscalenotes[mysound]/4096.0*5.0;
          volt=(1.0*analogRead(TUNE_IN_POT)/CALIBRATE_MAX_POT*KNOB_SCALE)+(myvolt/204.0*VOCT_SCALE);
          old_mysound=mysound;
        }
      }
      else if (usescale==0)
      {
        volt = (1.0 * analogRead(TUNE_IN_POT)/CALIBRATE_MAX_POT*KNOB_SCALE) + (analogRead(VOCT_IN_PIN)/204.0*VOCT_SCALE);
      }
      else if (usescale==-1)
      {
        volt = (map(analogRead(TUNE_FROM_MCP4725),0,CALIBRATE_MAX_POT,0,5));
      }
      // 0.00 <-> 1.15 + 0.00 <-> 5.1 ==> 0.<->6.16
      volt = smooth(volt);
      volt = quantize(volt);   
      // silence
      updateCV(ZERO); // SILENCE ZERO=2046
      // choose wavelength
      wl = (round(volt2freq(volt)) % (BUF_SIZE - MIN_WL)) + MIN_WL; // wl=(round() % (64-5))+5; wl=(round() % 59)+5
      // initialize Karplus-Strong algorithm with this wavelength
      ks_init(wl);
      is_sustaining = true;
      sustain_count = 0;
      last_led_value = 30;
      digitalWrite(CLOCK_LED, HIGH);
    }

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •