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