Forum Rule: Always post complete source code & details to reproduce any issue!
Results 1 to 13 of 13

Thread: MIDI Solenoid instrument - need to optimize code

  1. #1
    Junior Member
    Join Date
    Jan 2021
    Posts
    8

    MIDI Solenoid instrument - need to optimize code

    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
    Attached Files Attached Files

  2. #2
    Senior Member
    Join Date
    Jul 2020
    Posts
    666
    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

  3. #3
    Junior Member
    Join Date
    Jan 2021
    Posts
    8
    Thanks a lot for the tip. I will read up and try it out

  4. #4
    Senior Member
    Join Date
    Aug 2013
    Location
    Gothenburg, Sweden
    Posts
    354
    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}

  5. #5
    Junior Member
    Join Date
    Jan 2021
    Posts
    8
    Thanks a lot, mlu! Great tip. Guess there's also some circular buffer libraries for Arduino I can try with Teensy.

  6. #6
    Senior Member
    Join Date
    Aug 2013
    Location
    Gothenburg, Sweden
    Posts
    354
    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.

  7. #7
    Senior Member
    Join Date
    Apr 2020
    Location
    DFW area in Texas
    Posts
    187
    Quote Originally Posted by mlu View Post
    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 by kd5rxt-mark; 01-23-2021 at 03:05 PM.

  8. #8
    Senior Member
    Join Date
    Aug 2013
    Location
    Gothenburg, Sweden
    Posts
    354
    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.

  9. #9
    Junior Member
    Join Date
    Jan 2021
    Posts
    8
    Quote Originally Posted by mlu View Post
    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.

  10. #10
    Junior Member
    Join Date
    Jan 2021
    Posts
    8
    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!

  11. #11
    Senior Member
    Join Date
    Aug 2013
    Location
    Gothenburg, Sweden
    Posts
    354
    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;
    }

  12. #12
    Junior Member
    Join Date
    Jan 2021
    Posts
    8
    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!

  13. #13
    Senior Member
    Join Date
    Apr 2020
    Location
    DFW area in Texas
    Posts
    187
    Quote Originally Posted by mlu View Post
    Its not my project, but I agree
    @zangpa & @mlu:

    Sorry for the incorrect attribution !! Not enough coffee . . . working to fix that now !!

    Mark J Culross
    KD5RXT

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •