QC

Creating a MIDI Pass-Through Recorder

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.

SendData

Điều khiển trạng thái qua Firebase Trạng thái hiện tại: Đang tải... ĐỔI TRẠNG THÁI