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

Thread: Can i modulate the delay time?

  1. #1

    Can i modulate the delay time?

    Hi, was hoping to implement a crude emulation of the old oil-can delay effect using a digital delay line with two taps and a configurable bandpass filter feedback loop.

    I was wondering if I can modulate the delay time with an LFO to simulate the 'wobbliness' of the original oil can effect or whether that's likely to introduce zipper artifacts. I guess the other question is how do i introduce wow into a signal (i.e. a time-varying playback-speed effect). I mean it's easy with an oscillator- just add LFO FM but how do you do it in a delay?

    Suggestions anyone? I'd love to have something like this to play with.

    Dirk

  2. #2
    Senior Member
    Join Date
    Oct 2015
    Location
    Vermont, USA
    Posts
    288
    The short answer (which is not helpful) is that you can do anything you want...but it could take a lot of development and effort.

    The longer answer is that you could modify the delay class to allow for better modulation of the delay time. It would have a cool sound. But, it would sound like a non-professional late-80s digital effects processor (for good or for ill). I don't think that it would sound like an oil can (or tape) wow and flutter.

    To actually do what you want, you need to model the fact that the audio is read from the can (or tape) at slightly different speed than the speed when it was written. The only way that I can think to do this is by resampling in real time...and sampling at smoothly changing non-integer ratios so as to model the smoothly changing wow and flutter. This seems hard to do.

    Since people have implemented this wobbly sound in delay pedals before (strymon), there must be a simpler way. I'd love to hear it!

    Chip

  3. #3
    Senior Member
    Join Date
    Oct 2015
    Location
    Vermont, USA
    Posts
    288
    If you use a regular delay with a fixed dealt time (like is currently in the library) you would get a more 'analog-style' delay if you could smoothly vary / modulate the sample rate of the whole system. That gets you the effect of the playback speed being different than the speed when the audio was recorded.

    But, the default teensy library is set for a constant sample rate. There are definitely posts here on how to change the sample rate (I've used those posts myself). You could use that info to try this.

    Note that those posts are aimed at big changes in sample rate. You only need small changes. A few percent up and down? You'll have to dial those examples way back.

    It's possible that you won't be able to change the sample rate in fine enough increments to sound smooth.

  4. #4
    Senior Member PaulStoffregen's Avatar
    Join Date
    Nov 2012
    Posts
    23,980
    Signal controlled delay (or voltage controlled delay if you like to think in modular synth terms) is on a long list of audio library features I hope to implement this summer. The reality is I've not managed to do much on the audio library for the last 2 years while making Teensy 4.0 & 4.1. But once we have 1.53 released, the bootloader chip for sale, and a start on 1.54 (mainly better file & filesystem support), I really want to build many of these long-requested features like signal modulated delay.

  5. #5
    Yea, i had a look at the delay object in the audio library and the delay was configurable for each tap, but not modulatable. We're also short of a pitch shifter/playback speed object. And I'm no whiz at DSP. The design of a modulated delay is rather beyond me.

    Now filesystems I can help with because i've ported the whole of the ESP8266 FS and File objects to work with SDFat-beta and LittleFS for a drum synth with automatic caching in flash RAM.

  6. #6
    Senior Member PaulStoffregen's Avatar
    Join Date
    Nov 2012
    Posts
    23,980
    Quote Originally Posted by dirkenstein View Post
    We're also short of a pitch shifter/playback speed object.
    The wavetable synth has pitch shifting of its samples.

  7. #7
    Forgive the revival of a dead thread, but I've been looking into this a little and am hitting a roadblock --- specifically for adding doppler shifts when modifying delay times (that classic tape-like pitch modulation when quickly changing delay time).

    If you use a regular delay with a fixed dealt time (like is currently in the library) you would get a more 'analog-style' delay if you could smoothly vary / modulate the sample rate of the whole system. That gets you the effect of the playback speed being different than the speed when the audio was recorded.
    I've been able to successfully reproduce and solve this problem in Max using gen~ (another synchronous environment) by introducing linear interpolation and feedback (e.g. lowpassing) on the modulation input. This slow low passed signal on the delay time input hits exactly on the mark the pitch-modulated shift effect that I'm looking for --- quick changes are ignored; the rate slowly climbs to its desired value over a much longer time. Clearly doesn't work the same way using the Teensy audio's effect_delay time parameter; the sampling rate is unaffected.

    I've seen posts about modifying the I2S sample rate, but what about a more general approach or workaround for those not using I2S inputs (like me)? I've never tried modifying these speeds during runtime, so this is new territory for me. Any pointers in the right direction, or suggestions for other implementations (like modeling this effect with the granular or wavetable objects) most appreciated.

  8. #8
    Senior Member PaulStoffregen's Avatar
    Join Date
    Nov 2012
    Posts
    23,980
    Sadly, a very long list of audio features I was planning to write this year didn't happen due to the pandemic (or more specifically, due to PJRC running short-staffed due to social distancing requirements). A signal controlled delay was near the top of that list.


    I've seen posts about modifying the I2S sample rate, but what about a more general approach or workaround for those not using I2S inputs (like me)?
    It really depends on which board and which inputs & outputs you are using.

    But the main theme is smooth adjustment of the audio sample rate is only possible on Teensy 4.x. On Teensy 3.x, the sample rate is created by various ways which are all integer division of the system clock.


    I've never tried modifying these speeds during runtime, so this is new territory for me.
    Until just a couple weeks ago, nobody tried it, because NXP's documentation said it wasn't possible. But it turns out you can write to the PLL4 numerator and denominator registers while it's running, and indeed that does make minor changes to the clock which becomes the sample rate.


    Any pointers in the right direction, or suggestions for other implementations (like modeling this effect with the granular or wavetable objects) most appreciated.
    The answer is it depends.

    The nature of your sound source is the biggest factor. The hardest is changing live sound like a microphone. The easiest is when everything is being synthesized. Playback of pre-recorded sounds falls somewhere in the middle of those 2 extremes.

    It also depends on whether you intend to get into the actual DSP coding of audio processing objects in the library, or just use the stuff which is already written, with maybe minor hacks like messing with the PLL4 settings.

  9. #9
    I did some (very fun!) exercises in approximating the effect I'm looking for without modifying the sample rate, using existing objects.
    When playing with tape delays, it seems that the amount of pitch shift has to do with the delay parameter's rate of change rather than the absolute value. This makes intuitive sense when you think about yanking a piece of tape on a play head, or quickly scratching a record.

    I think it sounds... not bad! The granular object adds some imperfections, but the concept works pretty well. Any feedback much appreciated --- still fine-tuning response times and pitch ranges, but it's a nice first step towards a simple implementation. If anyone wants to make changes or collaborate, be my guest.

    Here's how it sounds:
    https://soundcloud.com/ro-web/dopple.../s-uo4ppqFzRAy

    It uses houtson's excellent effect object tapeDelay10tap, which has smooth modulation of delay time (no pitch modulation). It can be found here (thanks houtson!): https://forum.pjrc.com/threads/62739...ging-tap-times

    It's implemented like this (with delay1 being TapeDelay10tap and not the default teensy delay object):

    Click image for larger version. 

Name:	Screen Shot 2020-12-29 at 16.11.14.png 
Views:	43 
Size:	54.3 KB 
ID:	23024


    Code:
    #include <Audio.h>
    #include <Wire.h>
    #include <SPI.h>
    #include <SD.h>
    #include <SerialFlash.h>
    
    AudioEffectTapeDelay10tap delay1;
    
    AudioInputUSB            usb1;
    AudioEffectGranular      granular1;
    
    AudioOutputI2S           i2s2; //unconnected
    AudioOutputUSB           usb2;
    
    AudioMixer4              mixer1;
    AudioMixer4              mixer2;
    AudioMixer4              mixer3;
    
    AudioConnection patchCord10(usb1, 0, mixer3, 0);      //input stereo to mono
    AudioConnection patchCord11(usb1, 1, mixer3, 1);
    AudioConnection patchCord12(mixer3, 0, granular1, 0);
    AudioConnection patchCord1(granular1, 0, mixer1, 0);  //granular object into delay input mixer
    AudioConnection patchCord3(mixer1, delay1);
    AudioConnection patchCord4(mixer3, 0, mixer1, 2);   //dry signal to delay (no granular)
    AudioConnection patchCord5(delay1, 0, mixer2, 0);
    //AudioConnection patchCord6(delay1, 1, mixer2, 1);   //extra delay taps (unused)
    //AudioConnection patchCord7(delay1, 2, mixer1, 2);
    AudioConnection patchCord8(mixer2, 0, usb2, 0);
    AudioConnection patchCord9(mixer2, 0, usb2, 1);
    AudioConnection patchCord13(mixer2, 0, mixer1, 1);    //delay feedback
    
    
    ////////////////////////midi
    
    #if defined(USBCON)
    #include <midi_UsbTransport.h>
    static const unsigned sUsbTransportBufferSize = 16;
    typedef midi::UsbTransport<sUsbTransportBufferSize> UsbTransport;
    UsbTransport sUsbTransport;
    MIDI_CREATE_INSTANCE(UsbTransport, sUsbTransport, MIDI);
    #endif
    #include <MIDI.h>
    ///
    
    
    #define GRANULAR_MEMORY_SIZE 12800  // enough for 290 ms at 44.1 kHz
    int16_t granularMemory[GRANULAR_MEMORY_SIZE];
    
    #define DELAY_MAX_LEN 22050  // buffer for samples @44100 samples per second, 22050 = 0.5s
    int16_t sample_delay_line[DELAY_MAX_LEN] = {};
    
    float response = 0.9; //this sets the response time of the delay/pitch slider. higher value = slower response.
    
    int16_t dtime = 100;
    float itime = 3.0;
    float ptch = 100.0;
    
    int fvalue;                       //for midi control
    double mvalue;
    void doControlChange(byte channel, byte control, byte value) {
      mvalue = (double)value/127.0;
      
      if(control==1){                               //if new midi control signal is received, update delay time and pitch
        fvalue = (int16_t)map(value, 0,127,0,500); //midi cc is 0-127
        dtime = fvalue;
        delay1.delayfade(0, dtime, itime);                
        int tptch = (int16_t)map(value, 0,127,100,8000); //arbitrary values, to be fine-tuned
        ptch = tptch/100.0; 
        }
      
      if(control==2){                             //feedback
        mixer1.gain(1, mvalue);
      }
    }
    
    void setup(void) {
      Serial.begin(9600);
      
      usbMIDI.begin();
      usbMIDI.setHandleControlChange(doControlChange);
    
      AudioMemory(150 * (128 / AUDIO_BLOCK_SAMPLES));
    
      mixer1.gain(0, 0.7);
      mixer1.gain(1, 0.0);
    
      //still need to set mixer gains for delay taps (there are currently three connected, only one in use)
    
      // initialise the delayline
      
        delay1.begin(sample_delay_line, DELAY_MAX_LEN);
        delay1.delayfade(0, dtime, itime);                
    
        granular1.begin(granularMemory, GRANULAR_MEMORY_SIZE);
    
        granular1.beginPitchShift(290.0); // can experiment with longer (this is set once in setup)
    }
    
    
    
    void loop(void) {
      usbMIDI.read();
      rateofchange();
    }
    
    
    elapsedMillis oTime;
    void updatei(){
      if (oTime > 30) {
        dtime = lowpass_d_d(fvalue);
        delay1.delayfade(0, dtime, itime);
        oTime=0;
      }
    }
    
    elapsedMillis rTime;
    float x = 0.0;
    float y = 0.0;
    float pitchShift;
    
    void rateofchange(){
      if (rTime > 30) {
          y = ptch;
          float rateOfChange = (x - y);
          rTime=0;
          x = ptch;
          pitchShift = lowpass_d_d(rateOfChange);
          if (pitchShift < 0.1 && pitchShift > -.1){                    //the granular effect introduces some subtle stuttering when passing audio. when there's no effect, pass dry audio.
            //granular1.stop();
            mixer1.gain(0,0.0); //mute granular effect
            mixer1.gain(2,1.0); //pass dry audio
          }
          else {
            mixer1.gain(0,1.0); //enable granular effect
            mixer1.gain(2,0.0); //mute dry audio
          granular1.setSpeed(pitchShift+1.0); //right now there is a big negative swing (which is approx -3 to 3 when its quick) which should be offset to 0.1 to 8
          }
          //Serial.print("\t");
          Serial.print(pitchShift);
          //Serial.print("\t");
          //Serial.println(rateOfChange);
      } 
    }
    
    float LPhistory;
    float LPout;
    float lowpass_d_d(int input) {                            //this is a simple interpolating lowpass filter to slow down the response of the pitch/delay slider
        float LPhistory = (input + (response * (LPout - input)));
        LPout = LPhistory;
        return LPout;
    }

    Note that I am not using the teensy audio shield --- I test patches by laying out the hardware interface in Max, with each parameter sending MIDI to teensy, then route USB audio in and out. Here is a very simple max patcher to test the delay time and feedback parameters (shouldn't require a license to open).
    Modifying this patch to work on the audio shield should be very simple --- just update the contents of doControlChange to read a pot instead.
    Code:
    ----------begin_max5_patcher----------
    826.3ocuWszbaBCD9r8uBM5Lkfdhn2xL8RNja8VZmLXrbpR.IOfHwoYx+8JI
    .GmX2XriSXFiFVj1uc+1Gr9ooSfyLqjMPv2AWAlL4ooSlDD4ELo+4Ivp7UEk
    4MgsAKLUURsEF08NqbkMHegTNeVdwcCuP2VozkRa3TndgKysE+Qou45ZYgsC
    UTFONIBfvjvBKwufc2A+9EMYZsCpJoWpZd.Vyra+FF5E87zo9aQeP+3Ao8r4
    0ONHeYsrwssbqxn2vpyPYwhTJAK3H2MDlEAD3rXNIIgyxPoYbQpSFmFy8WTm
    +wHLDiusyMNZhkjEynnDmJ7fkQIwYLgfiIIzNrNJtCkf2kqFrlSHqVaLUfF0
    ek+RuGlUP3wD2EBmIDqY1zsjw6DgEXZZRZh6cQ.B8XXVBhGyvoDJIMPrNkr9
    odN8sJdObJleJ4zVGi1NnvEFscQdgbSnGqmRDowdehhCKLQHcg7ecMxfqYkU
    88HfVC3x7U.DLB.i725EfWK3mRot4QvkW7iKN6714JCbs58FuOIHXm958Nwc
    PZeborGDkOCB.C+Ve3MHX5K4r404URqr9ZoNeV4qnEOZZ2aCG67ZUd4wzmPK
    evA4VIzUp4JkdgYms6v6OFfR7KL1dBAnCj41McQfmTJoR1zjeibKNovo8ZSY
    ort4HoEesWDHM4KgUnvuhDkBaoyd.XeIyN3Dx60xGG58jf7Krz.ofOnOLxOF
    GooTMWVum1zXdvpxxF9jiuuxw8gsfigRXuil1QrebAYb5X5TbJ5R+9geDfbn
    g+Odzm7oE8QDRlevlTpXX5CBi8QSE573OqLA5XxDtSalUXJM0cpJXCtN0LbF
    EgR4tQMbyb4EwwoT2HYhDRF1kB6rzMryOyIorpJ4gNisPrwH1D5QLkHayTov
    4fkJ8a+mCAn8xese0XZqKFBNCUkfWfetrwpzqYqqV27JrocxciFI5XPhbBPx
    OShqLeb9zPlasuTyS4eLnGiSROINIYLHg+xnSxtoSzVIq4KWduarj9iGP0Ut
    caWctHJ7nR28XH+GVKuWMr+vfKv7ZWck0UT0V2MpvJdWOEXkwArtU0isyecP
    FJk8iUzrred8PE+zmm9Ozk+iBB
    -----------end_max5_patcher-----------

  10. #10
    Been having fun with this pseudo-version in the past few days - if anyone is interested in an updated version, feel free to PM.

    I'm thinking now about how this could be implemented within the delay object.
    Stepping through logically --- with a fixed sample rate, stepping through the numbers 0-9 should take 10 samples. Only playing the even numbers should take half the time to step through the entire set, meaning it will sound “higher pitch”. The way to recreate this doppler pitch modulation might be to effectively "skip" samples in the audio queue when shortening delay time / speeding up, and "loop" samples when lengthening / slowing down. The number of samples to skip or loop would depend on the rate of change of the parameter, akin to pulling on a piece of tape.
    Given an audio queue long enough, there will always be samples to skip (or loop).

    I haven't modified audio objects within the teensy library yet, but it looks doable. Before I get started, are there flaws in this approach?

  11. #11
    Senior Member houtson's Avatar
    Join Date
    Aug 2015
    Location
    Scotland
    Posts
    157
    hi @lucian_dusk

    I think we've both been trying to do the same thing....

    The tapeDelay10tap object that you're using changes from one delay time to another by fading between two different taps.

    I've added to that tapeDelay10tap object a new delaySmooth method that implements a fractional delay line which means that you can scrub through the delay line in fractions of samples rather than whole sample (using a simple allpass interpolation at the moment).

    I found, just as you mention, if you vary the speed that you scrub through depending on the size of the change you are trying to make it sounds better.

    The method picks a speed based on the size of the delay time change and pitches up or down in semitones (from 1 semitone to an octave depending on how large the change is).

    The code is mixed in with some other stuff - I'll unpick the other stuff, add some comments and post it here over next couple of day.

    With the 10 tap fractional delay line you can do a lot of different things so you should be able to modify it to your needs.

    Cheers Paul
    Last edited by houtson; 01-06-2021 at 08:55 PM.

  12. #12
    Hi Paul,
    Fantastic! I've had a great time working with your code. I'd love to see what you've come up with when it comes to pitch modulation.
    Looking forward!

  13. #13
    Senior Member houtson's Avatar
    Join Date
    Aug 2015
    Location
    Scotland
    Posts
    157
    hi @lucian_dusk

    I've stripped out and added lots of comments as below - if anything doesn't make sense let me know.

    I've included an example that is expecting an audio input through line in and a pot on pin A0 to control delay time.

    A few notes:
    - the code is not optimised, it runs fine on a Teensy 4, not tried it on anything else.
    - the new delaySmooth method smoothly moves from one time to another incrementing by fractions of a sample.
    - it picks the size of increment based on how 'far' you are looking to move (i.e. delay time of 100ms to a new delay time of 800ms) and looks to do think is semitone increments (1 to and octave)
    - you can easily re-write the code to move at a specific speed or pass a speed to it or have another input modulate it if that suits your application.
    - if you are sweeping a pot to change the time and checking that pot regularly (the example checks every 20ms) you will get a number of calls to the effect over one sweep of the pot speeding up and slowing down as you move.
    - I updated the delayFade bit also (which fades between delay times) changing it form linear to exponential curves - don't think it makes that much difference.

    Let me know how you get on with it. Cheers Paul


    delayExample.ino
    Code:
    // example of effect_delay10tap
    //
    
    #include <Audio.h>
    #include <ResponsiveAnalogRead.h>
    #include <SD.h>
    #include <SPI.h>
    
    #include "effect_delay10tap.h"
    
    // GUItool: begin automatically generated code
    AudioInputI2S i2s_in;          // xy=196,180
    AudioMixer4 mixer1;            // xy=434,190
    AudioEffectDelay10tap delay1;  // xy=638,182
    AudioOutputI2S i2s_out;        // xy=1038,218
    AudioConnection patchCord1(i2s_in, 0, mixer1, 0);
    AudioConnection patchCord2(i2s_in, 1, mixer1, 1);
    AudioConnection patchCord3(mixer1, delay1);
    AudioConnection patchCord4(delay1, 0, mixer1, 2);
    AudioConnection patchCord5(delay1, 0, i2s_out, 0);
    AudioConnection patchCord6(delay1, 0, i2s_out, 1);
    AudioControlSGTL5000 sgtl5000_1;  // xy=407,367
    // GUItool: end automatically generated code
    
    // delay line
    #define DELAYLINE_MAX_LEN 45159  // number of samples at 44100 samples a second
    int16_t delay_line[DELAYLINE_MAX_LEN] = {};
    
    // main timing loop
    #define LOOP0_DURATION 20  // interval time in millis
    elapsedMillis loop0_timer;
    // pot to control delaytime
    const int DELAY_TIME_KNOB_PIN = A0;  // A12 gain
    ResponsiveAnalogRead delayTimeKnob(DELAY_TIME_KNOB_PIN, true);
    
    void setup() {
      Serial.begin(9600);
    
      // Enable the audio shield and set the output volume.
      sgtl5000_1.enable();
      sgtl5000_1.inputSelect(AUDIO_INPUT_LINEIN);
      sgtl5000_1.volume(0.5);
    
      // alocate some audio memory, don't need much as passing an array to the effect
      AudioMemory(15);
    
      // set up the mixer including some feedback
      mixer1.gain(0, 0.5);
      mixer1.gain(1, 0.5);
      mixer1.gain(2, 0.6);
    
      // start up the effect and pass it an array to store the samples
      delay1.begin(delay_line, DELAYLINE_MAX_LEN);
    }
    
    void loop() {
      // loop timer
      if (loop0_timer >= LOOP0_DURATION) {
        // update and check the pot
        delayTimeKnob.update();
        if (delayTimeKnob.hasChanged()) {
          //Serial.printf("Delay Time Knob:%d\n", delayTimeKnob.getValue());
          delay1.delaysmooth(0, delayTimeKnob.getValue());
        }
        loop0_timer = 0;
      }
    }

    effect_delay10tap.h
    Code:
    /* Audio Library for Teensy 4.x
     * Modified to extend to 10 taps PMF 16-03-2020
     * Modified for single samples delay line (rather than blocks) and tape delay like behaviour PMF 02-09-2020
     * added delayfade to fade between old and new delay time with expo cross fade PMF 04-09-2020
     * added delaysmooth to smoothly delay from old to new time PMF 14-10-2020
     */
    
    #ifndef effect_delay10tap_h_
    #define effect_delay10tap_h_
    #include "Arduino.h"
    #include "AudioStream.h"
    
    #define DELAY_NUM_TAPS 10                       // max numer of fixed delay taps / channels
    #define DELAY_INC 0.19                          // delaysmooth, default increment per samples for delaysmooth
    
    class AudioEffectDelay10tap : public AudioStream {
     public:
      AudioEffectDelay10tap(void) : AudioStream(1, inputQueueArray) {}
      // initialise the delay line
      void begin(int16_t *delay_line, uint32_t max_delay_length);
      // activate a tap and/or change time with a fade between old and new time (no clicks), transition time in millis
      uint32_t delayfade(uint8_t channel, float milliseconds, float transition_time);
      uint32_t delaysmooth(uint8_t channel, float milliseconds);
      void setDelayIncPerSample(uint8_t channel, float _DELAYINC);
      // disable a tap
      void disable(uint8_t channel);
      // main update routine
      virtual void update(void);
    
      void setBufferFreeze(bool _FREEZE) { freezeBuffer = _FREEZE; };
      bool returnBufferFreeze() { return freezeBuffer; };
    
      void inspect(void) { dump_samples = true; };
    
     private:
      // linear interpolation between two samples (frac = 0 - 1)
      int16_t lerpSamples(int16_t sample1, int16_t sample2, float frac) {
        return static_cast<int16_t>(static_cast<float>(sample1) + static_cast<float>(sample2 - sample1) * frac);
      };
      // all pass interpol sample 1= current sample, 2= next sample, 3= last sample
      int16_t allPassInterpolSamples(int16_t sample1, int16_t sample2, int16_t sample3, float frac) {
        return int16_t(sample2 + (1 - frac) * sample1 - (1 - frac) * sample3);
      };
      // convert milliseconds to number of samples at sample rate
      int32_t millisToSamples(float milliseconds) { return milliseconds * (AUDIO_SAMPLE_RATE_EXACT / 1000.0) + 0.5; };
    
      audio_block_t *inputQueueArray[1];
      uint32_t max_delay_length_samples;  // lenght of the delay line in samples
      uint32_t write_index;               // write head position
      uint16_t activemask;                // which taps/channels are active
      int16_t *delay_line;                // pointer to delay line
    
      // delay modes
      enum delay_modes { DELAY_MODE_NORMAL, DELAY_MODE_SMOOTH, DELAY_MODE_FADE };
    
      // struct for tap
      typedef struct {
        int32_t current_delay;  // actual # of sample delay for each channel
        int32_t desired_delay;  // desired # of sample delay for each channel
        uint32_t fade_to_delay_samples;
        uint32_t fade_transition_time;
        uint32_t fade_samples_to_complete_transition;
        int16_t last_sample;
        double fade_multiplier_out;
        double fade_multiplier_in;
        double fade_expo_multiplier;
        int16_t inc_direction = 1;   // direction (+/-) to increment if delaysmooth
        float inc = 0.0;             // cummulative increment if delaysmooth
        float inc_per_sample = 0.0;  // increment per sample if delaysmooth
        int16_t delay_mode = DELAY_MODE_NORMAL;
      } tap_struct;
      tap_struct tap[DELAY_NUM_TAPS];
      
      // smooth delay increments for changing delay times in semitones
      float delay_inc_per_semitone[13] = {0.0000000, 0.0594631, 0.1224620, 0.1892071, 0.2599210, 0.3348399, 0.4142136,
                                          0.4983071, 0.5874011, 0.6817928, 0.7817974, 0.8877486, 1.0000000};
      
      
      uint32_t temp_timer = 0;
      boolean freezeBuffer = false;
      boolean dump_samples = false;
    };
    #endif
    effect_delay10tap.cpp
    Code:
    /* Audio Library for Teensy 4.x
     * Modified to extend to 10 taps PMF 16-03-2020
     * Modified for single samples delay line (rather than blocks) and tape delay like behaviour PMF 02-09-2020
     * added delayfade to fade between old and new delay time with expo cross fade PMF 04-09-2020
     * added delaysmooth to smoothly delay from old to new time PMF 14-10-2020
     */
    #include "effect_delay10tap.h"
    
    #include <Arduino.h>
    
    void AudioEffectDelay10tap::begin(int16_t *delay_l, uint32_t max_delay_length) {
      delay_line = delay_l;
      max_delay_length_samples = max_delay_length - 1;
      write_index = 0;
    }
    
    // activate a tap and/or change time with a fade between old and new time (no clicks), transition time in millis
    uint32_t AudioEffectDelay10tap::delayfade(uint8_t channel, float milliseconds, float transition_time) {
      if (channel >= DELAY_NUM_TAPS) return 0;
      if (milliseconds < 0.0) {
        milliseconds = 0.0;
      }
      if (transition_time < 0.0) transition_time = 0.0;
    
      uint32_t delay_length_samples = millisToSamples(milliseconds);
      if (delay_length_samples > max_delay_length_samples) delay_length_samples = max_delay_length_samples;
    
      __disable_irq();
      // enable disabled channel
      if (!(activemask & (1 << channel))) {
        // if channel not active then activate and delay as normal, no fade
        tap[channel].fade_to_delay_samples = tap[channel].current_delay = tap[channel].desired_delay = delay_length_samples;
        tap[channel].fade_samples_to_complete_transition = 0;
        activemask |= (1 << channel);
        tap[channel].delay_mode = DELAY_MODE_NORMAL;
      } else {
        // if already active check if currently fading
        if (tap[channel].fade_samples_to_complete_transition == 0) {
          // not currently fading, set up for a expo fade over transition time millis
          tap[channel].fade_to_delay_samples = tap[channel].desired_delay = delay_length_samples;                 // where in the delay are we fading to
          tap[channel].fade_transition_time = transition_time;                                                    // fade over xx milis
          tap[channel].fade_samples_to_complete_transition = millisToSamples(tap[channel].fade_transition_time);  // counter to fade over xx samples
          double tau = -1.0 * transition_time / log(1.0 - 1.0 / 1.01);                                            // calculate a multiplier for a exponential fade (with 1.01 overshoot)
          tap[channel].fade_expo_multiplier = pow(exp(-1.0 / tau), 1.0 / (AUDIO_SAMPLE_RATE_EXACT / 1000.0));
          tap[channel].fade_multiplier_out = 1.0;  // initialise the multiplier to be applied as gain on outgoing tap
          tap[channel].fade_multiplier_in = 0.01;  // initialise the multiplier to be applied as gain on incoming tap
          tap[channel].delay_mode = DELAY_MODE_FADE;
        } else {
          // currently fading, want to let that play out then fade again to desired so set desired as new target
          tap[channel].desired_delay = delay_length_samples;
        }
      }
      __enable_irq();
      return tap[channel].current_delay;
    }
    
    // activate a tap and/or change time without fading but smoothly changing delaytime
    uint32_t AudioEffectDelay10tap::delaysmooth(uint8_t channel, float milliseconds) {
      long temp;
      if (channel >= DELAY_NUM_TAPS) return 0;
      if (milliseconds < 0.0) milliseconds = 0.0;
    
      uint32_t delay_length_samples = millisToSamples(milliseconds);
      if (delay_length_samples > max_delay_length_samples) delay_length_samples = max_delay_length_samples;
    
      __disable_irq();
      // enable disabled channel
      if (!(activemask & (1 << channel))) {
        // if not previously activie just move straight to it
        tap[channel].delay_mode = DELAY_MODE_NORMAL;
        tap[channel].current_delay = tap[channel].desired_delay = delay_length_samples;
        tap[channel].fade_samples_to_complete_transition = 0;
        activemask |= (1 << channel);
      } else {
        // if already active...set desired and current delay
        tap[channel].desired_delay = delay_length_samples;
        tap[channel].current_delay = tap[channel].current_delay + (static_cast<int32_t>(tap[channel].inc) * tap[channel].inc_direction);
        tap[channel].fade_samples_to_complete_transition = 0;
        // if desire and current different set a rate to increment to desired
        if (tap[channel].current_delay != tap[channel].desired_delay) {
          // set direction to increment in, initialise and set the rate from 1 semiton to 1 octave based on size of change required
          if (tap[channel].current_delay > tap[channel].desired_delay) tap[channel].inc_direction = -1;
          if (tap[channel].current_delay < tap[channel].desired_delay) tap[channel].inc_direction = 1;
          tap[channel].inc = 0.0;
          tap[channel].last_sample = 0;
          // set the speed of increment based on how much delaytime has change - this is completely arbitary/tune to your needs
          // the delay_inc_per_semitone[] is an array of calculated increments to semitone pitch changes when incrementing
          temp = map(abs(tap[channel].current_delay - tap[channel].desired_delay), 0, (static_cast<float>(max_delay_length_samples) * .8), 1, 12);
          tap[channel].inc_per_sample = delay_inc_per_semitone[constrain(temp, 0, 12)];
          tap[channel].delay_mode = DELAY_MODE_SMOOTH;
          // Serial.printf("delaySmooth time:%d increment:%d (semitones)\n", tap[channel].desired_delay, temp);
        } else {
          // desired and current are equal so normal delay and no change
          tap[channel].delay_mode = DELAY_MODE_NORMAL;
          tap[channel].current_delay = tap[channel].desired_delay;
        }
      }
      __enable_irq();
      return tap[channel].current_delay;
    }
    
    void AudioEffectDelay10tap::disable(uint8_t channel) {
      if (channel >= DELAY_NUM_TAPS) return;
      // disable this channel
      activemask &= ~(1 << channel);
    };
    
    void AudioEffectDelay10tap::update(void) {
      audio_block_t *input, *output;
      int16_t *input_data_pointer, *output_data_pointer;
      uint32_t read_index, start_index;
      uint32_t fade_to_read_index = 0;
      uint8_t channel;
    
      int16_t next_sample = 0;
      int16_t sample = 0;
    
      int32_t inc_samples;
      float inc_frac;
    
      if (delay_line == NULL) return;
      // reading and wriitng the block separately so grab a copy of the write_index starting poisition
      start_index = write_index;
    
      // write incoming block of samples to buffer if not freezing buffer
      input = receiveReadOnly();
      if (input) {
        if (!freezeBuffer) {
          input_data_pointer = input->data;
          for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) {
            delay_line[write_index++] = *input_data_pointer++;
            if (write_index >= max_delay_length_samples) write_index = 0;
          }
        } else {
          // if buffer frozen move write index on a block
          write_index += AUDIO_BLOCK_SAMPLES;
          if (write_index >= max_delay_length_samples) write_index = (write_index + max_delay_length_samples) % max_delay_length_samples;
        }
        release(input);
      }
    
      // delay
      // process each tap and write out delayed samples
      for (channel = 0; channel < DELAY_NUM_TAPS; channel++) {
        // check if channel is active
        if (!(activemask & (1 << channel))) continue;
        output = allocate();
        if (!output) continue;
        output_data_pointer = output->data;
    
        // if fading between current delay to desired , position desired read head
        if (tap[channel].fade_samples_to_complete_transition > 0)
          fade_to_read_index = ((start_index - tap[channel].fade_to_delay_samples + max_delay_length_samples) % max_delay_length_samples);
    
        // position the main read head (current_delay_) for this channel / tap
        read_index = ((start_index - tap[channel].current_delay + max_delay_length_samples) % max_delay_length_samples);
    
        // process each sample in the audio block
        for (int i = 0; i < AUDIO_BLOCK_SAMPLES; i++) {
          //
          // if fading, cross mix in fade_length_samples steps
          //
          if (tap[channel].delay_mode == DELAY_MODE_FADE) {
            // update the fade multiplier for expo fade curves
            tap[channel].fade_multiplier_out *= tap[channel].fade_expo_multiplier;
            tap[channel].fade_multiplier_in /= tap[channel].fade_expo_multiplier;
            // read the two points from the delay line, crossfade and send to the output block
            *output_data_pointer++ = (int16_t)(delay_line[read_index] * tap[channel].fade_multiplier_out) + (int16_t)(delay_line[fade_to_read_index] * tap[channel].fade_multiplier_in);
            tap[channel].fade_samples_to_complete_transition--;
            if (tap[channel].fade_samples_to_complete_transition == 0) {  // got to end of fade
              // make the current_delay the fade_to_
              tap[channel].current_delay = tap[channel].fade_to_delay_samples;
              read_index = fade_to_read_index;
              tap[channel].delay_mode = DELAY_MODE_NORMAL;
              // if stil not at desired delay then start another fade to it
              if (tap[channel].desired_delay != tap[channel].current_delay) {
                tap[channel].fade_to_delay_samples = tap[channel].desired_delay;
                tap[channel].fade_samples_to_complete_transition = millisToSamples(tap[channel].fade_transition_time);  // counter to fade over xx samples
                fade_to_read_index = ((start_index - tap[channel].fade_to_delay_samples + max_delay_length_samples) % max_delay_length_samples);
                // re-set fade multipliers
                tap[channel].fade_multiplier_out = 1.0;  // initialise the multiplier to be applied as gain on outgoing tap
                tap[channel].fade_multiplier_in = 0.01;  // initialise the multiplier to be applied as gain on incoming tap
                tap[channel].delay_mode = DELAY_MODE_FADE;
              }
            }
            // increment and wrap around the fade_to_read_index
            fade_to_read_index++;
            if (fade_to_read_index >= max_delay_length_samples) fade_to_read_index = 0;
          }
          //
          // smooth transition from one delay time to another
          //
          if (tap[channel].delay_mode == DELAY_MODE_SMOOTH) {
            // move the delay time by a small increment (inc), split inc into numbers of sample + frac apply direction +-
            tap[channel].inc += tap[channel].inc_per_sample;
            inc_samples = static_cast<int32_t>(tap[channel].inc);
            inc_frac = tap[channel].inc - static_cast<float>(inc_samples);
            inc_samples *= tap[channel].inc_direction;
            // check if reached desired delay
            if (tap[channel].current_delay + inc_samples == tap[channel].desired_delay) {
              // reached desire delay, re-set current and re-position index and change to normal delay
              tap[channel].current_delay = tap[channel].desired_delay;
              tap[channel].inc = 0.0;
              read_index = ((start_index + i - tap[channel].current_delay + max_delay_length_samples) % max_delay_length_samples);
              tap[channel].delay_mode = DELAY_MODE_NORMAL;
            } else {
              // not reached desired yet so get next sample and interpolate
              sample = delay_line[(read_index - inc_samples + max_delay_length_samples) % max_delay_length_samples];
              next_sample = delay_line[(read_index - inc_samples - tap[channel].inc_direction + max_delay_length_samples) % max_delay_length_samples];
              if (tap[channel].last_sample == 0) tap[channel].last_sample = sample;  // if just starting help the allpass tune in quickly
              *output_data_pointer++ = tap[channel].last_sample = allPassInterpolSamples(sample, next_sample, tap[channel].last_sample, inc_frac);
            }
          }
          //
          // normal delay
          //
          if (tap[channel].delay_mode == DELAY_MODE_NORMAL) {
            // read delay line and send sample to the output block
            *output_data_pointer++ = delay_line[read_index];
          }
    
          // increment and wrap around the  read index
          read_index++;
          if (read_index >= max_delay_length_samples) read_index = 0;
        }
        transmit(output, channel);
        release(output);
      }
      // if (dump_samples) dump_samples = false;
    }
    
    // set the increment for the smooth delay
    void AudioEffectDelay10tap::setDelayIncPerSample(uint8_t channel, float _DELAYINC) {
      tap[channel].inc_per_sample = _DELAYINC;
      if (tap[channel].inc_per_sample < 0.1) tap[channel].inc_per_sample = 0.1;
      if (tap[channel].inc_per_sample > 1.0) tap[channel].inc_per_sample = 1.0;
    };

  14. #14
    Hi @houtson,

    This is spot-on-the-mark exactly what I had in mind. Many, many thanks for an excellent implementation!
    I noticed that, after adjusting the parameter, the pitch hops back to regular speed fairly rigidly, so I added the same lowpass / interpolator-feedback function from my program to adjust in the update routine, and I really like how it sounds:

    Code:
    float response = 0.9;
    
    float lowpass_d_d(int input) {                            //this is a simple interpolating lowpass filter to slow down the response of the delaytime slider
        float LPhistory = (input + (response * (LPout - input)));
        LPout = LPhistory;
        return LPout;
    }
    the main update loop (dtime is the hardware potentiometer, in my case a midi slider read elsewhere).
    Code:
    void updatei(){
      if (oTime > 10) {
        delay1.delaysmooth(0, lowpass_d_d(dtime));
        oTime=0;
      }
    }
    This is great! Looking forward to playing with it more and to experiment with the fading routines.
    Cheers,
    L

  15. #15
    One question for now --- do you know what causes the repeats in the feedback loop to continue to be pitch shifted? e.g. the delay feedback audio becomes far higher (or lower) in pitch than the delay object's initial output.

  16. #16
    Senior Member houtson's Avatar
    Join Date
    Aug 2015
    Location
    Scotland
    Posts
    157
    Hi, good you got it up and running - I'm using it as part of a vintage reverb project that i'm playing with.

    One question for now --- do you know what causes the repeats in the feedback loop to continue to be pitch shifted? e.g. the delay feedback audio becomes far higher (or lower) in pitch than the delay object's initial output.
    Need to think about that.....Not entirely sure I understand but if there is a feedback loop back into the delay as it is pitching up (or down) that will in turn get pitched up again (and again) until it finds the desired_ time. You could split the feedback out and process it within the effect differently (move the feedback straight to the desired_ time and not pitch it up?). Could you post a screen shot of the audio lib objects from the designer tools to see how you have connected it up. If it is easy a recording would be good also.

    Cheers Paul

  17. #17
    Hi Paul,
    Sure, here's an audio sample. https://soundcloud.com/ro-web/delay10tap-feedback
    The setup is the same as in the example you posted:
    Code:
    AudioConnection patchCord1(i2s_in, 0, mixer1, 0);
    AudioConnection patchCord2(i2s_in, 1, mixer1, 1);
    AudioConnection patchCord3(mixer1, delay1);
    AudioConnection patchCord4(delay1, 0, mixer1, 2);
    AudioConnection patchCord5(delay1, 0, i2s_out, 0);
    AudioConnection patchCord6(delay1, 0, i2s_out, 1);
    It seems to be like the pitch stabilizes fairly quickly when listening to the signal without feedback, so I was surprised that this happened.
    Would splitting the effect and adding a "feedback input" that is simply passed through without pitch effect be the way out?
    Cheers,
    L

  18. #18
    Senior Member houtson's Avatar
    Join Date
    Aug 2015
    Location
    Scotland
    Posts
    157
    Hi, track shows up as 'This track was not found. Maybe it has been removed ' - is it maybe set to private?

  19. #19

  20. #20
    Senior Member houtson's Avatar
    Join Date
    Aug 2015
    Location
    Scotland
    Posts
    157
    Quote Originally Posted by lucian_dusk View Post
    - sounds great!!

    Repeats in the feedback
    - I need to do a bit more with that
    - When you say 'simply pass it through' would you want it straight through or would you want it delayed (again)? if delayed at the new delay time?

    Speed of change
    - I've changed the way it reacts to a change in delay time, think it sounds much better, if you try it let me know (just ignore the feedback which is not changed).
    - I've added a third parameter to delaySmooth to control the speed ..try between 1(slow) and 10 (faster)

    The code is a mess, if we get it sounding decent I'll tidy it up, I've put it on GitHub cheers, Paul

  21. #21
    hi paul!
    apologies for delay (ha), i've had computer troubles and had to migrate machines.

    in regards to "clean" feedback, here's a sample from my implementation in maxmsp that i've been working to recreate:
    https://soundcloud.com/ro-web/tapede.../s-dJM5JA2CtJq
    when i said (somewhat ambiguously) "simply pass it through", i mean that while the delay time is constant, the mix of feedback audio is delayed at the same rate as new input audio, without being processed again by the re-sampling algorithm that would affect its pitch.

    what I don't yet understand about the tapedelay10tap object is how it seems to treat new input audio and feedback audio differently --- when modulating the delay time with the feedback set to 0, it shifts quickly through samples and pitch shifting is over within a few milliseconds. when the fed-back audio is added to the input mix, the sample-dropping process is repeated indefinitely, dropping samples only for the audio that was previously processed --- i've never come across this before.

    really looking forward to checking out the github and getting it running! many thanks for sharing it there.

    best,
    L

Posting Permissions

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