Simple Tap Tempo example for Pod

I’ve written some code I thought I would share. I figured some people starting out might find it helpful. I’ve seen some similar topics discussed in this forum. I’m quite new to this type of programming myself so feedback from the more experienced is very welcome.

The objective is to control the frequency of an LFO with either knob 1 on the Pod or by tapping in the tempo with button 2. This is a feature seen on many guitar pedals, which allow a user to dial in a new setting by tapping with their foot (I’m awaiting the delivery of my terrarium pcb). In this example, the LFO is used to flash LED 2 at the given frequency.

#include "daisy_pod.h"
#include "daisysp.h"

using namespace daisy;
using namespace daisysp;

DaisyPod hw;

const float MAX_FREQ = 35;			//30ms period
const float MIN_FREQ = 0.333;		//3s period

int count, MAX_COUNT, MIN_COUNT;	//count between taps. MAX and MIN value
bool tapping = false;				//True when user is tapping
bool averaging = false;				//True after 2nd tap
float BPS; 							//Audio blocks per second
float freq_tt; 						//Frequency read from tap tempo

Parameter freq_k;   				//Frequency read from knob
float prev_k1 = -1.0f;				//Previous knob position. Used to detect change
bool use_tt = false;				//use tap tempo. Knob values arent read when true

float lfo_out;
Oscillator lfo;

void tap_tempo();					
void UpdateLeds();	
void UpdateKnobs();				

void AudioCallback(AudioHandle::InputBuffer in, AudioHandle::OutputBuffer out, size_t size)
{
	hw.ProcessAllControls();
	tap_tempo();
	UpdateKnobs();
	UpdateLeds();
	
	for (size_t i = 0; i < size; i++)
	{
		out[0][i] = in[0][i];
		out[1][i] = in[1][i];
		lfo_out = lfo.Process();
	}
}

void tap_tempo() {

	if(tapping) {
		count++;

		if(hw.button2.RisingEdge() && count > MIN_COUNT) {

			if (averaging) //3rd plus tap
				freq_tt = 0.6*(freq_tt) + 0.4*(BPS/count); // Weighted Averaging
			else { 	
				//2nd tap
				freq_tt = BPS/count; //frequency = BPS/count
				averaging = true;
			}

			use_tt = true;
			lfo.SetFreq(freq_tt);
			count = 0;
		}
		else if(count == MAX_COUNT) {   // After 1/MIN_FREQ seconds no tap, reset values
			count = 0;
			tapping = false;
			averaging = false;
		}
	}
	else if(hw.button2.RisingEdge()) //1st tap
		tapping = true;		
}

void UpdateKnobs()
{
	float new_k1 = hw.knob1.Process();

	if(!use_tt){
		lfo.SetFreq(freq_k.Process());
		prev_k1 = new_k1;
	}
	else if (new_k1 > prev_k1+0.05 || new_k1 < prev_k1-0.05) // When the knob value is changed set use_tt to false
		use_tt = false;
}

void UpdateLeds() {
	if(tapping)	
		hw.led1.Set(0, 0, 1); // blue
	else
		hw.led1.Set(0, 0, 0); // off


	if(lfo_out > 0)
		hw.led2.Set(1, 0, 0); // red
	else
		hw.led2.Set(0, 0, 0); // off

	hw.UpdateLeds();
}

int main(void)
{
	hw.Init();
	hw.SetAudioBlockSize(4); // number of samples handled per callback
	hw.SetAudioSampleRate(SaiHandle::Config::SampleRate::SAI_48KHZ);

	BPS = hw.AudioSampleRate()/hw.AudioBlockSize(); // Blocks per second = sample_rate/block_size
	MAX_COUNT = round(BPS/MIN_FREQ);      			// count = BPS/frequency
	MIN_COUNT = round(BPS/MAX_FREQ);   

	freq_k.Init(hw.knob1, MIN_FREQ, MAX_FREQ, freq_k.LOGARITHMIC); //Mapping knob 1 values to freq_k logarithmically

	//LFO Settings
	lfo.Init(hw.AudioSampleRate());
	lfo.SetWaveform(lfo.WAVE_SQUARE);
	lfo.SetPw(0.25);
	lfo.SetFreq(1.0f);

	hw.StartAdc();
	hw.StartAudio(AudioCallback);
	while(1) {}
}

The program is pretty straight forward. When the user starts tapping the button, a count is started which is incremented once every call to the audio processing block. LED 1 lights up Blue at this point. This count is saved when the user taps again, and then is used to calculate the new frequency value.

When the user continues tapping, a weighted average is used to optimise the calculated frequency. Here BPS stands for audio Blocks Per Second. 0.6 and 0.4 are the weightings i chose.

if (averaging) //3rd plus tap
				freq_tt = 0.6*(freq_tt) + 0.4*(BPS/count); // Weighted Averaging

This weighted averaging ensures that later intervals are favoured over previously entered ones. The belief being that the users beats will become more steady. I landed on this value of 60/40 after trying a few but 50/50 also works ok.

If the user moves knob1 a little, the program will revert to using that to set the frequency of the lfo. This is controlled by the bool use_tt, which is true when the program is using the tap tempo value

void UpdateKnobs()
{
	float new_k1 = hw.knob1.Process();

	if(!use_tt){
		lfo.SetFreq(freq_k.Process());
		prev_k1 = new_k1;
	}
	else if (new_k1 > prev_k1+0.05 || new_k1 < prev_k1-0.05) // When the knob value is changed set use_tt to false
		use_tt = false;
}

In this example, the audio is simply passed through as it entered, but the tap tempo controlled lfo could be hooked up to any of the many different effects that can be modulated this way.

Let me know what you think or of any possible improvements I could make

5 Likes

Thank you so much for sharing! This looks like a lot of fun to play with :slight_smile:

I have my Minilogue connected to my Pod right now, so I would love to try this out as soon as I can. I will keep you posted!
I think I’ll map the LFO to the Moog Ladder filter emulation…

1 Like

Thank you for providing the example. I was wondering if the audio callback is supposed to be


    for(size_t i = 0; i < size; i++)
    {
        lfo_out   = lfo.Process();
        out[0][i] = lfo_out * in[0][i];
        out[1][i] = lfo_out * in[1][i];
    }

Otherwise, the only thing affected by lfo_out is LED2.

I got better (tremolo-like) results with setting the LFO’s waveform to WAVE_SIN.

This is definitely something you can do with the tap tempo synced LFO! The purpose of the post was more of a proof of concept so just controlling the LEDs sufficed.

1 Like

A while ago, i created a handy header file for adding tap tempo to a project. Ive found it quiet useful, so i figured I’d share here

#pragma once
#ifndef CP_TT
#define CP_TT
#include <stdlib.h>
#include <stdint.h>

template<size_t min_count, size_t max_count>
class Tap_Tempo
{
    public:
        Tap_Tempo() {}
        ~Tap_Tempo() {}

        void Init(float samplerate, size_t blocksize) {
            samplerate_ = samplerate;
            blocksize_ = blocksize;

            tapping_ = false;
            averaging_ = false;
            use_tt_ = false;

            count_ = 0;
            count_avg_ = min_count;

        }

        //Called once every audio block. Increments counter and resets values when max_count time is exceeded
        void Process() {

            if(tapping_)
                count_ = count_ + blocksize_;
                
            if(count_  >= max_count) {   // After max_count samples without a tap, reset values
                count_ = 0;
                tapping_ = false;
                averaging_ = false;
            }
	
        }

        //Called when a tap occurs. Initiates counting and calculates count average
        void Tap() {
           
            if(tapping_) {

                if(count_ > min_count) {
                        
                    if (averaging_) //3rd plus tap
                        count_avg_ = 0.6*(count_avg_) + 0.4*(count_); // Weighted Averaging
                    else { 	
                        //2nd tap
                        count_avg_ = count_;
                        averaging_ = true;
                        use_tt_ = true;
                    }

                    count_ = 0;
                } 
            } else
                tapping_ = true; // 1st tap

        }

        //Returns True while the counting audio blocks between taps
        bool Tapping(){return tapping_;}

        //Use tap tempo. Used to override externally set value with tap tempo value. Must be manually deasserted with Stop_tt()
        bool Use_tt() {return use_tt_;}

        //Deassert tap tempo overide
        void Stop_tt() {use_tt_ = false;}

        //Returns the average tap time in samples 
        size_t Count_Avg() {return count_avg_;}

        //Returns the average tap time in hertz
        float Freq_Avg() {return (float)samplerate_ / (float)count_avg_;}

    private:

        bool tapping_; // True when user is tapping
        bool averaging_; // True after 2nd tap
        bool use_tt_; // Use tap tempo. Used to override externally set value with tap tempo value. Must be manually deasserted with Stop_tt()

        int samplerate_;
        size_t blocksize_;
        size_t count_; // counter to count time in samples between taps
        size_t count_avg_; // average count of time in samples between taps

};

#endif
4 Likes