MIDI Solenoid instrument - need to optimize code

Status
Not open for further replies.

zangpa

Member
I am a medium skilled arduino programmer who is migrating to teensy. I am working on a big art project and trying to create a MIDI instrument that uses a set of solenoids (will hopefully be between 50-100 at the end) to play different objects. The code reads all MIDI note on messages and uses them to trigger the solenoids. It then saves the time (millis()) of the note on message in an array, en turns off the solenoid after a certain time (in ms), noteOffDelay, has appeared.

I have tried my best to optimize the code and reduce the time it uses to perform its tasks since i wish to reduce the noteOffDelay as much as possible, but the teensy still uses too much time to perform the code. The result is that some notes are skipped when playing the solenoid instrument in a fast tempo.

So, i hope you good people can give me some advice. How can i write an even more efficient code? I would be very grateful for any suggestions.

Best,
Pål Asle
 

Attachments

  • MIDI_solenoid_instrument.ino
    4.8 KB · Views: 77
This is the classic scheduling problem, and a good data-structure to use is a heapsorted list, aka binary heap.

inserts and extracts are O(log(N)) so it scales well to 100's or 1000's of entries, but you'll just need a few dozen anyway.

You're shuffling lists which is O(N).

Binary heap: https://en.wikipedia.org/wiki/Binary_heap
 
With all delays equal and the time always monotonically increasing its a simple queue, I would use a circular buffer with the reference (pointer) to the head which is the next element to remove, and another reference to the tail, next position to insert a note.

With the circular queue the only time you have to step through it is when checking if a note is already playing. Inserts and removals are simple and doesnt depend on the number of playing notes.

Furthermore instead of a twodimensional array, I would use a circulart buffer of struct {int notetime; int noteval}
 
Thanks a lot, mlu! Great tip. Guess there's also some circular buffer libraries for Arduino I can try with Teensy.
 
I have tested your original code on a Teensy 3.6 and a virtual midi keyboard on MacOS, outputs just connected to led's. With only these 5 possible solenoids its very fast, in order of ten microseconds for the loop code. I have not been able to make it drop a note yet. It could get much slower for 50 or hundred outputs, so I added a delay(100) in the loop, then the output pin high were delayed when several notes arrived fast, but I couldn't see any notes dropped unless the note was already being played.
 
I have tested your original code on a Teensy 3.6 and a virtual midi keyboard on MacOS, outputs just connected to led's. With only these 5 possible solenoids its very fast, in order of ten microseconds for the loop code. I have not been able to make it drop a note yet. It could get much slower for 50 or hundred outputs, so I added a delay(100) in the loop, then the output pin high were delayed when several notes arrived fast, but I couldn't see any notes dropped unless the note was already being played.

@mlu:

Sounds like a fun project !! Would it be of any benefit to drive the solenoids from shift registers (74HC595 - I used these very nicely in my <TeensyMIDIPolySynth> project to drive 40 LEDs plus 2 x 7-segment displays) ?? You could load up the registers quickly (datasheet indicates clock frequency of 20MHz or better at 5VDC supply) & use the LATCH pin to activate all of the solenoids simultaneously. Should present a constant amount of time for loading & activating with each (noteOn/noteOff) update.

Good luck & have fun !!

Mark J Culross
KD5RXT
 
Last edited:
Its not my project, but I agree that this a could be a good way to handle something like 100 solenoids, a number to big for the Teensy pincounts. In this case there would be a solenoid array, or bitmap, on/off for the state of solenoids/output pins and the circular buffer with activation times and and midi note number.
 
I have tested your original code on a Teensy 3.6 and a virtual midi keyboard on MacOS, outputs just connected to led's. With only these 5 possible solenoids its very fast, in order of ten microseconds for the loop code. I have not been able to make it drop a note yet. It could get much slower for 50 or hundred outputs, so I added a delay(100) in the loop, then the output pin high were delayed when several notes arrived fast, but I couldn't see any notes dropped unless the note was already being played.

Hi, and thanks for testing. That's a bit strange. The code isn't that fast at all when i have tested the code at my computer. But good to know. Then i need to find out if there's some external conditions with my computer or the connection btw computer and teensy which slows stuff down.
 
Thanks al lot, kd5rxt-mark! I am not that familiar with shift registers, so will definitely look into it. I may actually end up with 100 solenoids at the end so important to take that into consideration at an early stage. Thanks for linking to your project! Very interesting!
 
Here is a ring buffer variant to test also

Code:
/* Control a set of solenoids with MIDI
 * Note on messages trigger solenoids, and they will be turned off after a certain amount of ms has passed
 */ 

int MIDI_note=-1;
int MIDI_Channel=1;

int sol1=2;
int sol2=3;
int sol3=4;
int sol4=5;
int sol5=6; 


const int NoteEvent_size=64; //last note in array (buffer)
const unsigned long noteOffDelay=1000; //time (in ms) before note is turned off
const int pinOffset=58; // Only use MIDI note range from 60 (C3) and up, so 60 will trigger pin 2
int note=0;

typedef struct {int midiNote;int noteTime;} NoteEvent_t;

/* NoteEvent_tail is index in circular buffer for the first note to be removed */
/* NoteEvent_head is index in circular buffer for the next note to be added */
/* If NoteEvent_count == 0 then list is empty */
/* If NoteEvent_count == NoteEvent_size then list is full */

NoteEvent_t NoteEvent[NoteEvent_size];
int NoteEvent_tail = 0;
int NoteEvent_head = 0;  
int NoteEvent_count = 0;  

bool inline NoteEventFull() {return NoteEvent_count == NoteEvent_size; };
bool inline NoteEventEmpty() {return NoteEvent_count == 0; };
int inline  NoteEventNext(int eventIndex) {return (eventIndex+1)%NoteEvent_size; };
void inline WriteNoteEventPin(int NoteEventIndex, int state) { 
  digitalWrite(NoteEvent[NoteEventIndex].midiNote-pinOffset,state); //turn off solenoid
}

void setup() {
  Serial.begin(115200);
  while (!Serial) {}
  usbMIDI.setHandleNoteOn(myNoteOn);
  //usbMIDI.setHandleNoteOff(myNoteOff);
  //usbMIDI.setHandleControlChange(myControlChange);

  pinMode(sol1, OUTPUT);
  pinMode(sol2, OUTPUT);
  pinMode(sol3, OUTPUT);
  pinMode(sol4, OUTPUT);
  pinMode(sol5, OUTPUT);
  
/*
  for (int i=0;i<(NoteEvent_size+1);i++){
    for (int j=0;j<2;j++) {
      noteOff[j][i]=0;
    }
  }
*/
}

int overflowevent = 0;
uint32_t maxupdatetime = 0;
elapsedMillis looptime;

uint32_t updatetime = 0;
 
void loop() {
  updatetime = micros();

  usbMIDI.read(MIDI_Channel);
  
  turnNotesOff();
  
  if ((MIDI_note>59)&&(MIDI_note<65)) {
    int checkNote=checkIfNotePlaying(MIDI_note);
    if (checkNote==0) {
      playNote(MIDI_note);
    }
    MIDI_note = -1;
  }

  //delay(5);

}

//Read note no messages from external units
void myNoteOn(byte channel, byte note, byte velocity) {
  MIDI_note=note;
}


//turn off all notes that have been on for more than noteOffDelay
void turnNotesOff() {
  unsigned long realTime=millis();
  while ((NoteEvent_count > 0) && ( (realTime-NoteEvent[NoteEvent_tail].noteTime)>=noteOffDelay)) {
    WriteNoteEventPin(NoteEvent_tail, 0);
    NoteEvent_count -= 1;
    NoteEvent_tail = (NoteEvent_tail+1) % NoteEvent_size;
  }
}

//check if note has been played recently without being turned off
int checkIfNotePlaying(int playNote) {
  for (int i = NoteEvent_tail, count=NoteEvent_count; count>0; i = (i+1)%NoteEvent_size, count=count-1) {
    if (NoteEvent[i].midiNote == playNote ) return 1;
  }
  return 0;
}


//play note
//save note information
void playNote (int newNote) {
  if (NoteEventFull()) { /* Buffer full, remove oldest, NoteEvent_tail */
    WriteNoteEventPin(NoteEvent_tail, 0);
    NoteEvent_count -= 1;
    NoteEvent_tail = (NoteEvent_tail+1) % NoteEvent_size; 
  }
  NoteEvent[NoteEvent_head].midiNote = newNote;
  Serial.printf("play %i on %i \n",newNote, NoteEvent[NoteEvent_head].midiNote-pinOffset);
  WriteNoteEventPin(NoteEvent_head, 1);
  unsigned long noteOffTime=millis();
  NoteEvent[NoteEvent_head].noteTime=noteOffTime;
  NoteEvent_count += 1;
  NoteEvent_head=(NoteEvent_head+1)%NoteEvent_size;
}
 
Oh, great. I am on my way to create a ring buffer myself, but will try out your code first on Monday when i access the studio. Your help is highly appreciated!
 
Status
Not open for further replies.
Back
Top