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

Thread: Simple Vibrato effect

  1. #1
    Senior Member
    Join Date
    Jul 2020
    Posts
    398

    Simple Vibrato effect

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

  2. #2
    THx, will look at this!

  3. #3
    Senior Member
    Join Date
    May 2018
    Posts
    123
    Quote Originally Posted by MarkT View Post
    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

  4. #4
    Senior Member
    Join Date
    Jul 2020
    Posts
    398
    Quote Originally Posted by MarkT View Post
    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.
    Click image for larger version. 

Name:	vibrato_spectra.png 
Views:	10 
Size:	69.7 KB 
ID:	21427

    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,
    Click image for larger version. 

Name:	vibrato_spectrograms.png 
Views:	14 
Size:	337.9 KB 
ID:	21428
    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:
    Click image for larger version. 

Name:	vibrato_detail.png 
Views:	7 
Size:	44.2 KB 
ID:	21429

    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... ]

  5. #5
    Senior Member
    Join Date
    Jul 2020
    Posts
    398
    Quote Originally Posted by C0d3man View Post
    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?

  6. #6
    Senior Member
    Join Date
    May 2018
    Posts
    123
    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.

    Quote Originally Posted by MarkT View Post
    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

  7. #7
    Senior Member
    Join Date
    Jul 2020
    Posts
    398
    Quote Originally Posted by C0d3man View Post
    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!

  8. #8
    Senior Member
    Join Date
    Jul 2020
    Posts
    398
    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:
    Click image for larger version. 

Name:	vibrato_triangle_spectra.png 
Views:	8 
Size:	83.5 KB 
ID:	21430

    Click image for larger version. 

Name:	vibrato_triangle_spectrograms.png 
Views:	8 
Size:	276.6 KB 
ID:	21431

    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.

  9. #9
    Senior Member
    Join Date
    Jul 2020
    Posts
    398
    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:
    Click image for larger version. 

Name:	accurate_vibrato.JPG 
Views:	7 
Size:	192.2 KB 
ID:	21441

    and for comparison the simple version spectrum is:
    Click image for larger version. 

Name:	vibrato.JPG 
Views:	7 
Size:	208.6 KB 
ID:	21442

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

  10. #10
    Senior Member
    Join Date
    Jul 2020
    Posts
    398
    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:
    Click image for larger version. 

Name:	sinc_vibrato.png 
Views:	8 
Size:	13.4 KB 
ID:	21459
    Click image for larger version. 

Name:	lerp_vibrato.png 
Views:	11 
Size:	12.8 KB 
ID:	21460
    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) ;
    }

  11. #11
    Senior Member
    Join Date
    May 2018
    Posts
    123
    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

Posting Permissions

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