├── README.md └── MIDI-Clock.ino /README.md: -------------------------------------------------------------------------------- 1 | # Arduino MIDI clock with tap tempo 2 | 3 | As seen on LittleBits: 4 | https://classroom.littlebits.com/inventions/littlebits-arduino-midi-master-clock-with-tap-tempo 5 | 6 | You can also do this with a regular Arduino, but you'll have some extra work soldering the button(s)/dimmer 7 | 8 | ## How to make a Arduino MIDI connector 9 | See the excellent tutorial at this link: 10 | http://www.instructables.com/id/Send-and-Receive-MIDI-with-Arduino/step3/Send-MIDI-Messages-with-Arduino-Hardware/ . 11 | 12 | Or check the electrical spec here: 13 | https://www.midi.org/articles/midi-electrical-specifications 14 | 15 | ## Connections 16 | 17 | ### For MIDI out 18 | 19 | - Connect MIDI ground to Arduino ground 20 | - Connect MIDI 5V to Arduino 5V **with a 220 Ohm resistor in between** 21 | - Connect MIDI signal line to Arduino serial input pin (D1) 22 | 23 | ### For tap in 24 | 25 | Connect a button to D2 26 | 27 | ## Usage 28 | 29 | - Upon startup, Arduino will start sending 100 BPM MIDI clock signal (or last saved BPM value if available). 30 | - Tap the tempo (minimum 3 times) 31 | - After the last tap, clock tempo will be updated and MIDI clock signal will send new BPM 32 | 33 | ### Features: 34 | - **MIDI clock output** on pin D1 TX 35 | - **Tap tempo** input 36 | - **Dimmer input** when a dimmer is connected to A0 to set the tempo by twisting the knob! 37 | - Tempo blinking **LED** on pin 5 38 | - Sync signal on pin 9 (for example to sync with Korg Monotribe...) 39 | - **MIDI real-time start/stop** is sent when button press is detected on A1 port 40 | - **BPM storage in EEPROM** and restores it on power up 41 | - **MIDI forwarding** if a MIDI input is present on pin D0 RX 42 | - Output of the BPM to a TM1637 **LED display** 43 | 44 | # Branches 45 | 46 | - master: Code for the Arduino Leonardo (or LittleBits Arduino) 47 | - arduino-uno: Code for Arduino Uno and compatible devices (D0/D1 RX/TX pins are used for MIDI in/out, so no debug console!) 48 | - olimex-midi-shield: Based on the arduino-uno branch, but with some tweaks to make the code work better with the Olimex MIDI shield (https://www.olimex.com/Products/Duino/Shields/SHIELD-MIDI/open-source-hardware) (Work in progress!) 49 | 50 | # Enjoy! :) 51 | -------------------------------------------------------------------------------- /MIDI-Clock.ino: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | /* 4 | * For all features: if you do not define the pin, the feature will be disabled! 5 | */ 6 | 7 | /* 8 | * FEATURE: TAP BPM INPUT 9 | */ 10 | #define TAP_PIN 2 11 | #define TAP_PIN_POLARITY RISING 12 | 13 | #define MINIMUM_TAPS 3 14 | #define EXIT_MARGIN 150 // If no tap after 150% of last tap interval -> measure and set 15 | 16 | /* 17 | * FEATURE: DIMMER BPM INPUT 18 | */ 19 | // #define DIMMER_INPUT_PIN A0 20 | 21 | #define DIMMER_CHANGE_MARGIN 20 // Big value to make sure this doesn't interfere. Tweak as needed. 22 | 23 | /* 24 | * FEATURE: DIMMER BPM INCREASE/DECREASE 25 | */ 26 | #define DIMMER_CHANGE_PIN A1 27 | #define DEAD_ZONE 50 28 | #define CHANGE_THRESHOLD 5000 29 | #define RATE_DIVISOR 30 30 | 31 | /* 32 | * FEATURE: BLINK TEMPO LED 33 | */ 34 | #define BLINK_OUTPUT_PIN 5 35 | #define BLINK_PIN_POLARITY 0 // 0 = POSITIVE, 255 - NEGATIVE 36 | #define BLINK_TIME 4 // How long to keep LED lit in CLOCK counts (so range is [0,24]) 37 | 38 | /* 39 | * FEATURE: SYNC PULSE OUTPUT 40 | */ 41 | #define SYNC_OUTPUT_PIN 9 // Can be used to drive sync analog sequencer (Korg Monotribe etc ...) 42 | #define SYNC_PIN_POLARITY 0 // 0 = POSITIVE, 255 - NEGATIVE 43 | 44 | /* 45 | * FEATURE: Send MIDI start/stop 46 | */ 47 | #define START_STOP_INPUT_PIN A1 48 | #define START_STOP_PIN_POLARITY 0 // 0 = POSITIVE, 1024 = NEGATIVE 49 | 50 | #define MIDI_START 0xFA 51 | #define MIDI_STOP 0xFC 52 | 53 | #define DEBOUNCE_INTERVAL 500L // Milliseconds 54 | 55 | /* 56 | * FEATURE: EEPROM BPM storage 57 | */ 58 | #define EEPROM_ADDRESS 0 // Where to save BPM 59 | #ifdef EEPROM_ADDRESS 60 | #include 61 | #endif 62 | 63 | /* 64 | * FEATURE: MIDI forwarding 65 | */ 66 | #define MIDI_FORWARD 67 | 68 | /* 69 | * FEATURE: TM1637 display BPM output 70 | */ 71 | #define TM1637_DISPLAY 72 | #ifdef TM1637_DISPLAY 73 | #include 74 | #define TM1637_CLK_PIN 3 75 | #define TM1637_DIO_PIN 4 76 | #define TM1637_BRIGHTNESS 0x0f 77 | #endif 78 | 79 | /* 80 | * GENERAL PARAMETERS 81 | */ 82 | #define MIDI_TIMING_CLOCK 0xF8 83 | #define CLOCKS_PER_BEAT 24 84 | #define MINIMUM_BPM 400 // Used for debouncing 85 | #define MAXIMUM_BPM 3000 // Used for debouncing 86 | 87 | long intervalMicroSeconds; 88 | int bpm; // BPM in tenths of a BPM!! 89 | 90 | boolean initialized = false; 91 | long minimumTapInterval = 60L * 1000 * 1000 * 10 / MAXIMUM_BPM; 92 | long maximumTapInterval = 60L * 1000 * 1000 * 10 / MINIMUM_BPM; 93 | 94 | volatile long firstTapTime = 0; 95 | volatile long lastTapTime = 0; 96 | volatile long timesTapped = 0; 97 | 98 | volatile int blinkCount = 0; 99 | 100 | int lastDimmerValue = 0; 101 | 102 | boolean playing = false; 103 | long lastStartStopTime = 0; 104 | 105 | #ifdef TM1637_DISPLAY 106 | TM1637Display display(TM1637_CLK_PIN, TM1637_DIO_PIN); 107 | uint8_t tm1637_data[4] = {0x00, 0x00, 0x00, 0x00}; 108 | #endif 109 | 110 | #ifdef DIMMER_CHANGE_PIN 111 | long changeValue = 0; 112 | #endif 113 | 114 | void setup() { 115 | Serial.begin(38400); 116 | // Set MIDI baud rate: 117 | Serial1.begin(31250); 118 | 119 | // Set pin modes 120 | #ifdef BLINK_OUTPUT_PIN 121 | pinMode(BLINK_OUTPUT_PIN, OUTPUT); 122 | #endif 123 | #ifdef SYNC_OUTPUT_PIN 124 | pinMode(SYNC_OUTPUT_PIN, OUTPUT); 125 | #endif 126 | #ifdef DIMMER_INPUT_PIN 127 | pinMode(DIMMER_INPUT_PIN, INPUT); 128 | #endif 129 | #ifdef START_STOP_INPUT_PIN 130 | pinMode(START_STOP_INPUT_PIN, INPUT); 131 | #endif 132 | 133 | #ifdef EEPROM_ADDRESS 134 | // Get the saved BPM value from 2 stored bytes: MSB LSB 135 | bpm = EEPROM.read(EEPROM_ADDRESS) << 8; 136 | bpm += EEPROM.read(EEPROM_ADDRESS + 1); 137 | if (bpm < MINIMUM_BPM || bpm > MAXIMUM_BPM) { 138 | bpm = 1200; 139 | } 140 | #endif 141 | 142 | #ifdef TAP_PIN 143 | // Interrupt for catching tap events 144 | attachInterrupt(digitalPinToInterrupt(TAP_PIN), tapInput, TAP_PIN_POLARITY); 145 | #endif 146 | 147 | // Attach the interrupt to send the MIDI clock and start the timer 148 | Timer1.initialize(intervalMicroSeconds); 149 | Timer1.setPeriod(calculateIntervalMicroSecs(bpm)); 150 | Timer1.attachInterrupt(sendClockPulse); 151 | 152 | #ifdef DIMMER_INPUT_PIN 153 | // Initialize dimmer value 154 | lastDimmerValue = analogRead(DIMMER_INPUT_PIN); 155 | #endif 156 | 157 | #ifdef TM1637_DISPLAY 158 | display.setBrightness(TM1637_BRIGHTNESS); 159 | setDisplayValue(bpm); 160 | #endif 161 | } 162 | 163 | void loop() { 164 | long now = micros(); 165 | 166 | #ifdef TAP_PIN 167 | /* 168 | * Handle tapping of the tap tempo button 169 | */ 170 | if (timesTapped > 0 && timesTapped < MINIMUM_TAPS && (now - lastTapTime) > maximumTapInterval) { 171 | // Single taps, not enough to calculate a BPM -> ignore! 172 | // Serial.println("Ignoring lone taps!"); 173 | timesTapped = 0; 174 | } else if (timesTapped >= MINIMUM_TAPS) { 175 | long avgTapInterval = (lastTapTime - firstTapTime) / (timesTapped - 1); 176 | if ((now - lastTapTime) > (avgTapInterval * EXIT_MARGIN / 100)) { 177 | bpm = 60L * 1000 * 1000 * 10 / avgTapInterval; 178 | updateBpm(now); 179 | 180 | // Update blinkCount to make sure LED blink matches tapped beat 181 | blinkCount = ((now - lastTapTime) * 24 / avgTapInterval) % CLOCKS_PER_BEAT; 182 | 183 | timesTapped = 0; 184 | } 185 | } 186 | #endif 187 | 188 | #ifdef DIMMER_INPUT_PIN 189 | /* 190 | * Handle change of the dimmer input 191 | */ 192 | int curDimValue = analogRead(DIMMER_INPUT_PIN); 193 | if (curDimValue > lastDimmerValue + DIMMER_CHANGE_MARGIN 194 | || curDimValue < lastDimmerValue - DIMMER_CHANGE_MARGIN) { 195 | // We've got movement!! 196 | bpm = map(curDimValue, 0, 1024, MINIMUM_BPM, MAXIMUM_BPM); 197 | 198 | updateBpm(now); 199 | lastDimmerValue = curDimValue; 200 | } 201 | #endif 202 | 203 | #ifdef DIMMER_CHANGE_PIN 204 | int curDimValue = analogRead(DIMMER_CHANGE_PIN); 205 | if (bpm > MINIMUM_BPM && curDimValue < (512 - DEAD_ZONE)) { 206 | int val = (512 - DEAD_ZONE - curDimValue) / RATE_DIVISOR; 207 | changeValue += val * val; 208 | } else if (bpm < MAXIMUM_BPM && curDimValue > (512 + DEAD_ZONE)) { 209 | int val = (curDimValue - 512 - DEAD_ZONE) / RATE_DIVISOR; 210 | changeValue += val * val; 211 | } else { 212 | changeValue = 0; 213 | } 214 | if (changeValue > CHANGE_THRESHOLD) { 215 | bpm += curDimValue < 512 ? -1 : 1; 216 | updateBpm(now); 217 | changeValue = 0; 218 | } 219 | #endif 220 | 221 | #ifdef START_STOP_INPUT_PIN 222 | /* 223 | * Check for start/stop button pressed 224 | */ 225 | boolean startStopPressed = (START_STOP_PIN_POLARITY - analogRead(START_STOP_INPUT_PIN)) > 1024 / 2 ? true : false; 226 | if (startStopPressed && (lastStartStopTime + (DEBOUNCE_INTERVAL * 1000)) < now) { 227 | startOrStop(); 228 | lastStartStopTime = now; 229 | } 230 | #endif 231 | 232 | #ifdef MIDI_FORWARD 233 | /* 234 | * Forward received serial data 235 | */ 236 | while (Serial1.available()) { 237 | int b = Serial1.read(); 238 | Serial1.write(b); 239 | } 240 | #endif 241 | } 242 | 243 | void tapInput() { 244 | long now = micros(); 245 | if (now - lastTapTime < minimumTapInterval) { 246 | return; // Debounce 247 | } 248 | 249 | if (timesTapped == 0) { 250 | firstTapTime = now; 251 | } 252 | 253 | timesTapped++; 254 | lastTapTime = now; 255 | Serial.println("Tap!"); 256 | } 257 | 258 | void startOrStop() { 259 | if (!playing) { 260 | Serial.println("Start playing"); 261 | Serial1.write(MIDI_START); 262 | } else { 263 | Serial.println("Stop playing"); 264 | Serial1.write(MIDI_STOP); 265 | } 266 | playing = !playing; 267 | } 268 | 269 | void sendClockPulse() { 270 | // Write the timing clock byte 271 | Serial1.write(MIDI_TIMING_CLOCK); 272 | 273 | blinkCount = (blinkCount + 1) % CLOCKS_PER_BEAT; 274 | if (blinkCount == 0) { 275 | // Turn led on 276 | #ifdef BLINK_OUTPUT_PIN 277 | analogWrite(BLINK_OUTPUT_PIN, 255 - BLINK_PIN_POLARITY); 278 | #endif 279 | 280 | #ifdef SYNC_OUTPUT_PIN 281 | // Set sync pin to HIGH 282 | analogWrite(SYNC_OUTPUT_PIN, 255 - SYNC_PIN_POLARITY); 283 | #endif 284 | } else { 285 | #ifdef SYNC_OUTPUT_PIN 286 | if (blinkCount == 1) { 287 | // Set sync pin to LOW 288 | analogWrite(SYNC_OUTPUT_PIN, 0 + SYNC_PIN_POLARITY); 289 | } 290 | #endif 291 | #ifdef BLINK_OUTPUT_PIN 292 | if (blinkCount == BLINK_TIME) { 293 | // Turn led on 294 | analogWrite(BLINK_OUTPUT_PIN, 0 + BLINK_PIN_POLARITY); 295 | } 296 | #endif 297 | } 298 | } 299 | 300 | void updateBpm(long now) { 301 | // Update the timer 302 | long interval = calculateIntervalMicroSecs(bpm); 303 | Timer1.setPeriod(interval); 304 | 305 | #ifdef EEPROM_ADDRESS 306 | // Save the BPM in 2 bytes, MSB LSB 307 | EEPROM.write(EEPROM_ADDRESS, bpm / 256); 308 | EEPROM.write(EEPROM_ADDRESS + 1, bpm % 256); 309 | #endif 310 | 311 | Serial.print("Set BPM to: "); 312 | Serial.print(bpm / 10); 313 | Serial.print('.'); 314 | Serial.println(bpm % 10); 315 | 316 | #ifdef TM1637_DISPLAY 317 | setDisplayValue(bpm); 318 | #endif 319 | } 320 | 321 | long calculateIntervalMicroSecs(int bpm) { 322 | // Take care about overflows! 323 | return 60L * 1000 * 1000 * 10 / bpm / CLOCKS_PER_BEAT; 324 | } 325 | 326 | #ifdef TM1637_DISPLAY 327 | void setDisplayValue(int value) { 328 | tm1637_data[0] = value >= 1000 ? display.encodeDigit(value / 1000) : 0x00; 329 | tm1637_data[1] = value >= 100 ? display.encodeDigit((value / 100) % 10) : 0x00; 330 | tm1637_data[2] = value >= 10 ? display.encodeDigit((value / 10) % 10) : 0x00; 331 | tm1637_data[3] = display.encodeDigit(value % 10); 332 | display.setSegments(tm1637_data); 333 | } 334 | #endif 335 | 336 | 337 | --------------------------------------------------------------------------------