HermesTheNurse
Member
Hi @all,
I apologize in advance if I violate any forum conventions here. If that happens I'll be sure to take note moving forward. Seeking to hopefully clear up some of my confusion:
--QUESTION #1--
Does anyone know whether the order of listed parameters for these two libraries (<MIDI.h> and <usbmidi.h>) is arbitrary? Or do I need to pay close attention to the order in which "Channel", "Note/Pitch", and "Velocity" bytes are listed? It seems to me that the <midi.h> library sometimes lists parameters “…(byte channel, byte pitch, byte velocity) respectively, whereas the <usbmidi.h> library lists these as “…(note, velocity, channel)”, respectively:
Example(s): <midi.h>
Example: <usbmidi.h>
--QUESTION #2--
Using a Teensy 4.1 w/ Rev D2 Audio Shield attached I've got Sketch(A) successfully saving ".mid" files to a microSD card when supplying MIDI keyboard input via 5-pin DIN (pinRX1<<MIDI-IN). BUT!!!--when substituting 5-pin input with four FSR sensors (Analog pins A17, A16, A15, A14), the resulting microSD ".mid" files no longer contain data.
I probably just need to try a few more example sketches until the key difference(s) sink in. Though as of now I’m trying to append an existing Sketch(A) using <MIDI.h> library functions, with code from an older Sketch(B) calling verbs like “usbMIDI.sendNoteOn”, which I understand is part of the <usbmidi.h> library, graciously explained here:
https://www.pjrc.com/teensy/td_midi.html
Sorry for rambling. Basically, I need (B) to fit inside of (A), in order to write incoming FSR sensor data to .mid files on my Rev D2 Audio Shield's microSD card. Here are the aforementioned sketches, and my failed attempt to combine the two in Sketch(C):
Sketch(A): --working (5-pin DIN MIDI keyboard input >> pinRX1 >> .mid file @ microSD card output)
Sketch(B): --working (FSR sensor input >> analog pins >> MIDI+audio output)
Sketch(C): -- not working (FSR sensor input >> analog pins >> MIDI+audio ANNND .mid file @ microSD card output)
(Pardon my ridiculous use of "//" and "/*...*/")
Citations & Borrowed Code:
1. Pomax Midi Field Recorder
2. Adrian FSR Sensor Sketch
3. PRJC's GUI Tool
I apologize in advance if I violate any forum conventions here. If that happens I'll be sure to take note moving forward. Seeking to hopefully clear up some of my confusion:
--QUESTION #1--
Does anyone know whether the order of listed parameters for these two libraries (<MIDI.h> and <usbmidi.h>) is arbitrary? Or do I need to pay close attention to the order in which "Channel", "Note/Pitch", and "Velocity" bytes are listed? It seems to me that the <midi.h> library sometimes lists parameters “…(byte channel, byte pitch, byte velocity) respectively, whereas the <usbmidi.h> library lists these as “…(note, velocity, channel)”, respectively:
Example(s): <midi.h>
Code:
void setup() {
MIDI.setHandleNoteOn(handleNoteOn);
...}
void HandleNoteOn(byte channel, byte pitch, byte velocity) {
…}
Example: <usbmidi.h>
Code:
void NoteOnSend (int j) {
int FSRRead = analogRead(FSRpin [j]);
int velocity = map (FSRRead, 0, 800, MIDIMIN, 127);
usbMIDI.sendNoteOn (Note[j], velocity, 10);
}
--QUESTION #2--
Using a Teensy 4.1 w/ Rev D2 Audio Shield attached I've got Sketch(A) successfully saving ".mid" files to a microSD card when supplying MIDI keyboard input via 5-pin DIN (pinRX1<<MIDI-IN). BUT!!!--when substituting 5-pin input with four FSR sensors (Analog pins A17, A16, A15, A14), the resulting microSD ".mid" files no longer contain data.
I probably just need to try a few more example sketches until the key difference(s) sink in. Though as of now I’m trying to append an existing Sketch(A) using <MIDI.h> library functions, with code from an older Sketch(B) calling verbs like “usbMIDI.sendNoteOn”, which I understand is part of the <usbmidi.h> library, graciously explained here:
https://www.pjrc.com/teensy/td_midi.html
Sorry for rambling. Basically, I need (B) to fit inside of (A), in order to write incoming FSR sensor data to .mid files on my Rev D2 Audio Shield's microSD card. Here are the aforementioned sketches, and my failed attempt to combine the two in Sketch(C):
Sketch(A): --working (5-pin DIN MIDI keyboard input >> pinRX1 >> .mid file @ microSD card output)
Code:
// File and MIDI handling
#include <SD.h>
#include <MIDI.h>
// Our Real Time Clock
#include <RTClib.h>
RTC_DS3231 RTC;
bool HAS_RTC = false;
// Audio pins and values
#define AUDIO 8
#define AUDIO_DEBUG_PIN 3
int lastPlayState = 0;
bool play = false;
// Marker pins and values
#define PLACE_MARKER_PIN 5
int lastMarkState = 0;
int nextMarker = 1;
const int chipSelect = 10;
#define CHIP_SELECT 10
#define HAS_MORE_BYTES 0x80
#define NOTE_OFF_EVENT 0x80
#define NOTE_ON_EVENT 0x90
#define CONTROL_CHANGE_EVENT 0xB0
#define PITCH_BEND_EVENT 0xE0
// we use a 2 minute idling timeout (in millis)
#define RECORDING_TIMEOUT 120000
unsigned long lastLoopCounter = 0;
unsigned long loopCounter = 0;
unsigned long startTime = 0;
unsigned long lastTime = 0;
#define FILE_FLUSH_INTERVAL 400
String filename;
File file;
MIDI_CREATE_DEFAULT_INSTANCE();
/**
Set up our inline MIDI recorder
*/
void setup() {
// set up MIDI handling
MIDI.begin(MIDI_CHANNEL_OMNI);
MIDI.setHandleNoteOn(handleNoteOn);
MIDI.setHandleNoteOff(handleNoteOff);
MIDI.setHandlePitchBend(handlePitchBend);
MIDI.setHandleControlChange(handleControlChange);
// set up the tone playing button
pinMode(AUDIO_DEBUG_PIN, INPUT);
pinMode(AUDIO, OUTPUT);
// tone(AUDIO, 440, 200);
// set up the MIDI marker button
pinMode(PLACE_MARKER_PIN, INPUT);
// set up RTC interfacing
if (RTC.begin()) {
// uncomment this line to set the current date/time on the RTC
// 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;
// tone(AUDIO, 880, 100);
}
// set up SD card functionality and allocate a file
pinMode(CHIP_SELECT, OUTPUT);
if (SD.begin(CHIP_SELECT)) {
creatNextFile();
if (file) {
writeMidiPreamble();
// tone(AUDIO, 1760, 100);
}
}
}
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());
}
/**
We could use the EEPROM to store this number,
but since we're not going to get timestamped
files anyway, just looping is also fine.
*/
void creatNextFile() {
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.c_str())) {
file = SD.open(filename.c_str(), FILE_WRITE);
return;
}
}
}
/**
Set up a new MIDI file with some boiler plate byte code
*/
void writeMidiPreamble() {
byte header[] = {
0x4D, 0x54, 0x68, 0x64, // "MThd" chunk
0x00, 0x00, 0x00, 0x06, // chunk length (from this point on)
0x00, 0x00, // format 0
0x00, 0x01, // one track
0x01, 0xD4 // data rate = 458 ticks per quarter note
};
file.write(header, 14);
byte track[] = {
0x4D, 0x54, 0x72, 0x6B, // "MTrk" chunk
0x00, 0x00, 0x00, 0x00 // chunk length placeholder (MSB)
};
file.write(track, 8);
byte tempo[] = {
0x00, // time delta (of zero)
0xFF, 0x51, 0x03, // tempo op code
0x06, 0xFD, 0x1F // real rate = 458,015μs per quarter note (= 134.681 BPM)
};
file.write(tempo, 7);
}
/**
The program loop consists of flushing our file to disk,
checking our buttons to see if they just got pressed,
and then handling MIDI input, if there is any.
*/
void loop() {
checkForMarker();
setPlayState();
updateFile();
MIDI.read();
}
// ======================================================================================
/**
We flush the file's in-memory content to disk
every 400ms, allowing. That way if we take the
SD card out, it's basically impossible for any
data to have been lost.
*/
void updateFile() {
loopCounter = millis();
if (loopCounter - lastLoopCounter > FILE_FLUSH_INTERVAL) {
checkReset();
lastLoopCounter = loopCounter;
file.flush();
}
}
/**
This "function" would normally crash any kernel that tries
to run it by violating memory access. Instead, the Arduino's
watchdog will auto-reboot, giving us a software "reset".
*/
void(* resetArduino) (void) = 0;
/**
if we've not received any data for 2 minutes, and we were
previously recording, we reset the arduino so that when
we start playing again, we'll be doing so in a new file,
rather than having multiple sessions with huge silence
between them in the same file.
*/
void checkReset() {
if (startTime == 0) return;
if (!file) return;
if (millis() - lastTime > RECORDING_TIMEOUT) {
file.close();
resetArduino();
}
}
/**
A little audio-debugging: pressing the button tied to the
audio debug pin will cause the program to play notes for
every MIDI note-on event that comes flying by.
*/
void setPlayState() {
int playState = digitalRead(AUDIO_DEBUG_PIN);
if (playState != lastPlayState) {
lastPlayState = playState;
if (playState == 1) {
play = !play;
}
}
}
/**
This checks whether the MIDI marker button got pressed,
and if so, writes a MIDI marker message into the track.
*/
void checkForMarker() {
int markState = digitalRead(PLACE_MARKER_PIN);
if (markState != lastMarkState) {
lastMarkState = markState;
if (markState == 1) {
writeMidiMarker();
}
}
}
/**
Write a MIDI marker to file, by writing a delta, then
the op code for "midi marker", the number of letters
the marker label has, and then the label (using ASCII).
For simplicity, the marker labels will just be a
sequence number starting at "1".
*/
void writeMidiMarker() {
if (!file) return;
// delta + event code
writeVarLen(file, getDelta());
file.write(0xFF);
file.write(0x06);
// If we have an RTC available, we can write the clock time
// Otherwise, write a sequence number.
if (HAS_RTC) {
DateTime d = RTC.now();
byte len = 20;
writeVarLen(file, len);
char marker[len]; // will hold strings like "2021/01/23, 10:53:31"
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 {
// how many letters are we writing?
byte len = 1;
if (nextMarker > 9) len++;
if (nextMarker > 99) len++;
if (nextMarker > 999) len++;
writeVarLen(file, len);
// our label:
byte marker[len];
String(nextMarker++).getBytes(marker, len);
file.write(marker, len);
}
}
// ======================================================================================
void handleNoteOff(byte channel, byte pitch, byte velocity) {
writeToFile(NOTE_OFF_EVENT, pitch, velocity, getDelta());
}
void handleNoteOn(byte channel, byte pitch, byte velocity) {
writeToFile(NOTE_ON_EVENT, pitch, velocity, getDelta());
if (play) tone(AUDIO, 440 * pow(2, (pitch - 69.0) / 12.0), 100);
}
void handleControlChange(byte channel, byte cc, byte value) {
writeToFile(CONTROL_CHANGE_EVENT, cc, value, getDelta());
}
void handlePitchBend(byte channel, int bend) {
bend += 0x2000; // MIDI bend uses the range 0x0000-0x3FFF, with 0x2000 as center.
byte lsb = bend & 0x7F;
byte msb = bend >> 7;
writeToFile(PITCH_BEND_EVENT, lsb, msb, getDelta());
}
/**
This calculates the number of ticks since the last MIDI event
*/
int getDelta() {
if (startTime == 0) {
// if this is the first event, even if the Arduino's been
// powered on for hours, this should be delta zero.
startTime = millis();
lastTime = startTime;
return 0;
}
unsigned long now = millis();
unsigned int delta = (now - lastTime);
lastTime = now;
return delta;
}
/**
Write "common" MIDI events to file, where common MIDI events
all use the following data format:
<delta> <event code> <byte> <byte>
See the "Standard MIDI-File Format" for more information.
*/
void writeToFile(byte eventType, byte b1, byte b2, int delta) {
if (!file) return;
writeVarLen(file, delta);
file.write(eventType);
file.write(b1);
file.write(b2);
}
/**
Encode a unsigned 32 bit integer as variable-length byte sequence
of, at most, 4 7-bit-with-has-more bytes. This function is supplied
as part of the MIDI file format specification.
*/
void writeVarLen(File file, unsigned long value) {
// capture the first 7 bit block
unsigned long buffer = value & 0x7f;
// 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;
}
}
}
Sketch(B): --working (FSR sensor input >> analog pins >> MIDI+audio output)
Code:
// Libraries
#include "AudioSampleKickkhronos.h"
#include "AudioSampleHh1khronos.h"
#include "AudioSampleSnarekhronos.h"
#include "AudioSampleHh2khronos.h"
#include <Bounce.h> // Don't think I still need this. TBD.
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>
// GUItool: begin automatically generated code
AudioPlayMemory playMem3; //xy=248.75,260.0000057220459
AudioPlayMemory playMem4; //xy=248.75,325.0000057220459
AudioPlayMemory playMem2; //xy=251.25,193.7500057220459
AudioPlayMemory playMem1; //xy=256.25,135.0000057220459
AudioMixer4 mixer1; //xy=498.75000762939453,230.00000381469727
AudioOutputI2S i2s1; //xy=735.0000114440918,230.00000190734863
AudioConnection patchCord1(playMem3, 0, mixer1, 2);
AudioConnection patchCord2(playMem4, 0, mixer1, 3);
AudioConnection patchCord3(playMem2, 0, mixer1, 1);
AudioConnection patchCord4(playMem1, 0, mixer1, 0);
AudioConnection patchCord5(mixer1, 0, i2s1, 0);
AudioConnection patchCord6(mixer1, 0, i2s1, 1);
AudioControlSGTL5000 sgtl5000_1; //xy=433.75000762939453,493.7500057220459
// GUItool: end automatically generated code
//// GLOBAL VARIABLES ////
// INDEX # --> 0 1 2 3
int FSRpin[] = {A17, A16, A15, A14}; /*"Relevant analog pins connected to FSRs"
--"FSRpin[]" later gets read by "analogRead(..." function
--in void loop(){} section! */
const int FSRs = 4; /*number of FSRs //
// --"FSRs" later called in Condition
// --part of For-Loop in void loop(){} section!*/
int Note [] = {60, 61, 62, 63}; /*MIDI note numbers ... listed in order of FSRpin*/
int counter [FSRs]; /*"counter[4]", in other words.*/
int VELMASK = 0;
int ATMASK = 0;
int AFTERTHRESH = 50; /*"Analog sensor value above which aftertouch messages are sent"
50 in decimal = 0110010 in binary*/
int THRESH = 45; /*"Analog sensor value above which note / velocity is recognised"
--"THRESH", an integer-type variable, is later
--called in void loop() {...}
45 in decimal = 0101101 in binary)*/
int VELTIME = 500; /*"Counter value at which point velocity is sampled ...
counter is zero when first touched, velocity is sampled X ticks later
500 ticks sounds like a lot, but the teensy LC is clocked at 48Mhz"
500 in decimal (0111110100 binary)*/
int AFTERTIME = 2500; /*"Counter value at which point aftertouch is sampled ...
every X ticks of a touch, until released ...
you don't want too many aftertouch messages per touch,
and 2500 gives a surprising number"
2500 in decimal (0100111000100 in binary)*/
int MIDIMIN = 20; /*bottom MIDI value for the MIDI velocity AND aftertouch messages*/
void NoteOnSend (int); /*One of 3 main functions in addition to void setup(); and void loop();*/
void PolyTouchSend (int); /*One of 3 main functions in addition to void setup(); and void loop();*/
////following global variables relating to Wav2Sketch/output audio:
int channel [] = {0, 1, 2, 3}; /*Mixer channels for wav2sketch audio samples, listed
in order of their respective .wav samples & analog pins*/
const int SAMPLEs = 4;
int counter2 [SAMPLEs];
void SampleOnSend (int); /*One of 3 main functions in addition to void setup(); and void loop(); */
void gain (unsigned int channel, float gain); /*Function that goes within void SampleOnSend(int); function.
Confusing to set up because the names for second parameter
"gain" varied as "level" in the ArduinoIDE>TeensyExample sketches*/
void setup() {
Serial.begin (32500);
AudioMemory(10);
sgtl5000_1.enable();
sgtl5000_1.volume(0.5);
mixer1.gain(0, 0.4);
mixer1.gain(1, 0.4);
mixer1.gain(2, 0.4);
mixer1.gain(3, 0.4);
}
void loop () {
for (int i = 0; i < FSRs; i++) {
int FSRRead = analogRead(FSRpin[i]);
if (FSRRead > THRESH) {
counter[i] ++;
if (!(VELMASK & (1 << i)) && (counter[i] == VELTIME)) { /*"(1 << i)" means "(1x2)^i", and is: 1 if
i=0, 2 if i=1, 4 if i=2, or 8 if i=3*/
VELMASK |= (1 << i);
counter [i] = 0;
NoteOnSend (i);
SampleOnSend (i); /*Key to getting variable-velocity wav2sketch
samples playing on Adrian's FSR sketch*/
}
if (counter [i] == AFTERTIME) {
counter [i] = 0;
PolyTouchSend(i);
}
}
else { /*When FSRRead(0-1023) is NOT greater than 45,
do this: {...}*/
if (VELMASK & (1 << i)) {
usbMIDI.sendNoteOff (Note[i], 0, 10);
VELMASK &= ~ (1 << i);
counter [i] = 0;
}
}
}
}
void NoteOnSend (int j) {
int FSRRead = analogRead(FSRpin [j]); /*"int FSRRead": range of 0 to 1023*/
int velocity = map (FSRRead, 0, 800, MIDIMIN, 127); /*For input range why not 0 to 1023?*/
usbMIDI.sendNoteOn (Note[j], velocity, 10);
}
void SampleOnSend (int k) { /*Function added to Adrian's sketch for getting
audio samples to play simulataneously with existing
MIDI output signal coded in void
NoteOnSend(int j) function*/
analogReadResolution(7);
int FSRRead2 = analogRead(FSRpin [k]);
float gain = map (FSRRead2, 0, 127, .2, 1.0);
mixer1.gain(channel[k], gain);
if (analogRead(A17) >= THRESH) {
playMem1.play(AudioSampleHh2khronos); }
if (analogRead(A16) >= THRESH) {
playMem2.play(AudioSampleSnarekhronos); }
if (analogRead(A15) >= THRESH) {
playMem3.play(AudioSampleHh1khronos); }
if (analogRead(A14) >= THRESH) {
playMem4.play(AudioSampleKickkhronos); }
}
void PolyTouchSend (int j) { /*Don't think this is needed, but why mess with
what's working right?*/
int FSRRead = analogRead(FSRpin [j]);
if (FSRRead > AFTERTHRESH) {
int aftertouch = map (FSRRead, 0, 800, MIDIMIN, 127); /*Again, why input range of 0-800?*/
usbMIDI.sendPolyPressure (Note[j], aftertouch, 10);
}
}
Sketch(C): -- not working (FSR sensor input >> analog pins >> MIDI+audio ANNND .mid file @ microSD card output)
(Pardon my ridiculous use of "//" and "/*...*/")
Code:
////LIBRARIES
// File and MIDI handling
#include <SD.h>
#include <usb_midi.h>
#include <MIDI.h>
// from Sketch(B)
#include "AudioSampleKickkhronos.h"
#include "AudioSampleHh1khronos.h"
#include "AudioSampleSnarekhronos.h"
#include "AudioSampleHh2khronos.h"
#include <Bounce.h> // Don't think I still need this. TBD.
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>
// Our Real Time Clock
#include <RTClib.h>
RTC_DS3231 RTC;
bool HAS_RTC = false;
// GUItool: begin automatically generated code
AudioPlayMemory playMem3; //xy=248.75,260.0000057220459
AudioPlayMemory playMem4; //xy=248.75,325.0000057220459
AudioPlayMemory playMem2; //xy=251.25,193.7500057220459
AudioPlayMemory playMem1; //xy=256.25,135.0000057220459
AudioMixer4 mixer1; //xy=498.75000762939453,230.00000381469727
AudioOutputI2S i2s1; //xy=735.0000114440918,230.00000190734863
AudioConnection patchCord1(playMem3, 0, mixer1, 2);
AudioConnection patchCord2(playMem4, 0, mixer1, 3);
AudioConnection patchCord3(playMem2, 0, mixer1, 1);
AudioConnection patchCord4(playMem1, 0, mixer1, 0);
AudioConnection patchCord5(mixer1, 0, i2s1, 0);
AudioConnection patchCord6(mixer1, 0, i2s1, 1);
AudioControlSGTL5000 sgtl5000_1; //xy=433.75000762939453,493.7500057220459
// GUItool: end automatically generated code
//// GLOBAL VARIABLES ////
// INDEX # --> 0 1 2 3
int FSRpin[] = {A17, A16, A15, A14}; /*"Relevant analog pins connected to FSRs"
--"FSRpin[]" later gets read by "analogRead(..." function
--in void loop(){} section! */
const int FSRs = 4;
int Note [] = {60, 61, 62, 63}; /*MIDI note numbers ... listed in order of FSRpin*/
int counter [FSRs]; /*"counter[4]", in other words.*/
int VELMASK = 0;
int ATMASK = 0;
int AFTERTHRESH = 50; /*"Analog sensor value above which aftertouch messages are sent"
50 in decimal = 0110010 in binary*/
int THRESH = 45; /*"Analog sensor value above which note / velocity is recognised"
--"THRESH", an integer-type variable, is later
--called in void loop() {...}
45 in decimal = 0101101 in binary)*/
int VELTIME = 500; /*"Counter value at which point velocity is sampled ...
counter is zero when first touched, velocity is sampled X ticks later
500 ticks sounds like a lot, but the teensy LC is clocked at 48Mhz"
500 in decimal (0111110100 binary)*/
int AFTERTIME = 2500; /*"Counter value at which point aftertouch is sampled ...
every X ticks of a touch, until released ...
you don't want too many aftertouch messages per touch,
and 2500 gives a surprising number"
2500 in decimal (0100111000100 in binary)*/
int MIDIMIN = 20; /*bottom MIDI value for the MIDI velocity AND aftertouch messages*/
//Functions declared pre-"void setup() {}"
void NoteOnSend (int); /*One of 3 main functions in addition to void setup(); and void loop();*/
void PolyTouchSend (int); /*One of 3 main functions in addition to void setup(); and void loop();*/
//void NoteOffSend (int);
//void handleNoteOff (int);
//void handleNoteOn (int);
//void handlePitchBend (int);
//void handleControlChange (int);
////following global variables relating to Wav2Sketch/output audio:
int channel [] = {0, 1, 2, 3}; /*Mixer channels for wav2sketch audio samples, listed
in order of their respective .wav samples & analog pins*/
const int SAMPLEs = 4;
int counter2 [SAMPLEs];
void SampleOnSend (int); /*One of 3 main functions in addition to void setup(); and void loop(); */
void gain (unsigned int channel, float gain); /*Function that goes within void SampleOnSend(int); function.
Confusing to set up because the names for second parameter
"gain" varied as "level" in the ArduinoIDE>TeensyExample sketches*/
// Audio pins and values
#define AUDIO 8 // want to change this to wav2Sketch's output sounds, instead of Buzzer
#define AUDIO_DEBUG_PIN 3 // audio debug Button corresponding to Buzzer, pin3
int lastPlayState = 0;
bool play = false;
// Marker pins and values
#define PLACE_MARKER_PIN 5 //MIDI Marker Button, pin5
int lastMarkState = 0;
int nextMarker = 1;
const int chipSelect = 10;
#define CHIP_SELECT 10
#define HAS_MORE_BYTES 0x80 //write varlen function related // "sprintf" macro related?
#define NOTE_OFF_EVENT 0x80
#define NOTE_ON_EVENT 0x90
#define CONTROL_CHANGE_EVENT 0xB0
#define PITCH_BEND_EVENT 0xE0
//#define POLY_TOUCH_EVENT 0xA0
// we use a 2 minute idling timeout (in millis)
#define RECORDING_TIMEOUT 120000
unsigned long lastLoopCounter = 0;
unsigned long loopCounter = 0;
unsigned long startTime = 0; //timer related global variables
unsigned long lastTime = 0; //timer related global variables
#define FILE_FLUSH_INTERVAL 400 //millisecond interval in which file(s) are written to microSD card
String filename;
File file; //creates an object called "file"
MIDI_CREATE_DEFAULT_INSTANCE(); // This is required to set up the MIDI.h library.
// The default MIDI setup uses the built-in serial port.
/**
Set up our inline MIDI recorder
*/
void setup() {
//Serial.begin (32500);
AudioMemory(10);
sgtl5000_1.enable();
sgtl5000_1.volume(0.5);
mixer1.gain(0, 0.4);
mixer1.gain(1, 0.4);
mixer1.gain(2, 0.4);
mixer1.gain(3, 0.4);
// set up MIDI handling
//MIDI.begin(MIDI_CHANNEL_OMNI); //This listens to all MIDI channels. They can be filtered out later...
//MIDI.turnThruOff(); // Disable automatic MIDI Thru function
/*
//MIDI.h version of "usbMIDI.setHandle..." functions:
MIDI.setHandleNoteOn(handleNoteOn);
MIDI.setHandleNoteOff(handleNoteOff);
MIDI.setHandlePitchBend(handlePitchBend);
MIDI.setHandleControlChange(handleControlChange);
*/
/* For each function you create, you must use the corresponding
"setHandle" functions to tell usbMIDI to call it when that message
type is read. -- https://www.pjrc.com/teensy/td_midi.html --
*/
usbMIDI.begin();
// set up usbMIDI handling (instead of MIDI handling)
usbMIDI.setHandleNoteOn(handleNoteOn); // "...(handleNoteOn)" used instead of PJRC suggested "...(myNoteOn)"
usbMIDI.setHandleNoteOff(handleNoteOff);
usbMIDI.setHandlePitchChange(handlePitchBend);
usbMIDI.setHandleControlChange(handleControlChange);
//usbMIDI.setHandleAfterTouchPoly(handleAfterTouchPoly); // this is in addition to the four messages MIDIFieldRecorder sketch originally uses
// set up the tone playing button
pinMode(AUDIO_DEBUG_PIN, INPUT); //related to audio debug pin3
pinMode(AUDIO, OUTPUT); //related to audio buzzer output pin8
tone(AUDIO, 440, 200);
// set up the MIDI marker button
pinMode(PLACE_MARKER_PIN, INPUT); //related to maker button, pin5
// set up RTC interfacing
if (RTC.begin()) {
// uncomment this line to set the current date/time on the RTC
// 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;
tone(AUDIO, 880, 100);
}
// set up SD card functionality and allocate a file
pinMode(CHIP_SELECT, OUTPUT);
if (SD.begin(CHIP_SELECT)) {
creatNextFile();
if (file) {
writeMidiPreamble();
tone(AUDIO, 1760, 100);
}
}
}
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());
}
/**
We could use the EEPROM to store this number,
but since we're not going to get timestamped
files anyway, just looping is also fine.
*/
void creatNextFile() {
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.c_str())) {
file = SD.open(filename.c_str(), FILE_WRITE);
return;
}
}
}
/**
Set up a new MIDI file with some boiler plate byte code
*/
void writeMidiPreamble() {
byte header[] = {
0x4D, 0x54, 0x68, 0x64, // "MThd" chunk
0x00, 0x00, 0x00, 0x06, // chunk length (from this point on)
0x00, 0x00, // format 0
0x00, 0x01, // one track
0x01, 0xD4 // data rate = 458 ticks per quarter note
};
file.write(header, 14); /*size_t is a data type capable of
representing the size of any object
in bytes. Examples of the use of size_t
are the return type of sizeof() and
Serial.*/
byte track[] = {
0x4D, 0x54, 0x72, 0x6B, // "MTrk" chunk
0x00, 0x00, 0x00, 0x00 // chunk length placeholder (MSB)
};
file.write(track, 8);
byte tempo[] = {
0x00, // time delta (of zero)
0xFF, 0x51, 0x03, // tempo op code
0x06, 0xFD, 0x1F // real rate = 458,015μs per quarter note (= 134.681 BPM)
};
file.write(tempo, 7);
}
/**
The program loop consists of flushing our file to disk,
checking our buttons to see if they just got pressed,
and then handling MIDI input, if there is any.
*/
void loop() {
checkForMarker(); //#1 related to marker pin5
setPlayState(); //#2 related to audio debug pin3_pin8
updateFile(); //#3 related to flushing files to microSD card
MIDI.read(); //#4
/*
Once everything is set up, you then need the "read" function
to actually read data. The callback functions are only called
when you use read. There are 2 choices for read:
*/
//usbMIDI.read(); /* (1) All Channels. */
//usbMIDI.read(0); /* (2) One Specific Channel (1 to 16). // or " int usb_midi_read(uint32_t channel); "?
//Leave blank to listen to all channels) */
// Velocity Sensing Of Analog Pin Array (4 FSR Sensors)
for (int i = 0; i < FSRs; i++) {
int FSRRead = analogRead(FSRpin[i]);
if (FSRRead > THRESH) {
counter[i] ++;
if (!(VELMASK & (1 << i)) && (counter[i] == VELTIME)) { /*"(1 << i)" means "(1x2)^i", and is: 1 if
i=0, 2 if i=1, 4 if i=2, or 8 if i=3*/
VELMASK |= (1 << i);
counter [i] = 0;
NoteOnSend (i);
SampleOnSend (i); /*Key to getting variable-velocity wav2sketch
samples playing on Adrian's FSR sketch*/
}
if (counter [i] == AFTERTIME) {
counter [i] = 0;
PolyTouchSend(i);
}
}
else { /*When FSRRead(0-1023) is NOT greater than 45,
do this: {...}*/
if (VELMASK & (1 << i)) {
//NoteOffSend (i);
usbMIDI.sendNoteOff (Note[i], 0, 0);
VELMASK &= ~ (1 << i);
counter [i] = 0;
}
}
}
}
// ======================================================================================
////needs to combinevvv
void NoteOnSend (int j) {
int FSRRead = analogRead(FSRpin [j]); // "int FSRRead": range of 0 to 1023
int velocity = map (FSRRead, 0, 800, MIDIMIN, 127); // For input range why not 0 to 1023?
//handleNoteOn;
usbMIDI.sendNoteOn (Note[j], velocity, 0);
}
void SampleOnSend (int k) { /*Debugging, but also User-feedback function appended
to Adrian's sketch for getting audio samples
to play simulataneously with existing MIDI
output signal coded in void NoteOnSend(int j)
function*/
analogReadResolution(7);
int FSRRead2 = analogRead(FSRpin [k]);
float gain = map (FSRRead2, 0, 127, .2, 1.0);
mixer1.gain(channel[k], gain);
if (analogRead(A17) >= THRESH) {
playMem1.play(AudioSampleHh2khronos); }
if (analogRead(A16) >= THRESH) {
playMem2.play(AudioSampleSnarekhronos); }
if (analogRead(A15) >= THRESH) {
playMem3.play(AudioSampleHh1khronos); }
if (analogRead(A14) >= THRESH) {
playMem4.play(AudioSampleKickkhronos); }
}
void PolyTouchSend (int j) { //Don't think this is needed, but why mess with
// what's working right?
int FSRRead = analogRead(FSRpin [j]);
if (FSRRead > AFTERTHRESH) {
int aftertouch = map (FSRRead, 0, 800, MIDIMIN, 127); //Again, why input range of 0-800?
usbMIDI.sendPolyPressure (Note[j], aftertouch, 1);
}
}
// ======================================================================================
/**
We flush the file's in-memory content to disk
every 400ms, allowing. That way if we take the
SD card out, it's basically impossible for any
data to have been lost.
*/
void updateFile() { //#3 related to flushing files to microSD card
loopCounter = millis();
if (loopCounter - lastLoopCounter > FILE_FLUSH_INTERVAL) {
checkReset();
lastLoopCounter = loopCounter;
file.flush();
}
}
/**
This "function" would normally crash any kernel that tries
to run it by violating memory access. Instead, the Arduino's
watchdog will auto-reboot, giving us a software "reset".
*/
void(* resetArduino) (void) = 0; //#3 related to flushing files to microSD card
/**
if we've not received any data for 2 minutes, and we were
previously recording, we reset the arduino so that when
we start playing again, we'll be doing so in a new file,
rather than having multiple sessions with huge silence
between them in the same file.
*/
void checkReset() { //#3 related to flushing files to microSD card
if (startTime == 0) return;
if (!file) return;
if (millis() - lastTime > RECORDING_TIMEOUT) {
file.close();
resetArduino();
}
}
/**
A little audio-debugging: pressing the button tied to the
audio debug pin will cause the program to play notes for
every MIDI note-on event that comes flying by.
*/
// Audio Feedback Debugging. I can use this at some point to toggle the Wav2Sketch audio samples via Rev D2 Audio Shield's Aux port on/off
void setPlayState() { //#2 related to audio debug pin3_pin8
int playState = digitalRead(AUDIO_DEBUG_PIN);
if (playState != lastPlayState) {
lastPlayState = playState;
if (playState == 1) {
play = !play;
}
}
}
/**
This checks whether the MIDI marker button got pressed,
and if so, writes a MIDI marker message into the track.
*/
void checkForMarker() { //#1 related to marker pin5
int markState = digitalRead(PLACE_MARKER_PIN);
if (markState != lastMarkState) {
lastMarkState = markState;
if (markState == 1) {
writeMidiMarker();
}
}
}
/**
Write a MIDI marker to file, by writing a delta, then
the op code for "midi marker", the number of letters
the marker label has, and then the label (using ASCII).
For simplicity, the marker labels will just be a
sequence number starting at "1".
*/
void writeMidiMarker() { //#1 related to marker pin5
if (!file) return;
// delta + event code
writeVarLen(file, getDelta());
file.write(0xFF);
file.write(0x06);
// If we have an RTC available, we can write the clock time
// Otherwise, write a sequence number.
if (HAS_RTC) {
DateTime d = RTC.now();
byte len = 20;
writeVarLen(file, len);
char marker[len]; // will hold strings like "2021/01/23, 10:53:31"
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 {
// how many letters are we writing?
byte len = 1;
if (nextMarker > 9) len++;
if (nextMarker > 99) len++;
if (nextMarker > 999) len++;
writeVarLen(file, len);
// our label:
byte marker[len];
String(nextMarker++).getBytes(marker, len);
file.write(marker, len);
}
}
// ======================================================================================
/* COMMENT:
"void handleNoteOn(...)" specified
under void setup() within the specific
message type, ex) MIDI.setHandleNoteOn(handleNoteOn);
*/
void handleNoteOff (byte channel, byte pitch, byte velocity) {
writeToFile(NOTE_OFF_EVENT, pitch, velocity, getDelta());
}
//MIDIFieldRecorder's MIDI message originally pointed to MIDI.h -- now is usb_MIDI.h
void handleNoteOn(byte channel, byte pitch, byte velocity) {
writeToFile(NOTE_ON_EVENT, pitch, velocity, getDelta());
if (play) tone(AUDIO, 440 * pow(2, (pitch - 69.0) / 12.0), 100);
}
void handleControlChange(byte channel, byte cc, byte value) {
writeToFile(CONTROL_CHANGE_EVENT, cc, value, getDelta());
}
void handlePitchBend(byte channel, int bend) {
bend += 0x2000; // MIDI bend uses the range 0x0000-0x3FFF, with 0x2000 as center.
byte lsb = bend & 0x7F;
byte msb = bend >> 7;
writeToFile(PITCH_BEND_EVENT, lsb, msb, getDelta());
}
/*
//drafted based on MIDIFieldRecorder's MIDI message format...get rid of this if troubleshooting
void handleAfterTouchPoly(byte channel, byte pitch, byte pressure) {
writeToFile(POLY_TOUCH_EVENT, pitch, pressure, getDelta());
}
*/
//===================================================================
/**
This calculates the number of ticks since the last MIDI event
*/
int getDelta() {
if (startTime == 0) {
// if this is the first event, even if the Arduino's been
// powered on for hours, this should be delta zero.
startTime = millis();
lastTime = startTime;
return 0;
}
unsigned long now = millis();
unsigned int delta = (now - lastTime);
lastTime = now;
return delta;
}
/**
Write "common" MIDI events to file, where common MIDI events
all use the following data format:
<delta> <event code> <byte> <byte>
See the "Standard MIDI-File Format" for more information.
<MTrk event> = <delta-time><event>
<delta-time> is stored as a variable-length quantity.
It represents the amount of time before the following event.
If the first event in a track occurs at the very beginning
of a track, or if two events occur simultaneously, a delta-time
of zero is used. Delta-times are always present. (Not storing
delta-times of 0 requires at least two bytes for any other
value, and most delta-times aren't zero.) Delta-time is in some
fraction of a beat (or a second, for recording a track with
SMPTE times), as specified in the header chunk.
<event> = <MIDI event> | <sysex event> | <meta-event>
<byte> <byte>
<MIDI event> is any MIDI channel message See
Appendix 1 - MIDI Messages. Running status is used:
status bytes of MIDI channel messages may be omitted if
the preceding event is a MIDI channel message with the
same status. The first event in each MTrk chunk must specify
status. Delta-time is not considered an event itself: it is
an integral part of the syntax for an MTrk event. Notice that
running status occurs across delta-times.
<sysex event> is used to specify a MIDI system exclusive message,
either as one unit or in packets, or as an "escape" to specify
any arbitrary bytes to be transmitted. See Appendix 1 - MIDI
Messages. A normal complete system exclusive message is stored
in a MIDI File in this way: F0 <length> <bytes to be transmitted after F0>
<meta-event> specifies non-MIDI information useful to this format
or to sequencers, with this syntax: FF <type> <length> <bytes>
All meta-events begin with FF, then have an event type byte
(which is always less than 128), and then have the length of
the data stored as a variable-length quantity, and then the data
itself. If there is no data, the length is 0.
*/
//writeToFile( event, pitch/note, velocity, getDelta )
void writeToFile(byte eventType, byte b1, byte b2, int delta) {
if (!file) return;
writeVarLen(file, delta);
file.write(eventType);
file.write(b1);
file.write(b2);
}
/**
Encode a unsigned 32 bit integer as variable-length byte sequence
of, at most, 4 7-bit-with-has-more bytes. This function is supplied
as part of the MIDI file format specification.
*/
void writeVarLen(File file, unsigned long value) { /* -- http://www.music.mcgill.ca/~ich/classes/mumt306/StandardMIDIfileformat.html */
// capture the first 7 bit block
unsigned long buffer = value & 0x7f;
// 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;
}
}
}
Citations & Borrowed Code:
1. Pomax Midi Field Recorder
2. Adrian FSR Sensor Sketch
3. PRJC's GUI Tool