|
| 1 | +#include <ButtonDebounce.h> |
| 2 | +// tap button input - any digital input |
| 3 | +#define TAP_PIN 14 |
| 4 | +// ignore switch/pedal bouncing up to this many miliseconds |
| 5 | +// (this sets an upper limit to the bpm that can be tapped) |
| 6 | +ButtonDebounce button(TAP_PIN, 50); |
| 7 | + |
| 8 | +// all time periods are in MICRO seconds (there's 1.000.000 of those in a second) |
| 9 | +// tap intervals longer than 3s are interpreted as onset of a new tap sequence. |
| 10 | +// this sets a lower limit on the frequency of tapping: 20bpm |
| 11 | +#define MAXIMUM_TAP_INTERVAL 1000L * 3000 |
| 12 | +// hold for 1s to reset tempo and stop sending clock signals |
| 13 | +#define HOLD_RESET_DURATION 1000L * 1000 |
| 14 | + |
| 15 | +// how many taps should be remembered? the clock period will be calculated based on |
| 16 | +// all remembered taps, i.e. it will be the average of the last TAP_MEMORY-1 periods |
| 17 | +#define TAP_MEMORY 4 |
| 18 | +long tapTimes[TAP_MEMORY]; |
| 19 | +// counter |
| 20 | +long timesTapped = 0; |
| 21 | + |
| 22 | + |
| 23 | +// to be able to debug via the USB Serial interface, write the MIDI |
| 24 | +// messages to another set of digital pins using SoftwareSerial |
| 25 | +#include <SoftwareSerial.h> |
| 26 | +#define MIDI_RX_PIN 2 // not actually used (should be an interruptable pin in theory) |
| 27 | +#define MIDI_TX_PIN 10 // SoftwareSerial output port -- connect to MIDI jack pin 5 via 220 Ohm resistor |
| 28 | +// more details on MIDI jack wiring: https://www.arduino.cc/en/uploads/Tutorial/MIDI_bb.png |
| 29 | +SoftwareSerial Midi(MIDI_RX_PIN, MIDI_TX_PIN); |
| 30 | + |
| 31 | +// use the PWM-compatible pins 3, 5, 6 or 11 if you want a nice logarithmic fade |
| 32 | +// (PWM on pins 9 and 10 is blocked by the hardware timer used for the MIDI message |
| 33 | +// interrupt, see below) |
| 34 | +#define LED_PIN 6 |
| 35 | + |
| 36 | +// counts up to CLOCKS_PER_BEAT to control the status LED indicating the current |
| 37 | +// tempo. volatile because it is reset to 0 (full brightness) when button is tapped |
| 38 | +volatile int blinkCount = 0; |
| 39 | + |
| 40 | +// use nice (base 12) logarithmic fade-out for the LED blink |
| 41 | +const int LED_BRIGHTNESS[24] = { 255, 255, 255, 255, 255, 246, 236, 225, 213, 200, 184, 165, 142, 113, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; |
| 42 | +// whenever idle, keep LED on some low brightness level to indicate pedal is on |
| 43 | +#define READY_BRIGHTNESS 30 |
| 44 | + |
| 45 | +#include <TimerOne.h> |
| 46 | +// MIDI requires 24 clock pulse messages per beat (quaver) |
| 47 | +#define CLOCKS_PER_BEAT 24 |
| 48 | + |
| 49 | +// this stores the MIDI clock period, i.e. the calculated average period of the |
| 50 | +// tapping divided by CLOCKS_PER_BEAT |
| 51 | +long clockPeriod; |
| 52 | + |
| 53 | +// only start sending clocks once we've been tapped at least twice |
| 54 | +bool clockPulseActive = false; |
| 55 | + |
| 56 | +// helper variable that can be set by interrupts, causing the next |
| 57 | +// iteration of loop() to do cleanup (and play a nice led animation) |
| 58 | +volatile bool reset = false; |
| 59 | + |
| 60 | +void setup() { |
| 61 | + // fire up |
| 62 | + pinMode(LED_PIN, OUTPUT); |
| 63 | + digitalWrite(LED_PIN, HIGH); |
| 64 | + |
| 65 | + // use hardware serial for debugging |
| 66 | + Serial.begin(9600); |
| 67 | + // software serial to send MIDI clocks |
| 68 | + Midi.begin(31250); |
| 69 | + |
| 70 | + // set up tap input pin and callback from ButtonDebounce library |
| 71 | + pinMode(TAP_PIN, INPUT_PULLUP); |
| 72 | + button.setCallback(tapped); |
| 73 | + |
| 74 | + // initialise timer - this breaks PWM (analogWrite) on pins 9+10. |
| 75 | + // the clock pulse really only starts sending once the interrupt callback |
| 76 | + // function is set in setClockPulse() below |
| 77 | + Timer1.initialize(); |
| 78 | + // cause first call to loop() to play the LED animation to signal the pedal is ready |
| 79 | + reset = true; |
| 80 | +} |
| 81 | + |
| 82 | +// MIDI messages are sent by the interrupt-based timer, only need to read the |
| 83 | +// debounced tap inputs in the loop |
| 84 | +void loop() { |
| 85 | + if (reset) { |
| 86 | + timesTapped = 0; |
| 87 | + clockPulseActive = false; |
| 88 | + // play nice reset animation |
| 89 | + for (int i = 0; i <= 5; i++) { |
| 90 | + digitalWrite(LED_PIN, HIGH); |
| 91 | + delay(80); |
| 92 | + analogWrite(LED_PIN, READY_BRIGHTNESS); |
| 93 | + delay(80); |
| 94 | + } |
| 95 | + reset = false; |
| 96 | + } |
| 97 | + // don't let the code that starts and stops the interrupts (invoked via |
| 98 | + // callback by button.update()) be interrupted |
| 99 | + noInterrupts(); |
| 100 | + button.update(); |
| 101 | + interrupts(); |
| 102 | +} |
| 103 | + |
| 104 | +// update the |
| 105 | +void setClockPulse() { |
| 106 | + clockPeriod = (tapTimes[timesTapped - 1] - tapTimes[0]) / ((timesTapped - 1) * CLOCKS_PER_BEAT); |
| 107 | + Serial.print("New tap period (ms): "); |
| 108 | + Serial.println(clockPeriod * CLOCKS_PER_BEAT / 1000); |
| 109 | + if (clockPulseActive) { |
| 110 | + Timer1.setPeriod(clockPeriod); |
| 111 | + } else { |
| 112 | + clockPulseActive = true; |
| 113 | + Timer1.attachInterrupt(sendClockPulse, clockPeriod); |
| 114 | + // syncing the onset of the arpeggiator with the actual time of the tap (rather |
| 115 | + // than just the tempo of the tapping) would be nice, but a midi start alone |
| 116 | + // sadly doesn't do the trick |
| 117 | + // TODO: try out the Song Position Pointer message to reset arpeggiator: |
| 118 | + //Midi.write(0xF2); |
| 119 | + //Midi.write(0x00); |
| 120 | + //Midi.write(0x00); |
| 121 | + } |
| 122 | +} |
| 123 | + |
| 124 | +void stopClockPulse() { |
| 125 | + Timer1.detachInterrupt(); |
| 126 | + // could send midi stop as well to kill the arpeggiator |
| 127 | + //Midi.write(0xFC); |
| 128 | + // this function is called from the sendClockPulse() timer interrupt callback, |
| 129 | + // so delegate cleanup (and the led animation) to the main loop |
| 130 | + reset = true; |
| 131 | +} |
| 132 | + |
| 133 | +// callback for the debounced button |
| 134 | +void tapped(int state) { |
| 135 | + if (!state) { |
| 136 | + // overengineering opportunity: could measure how long the pedal is held |
| 137 | + // down to adapt the duration of the blinking to how the user is tapping..? |
| 138 | + return; |
| 139 | + } |
| 140 | + long now = micros(); |
| 141 | + long timeSinceLastTap = now - tapTimes[max(0, timesTapped - 1)]; |
| 142 | + |
| 143 | + // reset led to the (bright) beginning of the blinking cycle |
| 144 | + blinkCount = 0; |
| 145 | + |
| 146 | + if (timesTapped == 0 or timeSinceLastTap > MAXIMUM_TAP_INTERVAL) { |
| 147 | + // new tap sequence |
| 148 | + tapTimes[0] = now; |
| 149 | + timesTapped = 1; |
| 150 | + if (!clockPulseActive) { |
| 151 | + // first tap: indicate that we're listening by turning on the led for as |
| 152 | + // long as we're listening |
| 153 | + digitalWrite(LED_PIN, HIGH); |
| 154 | + Timer1.attachInterrupt(stopWaiting, MAXIMUM_TAP_INTERVAL); |
| 155 | + // reset timer to beginning, otherwise it will be based on the counter value |
| 156 | + // from before attachInterrupt() |
| 157 | + Timer1.start(); |
| 158 | + } |
| 159 | + return; |
| 160 | + |
| 161 | + } else if (timesTapped > 1) { |
| 162 | + // when the time between 2 taps is less than the maximum tap interval but much |
| 163 | + // different from the average of the tap sequence so far, reset it, as it |
| 164 | + // probably means a new tempo is being tapped |
| 165 | + float ratio = float(timeSinceLastTap) / (clockPeriod * CLOCKS_PER_BEAT); |
| 166 | + if (ratio > 1.5 or ratio < 0.5) { |
| 167 | + // put last of previous tap sequence as first of new one in memory |
| 168 | + tapTimes[0] = tapTimes[timesTapped - 1]; |
| 169 | + timesTapped = 1; |
| 170 | + } |
| 171 | + } |
| 172 | + |
| 173 | + if (timesTapped < TAP_MEMORY) { |
| 174 | + // write to next free slot |
| 175 | + timesTapped++; |
| 176 | + } else { |
| 177 | + // shift memory content forward (drop earliest tap time) |
| 178 | + memcpy(tapTimes, &tapTimes[1], sizeof(long) * (TAP_MEMORY - 1)); |
| 179 | + } |
| 180 | + tapTimes[timesTapped - 1] = now; |
| 181 | + |
| 182 | + // update timer interrupt to new period |
| 183 | + setClockPulse(); |
| 184 | +} |
| 185 | + |
| 186 | +void stopWaiting() { |
| 187 | + // Timer1.attachInterrupt() doesn't wait for a full cycle of the timer but triggers |
| 188 | + // this callback almost immediately, so we need to check that one cycle has elapsed |
| 189 | + // before turning off the LED (and disconnecting the interrupt again). we divide by |
| 190 | + // two in the comparison to work around a wonderful heisenbug caused by the interrupt |
| 191 | + // actually always being triggered a bit too early unless you put a debugging |
| 192 | + // Serial.println() before micros() of course... |
| 193 | + if (micros() - tapTimes[0] >= MAXIMUM_TAP_INTERVAL / 2) { |
| 194 | + Timer1.detachInterrupt(); |
| 195 | + reset = true; |
| 196 | + } |
| 197 | +} |
| 198 | + |
| 199 | +// this function is called by the Timer interrupt |
| 200 | +void sendClockPulse() { |
| 201 | + // check if lastTapTime has been ages ago and, if the |
| 202 | + // debounced button is still HIGH, stop the clock |
| 203 | + if (button.state() == HIGH and micros() - tapTimes[timesTapped - 1] > HOLD_RESET_DURATION) { |
| 204 | + Serial.println("Reset"); |
| 205 | + stopClockPulse(); |
| 206 | + return; |
| 207 | + } |
| 208 | + |
| 209 | + // send MIDI clock |
| 210 | + Midi.write(0xF8); |
| 211 | + |
| 212 | + analogWrite(LED_PIN, LED_BRIGHTNESS[blinkCount]); |
| 213 | + blinkCount = (blinkCount + 1) % CLOCKS_PER_BEAT; |
| 214 | +} |
| 215 | + |
0 commit comments