Using MIDI.h to write analog pin input to .mid file. No computer.

Does midi.read(); detect midi information generated from within functions that are outside the main loop? Or similarly, does midi.read(); have to be in the same function as where the midi messages are generated? I'm trying to read FSR sensor input from Teensy 4.1's analog pins, send that to a send.NoteOn(); command:

Code:
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <MIDI.h>
#include <SerialFlash.h>

int FSRpin[] = {A17, A16, A15, A14};
const int FSRs = 4;  
int Note [] = {60, 61, 62, 63};  
int counter [FSRs];         
int VELMASK = 0;
int ATMASK = 0;
int AFTERTHRESH = 50; 
int THRESH = 45; 
int VELTIME = 500;   
int AFTERTIME = 2500;  
int MIDIMIN = 20;  
void NoteOnSend (int);   

MIDI_CREATE_DEFAULT_INSTANCE();

void setup() {
  MIDI.begin(MIDI_CHANNEL_OMNI);
  MIDI.setHandleNoteOn(handleNoteOn);
  MIDI.setHandleNoteOff(handleNoteOff);
  MIDI.setHandlePitchBend(handlePitchBend);
  MIDI.setHandleControlChange(handleControlChange);

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)) {
        VELMASK |= (1 << i);                   
        counter [i] = 0;
        NoteOnSend (i);
        SampleOnSend (i);                                      
        }
      if (counter [i] == AFTERTIME) {
        counter [i] = 0;
      }
    }
    else {                                                    
      if (VELMASK & (1 << i)) {           
        MIDI.sendNoteOff (Note[i], 0, 1);
        VELMASK &= ~ (1 << i);                 
        counter [i] = 0;
      }
    }
  }
  checkForMarker();
  setPlayState();
  updateFile();
  MIDI.read();
}

void NoteOnSend (int j) {
  int FSRRead = analogRead(FSRpin [j]);                   
  int velocity = map (FSRRead, 0, 800, MIDIMIN, 127);       
  MIDI.sendNoteOn (Note[j], velocity, 1)
}



...and then proceed to store the bytes in the SMF format using the following set of functions:



Code:
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; 
  byte lsb = bend & 0x7F;
  byte msb = bend >> 7;
  writeToFile(PITCH_BEND_EVENT, lsb, msb, getDelta());
}

...all without using a PC. What I am really trying to sort out here is how to best go about forwarding the bits sent within a sendNoteOn(); command to the writeToFile(); command. There is a Rev D2 Audio shield installed on the Teensy, and its microSD card is where the files need to be written to. I am not averse to back doors/ditching the library approach.

Here is my sketch for full context: View attachment MidiFieldRecorderSketch_WORKING_9.17.ino
 
Last edited:
Solved this! Just had myself thinking in the wrong circles. What I had to do was call the intermediary function "myNoteOn" using the parameters "note, velocity, channel" that I had used within "usbMIDI.sendNoteOn (Note[j], velocity, 1);".

The intermediary "myNoteOn" just re-orders these parameters as "channel, note, velocity" to what the "void writeToFile(){;}" section is expecting to receive:

Code:
// File and MIDI handling
#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 <MIDI.h> // Don't think I still need this. TBD.
#include <SerialFlash.h>

// INDEX # -->  0     1     2    3
int FSRpin[] = {A17, A16, A15, A14};
const int FSRs = 4;  
int Note [] = {60, 61, 62, 63};  
int counter [FSRs];         
int VELMASK = 0;
int ATMASK = 0;
int AFTERTHRESH = 50; 
int THRESH = 45; 
int VELTIME = 500;   
int AFTERTIME = 2500;  
int MIDIMIN = 20;  
void NoteOnSend (int);   
void PolyTouchSend (int);
//void testingArguments (int);  

int channel [] = {0, 1, 2, 3};    
const int SAMPLEs = 4;
int counter2 [SAMPLEs]; 
void SampleOnSend (int);    
void gain (unsigned int channel, float gain);

// 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
#define AFTER_TOUCH_POLY_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;
unsigned long lastTime = 0;

#define FILE_FLUSH_INTERVAL 400
String filename;
File file;

// 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

MIDI_CREATE_DEFAULT_INSTANCE();

/**
   Set up our inline MIDI recorder
*/
void setup() {
  //MIDI.begin();
  usbMIDI.begin();
  Serial.begin (32500);
  //Serial.begin (31250); //either baud rate works
  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);
  usbMIDI.begin();

  usbMIDI.setHandleNoteOff(myNoteOff);
  usbMIDI.setHandleNoteOn(myNoteOn);
  usbMIDI.setHandleControlChange(myControlChange);
  usbMIDI.setHandlePitchChange(myPitchChange);
  usbMIDI.setHandleAfterTouchPoly(myAfterTouchPoly);

  /*
  MIDI.setHandleNoteOn(handleNoteOn);
  MIDI.setHandleNoteOff(handleNoteOff);
  MIDI.setHandlePitchBend(handlePitchBend);
  MIDI.setHandleControlChange(handleControlChange);
  //MIDI.setHandleAfterTouchPoly(handlePolyAfterTouch);
  */

//==========================================================================
  // 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() {
    for (int i = 0; i < FSRs; i++) {
    int FSRRead = analogRead(FSRpin[i]);
    if (FSRRead > THRESH) {
      counter[i] ++;
      if (!(VELMASK & (1 << i)) && (counter[i] == VELTIME)) {
        VELMASK |= (1 << i);                   
        counter [i] = 0;
        NoteOnSend (i);
        //testingArguments (i);
        SampleOnSend (i);                                      
        }
      if (counter [i] == AFTERTIME) {
        counter [i] = 0;
        PolyTouchSend(i);
      }
    }
    else {                                                    
      if (VELMASK & (1 << i)) {           
        //MIDI.sendNoteOff (Note[i], 0, 1);
        usbMIDI.sendNoteOff (Note[i], 0, 1);
        myNoteOff(1, Note[i], 0);
        VELMASK &= ~ (1 << i);                 
        counter [i] = 0;
      }
    }
  }
  checkForMarker();
  setPlayState();
  updateFile();
  //MIDI.read();
  usbMIDI.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);                             // https://www.recordingblogs.com/wiki/midi-marker-meta-message#:~:text=The%20second%20byte%20is%20the,marker%20comment%20in%20ASCII%20text.
                                                // Oddson using usbmidi.h -- https://forum.pjrc.com/threads/38367-Teensy-2-0-USB-MIDI-and-SysEx
  // 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 NoteOnSend (int j) {
  int FSRRead = analogRead(FSRpin [j]);                       
  int velocity = map (FSRRead, 0, 800, MIDIMIN, 127);     
  //MIDI.sendNoteOn (Note[j], velocity, 1);
  usbMIDI.sendNoteOn (Note[j], velocity, 1);
  myNoteOn(1, Note[j], velocity);
}

// ======================================================================================

void PolyTouchSend (int j) {                                 
  int FSRRead = analogRead(FSRpin [j]);
  if (FSRRead > AFTERTHRESH) {
    int pressure = map (FSRRead, 0, 800, MIDIMIN, 127);     
    //MIDI.sendPolyPressure (Note[j], aftertouch, 1)
    usbMIDI.sendPolyPressure (Note[j], pressure, 1);
    myAfterTouchPoly(1, Note[j], pressure);
  }
}

// ======================================================================================
/*
  Reference to callbacks in void setup():
    usbMIDI.setHandleNoteOff(myNoteOff);
    usbMIDI.setHandleNoteOn(myNoteOn);
    usbMIDI.setHandleControlChange(myControlChange);
    usbMIDI.setHandlePitchChange(myPitchChange);
    //usbMIDI.setHandleAfterTouchPoly(myAfterTouchPoly);

    Order in which parameters are stored to writeToFile(); function:
      writeToFile(byte eventType, byte b1, byte b2, int delta) -> void
*/

// usbMIDI.h callbacks / writeToFile hand-offs ////
void myNoteOff(byte channel, byte note, byte velocity) {
  writeToFile(NOTE_OFF_EVENT, note, velocity, getDelta());
}
void myNoteOn(byte channel, byte note, byte velocity) {
  writeToFile(NOTE_ON_EVENT, note, velocity, getDelta());
  if (play) tone(AUDIO, 440 * pow(2, (note - 69.0) / 12.0), 100);
}
void myControlChange(byte channel, byte control, byte value) {
  writeToFile(CONTROL_CHANGE_EVENT, control, value, getDelta());
}
void myPitchChange(byte channel, int bend) {
  bend += 0x2000; // MIDI bend uses the range 0x0000-0x3FFF, with 0x2000 as center. Double the size of other data byte channel messages. A 14-bit thing.
  byte lsb = bend & 0x7F;
  byte msb = bend >> 7;
  writeToFile(PITCH_BEND_EVENT, lsb, msb, getDelta());
}
void myAfterTouchPoly(byte channel, byte note, byte pressure)  {
  writeToFile(AFTER_TOUCH_POLY_EVENT, note, pressure, getDelta());
}



// MIDI.h callbacks / writeToFile hand-offs ////
/*
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());
}
*/

/*
void handlePolyAfterTouch(byte channel, byte pitch, byte pressure)  {
  writeToFile(AFTER_TOUCH_POLY_EVENT, pitch, pressure, getDelta());
}
*/

// ======================================================================================

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); }
}

// ======================================================================================

/**
   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     eventType    b1     b2
     <delta> <event code> <byte> <byte>

   See the "Standard MIDI-File Format" for more information -- 
   http://www.music.mcgill.ca/~ich/classes/mumt306/StandardMIDIfileformat.html
*/
/*
  Try:
    usbMIDI.getData1()
    usbMIDI.getData2()
    usbMIDI.getChannel()
  ...if data isn't passing from " usbMIDI.h callbacks / writeToFile hand-offs "
*/
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;
    }
  }
}

/*
void testingArguments (int q) {

}
*/

/**
  Citations and Useful Links
    1--https://github.com/Pomax/arduino-midi-recorder
      Pomax's "MIDI Field Recorder" 
    2--https://forum.pjrc.com/threads/31797-Teensy-FSR-based-MIDI-controller
      Adrian's FSR Sensor Sketch
    3--https://www.pjrc.com/teensy/gui/
      PRJC's Auto-Generating Code GUI
*/

...at least this is what I believe the issue had been. It's working though🤷🏽 Now it's on to tackle transport controls and .mid file playback!

View attachment FSRIn_MIDI.midFileOut--SketchC_HybridSimplified_WORKING_9.18.ino

Blessed,
Colby
 
Back
Top