Skip to content

Commit 960079d

Browse files
committed
Initial commit
0 parents  commit 960079d

File tree

3 files changed

+236
-0
lines changed

3 files changed

+236
-0
lines changed

README.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
## arduino tap midi clock
2+
3+
This sketch implements a [MIDI beat clock](https://en.wikipedia.org/wiki/MIDI_beat_clock) driver which can be controlled by tapping a button or pedal (such as a digital keyboard sustain pedal) connected to the Arduino. Holding the button/pedal down for 1 second stops the beat messages until a new tempo is tapped in.
4+
5+
The default pin setting I used for building a compact unit out of an Arduino Nano is as follows:
6+
7+
* External power supply (5-20V) to pins `VIN` and `GND`
8+
* Button or pedal to pins `GND` and `14` aka `A0` (used in `INPUT_PULLUP` mode)
9+
* MIDI DIN connector pin 2 to Arduino `GND`
10+
* MIDI DIN connector pin 4 to Arduino pin `5V` *through a 220 Ohm resistor*
11+
* MIDI DIN connector pin 5 to Arduino pin `10` *through a 220 Ohm resistor*
12+
* Status LED to Arduino pin `6` (through a suitable resistor)
13+
14+
<img src="https://raw.githubusercontent.com/kevinstadler/arduino-tap-midi-clock/master/arduino-tap-midi-clock.jpg" alt="Arduino Tap Midi Clock hardware" title="Arduino Tap Midi Clock hardware" align="center" width="100%" />
15+
16+
### Dependencies
17+
18+
This sketch requires two libraries, both available for download from within Arduino:
19+
20+
* [ButtonDebounce](https://github.com/maykon/ButtonDebounce)
21+
* [TimerOne](https://github.com/PaulStoffregen/TimerOne)

arduino-tap-midi-clock.jpg

170 KB
Loading

miditap.ino

+215
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
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

Comments
 (0)