Teensy V/OCT to VCO module

Status
Not open for further replies.

ClaudioModular

New member
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!!
 
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.
 
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 ,
 
- Teensy with DAC output: does it work ? Could analogWrite of a packet of Voltage values work?
Yes.
 
- 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.
 
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!
 
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.

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.

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.

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);
}
 
Status
Not open for further replies.
Back
Top