Create an online MIDI recording with an Arduino, SD card module, and never lose your musical noodles again!
Devices and components
Arduino Uno Rev3
4.7k ohm resistance
10k ohm resistance
MicroSD card module
Opto-isolator
10k ohm resistance
Buzzer
Resistance 220 ohms
5-pin MIDI DIN connector
4 pin button
Zener Diode 3.6V 0.5W
Software and tools
Arduino IDE
Project description
Creating a pass-through MIDI recorder
.wav
.mp3
.mid
The circuit
An Arduino SD card module (~$10 for a pack of five)
Two 5-pin female DIN connectors (~$5 for a pack of ten)
A 6N138 optocoupler (~$10 for a pack of ten)
Optional: an RTC module based on DS3231
An Arduino UNO R3 board or equivalent
3x 220 ohm resistors
1x 4.7k ohm resistor
2x 10k resistors
A diode
A piezo buzzer
Two clickable push buttons
The MIDI part of our recorder
RX<-0
RX<-0
The SD part of our recorder
Added a MIDI marker button
Added a beep, for debugging
Optional: Add a real-time clock
The software
Program Basics
Basic signal management (MIDI library)
Writing Basic Files (SD Library)
Saving MIDI markers
Audio debugging (beep beep)
Usability bonus: "clean reboot" while idle
Usability Bonus 2: “correct track length” script
Program Basics
#include <SD.h>
#include <MIDI.h>
MIDI_CREATE_DEFAULT_INSTANCE();
void setup() {
// we'll put some more code here in the next sections
}
void loop() {
// we'll put some more code here in the next sections
}
MIDI management
void setup() {
MIDI.begin(MIDI_CHANNEL_OMNI);
MIDI.setHandleNoteOn(handleNoteOn);
MIDI.setHandleNoteOff(handleNoteOff);
MIDI.setHandlePitchBend(handlePitchBend);
MIDI.setHandleControlChange(handleControlChange);
}
void loop() {
checkForMarker();
setPlayState();
updateFile();
MIDI.read();
}
RX<-0
loop()
#define NOTE_OFF_EVENT 0x80
#define NOTE_ON_EVENT 0x90
#define CONTROL_CHANGE_EVENT 0xB0
#define PITCH_BEND_EVENT 0xE0
void handleNoteOff(byte channel, byte pitch, byte velocity) {
writeToFile(NOTE_OFF_EVENT, pitch, velocity);
}
void handleNoteOn(byte channel, byte pitch, byte velocity) {
writeToFile(NOTE_ON_EVENT, pitch, velocity);
}
void handleControlChange(byte channel, byte controller, byte value) {
writeToFile(CONTROL_CHANGE_EVENT, controller, value);
}
void handlePitchBend(byte channel, int bend_value) {
// First off, we need to "re-center" the bend value,
// because in MIDI, the bend value is a positive value
// in the range 0x0000-0x3FFF with 0x2000 considered
// the "neutral" mid point, whereas the MIDI library
// gives us a signed integer value that uses 0 as its
// midpoint and negative numbers to signify "down".
bend_value += 0x2000;
// Then, per the MIDI spec, we need to encode the 14 bit
// bend value as two 7-bit bytes, where the first byte
// contains the lowest 7 bits of our bend value, and second
// byte contains the highest 7 bits of our bend value:
byte lowBits = (byte) (bend_value & 0x7F);
byte highBits = (byte) ((bend_value >> 7) & 0x7F);
writeToFile(PITCH_BEND_EVENT, lowBits, highBits);
}
channel
NOTE_OFF_EVENT
getDelta()
getDelta()
unsigned long startTime = 0;
unsigned long lastTime = 0;
int getDelta() {
if (startTime == 0) {
startTime = millis();
lastTime = startTime;
return 0;
}
unsigned long now = millis();
unsigned int delta = (now - lastTime);
lastTime = now;
return delta;
}
lastTime=millis()
setup()
getDelta
timeDelta
lastTime
startTime
void handleNoteOn(byte channel, byte pitch, byte velocity) {
...
writeToFile(..., getDelta());
}
void handleNoteOff(byte channel, byte pitch, byte velocity) {
...
writeToFile(..., getDelta());
}
void handleControlChange(byte channel, byte controller_code, byte value) {
...
writeToFile(..., getDelta());
}
void handlePitchBend(byte channel, int bend_value) {
...
writeToFile(..., getDelta());
}
File management
SD
#define CHIP_SELECT 9
String filename;
File file;
void setup() {
// ...previous code...
pinMode(CHIP_SELECT, OUTPUT);
if (SD.begin(CHIP_SELECT)) {
findNextFilename();
if (file) {
createMidiFile();
}
}
}
void findNextFilename() {
for (int i = 1; i < 1000; i++) {
filename = "file-";
if (i < 10) filename += "0";
if (i < 100) filename += "0";
filename += String(i);
filename += String(".mid");
if (!SD.exists(filename)) {
file = SD.open(filename, FILE_WRITE);
return;
}
}
}
SD
file-xxx.mid
xxx
001
999
001
FILE_WRITE
APPEND
void createMidiFile() {
byte header[] = {
0x4D, 0x54, 0x68, 0x64, // "MThd" chunk
0x00, 0x00, 0x00, 0x06, // chunk length (from this point on): 6 bytes
0x00, 0x00, // format: 0
0x00, 0x01, // number of tracks: 1
0x01, 0xC2 // data rate: 450 ticks per quaver/quarter note
};
file.write(header, 14);
byte track[] = {
0x4D, 0x54, 0x72, 0x6B, // "MTrk" chunk
0x00, 0x00, 0x00, 0x00 // chunk length placeholder
};
file.write(track, 8);
byte tempo[] = {
0x00, // time delta for the first MIDI event: zero
0xFF, 0x51, 0x03, // MIDI event type: "tempo" instruction
0x06, 0xDD, 0xD0 // tempo value: 450,000μs per quaver/quarter note
};
file.write(tempo, 7);
}
we can choose the data rate in the header, and we opted for 450 ticks per eighth note/quarter note, and
we can also choose the "playback speed", which we set to just under half a second per eighth note/quarter note.
.mid
writeToFile
void writeToFile(byte eventType, byte b1, byte b2, int delta) {
if (!file) return;
writeVarLen(delta);
file.write(eventType);
file.write(b1);
file.write(b2);
}
writeVarLen()
#define HAS_MORE_BYTES 0x80
void writeVarLen(unsigned long value) {
// Start with the first 7 bit block
unsigned long buffer = value & 0x7f;
// Then shift in 7 bit blocks with "has-more" bit from the
// right for as long as `value` has more bits to encode.
while ((value >>= 7) > 0) {
buffer <<= 8;
buffer |= HAS_MORE_BYTES;
buffer |= value & 0x7f;
}
// Then unshift bytes one at a time for as long as the has-more bit is high.
while (true) {
file.write((byte)(buffer & 0xff));
if (buffer & HAS_MORE_BYTES) {
buffer >>= 8;
} else {
break;
}
}
}
0
1
while(true)
Saving MIDI markers
#define PLACE_MARKER_PIN 4
int lastMarkState = 0;
int nextMarker = 1;
void setup() {
// ...previous code...
pinMode(PLACE_MARKER_PIN, INPUT);
}
checkForMarkers()
loop()
void checkForMarker() {
int markState = digitalRead(PLACE_MARKER_PIN);
if (markState != lastMarkState) {
lastMarkState = markState;
if (markState == 1) {
writeMidiMarker();
}
}
}
writeMidiMarker()
FF 06
"1"
"2"
"1"
"10"
void writeMidiMarker() {
if (!file) return;
// Delta + event code
writeVarLen(file, getDelta());
file.write(0xFF);
file.write(0x06);
// How many bytes are we writing?
byte len = 1;
if (nextMarker > 9) len++; // Allowing for more than 9 markers is fair.
if (nextMarker > 99) len++; // ... but this would be a lot of markers.
if (nextMarker > 999) len++; // ... and at this point, I don't think this is the right hardware for you! O_O
writeVarLen(file, len);
// Then we convert our sequence number to a string,
// and write that to file as a byte sequence:
byte marker[len];
String(nextMarker++).getBytes(marker, len);
file.write(marker, len);
}
Make a few beeps
#define AUDIO_DEBUG_PIN 2
int lastPlayState = 0;
bool play = false;
void setup() {
// ...previous code...
pinMode(AUDIO_DEBUG_PIN, INPUT);
}
void setPlayState() {
int playState = digitalRead(AUDIO_DEBUG_PIN);
if (playState != lastPlayState) {
lastPlayState = playState;
if (playState == 1) play = !play;
}
}
play
false
true
true
false
void handleNoteOn(byte CHANNEL, byte pitch, byte velocity) {
writeToFile(NOTE_ON_EVENT | CHANNEL, pitch, velocity, getDelta());
if (play) tone(AUDIO, 440 * pow(2, (pitch - 69.0) / 12.0), 100);
}
tone()
pitch
what type of tuning system we want to use, and
what is the base frequency for A over the average C.
frequency in Herz = 440 * 2^((MIDI
tone()
Added real time clock
give us the actual dates and times of our files, and
allowing us to set MIDI markers with the actual time you pressed the marker button
#include <SD.h>
#include <MIDI.h>
#include <RTClib.h>
RTC_DS3231 RTC;
bool HAS_RTC = false;
setup
void setup() {
...
if (RTC.begin()) {
// This line is special: we only need it once, and after that
// we're deleting it:
RTC.adjust(DateTime(F(__DATE__), F(__TIME__)));
// if the RTC works, we can tell the SD library
// how it can check for the current time when it
// needs timestamping for file creation/writing.
SdFile::dateTimeCallback(dateTime);
HAS_RTC = true;
}
...
}
void dateTime(uint16_t* date, uint16_t* time) {
DateTime d = RTC.now();
*date = FAT_DATE(d.year(), d.month(), d.day());
*time = FAT_TIME(d.hour(), d.minute(), d.second());
}
RTC.adjust(...)
F(__DATE__)
F(__TIME__)
SdFile::dateTimeCallback(dateTime)
dateTime
void writeMidiMarker() {
if (!file) return;
writeVarLen(file, getDelta());
file.write(0xFF);
file.write(0x06);
if (HAS_RTC) {
DateTime d = RTC.now();
byte len = 20;
writeVarLen(file, len);
char marker[len];
sprintf(
marker,
"%04d/%02d/%02d, %02d:%02d:%02d",
d.year(), d.month(), d.day(), d.hour(), d.minute(), d.second()
);
file.write(marker, len);
}
else {
// this is where we put the code we originally wrote.
}
}
sprintf
Creating a new file in slow motion
// we use a 2 minute idling timeout, expressed in milliseconds
#define RECORDING_TIMEOUT 120000
#define FILE_FLUSH_INTERVAL 400
unsigned long lastLoopCounter = 0;
unsigned long loopCounter = 0;
void updateFile() {
loopCounter = millis();
if (loopCounter - lastLoopCounter > FILE_FLUSH_INTERVAL) {
checkReset();
lastLoopCounter = loopCounter;
file.flush();
}
}
void checkReset() {
if (startTime == 0) return;
if (!file) return;
if (millis() - lastTime > RECORDING_TIMEOUT) {
file.close();
resetArduino();
}
}
void(* resetArduino) (void) = 0;
lastTime
millis()
resetArduino()
A final helper script
.mid
fix.py
import os
files
midi_files = [f for f in files
for filename in midi_files:
# Open the file in binary read/write mode:
file = open(filename, "rb+")
# As single-track MIDI files, we know that the track length is
# equal to the file size, minus the header size up to and
# including the length value, which is 22 bytes:
file_size = os.path.getsize(filename)
track_length = file_size - 22
# With that done, we can form our new byte values:
field_value = bytearray([
(track_length & 0xFF000000) >> 24,
(track_length & 0x00FF0000) >> 16,
(track_length & 0x0000FF00) >> 8,
(track_length & 0x000000FF),
])
# And then we write the update to our file:
file.seek(18)
file.write(field_value)
file.close()
print(f"Updated {filename} track length to {track_length}")
.mid
.mid
And that's it: it's over!
Import MIDI data into your DAW
.mid
.mid
.mid
Reaper 6 (Cockos)
.mid
Cubase 11 (Steinberg)
.mid
Studio One 5 (Presonus)
.mid
Ableton Live 10
.mid
Envelope Box
clip
Envelopes
FL Studio 20 (Image-Line)
.mid
Comments and/or questions
Github
https://github.com/Pomax/arduino-midi-recorder
midi-recorder.ino
arduino
Github
https://github.com/Pomax/arduino-midi-recorder
Downloadable files
Let's build an Arduino based MIDI recorder!
The repository for this tutorial, with all circuit diagrams and tutorial text in .md format
https://github.com/Pomax/arduino-midi-recorder
Let's build an Arduino based MIDI recorder!
The repository for this tutorial, with all circuit diagrams and tutorial text in .md format
https://github.com/Pomax/arduino-midi-recorder
Note: Content and images are from: https://projecthub.arduino.cc/, with some modifications.
If you want it removed due to copyright reasons, please leave a comment. Thank you.
I want to share this article more widely so that everyone knows about Arduino and your project.