The short answer would be "yes, it is possible". There are lots of gadgets around that record MIDI. They would have microprocessors in them, and they would have something like an SD card, which itself is like EEPROM in concept.
You may possibly have problems with storing the notes in RAM, and then writing them to the SD card fast enough not to lose some. Knowing the rate at which notes would arrive, the speed of the SD card, and the type of Arduino would help to answer if your specific case was likely to work.
I wrote a sketch that reads MIDI which shows that it is certainly possible to receive MIDI data and display it. Video of it running is here.
I don't personally know the format of MIDI files, but it is well documented.
(Edit) I do now - see further down.
Writing to SD cards is well documented. Making your idea work would be a case of taking stuff (like my code to decode MIDI), code to write to SD cards, and knowing the correct format.
(Edited to add)
See Standard MIDI-File Format Spec. 1.1 for an example of producing MIDI data. Also see The MIDI File Format and the Standard MIDI File (SMF) Format
Your basic technique would need to be (as suggested by gabonator):
Write a header chunk to the file, for example:
4D 54 68 64 // MThd
00 00 00 06 // chunk length
00 00 // format 0
00 01 // one track
00 60 // 96 pulses per quarter-note
Write a track chunk to the file (the length will be updated later)
4D 54 72 6B // MTrk
00 00 00 00 // <-- length to be updated later
Write instrument configurations (with time delta values of zero), as an example from that page:
00 FF 58 04 04 02 18 08 // 4 bytes; 4/4 time; 24 MIDI clocks/click,
// 8 32nd notes/ 24 MIDI clocks
// (24 MIDI clocks = 1 crotchet = 1 beat)
00 FF 51 03 07 A1 20 // 3 bytes: 500,000 µsec/ quarter note
// = 120 beats/minute (500000 in hex is 0x07A120)
00 C0 05 // Ch.1 Program Change 5 = GM
// Patch 6 = Electric Piano 2
When the player plays something, subtract the time that s/he last did something from the time now. This will give you the delta time.
Convert the time difference to ticks. From the header line above we have 96 pulses per quarter note (PPQ), and 500,000 µs (500 ms) per quarter note. Thus one second would be two quarter notes (2 beats per second, and therefore 120 beats per minute) and one second is therefore 192 ticks). Therefore the number of ticks, based on the time difference in milliseconds, would be:
unsigned long ticks = (time_difference * 192) / 1000
Output the delta time as a variable length number, where the high order bit is set for each byte, as necessary, until what is left is <= 0x7F. The page above gives sample code for that. For example, for a delta time of 199 ticks:
Decimal time of 199 ticks -> 0xC7
0xC7 -> 0b11000111
Since that exceeds 127, split into two bytes:
10000001 (the high order bits)
^^^^^^^ <- high order bits (the MSB must be one)
01000111 (the low-order 7 bits)
^^^^^^^ <- low order bits (the MSB must be zero)
Result is 0x81 0x47
Longer times may require even more bytes.
Something like this should do it (adapted from the linked page). Just change the Serial.write line to output to your disk file.
void WriteVarLen (unsigned long value)
{
unsigned long buffer;
buffer = value & 0x7f;
while ((value >>= 7) > 0)
{
buffer <<= 8;
buffer |= 0x80;
buffer |= value & 0x7f;
}
while (true)
{
Serial.write (buffer & 0xFF);
if (buffer & 0x80)
buffer >>= 8;
else
break;
}
} // end of WriteVarLen
Follow the delta time immediately by the "note on" or "note off" byte (depending on whether they started or stopped playing a note). For example, "note on" for channel 1 would be 0x90 because 0x9n is a note on command and n is the channel number, zero relative. A "note off" for channel 1 would be 0x80.
Follow the "note on" or "note off" by the note number (in other words, which note was just played or released) - one byte.
Octave # Note Numbers in decimal
C C# D D# E F F# G G# A A# B
-1 0 1 2 3 4 5 6 7 8 9 10 11
0 12 13 14 15 16 17 18 19 20 21 22 23
1 24 25 26 27 28 29 30 31 32 33 34 35
2 36 37 38 39 40 41 42 43 44 45 46 47
3 48 49 50 51 52 53 54 55 56 57 58 59
4 60 61 62 63 64 65 66 67 68 69 70 71
5 72 73 74 75 76 77 78 79 80 81 82 83
6 84 85 86 87 88 89 90 91 92 93 94 95
7 96 97 98 99 100 101 102 103 104 105 106 107
8 108 109 110 111 112 113 114 115 116 117 118 119
9 120 121 122 123 124 125 126 127
Typically middle C is considered to be C4 (C in the 4th octave), however apparently that can vary between devices.
Then output the velocity (one byte). A velocity of (decimal) 32 would be soft, 64 would be medium, 96 would be loud. The range for velocities is 0 to 127.
Repeat the above (except for writing the chunks and the configuration) until the player decides to stop playing.
Write an "end of track" marker:
00 FF 2F 00 // Delta time zero, end of track
Then seek back to the start of the file + where the length bytes are (18 bytes into the file by the looks of it) and update the length bytes (4 bytes).
Example of producing a MIDI file
This example code writes out a MIDI file. As an input device I used a 16-key keypad like this:
The columns were connected to pins 0, 1, 2, 3 of the Arduino Uno, and the rows to pins 4, 5, 6, 7. This makes the ribbon cable from the keypad keep the pins in a natural order. If you use different pins just change the keys
array in the sketch.
The library for keypads that I used is available at https://github.com/nickgammon/Keypad_Matrix
This particular library supports n-key rollover and key-down and key-up events. That is particularly useful for music, because it lets you hold multiple keys down at once, and the appropriate events will be generated. Obviously you need a key-down event to generate "note on" and a key-up event to generate "note-off".
I laid out the keys like this:
C C# D D#
E F F# G
G# A A# B
Down Soft Up Record
There is a "recording" LED connected to A0 which lights up when you start recording (by pressing the Record button).
Press the Record button again to stop recording and finalize the file. It is important to stop recording properly and not just power off the device, because the length of the file needs to be written near the start.
Press (and hold) Down alongside with another note to play an octave lower. Press (and hold) Up alongside with another note to play an octave higher.
Press (and hold) Soft alongside with another note to play at a softer velocity.
Using a keypad matrix is logical, because most keyboard devices would use a matrix, to reduce the number of wires you have to run.
Note that the matrix needs diodes wired for each switch as described here because otherwise if you press 3 or more keys at once you will get "ghost" keys.
In my case I opened up the commercial keypad and soldered in the diodes.
I also used a Micro-SD breakout adapter, connected to the SPI pins of the Arduino, like this:
The SDFat library was an archived one that I keep here: http://gammon.com.au/Arduino/SdFat-master.zip (2.2 Mb)
Unzip that file, and from within the folders inside it, copy the SdFat folder into your "libraries" folder (which is under your Arduino sketchbook folder - not inside the Arduino application folder). Then restart the Arduino IDE.
The code definitely compiles with that (with a handful of warnings). The later library from https://github.com/greiman/SdFat may be an improvement but I haven't tested that.
To keep track of file numbers I used the EEPROMAnything library, a copy of which can be found at http://gammon.com.au/Arduino/EEPROMAnything.zip
Each time you start a new recording it adds one to the file number, which wraps around to 0000 once it reaches 9999.
If there is an error initializing the file system (eg. no SD card) then the LED blinks slowly. If the file can't be created the LED blinks rapidly.
You can choose a MIDI instrument by changing the line:
const byte PATCH_NUMBER = 4; // electric piano 1
Code
/*
Sketch to write to a MIDI file from an instrument.
Author: Nick Gammon
Date: 10th May 2018
Copyright 2018 Nick Gammon.
PERMISSION TO DISTRIBUTE
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:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
LIMITATION OF LIABILITY
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.
*/
#include <Keypad_Matrix.h>
#include <SdFat.h>
#include <EEPROM.h>
#include <EEPROMAnything.h>
// For MIDI numbers see: http://www.pjb.com.au/muscript/gm.html
const byte PATCH_NUMBER = 4; // electric piano 1
const byte RECORDING_LED = A0;
// bottom row changes things
const char DOWN_OCTAVE = '-';
const char SOFT = 's';
const char UP_OCTAVE = '+';
const char RECORD = 'r';
// how the keypad has its keys laid out
// capital letters are for sharps
const byte ROWS = 4;
const byte COLS = 4;
const char keys[ROWS][COLS] = {
{'c', 'C', 'd', 'D'},
{'e', 'f', 'F', 'g'},
{'G', 'a', 'A', 'b'},
{DOWN_OCTAVE, SOFT, UP_OCTAVE, RECORD}, // - = down octave, s = soft, + = up octave, r = start/stop record
};
const byte colPins[COLS] = {0, 1, 2, 3}; //connect to the column pinouts of the keypad
const byte rowPins[ROWS] = {4, 5, 6, 7}; //connect to the row pinouts of the keypad
// Create the Keypad
Keypad_Matrix kpd = Keypad_Matrix( makeKeymap (keys), rowPins, colPins, ROWS, COLS );
// file system object
SdFat sd;
const byte chipSelect = SS;
// the current file we are recording to
SdFile myFile;
// are we recording right now?
bool recording = false;
const byte MIDI_META_EVENT = 0xFF;
const byte MIDI_SET_TEMPO = 0x51;
const byte MIDI_TIME_SIGNATURE = 0x58;
const byte MIDI_END_OF_TRACK = 0x2F;
const byte MIDI_PROGRAM_CHANGE = 0xC0;
const byte MIDI_NOTE_ON = 0x90; // channel 1
const byte MIDI_NOTE_OFF = 0x80; // channel 1
struct
{
char magic [4]; // MThd
uint32_t length;
uint16_t format;
uint16_t tracks;
uint16_t PPQ; // pulses per quarter note
} chunkHeader;
struct
{
char magic [4]; // MTrk
uint32_t length;
} trackHeader;
unsigned long trackHeaderPosition;
unsigned long timeLastAction;
int nextFileNumber;
uint32_t changeEndianness32(uint32_t val)
{
return (val << 24) |
((val << 8) & 0x00ff0000) |
((val >> 8) & 0x0000ff00) |
((val >> 24) & 0x000000ff);
} // end of changeEndianness32
uint16_t changeEndianness16(uint16_t val)
{
return (val << 8) | ((val >> 8) & 0x00ff);
} // end of changeEndianness16
void showError (const int delayTime)
{
while (true)
{
digitalWrite (RECORDING_LED, HIGH);
delay (delayTime);
digitalWrite (RECORDING_LED, LOW);
delay (delayTime);
}
} // end of showError
void startRecording ()
{
EEPROM_readAnything (0, nextFileNumber);
if (nextFileNumber > 9999 || nextFileNumber < 0)
nextFileNumber = 0;
char name[15];
sprintf (name, "SONG%04d.MID", nextFileNumber++);
// open the file for writing
if (!myFile.open(name, O_WRITE | O_CREAT | O_TRUNC))
showError (100); // fast flash - never returns
// update EEPROM for next time
EEPROM_writeAnything (0, nextFileNumber);
recording = true;
digitalWrite (RECORDING_LED, HIGH);
memcpy (chunkHeader.magic, "MThd", sizeof (chunkHeader.magic));
chunkHeader.length = changeEndianness32 (
sizeof (chunkHeader.format) +
sizeof (chunkHeader.tracks) +
sizeof (chunkHeader.PPQ));
chunkHeader.format = changeEndianness16 (0);
chunkHeader.tracks = changeEndianness16 (1);
chunkHeader.PPQ = changeEndianness16 (96);
myFile.write (&chunkHeader, sizeof (chunkHeader));
memcpy (trackHeader.magic, "MTrk", sizeof (trackHeader.magic));
trackHeader.length = 0; // to be filled in later
trackHeaderPosition = myFile.curPosition ();
myFile.write (&trackHeader, sizeof (trackHeader));
struct
{
byte deltaTime = 0;
byte metaCode [2] = { MIDI_META_EVENT, MIDI_TIME_SIGNATURE };
byte length = 4;
byte sig [2] = { 4, 2 }; // 4/4 time (denominator of 2 is 2^2)
byte clocksPerClick = 24; // MIDI clocks/click
byte clocksPerBeat = 8;
} timeSignature;
struct
{
byte deltaTime = 0;
byte metaCode [2] = { MIDI_META_EVENT, MIDI_SET_TEMPO };
byte length = 3;
byte usPerQuarterNote [3] = { 0x07, 0xA1, 0x20 }; // that is: 500000 µs per beat
} tempo;
struct
{
byte deltaTime = 0;
byte message = MIDI_PROGRAM_CHANGE;
byte patchNumber = PATCH_NUMBER;
} programChange;
myFile.write (&timeSignature, sizeof timeSignature);
myFile.write (&tempo, sizeof tempo);
myFile.write (&programChange, sizeof programChange);
timeLastAction = millis ();
} // end of startRecording
void stopRecording ()
{
struct
{
byte deltaTime = 0;
byte metaCode [2] = { MIDI_META_EVENT, MIDI_END_OF_TRACK };
byte length = 0;
} endOfTrack;
// write out "end of track" message
myFile.write (&endOfTrack, sizeof endOfTrack);
// find where we are
unsigned long finalPosition = myFile.curPosition ();
// go back to where the track header is
myFile.seekSet (trackHeaderPosition);
// work out how long the data was (excluding the headers)
trackHeader.length = changeEndianness32 (finalPosition - trackHeaderPosition - sizeof (trackHeader));
// update the file length
myFile.write (&trackHeader, sizeof (trackHeader));
myFile.close ();
// ensure flushed to disk
myFile.SdBaseFile::sync();
// no longer recording, turn off recording LED
recording = false;
digitalWrite (RECORDING_LED, LOW);
} // end of stopRecording
void writeVarLen(unsigned long value)
{
unsigned long buffer;
buffer = value & 0x7f;
while ((value >>= 7) > 0)
{
buffer <<= 8;
buffer |= 0x80;
buffer |= value & 0x7f;
} // end of while
while (true)
{
myFile.write ((byte) (buffer & 0xFF));
if (buffer & 0x80)
buffer >>= 8;
else
break;
} // end of while
} // end of writeVarLen
// turn the keypad code into a note number
byte codeToNote (const char which)
{
char notes [] = "cCdDefFgGaAb";
char * pos = strchr (notes, which);
if (pos == NULL)
return 0;
return pos - notes + 60; // start at middle C (C4)
} // end of codeToNote
// do a note on or note off
void doNoteAction (const char which, const byte action)
{
// we need to work out how many milliseconds have elapsed
unsigned long now = millis ();
unsigned long deltaTime = now - timeLastAction;
// remember when we did this
timeLastAction = now;
// write out the delta time
writeVarLen ((deltaTime * 192) / 1000);
// note information
struct
{
byte noteOn;
byte whichNote;
byte velocity;
} playNote;
playNote.noteOn = action;
playNote.whichNote = codeToNote (which);
if (kpd.isKeyDown (DOWN_OCTAVE))
playNote.whichNote -= 12; // one octave down
else if (kpd.isKeyDown (UP_OCTAVE))
playNote.whichNote += 12; // one octave up
playNote.velocity = 64; // medium velocity
if (kpd.isKeyDown (SOFT))
playNote.velocity = 32; // softer
myFile.write (&playNote, sizeof (playNote));
} // end of doNoteAction
void startPlaying (const char which)
{
doNoteAction (which, MIDI_NOTE_ON);
} // end of startPlaying
void stopPlaying (const char which)
{
doNoteAction (which, MIDI_NOTE_OFF);
} // end of stopPlaying
void keyDown (const char which)
{
switch (which)
{
case 'c':
case 'C':
case 'd':
case 'D':
case 'e':
case 'f':
case 'F':
case 'g':
case 'G':
case 'a':
case 'A':
case 'b':
startPlaying (which);
break;
default:
break;
} // end of switch on which key
} // end of keyDown
void keyUp (const char which)
{
switch (which)
{
case RECORD:
if (recording)
stopRecording ();
else
startRecording ();
break;
case 'c':
case 'C':
case 'd':
case 'D':
case 'e':
case 'f':
case 'F':
case 'g':
case 'G':
case 'a':
case 'A':
case 'b':
stopPlaying (which);
break;
default:
break;
} // end of switch on which key
} // end of keyUp
void setup()
{
kpd.begin ();
kpd.setKeyDownHandler (keyDown);
kpd.setKeyUpHandler (keyUp);
pinMode (RECORDING_LED, OUTPUT);
if (!sd.begin (chipSelect, SPI_HALF_SPEED))
showError (500); // never returns
} // end of setup
void loop()
{
kpd.scan ();
} // end of loop
I am recording from a self built replica of a theremin, which is basically two ultrasonic sensor readings for volume and pitch. Is this "too basic" for a MIDI file type?
It looks like this could be possible by using the MIDI "pitch bend" message. You would need to determine where your note value was (presumably most of the time between the natural notes like C and C#) and then send a bend message or "Pitch Wheel Change" (0xE0 for channel 1) to modify the main note. The main note would need to be updated if you moved more than a couple of semitones away.
See: