├── MIDI.png ├── README.md ├── RTC.png ├── banner-small.jpg ├── banner.jpg ├── beep.png ├── button.png ├── fix.py ├── midi-recorder.ino └── sd card.png /MIDI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/arduino-midi-recorder/1581795138eccd51e3043e0a33f0b3c49a8ad483/MIDI.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Creating a MIDI pass-through recorder 2 | 3 | If you've ever used audio software on the computer, you probably know that MIDI exists: a signalling protocol that allows controllers to control virtual instruments like synths. It's also the protocol used by real audio hardware to talk to each, and you can think of it as the language in which, rather than communicating a fluctuating voltage signal or series of discrete sample values, devices talk about what is being done on them ("A4 got pressed", "F4 got released", "the mod wheel moved down", etc). 4 | 5 | As such, there are two ways to record digital instruments (real or virtual): you can record the sound they're making, or you can record the MIDI events that cause them to make those sounds, and that's where things get interesting. 6 | 7 | There are many, _many_ ways to record audio, from microphones to line monitors to audio interfaces, on dedicated hardware, computers, phones, etc. etc., but there aren't all that many ways to record MIDI events. Essentially: unless you're running software that monitors MIDI events, there just isn't really any way to record MIDI. So I set out to change that: in the same way that you can just hook up an audio field recorder (like a Tascam DR-05) to sit between an audio-out on something that generates audio and an audio-in on something that should be listening to that audio, writing that to an SD card as `.wav` or `.mp3` or the like, I built a MIDI "field recorder" that you plug in between your MIDI-out and some MIDI-in, indiscriminately recording every MIDI event that gets sent over the wire to an SD card as a `.mid` file. 8 | 9 | You'd think this would be something that already exists as a product you can just buy (even if at probably quite the markup because it's made of "powder-coated extruded aluminium" with "audiophile quality" components, but still). Amazingly, it is not. There's just nothing. 10 | 11 | So: if you want a MIDI recorder, you'll have to build one... and if you want to build one, this post might be useful to you! 12 | 13 | 14 | ## Table of contents 15 | 16 | 1. [The circuitry](#the-circuitry) 17 | 1. [MIDI](#the-midi-part-of-our-recorder) 18 | 1. [SD card](#the-sd-part-of-our-recorder) 19 | 1. [MIDI marker button](#adding-a-midi-marker-button) 20 | 1. [All the beep beeps](#adding-a-beep-for-debugging) 21 | 1. [Optional: the real time clock](#bonus-adding-a-real-time-clock) 22 | 1. [The software](#the-software) 23 | 1. [Basics](#program-basics) 24 | 1. [MIDI handling](#midi-handling) 25 | 1. [File management](#file-management) 26 | 1. [Saving MIDI markers](#saving-midi-markers) 27 | 1. [Making some beeps](#making-some-beeps) 28 | 1. [Optional: the real time clock](#adding-the-real-time-clock) 29 | 1. [Idle handling](#creating-a-new-file-when-idling) 30 | 1. [A final helper script](#a-final-helper-script) 31 | 1. [Importing MIDI data into your DAW](#importing-midi-data-into-your-daw) 32 | 1. [Comments/questions](#comments-and-or-questions) 33 | 34 | 35 | ## The circuitry 36 | 37 | 38 | 39 | To build this, we're going to basically build a standard "MIDI-In + MIDI-Thru" circuit using an Arduino, with an SD card module hooked up so we can save the data that comes flying by. To build everything, we'll need some special components: 40 | 41 | 1. An Arduino SD card module (~$10 for a pack of five) 42 | 1. Two female 5-pin DIN connectors (~$5 for a pack of ten) 43 | 1. A 6N138 optocoupler (~$10 for a pack of ten) 44 | 1. Optional: a DS3231-based RTC module 45 | 46 | And of course, the bits that you'll get with pretty much any Arduino starter kit: 47 | 48 | 1. An Arduino UNO R3 or equivalent board 49 | 1. 2x 220 ohm resistors 50 | 1. 1x 4.7k ohm resistor 51 | 1. 2x 10k ohm resistors 52 | 1. A diode 53 | 1. A piezo buzzer 54 | 1. Two clicky pushy buttons 55 | 56 | 57 | ### The MIDI part of our recorder 58 | 59 | We set up MIDI-In on the Arduino `RX<-0` pin, with MIDI-Thru tapping straight into signal that's getting sent to `RX<-0`, too. The only tricky bit about this is that MIDI signals are isolated from the rest of the circuitry via an optocoupler (which gets around ground loop problems by literally transmitting signals by running them through a LED, which emits the electrical signal as light, which then gets picked up by a phototransistor that turns the light back into an electrical signal). When placing and connecting the optocoupler, it is very important to make sure you know which pin is pin 1: it'll have a little mark next to it (typically a dot on the chip casing) to tell you that that side has pins 1 through 4 running top to bottom, and pins 5 through 8 on the other side _running bottom to top_. Also note that we're not using pins 1 and 4 for this circuit: only pins 2 and 3 are connected to the MIDI-In connector, and pins 5 through 8 are connected to the various Arduino pins. 60 | 61 | MIDI circuit diagram 62 | 63 | (I know, "Thru isn't a word!", but that's what [the MIDI spec](http://www.shclemen.com/download/The%20Complete%20MIDI1.0%20Detailed%20Spec.pdf#page=7&zoom=auto,-206,478) calls it, so English gets to take a back seat here...) 64 | 65 | 66 | ### The SD part of our recorder 67 | 68 | The SD card circuitry is literally just a matter of "connect the pins to the pins", with the only oddity being that the pins don't _quite_ line up well enough to literally just stick the SD card module directly into the Arduino. 69 | 70 | However, note that your SD card module **may have a different pin layout** so be sure to double-check before wiring things up! 71 | 72 | SD module diagram 73 | 74 | ### Adding a MIDI marker button 75 | 76 | In order to make it easier to find particularly "worth revisiting" parts of what got recorded, we add a button that connects to pin 4, that we can use to write MIDI markers into our file. There is barely anything to this circuit: 77 | 78 | simple button diagram 79 | 80 | 81 | ### Adding a beep, for debugging 82 | 83 | And finally, we're going to add a little piezo speaker and a button that we can press to turn on (or off) playing a note corresponding to a MIDI note getting played, mostly as the audio equivalent of visual debugging. There's barely any work here: we hook up the "speaker" between pin 8 and ground, and the button to pin 2. Beep, beep! 84 | 85 | beep beep button diagram 86 | 87 | 88 | ### Optional: adding a Real Time Clock 89 | 90 | Our last bit of circuitry is not required in the slightest, but it does improve usability quite a bit: a real-time clock using a DS3231 chip, which is a "fancy" RTC with some smart bits that keeps it accurate regardless of temperature changes. Connecting it is pretty straight forward, and uses some pins on the side of the Arduino we've not used yet, connecting the SDA (or "D") pin to the A4 input, and the SCL (or "C") pin to the A5 input. What will this get us? For one we the SD card library can make us of it to make sure files have a real file date, and secondly, it'll allow us to write MIDI markers that are linked to dates and times, rather than being a simple sequence number. Both these things will make it easier to find past work more easily. 91 | 92 | DS3231 RTC diagram 93 | 94 | 95 | ## The Software 96 | 97 | With the circuitry set up, let's start writing our program, focussing on dealing with each circuit in its own section 98 | 99 | 1. [Program basics](#program-basics) 100 | 1. [Basic signal handling (MIDI library)](#midi-handling) 101 | 1. [Basic file writing (SD library)](#file-management) 102 | 1. [Saving MIDI markers](#saving-midi-markers) 103 | 1. [Audio debugging (beep beep)](#making-some-beeps) 104 | 1. [Usability bonus: adding the real-time clock](#adding-the-real-time-clock) 105 | 1. [Usability bonus 2: "clean restart" on idle](#creating-a-new-file-when-idling) 106 | 1. [Final usability bonus: "fix the track length" script](#a-final-helper-script) 107 | 108 | 109 | ### Program basics 110 | 111 | Our basic program will need to import the standard [SD](https://www.arduino.cc/en/reference/SD) library, as well as the [MIDI](https://github.com/FortySevenEffects/arduino_midi_library) library (which you'll probably need to [install first](https://github.com/FortySevenEffects/arduino_midi_library#getting-started)). 112 | 113 | Note that if you don't want to "follow along" and instead you just want the code, you can copy-paste the code found over in [midi-recorder.ino](https://raw.githubusercontent.com/Pomax/arduino-midi-recorder/master/midi-recorder.ino) into the Arduino IDE. 114 | 115 | ```c++ 116 | #include 117 | #include 118 | 119 | MIDI_CREATE_DEFAULT_INSTANCE(); 120 | 121 | void setup() { 122 | // we'll put some more code here in the next sections 123 | } 124 | 125 | void loop() { 126 | // we'll put some more code here in the next sections 127 | } 128 | ``` 129 | 130 | And we're done! 131 | 132 | Of course this doesn't _do_ anything yet, so let's add the rest of the code, too. 133 | 134 | 135 | ### MIDI handling 136 | 137 | For our MIDI handling, we'll need to set up listeners for MIDI events, and make sure to poll for that data during the program loop: 138 | 139 | ```c++ 140 | void setup() { 141 | MIDI.begin(MIDI_CHANNEL_OMNI); 142 | MIDI.setHandleNoteOn(handleNoteOn); 143 | MIDI.setHandleNoteOff(handleNoteOff); 144 | MIDI.setHandlePitchBend(handlePitchBend); 145 | MIDI.setHandleControlChange(handleControlChange); 146 | } 147 | 148 | void loop() { 149 | checkForMarker(); 150 | setPlayState(); 151 | updateFile(); 152 | MIDI.read(); 153 | } 154 | ``` 155 | 156 | This sets up MIDI listening on all MIDI channels (there are sixteen of them, and we don't want to guess which channels are active), and reads out the MIDI data from `RX<-0` - you may have noticed we don't explicitly set a baud rate: the MIDI spec only allows for 31,250 bits per second, so the Arduino MIDI library automatically makes sure to set the correct polling rate for us. 157 | 158 | You'll notice that `loop()` does four things: we'll only be looking at the MIDI reading in this section, with the next sections covering the other three steps. 159 | 160 | For now, assuming that the first three function calls work (because they will, later =), that leaves implementing our MIDI event handling: 161 | 162 | ```c++ 163 | #define NOTE_OFF_EVENT 0x80 164 | #define NOTE_ON_EVENT 0x90 165 | #define CONTROL_CHANGE_EVENT 0xB0 166 | #define PITCH_BEND_EVENT 0xE0 167 | 168 | void handleNoteOff(byte channel, byte pitch, byte velocity) { 169 | writeToFile(NOTE_OFF_EVENT, pitch, velocity); 170 | } 171 | 172 | void handleNoteOn(byte channel, byte pitch, byte velocity) { 173 | writeToFile(NOTE_ON_EVENT, pitch, velocity); 174 | } 175 | 176 | void handleControlChange(byte channel, byte controller, byte value) { 177 | writeToFile(CONTROL_CHANGE_EVENT, controller, value); 178 | } 179 | 180 | void handlePitchBend(byte channel, int bend_value) { 181 | // First off, we need to "re-center" the bend value, 182 | // because in MIDI, the bend value is a positive value 183 | // in the range 0x0000-0x3FFF with 0x2000 considered 184 | // the "neutral" mid point, whereas the MIDI library 185 | // gives us a signed integer value that uses 0 as its 186 | // midpoint and negative numbers to signify "down". 187 | bend_value += 0x2000; 188 | 189 | // Then, per the MIDI spec, we need to encode the 14 bit 190 | // bend value as two 7-bit bytes, where the first byte 191 | // contains the lowest 7 bits of our bend value, and second 192 | // byte contains the highest 7 bits of our bend value: 193 | byte lowBits = (byte) (bend_value & 0x7F); 194 | byte highBits = (byte) ((bend_value >> 7) & 0x7F); 195 | 196 | writeToFile(PITCH_BEND_EVENT, lowBits, highBits); 197 | } 198 | ``` 199 | 200 | (note that we're ignoring the `channel` byte: we'll be creating a "simple" format 0 MIDI file, and for maximum usability in terms of importing our data in a DAW, we're putting all the events on channel 1. We can see this in our event constants: events use two 4 bit "nibbles" with the first nibble being the event identifier, and the second nibble being the channel that the event happens in, so for example: `NOTE_OFF_EVENT` uses 0x80 for "note off on channel 1", but 0x8F for "note off on channel 16") 201 | 202 | This is a good start, but MIDI events are just that: events, and events happen "at some specific time" which we're still going to have to capture. Standard MIDI events don't rely on absolute values from some real time clock (which is good for us, because Arduinos don't have an RTC built in!) and instead rely on counting a "time delta": it marks events with the number of "MIDI clock ticks" since the previous event, with the very first event in the event stream having an explicit time delta of zero. 203 | 204 | So: let's write a `getDelta()` function that we can use to get the number of MIDI ticks since the last event (=since the last time `getDelta()` got called) so that we have all the data we need ready to start writing MIDI to file: 205 | 206 | ```c++ 207 | unsigned long startTime = 0; 208 | unsigned long lastTime = 0; 209 | 210 | int getDelta() { 211 | if (startTime == 0) { 212 | startTime = millis(); 213 | lastTime = startTime; 214 | return 0; 215 | } 216 | unsigned long now = millis(); 217 | unsigned int delta = (now - lastTime); 218 | lastTime = now; 219 | return delta; 220 | } 221 | ``` 222 | 223 | This function seems bigger than it has to be: we _could_ just start the clock when our sketch starts, setting `lastTime=millis()` in `setup()`, and then in `getDelta` only have the `timeDelta` calculation and `lastTime` update, but that would be explicitly encoding "a lot of nothing" at the start of our MIDI file: we'd be counting the ticks for the first event relative to starting the program, rather than treating the first event as starting at tick zero. So instead, we explicitly encode the time that the first event happens as `startTime` and then we start delta calculation relative to that, instead. 224 | 225 | That then leaves updating our handlers: 226 | 227 | ```c++ 228 | void handleNoteOn(byte channel, byte pitch, byte velocity) { 229 | ... 230 | writeToFile(..., getDelta()); 231 | } 232 | 233 | void handleNoteOff(byte channel, byte pitch, byte velocity) { 234 | ... 235 | writeToFile(..., getDelta()); 236 | } 237 | 238 | void handleControlChange(byte channel, byte controller_code, byte value) { 239 | ... 240 | writeToFile(..., getDelta()); 241 | } 242 | 243 | void handlePitchBend(byte channel, int bend_value) { 244 | ... 245 | writeToFile(..., getDelta()); 246 | } 247 | ``` 248 | 249 | That leaves the last step: writing our data to file, for which we'll need to first look at setting up a file and getting it ready for accepting MIDI events. On to the file management! 250 | 251 | 252 | ### File management 253 | 254 | The `SD` library makes working with SD cards super easy, but of course we're still going to have to write all the code for creating file handles, and writing binary data into them. So first, some setup: 255 | 256 | ```c++ 257 | #define CHIP_SELECT 9 258 | 259 | String filename; 260 | File file; 261 | 262 | void setup() { 263 | // ...previous code... 264 | 265 | pinMode(CHIP_SELECT, OUTPUT); 266 | 267 | if (SD.begin(CHIP_SELECT)) { 268 | findNextFilename(); 269 | if (file) { 270 | createMidiFile(); 271 | } 272 | } 273 | } 274 | 275 | void findNextFilename() { 276 | for (int i = 1; i < 1000; i++) { 277 | filename = "file-"; 278 | if (i < 10) filename += "0"; 279 | if (i < 100) filename += "0"; 280 | filename += String(i); 281 | filename += String(".mid"); 282 | 283 | if (!SD.exists(filename)) { 284 | file = SD.open(filename, FILE_WRITE); 285 | return; 286 | } 287 | } 288 | } 289 | ``` 290 | 291 | Our initial setup is fairly straight forward: we tell the `SD` library that we'll be communicating to the SD card using pin 9, and then we try to create a new file to write to. There's a number of ways of which we can do this, but the simplest is "build a filename, see if it exists, if it doesn't: use that filename". In this case, we create a filename with pattern `file-xxx.mid` where `xxx` ranges from `001` to `999` and we simply pick the first available filename. Another way to do this would be to use the Arduino's EEPROM to store a value so that we get a guaranteed new value each time the Arduino starts up, that would also mean that if we wipe the SD card and turn the Arduino on, we wouldn't start at `001` but some random number, and frankly that's silly. 292 | 293 | So: while this is _also_ silly, it's less silly and we're rolling with it. 294 | 295 | Next, when we have a filename that works we open the file in `FILE_WRITE` mode, which --perhaps counter-intuitively-- means we'll be opening the file in `APPEND` mode: we have read/write access, but the file point is "stuck" at the end of the file and any data we write gets appended to what's already there. For MIDI files, which are essentially streams of events, that's exactly what we need, so we move on: we need to write a bit of boilerplate data into our new file, after which we can start dealing with recording actual MIDI events that we see flying by in the MIDI handlers we wrote in the previous section. 296 | 297 | ```c++ 298 | void createMidiFile() { 299 | byte header[] = { 300 | 0x4D, 0x54, 0x68, 0x64, // "MThd" chunk 301 | 0x00, 0x00, 0x00, 0x06, // chunk length (from this point on): 6 bytes 302 | 0x00, 0x00, // format: 0 303 | 0x00, 0x01, // number of tracks: 1 304 | 0x01, 0xC2 // data rate: 450 ticks per quaver/quarter note 305 | }; 306 | file.write(header, 14); 307 | 308 | byte track[] = { 309 | 0x4D, 0x54, 0x72, 0x6B, // "MTrk" chunk 310 | 0x00, 0x00, 0x00, 0x00 // chunk length placeholder 311 | }; 312 | file.write(track, 8); 313 | 314 | byte tempo[] = { 315 | 0x00, // time delta for the first MIDI event: zero 316 | 0xFF, 0x51, 0x03, // MIDI event type: "tempo" instruction 317 | 0x06, 0xDD, 0xD0 // tempo value: 450,000μs per quaver/quarter note 318 | }; 319 | file.write(tempo, 7); 320 | } 321 | ``` 322 | 323 | Rather than explaining why we need this data, I will direct you to [The MIDI File Format](http://www.music.mcgill.ca/~ich/classes/mumt306/StandardMIDIfileformat.html) specification, but the short version is that this is all boiler plate bytecode if we want a single event stream MIDI file, with two custom values: 324 | 325 | 1. we get to choose the data rate in the header, and we went with 450 ticks per quaver/quarter note, and 326 | 2. we also get to choose the "play speed", which we set just a touch under half a second per quaver/quarter note. 327 | 328 | You may also notice that we've set the track length to zero: normally this value gets set to the byte length of the track when you save a `.mid` file on, say, your computer, but we don't know what that length is yet. In fact, we're never going to make our code figure that out: we'll write a small [Python](https://python.org) script to help set that value only when it's important (e.g. when you're ready to import the data into whatever audio application you have that you want to load MIDI data into). 329 | 330 | And with that, it's time to get to the entire reason you're reading along: the code that writes incoming MIDI signals to our file: let's implement `writeToFile`: 331 | 332 | ```c++ 333 | void writeToFile(byte eventType, byte b1, byte b2, int delta) { 334 | if (!file) return; 335 | writeVarLen(delta); 336 | file.write(eventType); 337 | file.write(b1); 338 | file.write(b2); 339 | } 340 | ``` 341 | 342 | That's... that's not a lot of code. And the reason it's not a lot of code is that MIDI was intended to be super small both to send and to read/write. The only tricky part is the `writeVarLen()` function, which turns integers into their corresponding byte sequences. Thankfully, the MIDI spec handily provides the code necessary to achieve this, so we simply adopt that for our Arduino program and we're good to go: 343 | 344 | ```c++ 345 | #define HAS_MORE_BYTES 0x80 346 | 347 | void writeVarLen(unsigned long value) { 348 | // Start with the first 7 bit block 349 | unsigned long buffer = value & 0x7f; 350 | 351 | // Then shift in 7 bit blocks with "has-more" bit from the 352 | // right for as long as `value` has more bits to encode. 353 | while ((value >>= 7) > 0) { 354 | buffer <<= 8; 355 | buffer |= HAS_MORE_BYTES; 356 | buffer |= value & 0x7f; 357 | } 358 | 359 | // Then unshift bytes one at a time for as long as the has-more bit is high. 360 | while (true) { 361 | file.write((byte)(buffer & 0xff)); 362 | if (buffer & HAS_MORE_BYTES) { 363 | buffer >>= 8; 364 | } else { 365 | break; 366 | } 367 | } 368 | } 369 | ``` 370 | 371 | This allocates 4 bytes and then copies the input value in 7-bit chunks for each byte, with the byte's highest bit set to `0` if there's going to be another byte, or `1` if this is the last byte. This turns the input value into a buffer that has MSBF-ordered bits-per-byte, but LSBF-ordered bytes-per-buffer. The `while(true)` then writes those bytes to file in reverse, so they end up MSBF-ordered in the file. Nothing fancy, but just fancy enough to be fast. 372 | 373 | 374 | ### Saving MIDI markers 375 | 376 | Sometimes you play something you like enough that you want to make sure you don't have to hunt for it later, but have a convenient marker that you can look for instead. You could go "that was great, let me play C1-C6-C1-C6-C1-C6 so I have a visual cue later!" but that's kind of silly when MIDI allows you to place markers: let's write some code that lets us place markers with the push of a button! 377 | 378 | First, we hook up our marker button on pin 4: 379 | 380 | ```c++ 381 | #define PLACE_MARKER_PIN 4 382 | 383 | int lastMarkState = 0; 384 | int nextMarker = 1; 385 | 386 | void setup() { 387 | // ...previous code... 388 | 389 | pinMode(PLACE_MARKER_PIN, INPUT); 390 | } 391 | ``` 392 | 393 | With that, we can implement the `checkForMarkers()` function that we saw in our `loop()` earlier, so that we can add MIDI marker file-writing whenever we press the "set a marker" button: 394 | 395 | ```c++ 396 | void checkForMarker() { 397 | int markState = digitalRead(PLACE_MARKER_PIN); 398 | if (markState != lastMarkState) { 399 | lastMarkState = markState; 400 | if (markState == 1) { 401 | writeMidiMarker(); 402 | } 403 | } 404 | } 405 | ``` 406 | 407 | The only special thing that's going on here is that when we press our button, we want that the program to know that it should write a MIDI marker to our SD card, so during the program loop we check to see if there's a "high" signal coming from our button. If there is, then we're pressing it, and we check whether we were previously _not_ pressing it so that we only do something "when the button gets pressed" rather than "for as long as the button is kept pressed". (If we didn't check whether the previous state was low, we'd end up writing MIDI markers to our SD card 32,150 times a second. Which would be bad!) 408 | 409 | That leaves implementing the actual `writeMidiMarker()` function, which needs to write a MIDI event like any other, using a time delta, the code `FF 06` to indicate this will be a MIDI marker, and a label (in ASCII) + "how many bytes in the label" value (although for parsing reasons, those last two are ordered as length first, then string data). 410 | 411 | We're not going to get fancy with our labels: plain sequential numbers will do, with our first marker having label `"1"`, the next having label `"2"`, etc. That sounds simple enough, but we do need to make sure we save those numbers _as text_ and not as numbers, so we'll need to be careful with the length value that is part of the event payload: while the text `"1"` has length 1, the text `"10"` has length 2 because it's two characters long. So let's get coding: 412 | 413 | ```c++ 414 | void writeMidiMarker() { 415 | if (!file) return; 416 | 417 | // Delta + event code 418 | writeVarLen(file, getDelta()); 419 | file.write(0xFF); 420 | file.write(0x06); 421 | 422 | // How many bytes are we writing? 423 | byte len = 1; 424 | if (nextMarker > 9) len++; // Allowing for more than 9 markers is fair. 425 | if (nextMarker > 99) len++; // ... but this would be a lot of markers. 426 | if (nextMarker > 999) len++; // ... and at this point, I don't think this is the right hardware for you! O_O 427 | writeVarLen(file, len); 428 | 429 | // Then we convert our sequence number to a string, 430 | // and write that to file as a byte sequence: 431 | byte marker[len]; 432 | String(nextMarker++).getBytes(marker, len); 433 | file.write(marker, len); 434 | } 435 | ``` 436 | 437 | And that's it! However, note that if you want to use markers you should be aware of [whether or not your preferred DAW of choice supports them](#importing-midi-data-into-your-daw). Not all of them do, and some of them make importing markers rather _a lot_ harder than it reasonably should be. So depending on which DAW you use, you might still want to tap C1-C6-C1-C6-C1-C6 or something as visual cue =( 438 | 439 | 440 | ### Making some beeps 441 | 442 | With MIDI handling and file writing taken care of, one thing that's just a nice-to-have is being able to confirm that your MIDI event handling works, for which we're going to use our "speaker" and button for. First, we set up the code that lets us decide whether to beep, or not to beep: 443 | 444 | ```c++ 445 | #define AUDIO_DEBUG_PIN 2 446 | 447 | int lastPlayState = 0; 448 | bool play = false; 449 | 450 | void setup() { 451 | // ...previous code... 452 | 453 | pinMode(AUDIO_DEBUG_PIN, INPUT); 454 | } 455 | 456 | void setPlayState() { 457 | int playState = digitalRead(AUDIO_DEBUG_PIN); 458 | if (playState != lastPlayState) { 459 | lastPlayState = playState; 460 | if (playState == 1) play = !play; 461 | } 462 | } 463 | ``` 464 | 465 | We use the same "check signal goes from low to high" as for our MIDI marker button, except we use it to toggle the `play` boolean: if it was `false`, we flip it to `true`, and if it was `true`, we flip it to `false`. 466 | 467 | So, with that part covered, let's add some beeps so that when we press a key on our MIDI device, we hear the corresponding note in all its pristine, high quality piezo-buzz audio: 468 | 469 | ```c++ 470 | void handleNoteOn(byte CHANNEL, byte pitch, byte velocity) { 471 | writeToFile(NOTE_ON_EVENT | CHANNEL, pitch, velocity, getDelta()); 472 | if (play) tone(AUDIO, 440 * pow(2, (pitch - 69.0) / 12.0), 100); 473 | } 474 | ``` 475 | 476 | Again, very little code, with the only surprise probably being that second argument for `tone()`: MIDI notes, while they claim to send a `pitch` value, actually send a pitch _identifier_, so rather than some audio frequency they say which "piano key" is active. To turn that into the corresponding audio frequency, we need to establish a few things: 477 | 478 | 1. what kind of tuning system we want to use, and 479 | 2. what the base frequency for A over middle C is. 480 | 481 | To keep things simple, because we're only writing this code for some debugging (and maybe fun), we'll use the standard [twelve tone equal temperament](https://en.wikipedia.org/wiki/12_equal_temperament) tuning, where every 12 note step doubles the audible frequency, with equal logarithmic steps from note to note, and a base frequency for A over middle C of [440 Herz](https://en.wikipedia.org/wiki/A440_(pitch_standard)). Of course, because there are _plenty_ of notes below A over middle C, we need to correct our pitch identifier for the MIDI pitch value for that key, which is 69, and so that gives us the formula: 482 | 483 | ``` 484 | (MIDI pitch - 69) / 12 485 | frequency in Herz = 440 * 2 486 | 487 | ``` 488 | 489 | So now if we start our program, and we press our button, playing notes on our MIDI device will make the Arduino beep along with what we're playing. Of course, the `tone()` function can only play a single note at a time, so it's going to sound wonky if we play chords, but it'll be beeping as best as it can. 490 | 491 | Beep, beep! 492 | 493 | 494 | ### Adding the real time clock 495 | 496 | We can improve our files and MIDI markers by adding the RTC to the mix, which will do two things for us: 497 | 498 | 1. give us real datetimes for our files, and 499 | 1. letting us set MIDI markers with the actual time you pressed the marker button 500 | 501 | So let's make that happen. First, in order to talk to an RTC we need to include a RTC library. I say "a" because there are quite a few to pick from, but I used the [RTClib](https://www.arduino.cc/reference/en/libraries/rtclib/) library by [Adafruit](https://www.adafruit.com/), so let's stick with that. 502 | 503 | ```c++ 504 | #include 505 | #include 506 | #include 507 | 508 | RTC_DS3231 RTC; 509 | bool HAS_RTC = false; 510 | ``` 511 | 512 | And then in `setup` we can both initialise the RTC interface, as well as let the SD module know that there's now an RTC that it can ask for date/time information when it needs it: 513 | 514 | ```C++ 515 | void setup() { 516 | ... 517 | 518 | if (RTC.begin()) { 519 | // This line is special: we only need it once, and after that we're deleting it: 520 | RTC.adjust(DateTime(F(__DATE__), F(__TIME__))); 521 | 522 | // if the RTC works, we can tell the SD library 523 | // how it can check for the current time when it 524 | // needs timestamping for file creation/writing. 525 | SdFile::dateTimeCallback(dateTime); 526 | HAS_RTC = true; 527 | } 528 | 529 | ... 530 | } 531 | 532 | void dateTime(uint16_t* date, uint16_t* time) { 533 | DateTime d = RTC.now(); 534 | *date = FAT_DATE(d.year(), d.month(), d.day()); 535 | *time = FAT_TIME(d.hour(), d.minute(), d.second()); 536 | } 537 | ``` 538 | 539 | You'll notice that `RTC.adjust(...)` line: this line "resets" the RTC to a new date and time, because the odds that you bought an RTC that already has the correct time set is practically zero. As such, this line, when compiled and uploaded to your Arduino, replaces those `F(__DATE__)` and `F(__TIME__)` with the actual date and time when the code gets compiled. But, we only need to do this once: immediately after uploading the sketch to the Arduino, we take that line out and _recompile and upload the code again_. 540 | 541 | We also see a call that to `SdFile::dateTimeCallback(dateTime)`, which tells the SD card library that whenever it needs to get a date/time value, it can call our `dateTime` function to get those values. 542 | 543 | With that covered, we can now also update our MIDI marker code: 544 | 545 | ```c++ 546 | void writeMidiMarker() { 547 | if (!file) return; 548 | 549 | writeVarLen(file, getDelta()); 550 | file.write(0xFF); 551 | file.write(0x06); 552 | 553 | if (HAS_RTC) { 554 | DateTime d = RTC.now(); 555 | byte len = 20; 556 | writeVarLen(file, len); 557 | 558 | char marker[len]; 559 | sprintf( 560 | marker, 561 | "%04d/%02d/%02d, %02d:%02d:%02d", 562 | d.year(), d.month(), d.day(), d.hour(), d.minute(), d.second() 563 | ); 564 | file.write(marker, len); 565 | } 566 | 567 | else { 568 | // this is where we put the code we originally wrote. 569 | } 570 | } 571 | ``` 572 | 573 | If we have an RTC available, rather than writing out a sequential number, we can write real date/time strings. We use `sprintf` to fill our marker label's char array with a string composed of RTC values, forming a string of the form "2021/01/15, 15:20:00" rather than a simple number "1", "2", etc. 574 | 575 | 576 | ### Creating a new file when idling 577 | 578 | There's one feature that's still missing... remember that the whole point of this recorder is to record the MIDI events that your MIDI-out device generates because you're using it. But what we _don't_ want it to do is "record an hour of silence because you stopped playing and went off to do something else for a bit"! 579 | 580 | To that end, what we would like is for our program to detect that you've _not_ been playing anything for a while (say, a few minutes) and then stop recording, starting recording on a new file when you _do_ start playing again. 581 | 582 | As it so happens, the first part is _constantly_ true, because we're only writing to our file when new data comes in, and we've _also_ already implemented the second part: that happens automatically when you turn on the Arduino, so the only thing we're missing is a way to detect whether there's not been any input for a while: 583 | 584 | ```c++ 585 | // we use a 2 minute idling timeout, expressed in milliseconds 586 | #define RECORDING_TIMEOUT 120000 587 | #define FILE_FLUSH_INTERVAL 400 588 | unsigned long lastLoopCounter = 0; 589 | unsigned long loopCounter = 0; 590 | 591 | void updateFile() { 592 | loopCounter = millis(); 593 | if (loopCounter - lastLoopCounter > FILE_FLUSH_INTERVAL) { 594 | checkReset(); 595 | lastLoopCounter = loopCounter; 596 | file.flush(); 597 | } 598 | } 599 | 600 | void checkReset() { 601 | if (startTime == 0) return; 602 | if (!file) return; 603 | if (millis() - lastTime > RECORDING_TIMEOUT) { 604 | file.close(); 605 | resetArduino(); 606 | } 607 | } 608 | 609 | void(* resetArduino) (void) = 0; 610 | ``` 611 | 612 | That might do more than you thought, so let's look at what's happening. 613 | 614 | First, we want to check whether any MIDI activity has happened during the program loop, but we _don't_ want to check that 32,150 times each second. So, instead, we set up some standard code to check every 400 milliseconds, where we check whether the difference between the `lastTime` (which is the millisecond timestamp for the last MIDI event) and the current `millis()` value is more than 2 minutes, counted in milliseconds. If it is, and we've already been recording MIDI events, then we're just idling and we can restart the Arduino so that we're "working" in a new file rather than writing multiple sessions to the same file with huge silences in between. Of course, if we've not recorded any MIDI events yet, we just do nothing: the first event we'll write "starts the clock", so we never have any "leading silence" in our MIDI file =) 615 | 616 | Also note that we're closing our file before we reset: while we're already flushing any in-memory file data to disk every 400 milliseconds, it's always good form to close a file handle when you can. 617 | 618 | Finally, there's the `resetArduino()` "function". This doesn't look like any function you've seen, and is really not so much a normal function as "an exploit of the Arduino chipset's watchdog": we're intentionally doing something illegal, which would normally cause a crash. However, rather than crashing, the Arduino has a watchdog that restarts the Arduino when it sees a crash, so your board is always either in a working state, or coming online. There are a million ways to make C++ code do something illegal, but in this case we're defining a function pointer that tries to execute whatever's in memory address 0. That's _incredibly wrong_ and so when we execute that call, the Arduino goes "Whoa, wait, what? ERK!" and then restarts. It's delightfully effective. 619 | 620 | 621 | ### A final helper script 622 | 623 | One last thing we'll want to do is not actually related to circuits or Arduino programming, but has to do with fixing a loose end: our track length. 624 | 625 | Remember that we're writing a MIDI file to our SD card, and MIDI files need to say how long each track is... but we don't know how long each track is because we're constantly changing the length with every MIDI event we're appending to the file. 626 | 627 | Now, some programs (like [FL Studio](https://www.image-line.com/)) don't even care if there _is_ a valid track length: a MIDI file with only a single track has, almost by definition, a track length that takes up "the rest of the file", but some programs (like [Reaper](https://reaper.fm)) care quite a lot, and to help those programs out we can write a very simple [Python]() script that can run through all `.mid` files on our SD card and set the track length to the correct bytes. 628 | 629 | We can create a new file called `fix.py` on our SD card, and then paste the follow code into it: 630 | 631 | ```python 632 | import os 633 | 634 | midi_files = [f for f in os.listdir('.') if os.path.isfile(f) and '.MID' in f] 635 | 636 | for filename in midi_files: 637 | # Open the file in binary read/write mode: 638 | file = open(filename, "rb+") 639 | 640 | # As single-track MIDI files, we know that the track length is 641 | # equal to the file size, minus the header size up to and 642 | # including the length value, which is 22 bytes: 643 | file_size = os.path.getsize(filename) 644 | track_length = file_size - 22 645 | 646 | # With that done, we can form our new byte values: 647 | field_value = bytearray([ 648 | (track_length & 0xFF000000) >> 24, 649 | (track_length & 0x00FF0000) >> 16, 650 | (track_length & 0x0000FF00) >> 8, 651 | (track_length & 0x000000FF), 652 | ]) 653 | 654 | # And then we write the update to our file: 655 | file.seek(18) 656 | file.write(field_value) 657 | file.close() 658 | print(f"Updated {filename} track length to {track_length}") 659 | ``` 660 | 661 | With this, every time we want to load our `.mid` files from SD card into a DAW or other MIDI-compatible program, we can just run this script first, and irrespective of whether our `.mid` files had valid track length values or not they will be guaranteed to have correct values once the script finishes. 662 | 663 | (You can also directly download this script [here](https://raw.githubusercontent.com/Pomax/arduino-midi-recorder/master/fix.py)) 664 | 665 | 666 | ### And that's it: we're done! 667 | 668 | That's it, all that reminds is to assemble the circuits, and put all the code together, and you have yourself an Arduino MIDI recorder! Thanks for reading along, and if you put this together and put it to good use, shoot me a message! I love hearing from folks who put what I make to good use (or _bad_!) use =D 669 | 670 | 671 | ## Importing MIDI data into your DAW 672 | 673 | Now that you've got something that create `.mid` files, you'll probably want to import those `.mid` files into your DAW: depending on your choice in DAW that may or may not be straightforward, so here are the various ways in which do achieve an import in some common DAWs, and if you use a different DAW but know how to import a `.mid` including its automation, let me know and we can add it to the list! 674 | 675 | 676 | ### Reaper 6 (Cockos) 677 | 678 | Drag the `.mid` file onto a track. If you have MIDI markers, you will be given the option to import them. 679 | 680 | To view your automation data, open the track's piano roll and use the events picker below the keyboard. 681 | 682 | 683 | ### Cubase 11 (Steinberg) 684 | 685 | Drag the `.mid` file onto a track. 686 | 687 | To view your automation data, open the track's piano roll and use the events picker below the keyboard. 688 | 689 | 690 | ### Studio One 5 (Presonus) 691 | 692 | Drag the `.mid` file onto a track. 693 | 694 | To view your automation data, select and double-click the track to open its piano roll, and use the events buttons below the piano roll/above the events pane. 695 | 696 | Importing markers is _technically_ possible, but it might as well not be: see [https://www.youtube.com/watch?v=Se1Anm1-yaw](https://www.youtube.com/watch?v=Se1Anm1-yaw) for how needlessly complicated this is. 697 | 698 | 699 | ### Ableton Live 10 700 | 701 | Drag the `.mid` file onto a MIDI track. 702 | 703 | To view your automation data, open the track's piano roll and turn on the `Envelope Box` in the channel strip (click the "linked dots" icon to the right of the music note icon at the bottom of the `clip` channel strip module) then pick the control you want to see the automation for in the `Envelopes` channel strip module to the right. 704 | 705 | Importing markers is not possible. 706 | 707 | 708 | ### FL Studio 20 (Image-Line) 709 | 710 | Drag the `.mid` file onto the application background (_not_ onto the channel rack or into an open pattern). When prompted to save changes, say "no", then uncheck "start new project" in the MIDI import dialog, then hit "accept". This will create a new channel rack entry and placable pattern. To see your data, place the pattern in the playlist, and to assign the instrument you want to play that data, simply drag any VST/AU/FL instrument onto the channel rack entry that got built for your import. 711 | 712 | To view your automation data, place the import-associated pattern and open its piano roll, then use the "control" picker to select the control you want to see the automation for. 713 | 714 | Importing markers is not possible. 715 | 716 | 717 | ## Comments and/or questions 718 | 719 | Hit up [the issue tracker](https://github.com/Pomax/arduino-midi-recorder/issues) if you want to have a detailed conversation, or reach out on [Mastodon](https://mastodon.social/@TheRealPomax) if you want the kind of momentary engagement the internet seems to be for these days =) 720 | 721 | — Pomax 722 | -------------------------------------------------------------------------------- /RTC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/arduino-midi-recorder/1581795138eccd51e3043e0a33f0b3c49a8ad483/RTC.png -------------------------------------------------------------------------------- /banner-small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/arduino-midi-recorder/1581795138eccd51e3043e0a33f0b3c49a8ad483/banner-small.jpg -------------------------------------------------------------------------------- /banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/arduino-midi-recorder/1581795138eccd51e3043e0a33f0b3c49a8ad483/banner.jpg -------------------------------------------------------------------------------- /beep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/arduino-midi-recorder/1581795138eccd51e3043e0a33f0b3c49a8ad483/beep.png -------------------------------------------------------------------------------- /button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/arduino-midi-recorder/1581795138eccd51e3043e0a33f0b3c49a8ad483/button.png -------------------------------------------------------------------------------- /fix.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | midi_files = [f for f in os.listdir('.') if os.path.isfile(f) and '.MID' in f] 4 | 5 | for filename in midi_files: 6 | file = open(filename, "rb+") 7 | file_size = os.path.getsize(filename) 8 | track_length = file_size - 22 9 | 10 | field_value = bytearray([ 11 | (track_length & 0xFF000000) >> 24, 12 | (track_length & 0x00FF0000) >> 16, 13 | (track_length & 0x0000FF00) >> 8, 14 | (track_length & 0x000000FF), 15 | ]) 16 | 17 | file.seek(18) 18 | file.write(field_value) 19 | file.close() 20 | print(f"Updated {filename} track length to {track_length}") 21 | -------------------------------------------------------------------------------- /midi-recorder.ino: -------------------------------------------------------------------------------- 1 | /********************************************************* 2 | 3 | This is the code for a prototype inline MIDI recorder 4 | that sits between a MIDI-OUT device (like a controller) 5 | and a MIDI-IN device (like a computer) for saving all 6 | note, pitch, and CC data that's being played without 7 | having any device specifically set to "record". 8 | 9 | This is Public Domain code, with all the caveats that 10 | comes with for you, because public domain is not the 11 | same as open source. 12 | 13 | See https://opensource.org/node/878 for more details. 14 | 15 | The reason this code is in the public domain is 16 | because anyone with half a brain and a need to 17 | create this functionality will reasonably end up 18 | with code that's so similar as to effectively be 19 | the same as what has been written here. 20 | 21 | Having said that, there are countries that do not 22 | recognize the Public Domain. In those countries, 23 | this code is to be considered as being provided 24 | under an MIT license. See the end of this file 25 | for its full license text. 26 | ********************************************************/ 27 | 28 | // File and MIDI handling 29 | #include 30 | #include 31 | 32 | // Our Real Time Clock 33 | #include 34 | RTC_DS3231 RTC; 35 | bool HAS_RTC = false; 36 | 37 | // Audio pins and values 38 | #define AUDIO 7 39 | #define AUDIO_DEBUG_PIN 2 40 | int lastPlayState = 0; 41 | bool play = false; 42 | 43 | // Marker pins and values 44 | #define PLACE_MARKER_PIN 4 45 | int lastMarkState = 0; 46 | int nextMarker = 1; 47 | 48 | #define CHIP_SELECT 9 49 | #define HAS_MORE_BYTES 0x80 50 | 51 | #define NOTE_OFF_EVENT 0x80 52 | #define NOTE_ON_EVENT 0x90 53 | #define CONTROL_CHANGE_EVENT 0xB0 54 | #define PITCH_BEND_EVENT 0xE0 55 | 56 | // we use a 2 minute idling timeout (in millis) 57 | #define RECORDING_TIMEOUT 120000 58 | unsigned long lastLoopCounter = 0; 59 | unsigned long loopCounter = 0; 60 | 61 | unsigned long startTime = 0; 62 | unsigned long lastTime = 0; 63 | 64 | #define FILE_FLUSH_INTERVAL 400 65 | String filename; 66 | File file; 67 | 68 | MIDI_CREATE_DEFAULT_INSTANCE(); 69 | 70 | /** 71 | Set up our inline MIDI recorder 72 | */ 73 | void setup() { 74 | // set up MIDI handling 75 | MIDI.begin(MIDI_CHANNEL_OMNI); 76 | MIDI.setHandleNoteOn(handleNoteOn); 77 | MIDI.setHandleNoteOff(handleNoteOff); 78 | MIDI.setHandlePitchBend(handlePitchBend); 79 | MIDI.setHandleControlChange(handleControlChange); 80 | 81 | // set up the tone playing button 82 | pinMode(AUDIO_DEBUG_PIN, INPUT); 83 | pinMode(AUDIO, OUTPUT); 84 | // tone(AUDIO, 440, 200); 85 | 86 | // set up the MIDI marker button 87 | pinMode(PLACE_MARKER_PIN, INPUT); 88 | 89 | // set up RTC interfacing 90 | if (RTC.begin()) { 91 | // uncomment this line to set the current date/time on the RTC 92 | // RTC.adjust(DateTime(F(__DATE__), F(__TIME__))); 93 | 94 | // if the RTC works, we can tell the SD library 95 | // how it can check for the current time when it 96 | // needs timestamping for file creation/writing. 97 | SdFile::dateTimeCallback(dateTime); 98 | HAS_RTC = true; 99 | // tone(AUDIO, 880, 100); 100 | } 101 | 102 | // set up SD card functionality and allocate a file 103 | pinMode(CHIP_SELECT, OUTPUT); 104 | if (SD.begin(CHIP_SELECT)) { 105 | creatNextFile(); 106 | if (file) { 107 | writeMidiPreamble(); 108 | // tone(AUDIO, 1760, 100); 109 | } 110 | } 111 | } 112 | 113 | void dateTime(uint16_t* date, uint16_t* time) { 114 | DateTime d = RTC.now(); 115 | *date = FAT_DATE(d.year(), d.month(), d.day()); 116 | *time = FAT_TIME(d.hour(), d.minute(), d.second()); 117 | } 118 | 119 | 120 | /** 121 | We could use the EEPROM to store this number, 122 | but since we're not going to get timestamped 123 | files anyway, just looping is also fine. 124 | */ 125 | void creatNextFile() { 126 | for (int i = 1; i < 1000; i++) { 127 | filename = "file-"; 128 | if (i < 10) filename += "0"; 129 | if (i < 100) filename += "0"; 130 | filename += String(i); 131 | filename += String(".mid"); 132 | 133 | if (!SD.exists(filename)) { 134 | file = SD.open(filename, FILE_WRITE); 135 | return; 136 | } 137 | } 138 | } 139 | 140 | /** 141 | Set up a new MIDI file with some boiler plate byte code 142 | */ 143 | void writeMidiPreamble() { 144 | byte header[] = { 145 | 0x4D, 0x54, 0x68, 0x64, // "MThd" chunk 146 | 0x00, 0x00, 0x00, 0x06, // chunk length (from this point on) 147 | 0x00, 0x00, // format 0 148 | 0x00, 0x01, // one track 149 | 0x01, 0xD4 // data rate = 458 ticks per quarter note 150 | }; 151 | file.write(header, 14); 152 | 153 | byte track[] = { 154 | 0x4D, 0x54, 0x72, 0x6B, // "MTrk" chunk 155 | 0x00, 0x00, 0x00, 0x00 // chunk length placeholder (MSB) 156 | }; 157 | file.write(track, 8); 158 | 159 | byte tempo[] = { 160 | 0x00, // time delta (of zero) 161 | 0xFF, 0x51, 0x03, // tempo op code 162 | 0x06, 0xFD, 0x1F // real rate = 458,015μs per quarter note (= 134.681 BPM) 163 | }; 164 | file.write(tempo, 7); 165 | 166 | // zero out the timer for this file. 167 | startTime = 0; 168 | } 169 | 170 | /** 171 | The program loop consists of flushing our file to disk, 172 | checking our buttons to see if they just got pressed, 173 | and then handling MIDI input, if there is any. 174 | */ 175 | void loop() { 176 | checkForMarker(); 177 | setPlayState(); 178 | updateFile(); 179 | MIDI.read(); 180 | } 181 | 182 | 183 | // ====================================================================================== 184 | 185 | 186 | /** 187 | We flush the file's in-memory content to disk 188 | every 400ms, allowing. That way if we take the 189 | SD card out, it's basically impossible for any 190 | data to have been lost. 191 | */ 192 | void updateFile() { 193 | loopCounter = millis(); 194 | if (loopCounter - lastLoopCounter > FILE_FLUSH_INTERVAL) { 195 | checkReset(); 196 | lastLoopCounter = loopCounter; 197 | file.flush(); 198 | } 199 | } 200 | 201 | /** 202 | This "function" would normally crash any kernel that tries 203 | to run it by violating memory access. Instead, the Arduino's 204 | watchdog will auto-reboot, giving us a software "reset". 205 | */ 206 | void(* resetArduino) (void) = 0; 207 | 208 | /** 209 | if we've not received any data for 2 minutes, and we were 210 | previously recording, we reset the arduino so that when 211 | we start playing again, we'll be doing so in a new file, 212 | rather than having multiple sessions with huge silence 213 | between them in the same file. 214 | */ 215 | void checkReset() { 216 | if (startTime == 0) return; 217 | if (!file) return; 218 | if (millis() - lastTime > RECORDING_TIMEOUT) { 219 | file.close(); 220 | resetArduino(); 221 | } 222 | } 223 | 224 | /** 225 | A little audio-debugging: pressing the button tied to the 226 | audio debug pin will cause the program to play notes for 227 | every MIDI note-on event that comes flying by. 228 | */ 229 | void setPlayState() { 230 | int playState = digitalRead(AUDIO_DEBUG_PIN); 231 | if (playState != lastPlayState) { 232 | lastPlayState = playState; 233 | if (playState == 1) { 234 | play = !play; 235 | } 236 | } 237 | } 238 | 239 | /** 240 | This checks whether the MIDI marker button got pressed, 241 | and if so, writes a MIDI marker message into the track. 242 | */ 243 | void checkForMarker() { 244 | int markState = digitalRead(PLACE_MARKER_PIN); 245 | if (markState != lastMarkState) { 246 | lastMarkState = markState; 247 | if (markState == 1) { 248 | writeMidiMarker(); 249 | } 250 | } 251 | } 252 | 253 | /** 254 | Write a MIDI marker to file, by writing a delta, then 255 | the op code for "midi marker", the number of letters 256 | the marker label has, and then the label (using ASCII). 257 | 258 | For simplicity, the marker labels will just be a 259 | sequence number starting at "1". 260 | */ 261 | void writeMidiMarker() { 262 | if (!file) return; 263 | 264 | // delta + event code 265 | writeVarLen(file, getDelta()); 266 | file.write(0xFF); 267 | file.write(0x06); 268 | 269 | // If we have an RTC available, we can write the clock time 270 | // Otherwise, write a sequence number. 271 | 272 | if (HAS_RTC) { 273 | DateTime d = RTC.now(); 274 | byte len = 20; 275 | writeVarLen(file, len); 276 | 277 | char marker[len]; // will hold strings like "2021/01/23, 10:53:31" 278 | sprintf(marker, "%04d/%02d/%02d, %02d:%02d:%02d", d.year(), d.month(), d.day(), d.hour(), d.minute(), d.second()); 279 | file.write(marker, len); 280 | } 281 | 282 | else { 283 | // how many letters are we writing? 284 | byte len = 1; 285 | if (nextMarker > 9) len++; 286 | if (nextMarker > 99) len++; 287 | if (nextMarker > 999) len++; 288 | writeVarLen(file, len); 289 | 290 | // our label: 291 | byte marker[len]; 292 | String(nextMarker++).getBytes(marker, len); 293 | file.write(marker, len); 294 | } 295 | } 296 | 297 | 298 | // ====================================================================================== 299 | 300 | 301 | void handleNoteOff(byte channel, byte pitch, byte velocity) { 302 | writeToFile(NOTE_OFF_EVENT, pitch, velocity, getDelta()); 303 | } 304 | 305 | void handleNoteOn(byte channel, byte pitch, byte velocity) { 306 | writeToFile(NOTE_ON_EVENT, pitch, velocity, getDelta()); 307 | if (play) tone(AUDIO, 440 * pow(2, (pitch - 69.0) / 12.0), 100); 308 | } 309 | 310 | void handleControlChange(byte channel, byte cc, byte value) { 311 | writeToFile(CONTROL_CHANGE_EVENT, cc, value, getDelta()); 312 | } 313 | 314 | void handlePitchBend(byte channel, int bend) { 315 | bend += 0x2000; // MIDI bend uses the range 0x0000-0x3FFF, with 0x2000 as center. 316 | byte lsb = bend & 0x7F; 317 | byte msb = bend >> 7; 318 | writeToFile(PITCH_BEND_EVENT, lsb, msb, getDelta()); 319 | } 320 | 321 | /** 322 | This calculates the number of ticks since the last MIDI event 323 | */ 324 | int getDelta() { 325 | if (startTime == 0) { 326 | // if this is the first event, even if the Arduino's been 327 | // powered on for hours, this should be delta zero. 328 | startTime = millis(); 329 | lastTime = startTime; 330 | return 0; 331 | } 332 | unsigned long now = millis(); 333 | unsigned int delta = (now - lastTime); 334 | lastTime = now; 335 | return delta; 336 | } 337 | 338 | /** 339 | Write "common" MIDI events to file, where common MIDI events 340 | all use the following data format: 341 | 342 | 343 | 344 | See the "Standard MIDI-File Format" for more information. 345 | */ 346 | void writeToFile(byte eventType, byte b1, byte b2, int delta) { 347 | if (!file) return; 348 | writeVarLen(file, delta); 349 | file.write(eventType); 350 | file.write(b1); 351 | file.write(b2); 352 | } 353 | 354 | /** 355 | Encode a unsigned 32 bit integer as variable-length byte sequence 356 | of, at most, 4 7-bit-with-has-more bytes. This function is supplied 357 | as part of the MIDI file format specification. 358 | */ 359 | void writeVarLen(File file, unsigned long value) { 360 | // capture the first 7 bit block 361 | unsigned long buffer = value & 0x7f; 362 | 363 | // shift in 7 bit blocks with "has-more" bit from the 364 | // right for as long as `value` has more bits to encode. 365 | while ((value >>= 7) > 0) { 366 | buffer <<= 8; 367 | buffer |= HAS_MORE_BYTES; 368 | buffer |= value & 0x7f; 369 | } 370 | 371 | // Then unshift bytes one at a time for as long as the has-more bit is high. 372 | while (true) { 373 | file.write((byte)(buffer & 0xff)); 374 | if (buffer & HAS_MORE_BYTES) { 375 | buffer >>= 8; 376 | } else { 377 | break; 378 | } 379 | } 380 | } 381 | 382 | /********************************************************* 383 | If you live in a country that does not recognise the 384 | Public Domain, the following (MIT) license applies: 385 | 386 | Copyright 2020, Pomax 387 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 388 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 389 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 390 | 391 | ********************************************************/ 392 | -------------------------------------------------------------------------------- /sd card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pomax/arduino-midi-recorder/1581795138eccd51e3043e0a33f0b3c49a8ad483/sd card.png --------------------------------------------------------------------------------