Simple Vibrato effect

Status
Not open for further replies.

MarkT

Well-known member
I was needing a vibrato effect, and one I found in some magazine article in Circuit Cellar happened to be
broken (used a triangle wave to modulate the sample interpolation, which produces step-changes in
frequency!).

So I wrote a simple one for myself I thought it might be useful to share.

It has a limit of about 126 samples +/- for the interpolation time shift, which is internally enforced, you
just need to provide a modulation frequency and percentage frequency wobble. Latency is 2.9ms I think.

It uses naive linear interpolation (which produces some distortion, but isn't too obvious I think). I've
also been investigating higher quality methods of doing vibrato, its a interesting problem (ie tough).

There maybe a few bugs, I'm not totally happy with how it goes about allocating Audio blocks for its internal
buffer for instance, it might be simpler if it used its own internal buffer.

vibrato.h
Code:
/* Vibrato object
 * Copyright (c) 2020, Mark Tillotson
 *
 * 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 __VIBRATO_h__
#define __VIBRATO_h__

#include <Arduino.h>
#include <Audio.h>
#include <AudioStream.h>
#include "arm_math.h"


class AudioEffect_Vibrato : public AudioStream
{
public:
  AudioEffect_Vibrato(void) : AudioStream(1, inputQueueArray)
  {
    mod_phase_acc = 0 ;
    modulation (5.0, 2.0) ;  // 5Hz, 2% vibrato  (6% is a semitone)
    for (int i = 0 ; i < 3 ; i++)
      blocks[i] = NULL ;
    vibrato = NULL ;
  }

  void modulation (float hertz, float percent) ;
  
  virtual void update(void);

protected:
  int16_t sine_lookup (uint32_t phase) ;

private:
  audio_block_t * inputQueueArray[1];
  audio_block_t * blocks [3] ;
  audio_block_t * vibrato ;
  uint32_t mod_phase_acc ;
  uint32_t mod_phase_inc ;
  int32_t mod_d ;  // fixpoint 8.8 modulation depth in samples, must stay in range -126.0 -> +126.0
};

#endif

vibrato.cpp
Code:
/* vibrato effect
 * Copyright (c) 2020, Mark Tillotson
 *
 * 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 <math.h>
#include <stdint.h>
#include <Arduino.h>
#include "vibrato.h"

// data_waveforms.c
extern "C" {
extern const int16_t AudioWaveformSine[257];
}


int16_t AudioEffect_Vibrato::sine_lookup (uint32_t phase)
{
  unsigned int index = phase >> 24 ;
  int fract = (phase >> 8) & 0xFFFF ;
  int32_t v1 = AudioWaveformSine[index] * (0x10000 - fract) ;
  int32_t v2 = AudioWaveformSine[index+1] * fract ;
  return (int16_t) ((v1 + v2) >> 16) ;
}
  
#define WORD_MAX 4294967296.0


void AudioEffect_Vibrato::modulation (float hertz, float percent)
{
  if (hertz < 0.5) hertz = 0.5 ;
  if (hertz > 20.0) hertz = 20.0 ;
  if (percent > 10.0) percent = 10.0 ;

  mod_phase_inc = (uint32_t) int (round (hertz * WORD_MAX / AUDIO_SAMPLE_RATE)) ;
  float mod_depth = (percent / 100.0) * AUDIO_SAMPLE_RATE / (2 * M_PI * hertz) ;
  if (mod_depth > 126.0) mod_depth = 126.0 ;
  if (mod_depth < 0) mod_depth = 0 ;
    
  mod_d = int (round (mod_depth * (1<<8))) ;   // need a conversion factor to fix8.8
  Serial.printf ("setup vibrato, hertz %f, percent %f, inx %i, mod_d 0x%x\n", hertz, percent, mod_phase_inc, mod_d) ;
}

void AudioEffect_Vibrato::update(void)
{
  audio_block_t * new_block = receiveReadOnly (0) ;
  
  // ensure we are setup
  for (int i = 0 ; i < 3 ; i++)
  {
    if (blocks[i] == NULL)
    {
      blocks[i] = allocate () ;
      if (blocks[i] != NULL)
      {
	for (int j = 0 ; j < AUDIO_BLOCK_SAMPLES ; j++)
	  blocks[i]->data[j] = 0 ;
      }
    }
  }

  // allocate starved of blocks?  Abandon
  for (int i = 0 ; i < 3 ; i++)
    if (blocks[i] == NULL)
    {
      if (new_block) release (new_block) ;
      return ;
    }

  if (new_block == NULL)
    return ;

  // setup new one
  vibrato = allocate () ;
  if (vibrato == NULL)
  {
    if (new_block) release (new_block) ;
    return ;
  }

  
  release (blocks[0]) ;  // release oldest block
  blocks[0] = blocks[1] ;
  blocks[1] = blocks[2] ;
  blocks[2] = new_block ;  // get new block, have 4 blocks contiguous samples in the buffer.
  

  for (int i = 0 ; i < AUDIO_BLOCK_SAMPLES ; i++)
  {
    int16_t mod = sine_lookup (mod_phase_acc) ;  // modulation waveform, fix 1.15
    mod = (int16_t) ((mod_d * mod) >> 16) ;  // scale by mod depth, to yield fix8.8 sample count

    if (mod > 0x7E00) mod = 0x7E00 ;
    if (mod < -0x7E00) mod = -0x7E00 ;

    int intpart = mod >> 8 ;   // integer part of sample offset
    int fract = mod & 0xFF ;   // fractional part of sample offset, 8 bits

    int j = AUDIO_BLOCK_SAMPLES + i + intpart ;  // index into buffer

    int32_t val1 = blocks [j>>7]->data[j&0x7F] ;  // get two adjacent samples
    j += 1 ;
    int32_t val2 = blocks [j>>7]->data[j&0x7F] ;

    int32_t interpolated = (0x100 - fract) * val1 + fract * val2 ;
    vibrato->data [i] = (int16_t) ((interpolated + 0x80) >> 8) ;

    mod_phase_acc += mod_phase_inc ;   // advance modulation phase
  }

  transmit (vibrato, 0) ;
  release (vibrato) ;
}
 
I was needing a vibrato effect, and one I found in some magazine article in Circuit Cellar happened to be
broken (used a triangle wave to modulate the sample interpolation, which produces step-changes in
frequency!).

[...]

Thanks for sharing! I made something similar nearly a year ago. I needed a chorus effect, so I wrote my own modulated delay line.

Input 0 is the original signal, Input 1 it the modulation signal (sine, triangle, ???). For my own application I am also filtering the modulation signal before sending into the modulated delay (only if it's triangle). Internally I am using linear interpolation and FP-math. The code is located at https://codeberg.org/dcoredump/effect_modulated_delay.

Regards, Holger
 
It uses naive linear interpolation (which produces some distortion, but isn't too obvious I think). I've
also been investigating higher quality methods of doing vibrato, its a interesting problem (ie tough).

So to expand on this, I've been experimenting in Python looking at the spectral signature of 4 ways to do
vibrato:

(1) Apply the varying frequency direct to the oscillator(s) from the start, like FM
(2) Use a sine modulation in the time domain to index an input buffer, interpolating linearly between samples
(the usual way to implement vibrato as in the code above)
(3) As above with quadratic interpolation instead of linear, using 3 consecutive samples
(4) By taking the input samples as is and applying them as sinc() wavelets onto an output buffer, again
interpolating the position between sample points (ie evaluating sinc at the right places).

The first has to be integrated into the oscillators from the start of course...

The last is slightly different in that the sinusoidal wobble it creates is in the period of the waveform rather
than the frequency likes the other methods. For a small amount of vibrato this is insignificant.

So doing these methods and taking spectra shows the artifacts of each method (1) is theoretically perfect
already of course.
vibrato_spectra.png

Yellow is the unmodulated original signal, 5 pure tones,
green is (1)
blue is (2)
red is (3)
cyan is (4)

Clearly the sinc() method is far superior as very little artifact is generated.

The interpolation methods show worse performance for higher frequency components
of the signal.

The spectrograms make it clearer what the nature of the distortion is,
vibrato_spectrograms.png
top left is (1), top right (2)
bot left (3) and bot right (4)

The spurious signals are wobbling far more than the true part of the signal and spread across the spectrum significantly.
Quadratic interpolation performs somewhat better than linear, but both have the same weakness.

Another issue with the interpolation method is the attenuation of high frequencies, here's zoomed
in detail of the highest frequency peak, showing the blue trace (2) attenuated by 1dB or so:
vibrato_detail.png

The quadratic interpolation is much better as it loses far less amplitude on high frequencies.

To conclude the standard method of doing vibrato is a poor performer spectrally, especially for high
frequency tones, but probably sounds acceptable due to perceptual masking, though I suspect the
rising of the noise floor for a complex signal may be discernable. A much better method using
sinc() pulses should be close to ideal in performance, but involves considerable computation.
[ My test framework is using 49 points on the sinc() curve currently for each input sample, so this
would be a candidate for Teensy 4.x rather than slower processors, when really high end effects
are wanted. I may have a go at implementing this... ]
 
For my own application I am also filtering the modulation signal before sending into the modulated delay (only if it's triangle).

I think its a common misconception to use triangle wave for delay modulation - the frequency effect is the differential of the
modulation waveform, so triangle gives a square wave frequency modulation (not standard vibrato!). But sawtooth is an interesting
way to shift frequency (with artifacts!). I'm wondering if cross-fading between two such modulated delays, each getting a sawtooth
modulation signal 180 degs out of phase could make a passable pitch-shift?
 
Hi Mark,

Thanks for investigating this problem. The pictures say a lot. Can you give me an information where I can read how to use sinc()-method for this? This approach is exciting.

I think its a common misconception to use triangle wave for delay modulation - the frequency effect is the differential of the
modulation waveform, so triangle gives a square wave frequency modulation (not standard vibrato!). But sawtooth is an interesting
way to shift frequency (with artifacts!). I'm wondering if cross-fading between two such modulated delays, each getting a sawtooth
modulation signal 180 degs out of phase could make a passable pitch-shift?

I used triangle to get a sound like my good old BOSS CE-1 Chorus Ensemble. This one seems to use also a triangle waveform as modulation. The "normal" use for a vibrato is a sine waveform?

Regards, Holger
 
Hi Mark,

Thanks for investigating this problem. The pictures say a lot. Can you give me an information where I can read how to use sinc()-method for this? This approach is exciting.
Well I didn't find a source for this, but did find mention of use of sinc pulses and of spline interpolation
mentioned in passing in some article.

What I do is to vary the index to the output sample array (unlike for interpolation where the input index is varying),
and plonk a sinc waveform (truncated and windowed) down at that output sample index (which is fractional). The
property of sinc() is it is strictly band-limited so doesn't generate aliases (its related to band-limited
waveform generation with BLITs/BLEPs/etc).

In Python it looks (simplified) like:
Code:
for i in range (NSAMPS):
    mod = sin (MOD_FREQ * i*2*pi / Fs)    # generate modulating sine wave
    output_index = i - mod * MOD_DEPTH  # float valued output index

    val = inbuf[i]   # input value to be imposed on output as sinc

    j = int (output_index)  # integer part of output index
    k = output_index - j    # fractional part

    for m in range (-SINC_N, SINC_N+1):   # SINC_N = 24 currently
        s = sinc (m-k) * val
        s *= 0.5 * (1 + cos (pi * (m-k) / SINC_N))  # cosine tapering of sinc, improves performance
        outbuf [j+m] += s   # add onto output buffer
I used triangle to get a sound like my good old BOSS CE-1 Chorus Ensemble. This one seems to use also a triangle waveform as modulation. The "normal" use for a vibrato is a sine waveform?

Regards, Holger
Well that's what vocals and instruments tend to do, for instance violin string or whammy bar
A slow vibrato using triangle sounds like a police siren!
 
Had a play with triangle vibrato (for method (1) this is a square wave directly in fact as it works in the frequency domain
directly). The graphs come out thus:
vibrato_triangle_spectra.png

vibrato_triangle_spectrograms.png

Basically there's a ton more spectral pollution due to the discontinuities in frequency, but the interpolation
methods still generate substantial artifacts/spurs on top of this.
 
And now I've got the accurate sinc-based vibrato coded up (method 4 from post #4) and working.

Bad news is it currently uses about 17% cpu on T4.0, but it is looping over every sample 49 times...

I've tied it to my high-resolution FFT routine to check the spectrum direct on the T4, so here it is:
accurate_vibrato.JPG

and for comparison the simple version spectrum is:
vibrato.JPG

The vertical scale is 10dB per division, the noise floor for the simple version is way higher if you look carefully...

The files:

accurate_vibrato.h
Code:
/* Accurate Vibrato object
 * Copyright (c) 2020, Mark Tillotson, markt@chaos.org.uk
 *
 * 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 __ACCURATE_VIBRATO_h__
#define __ACCURATE_VIBRATO_h__

#include <Arduino.h>
#include <Audio.h>
#include <AudioStream.h>

#if AUDIO_BLOCK_SAMPLES != 128
#  error "Only works for AUDIO_BLOCK_SAMPLES == 128 currently"
#endif


class AudioEffectHighQualityVibrato : public AudioStream
{
public:
  AudioEffectHighQualityVibrato(void) : AudioStream(1, inputQueueArray)
  {
    mod_phase_acc = 0 ;
    modulation (5.0, 2.0) ;  // 5Hz, 2% vibrato  (6% is a semitone)
    for (int i = 0 ; i < 4 * AUDIO_BLOCK_SAMPLES ; i++)
      buffer[i] = 0.0 ;
    bptr = int (AUDIO_BLOCK_SAMPLES * 1.5) ;   // writes into buffer with max offset +/- 1.5 * 128, so 3 buffers needed + 1 for overlap
    cptr = 0 ;
    if (cos_table [0] == 0.0)
    {
      for (int m = 0 ; m <= 24 ; m++)
        if (m < 12)
	  cos_table [m] = 1.0 ;
        else
          cos_table [m] = 0.5 * (1.0 + cos ((m-12) * M_PI / 12)) ;
    }
  }

  void modulation (float hertz, float percent) ;
  
  virtual void update(void);


private:
  audio_block_t * inputQueueArray[1];
  uint32_t mod_phase_acc ;
  uint32_t mod_phase_inc ;
  float buffer [4 * AUDIO_BLOCK_SAMPLES] ;
  int bptr, cptr ;   // circular buffer indices
  float mod_depth ;  // fixpoint 8.24 modulation depth in samples, must stay in range -126.0 -> +126.0
  static float cos_table [25] ;
};

#endif

accurate_vibrato.cpp:
Code:
/* Accurate Vibrato effect
 * Copyright (c) 2020, Mark Tillotson, markt@chaos.org.uk
 *
 * 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 <math.h>
#include <stdint.h>
#include <Arduino.h>
#include "accurate_vibrato.h"


#define SINC_SAMPLES 24   // assumed in code to be even
#define BUF_MASK 0x1FF    // 4 blocks
#if AUDIO_BLOCK_SAMPLES != 128
#  error "Only works for AUDIO_BLOCK_SAMPLES == 128 currently"
#endif

#define WORD_MAX 4294967296.0

float AudioEffectHighQualityVibrato::cos_table [25] ;

void AudioEffectHighQualityVibrato::modulation (float hertz, float percent)
{
  if (hertz < 0.5) hertz = 0.5 ;
  if (hertz > 20.0) hertz = 20.0 ;
  if (percent > 10.0) percent = 10.0 ;

  mod_phase_inc = (uint32_t) int (round (hertz * WORD_MAX / AUDIO_SAMPLE_RATE)) ;
  float mod_d = (percent / 100.0) * AUDIO_SAMPLE_RATE / (2 * M_PI * hertz) ;
  if (mod_d > 166.) mod_d = 166.0 ;
  if (mod_d < 0.0) mod_d = 0.0 ;
  mod_depth = mod_d ;
    
  Serial.printf ("setup vibrato, hertz %f, percent %f, inx %i, mod_d %f\n", hertz, percent, mod_phase_inc, mod_d) ;
}


void AudioEffectHighQualityVibrato::update(void)
{
  audio_block_t * vibrato = allocate () ;
  if (vibrato == NULL)
    return ;

  audio_block_t * new_block = receiveReadOnly (0) ;

  int16_t * data ;
  if (new_block != NULL)
  {
    data = new_block->data ;
    for (int i = 0 ; i < AUDIO_BLOCK_SAMPLES ; i++)
    {
      int16_t value = data[i] ;

      float mod = mod_depth * sin (mod_phase_acc / WORD_MAX * 2 * M_PI) ;  // scale by mod depth
      //vibrato->data[i] = int (round (5000 * mod)) ;

      int intpart = int (floor (mod)) ;
      float fract = mod - intpart ;   // fractional part of sample offset
     
      if (fract == 0.0)  // perfectly aligned case, would involve div by zero if we didn't handle specially.
      {
	buffer [(bptr + intpart) & BUF_MASK] += M_PI * value ;
      }
      else
      {
	float sine = sin (- fract * M_PI) ;  // only need one sine call for every point as spaced pi apart.
	// inner loop, this is where to optimize!
	for (int m = -SINC_SAMPLES ; m <= SINC_SAMPLES ; m++)
        {
	  float sinc = value * sine / (m - fract) ;
          sinc *= cos_table [m < 0 ? -m : m] ;
	  buffer [(bptr + intpart + m) & BUF_MASK] += sinc ;  // add sinc wave samples into buffer
	  value = -value ; // alternates sign for sinc
	}
      }
      mod_phase_acc += mod_phase_inc ;   // advance modulation phase
      bptr ++ ;
    }
    release (new_block) ;
  }
  else
  {
    mod_phase_acc += AUDIO_BLOCK_SAMPLES * mod_phase_inc ;
    bptr += AUDIO_BLOCK_SAMPLES ;
  }

  // copy out a block of data from circular buffer, converting to int
  data = vibrato->data ;
  
  for (int i = 0 ; i < AUDIO_BLOCK_SAMPLES ; i++)
  {
    data[i] = (int16_t) (round (buffer [cptr] / M_PI)) ;  // rescale from sinc loop
    buffer [cptr++] = 0 ;   // re-zero for next use
  }
  cptr &= BUF_MASK ;   // wrap round buffer
  
  transmit (vibrato, 0) ;
  release (vibrato) ;
}
 
Latest version is using sinc for interpolation, seems to be a little better behaved. Have optimized for more speed and
have got to about 10% CPU on 600MHz.

The scope output on a band-limited sawtooth waveforms at about 1kHz and 15kHz looks pretty clean compared to
linear tap interpolation:
sinc_vibrato.png
lerp_vibrato.png
Both the unintended tremolo and attenuation of the higher frequency component is visible on the linear interpolation
version.

accurate_vibrato2.h
Code:
/* Accurate Vibrato object 2
 * Copyright (c) 2020, Mark Tillotson, markt@chaos.org.uk
 *
 * 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 __ACCURATE_VIBRATO2_h__
#define __ACCURATE_VIBRATO2_h__

#include <Arduino.h>
#include <Audio.h>
#include <AudioStream.h>

#if AUDIO_BLOCK_SAMPLES != 128
#  error "Only works for AUDIO_BLOCK_SAMPLES == 128 currently"
#endif

#define VIBRATO_SINC_SAMPLES      16  // assumed in code to be even
#define VIBRATO_SINC_SAMPLES_FULL 10  // the range of sinc samples with no tapering applied
// so the sinc index varies -16 -- +16, with cosine tapering applied outside the range -10 -- +10
// Reasonable compromise between speed and effect accuracy.  For better accuracy use 24 & 16
// about 10% CPU on 600MHz T4, 40% CPU on 150MHz T4

class AudioEffectHighQualityVibrato : public AudioStream
{
public:
  AudioEffectHighQualityVibrato(void) ;// : AudioStream(1, inputQueueArray) {}

  void modulation (float hertz, float percent) ;
  
  virtual void update(void);


private:
  audio_block_t * inputQueueArray[1];
  uint32_t mod_phase_acc ;
  uint32_t mod_phase_inc ;
  float buffer [4 * AUDIO_BLOCK_SAMPLES] ;
  int bptr, cptr ;   // circular buffer indices
  float mod_depth ;  // fixpoint 8.24 modulation depth in samples, must stay in range -126.0 -> +126.0
  static float cos_table [VIBRATO_SINC_SAMPLES+1] ;
};

#endif

accurate_vibrato2.cpp
Code:
/* Accurate Vibrato effect 2
 * Copyright (c) 2020, Mark Tillotson, markt@chaos.org.uk
 *
 * 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 <math.h>
#include <stdint.h>
#include <Arduino.h>
#include "accurate_vibrato2.h"



#define BUF_MASK 0x1FF    // 4 blocks
#if AUDIO_BLOCK_SAMPLES != 128
#  error "Only works for AUDIO_BLOCK_SAMPLES == 128 currently"
#endif

#define WORD_MAX 4294967296.0

#define MAX_DEVIATION (190 - VIBRATO_SINC_SAMPLES)

float AudioEffectHighQualityVibrato::cos_table [VIBRATO_SINC_SAMPLES+1] ;

AudioEffectHighQualityVibrato::AudioEffectHighQualityVibrato(void) : AudioStream(1, inputQueueArray)
{
    mod_phase_acc = 0 ;
    modulation (5.0, 2.0) ;  // 5Hz, 2% vibrato  (6% is a semitone)
    for (int i = 0 ; i < 4 * AUDIO_BLOCK_SAMPLES ; i++)
      buffer[i] = 0.0 ;
    // interpolation from buffer with max offset +/- 1.5 * 128, so 3 buffers needed + 1 for buffering
    bptr = 3 * AUDIO_BLOCK_SAMPLES / 2 ;
    cptr = 0 ;
    while (cptr < 3 * AUDIO_BLOCK_SAMPLES)  // insert new data starting at last block, with first 3 doing the tap
      buffer [cptr++] = 0 ;

    // initialize cosine taper table.
    if (cos_table [0] == 0.0)
    {
      for (int m = 0 ; m <= VIBRATO_SINC_SAMPLES+1 ; m++)
        if (m < VIBRATO_SINC_SAMPLES_FULL)
          cos_table [m] = 1.0 ;
        else
          cos_table [m] = 0.5 * (1.0 + cos ((m - VIBRATO_SINC_SAMPLES_FULL) * M_PI / \
					    (VIBRATO_SINC_SAMPLES - VIBRATO_SINC_SAMPLES_FULL))) ;
    }
}

void AudioEffectHighQualityVibrato::modulation (float hertz, float percent)
{
  if (hertz < 0.5) hertz = 0.5 ;
  if (hertz > 20.0) hertz = 20.0 ;
  if (percent > 10.0) percent = 10.0 ;

  mod_phase_inc = (uint32_t) int (round (hertz * WORD_MAX / AUDIO_SAMPLE_RATE)) ;
  float mod_d = (percent / 100.0) * AUDIO_SAMPLE_RATE / (2 * M_PI * hertz) ;
  if (mod_d > MAX_DEVIATION) mod_d = MAX_DEVIATION ;
  if (mod_d < 0.0) mod_d = 0.0 ;
  mod_depth = mod_d ;
    
  //Serial.printf ("setup vibrato, hertz %f, percent %f, inx %i, mod_d %f\n", hertz, percent, mod_phase_inc, mod_d) ;
}


void AudioEffectHighQualityVibrato::update(void)
{
  audio_block_t * vibrato = allocate () ;
  if (vibrato == NULL)
    return ;

  int16_t * data ;
  audio_block_t * new_block = receiveReadOnly (0) ;
  // copy new_block to circular buffer or zero
  if (new_block != NULL)
  {
    data = new_block->data ;
    for (int i = 0 ; i < AUDIO_BLOCK_SAMPLES ; i++)
      buffer [cptr++] = data [i] ;
    release (new_block) ;
  }
  else
  {
    for (int i = 0 ; i < AUDIO_BLOCK_SAMPLES ; i++)
      buffer [cptr++] = 0 ;
  }
  cptr &= BUF_MASK ;

  data = vibrato->data ;
  for (int i = 0 ; i < AUDIO_BLOCK_SAMPLES ; i++)
  {
      float mod = mod_depth * sin (mod_phase_acc / WORD_MAX * 2 * M_PI) ;  // scale by mod depth

      int intpart = int (floor (mod)) ;
      float fract = mod - intpart ;   // fractional part of sample offset
     
      if (fract == 0.0)  // perfectly aligned case, would involve div by zero if we didn't handle specially.
      {
        data [i] = buffer [(bptr + intpart) & BUF_MASK] ;
      }
      else
      {
	float sum = 0.0;
	/*
        float sine = sin (- fract * M_PI) ;  // only need one sine call for every point as spaced pi apart.
        // inner loop, this is where to optimize!
        for (int m = -VIBRATO_SINC_SAMPLES ; m <= VIBRATO_SINC_SAMPLES ; m++)
        {
          float sinc = sine / (m - fract) ;
          sinc *= cos_table [m < 0 ? -m : m] ;
          sum += sinc * buffer [(bptr + intpart + m) & BUF_MASK] ;  // add sinc wave samples from buffer
          sine = -sine ; // alternates sign for sinc
        }
        */

	// optimized the loop a bit, remove conditionals and value sign alternation
        float y = sin (- fract * M_PI) ;  // numerator for sinc, pre-scaled by value
        int32_t p = bptr + intpart ;
        int32_t m = -VIBRATO_SINC_SAMPLES ;  // iterate m from -SINC_SAMPLES to +SINC_SAMPLES
        float   zz = m - fract ;   // denominator for sinc
        while (true)  // loop through -ve m
        {
          float sinc = y / zz * cos_table [-m] ; // even index
          sum += sinc * buffer [(p+m) & BUF_MASK] ;
          m ++ ; zz ++ ;
          sinc = -y / zz * cos_table [-m] ;     // odd index
          sum += sinc * buffer [(p+m) & BUF_MASK] ;
          m ++ ; zz ++ ;
          if (m >= -VIBRATO_SINC_SAMPLES_FULL)
            break ;  
	}
        while (true)  // loop through -ve m
        {
          float sinc = y / zz ;
          sum += sinc * buffer [(p+m) & BUF_MASK] ;
          m ++ ; zz ++ ;
          sinc = -y / zz ;
          sum += sinc * buffer [(p+m) & BUF_MASK] ;
          m ++ ; zz ++ ;
          if (m >= 0)
            break ;   // m no longer negative
        }
        // m is zero now
        zz = -fract ;  // need the denominator properly accurate for when m == 0
        while (true)  // loop through +ve/zero m
        {
          float sinc = y / zz ;
          sum += sinc * buffer [(p+m) & BUF_MASK] ;
          m ++ ; zz ++ ;
          sinc = -y / zz ;
          sum += sinc * buffer [(p+m) & BUF_MASK] ;
          m ++ ; zz ++ ;
          if (m >= VIBRATO_SINC_SAMPLES_FULL)
            break ;
        }
        while (true)  // loop through +ve/zero m
        {
          float sinc = y / zz * cos_table [m] ; // even index
          sum += sinc * buffer [(p+m) & BUF_MASK] ;
          if (m >= VIBRATO_SINC_SAMPLES)
            break ;
          m ++ ; zz ++ ;
          sinc = -y / zz * cos_table [m] ;      // odd index
          sum += sinc * buffer [(p+m) & BUF_MASK] ;
          m ++ ; zz ++ ;
        }

	data [i] = int (round (sum / M_PI)) ;
      }
      mod_phase_acc += mod_phase_inc ;   // advance modulation phase
      bptr ++ ;
  }


  transmit (vibrato, 0) ;
  release (vibrato) ;
}
 
Hi Mark,

that's really cool! But I can't try because I currently have no T_4.x setup running. Using it on MicroDexed (on a T_3.6) would be probably reduce the polyphony to 1. I hope I can get my T_4.1 running in my dev setup during the next weeks.

Regards, Holger
 
Status
Not open for further replies.
Back
Top