Friday, April 15, 2016

Guest Post: Alternative way of note selection (Johan Berglund)

One of the best things about the internet, is the way it brings all of us around the world together. Today, I'd like to bring your a guest post from Johan, with some insights on mapping key fingerings (via switches) to MIDI notes. 
The interesting thing to think about is - how efficient is the code that maps the fingered notes to a MIDI note value. Is it more efficient to take a value and do a bunch of additions/subtractions (Johan's code), or to do a linear search on an array (my code)? Is there a better way? Share your ideas!
Thank you, Johan, for sharing your work!
-Gordon
=============
 
Alternative way of note selection
guest post by Johan Berglund

My idea for a DIY wind controller was to build it small and light with cheap components, an Arduino Pro Mini, a DIN 5 MIDI output and the keys and fingering based on the Akai EWI. Going through the fingering chart in the EWI manual, I discovered that some of the fingerings that I use weren't even listed. How many unlisted fingerings could there be? When playing the EWI it seemed there was more of a logical pattern to the workings of each key than what could be expected if it was looking up combinations in a list. Looking for answers I found the blog of Bret Pimentel, and his post on flexible EWI fingerings.


He had it all figured out. Each key changes the note value a certain number of semitones up or down, some of them with conditions. Let's list them here as variable declarations for our Arduino sketch:

            // Key variables, TRUE (1) for pressed, FALSE (0) for not pressed
byte LH1;   // Left Hand key 1 (pitch change -2)
byte LHb;   // Left Hand bis key (pitch change -1 unless both LH1 and LH2 are pressed)
byte LH2;   // Left Hand key 2  (with LH1 also pressed pitch change is -2, otherwise -1)
byte LH3;   // Left Hand key 3 (pitch change -2)
byte LHp1;  // Left Hand pinky key 1 (pitch change +1)
byte LHp2;  // Left Hand pinky key 2 (pitch change -1)
byte RHs;   // Right Hand side key  (pitch change -2 unless LHp1 is pressed)
byte RH1;   // Right Hand key 1 (with LH3 also pressed pitch change is -2, otherwise -1)
byte RH2;   // Right Hand key 2 (pitch change -1)
byte RH3;   // Right Hand key 3 (pitch change -2)
byte RHp1;  // Right Hand pinky key 1 (pitch change +1)
byte RHp2;  // Right Hand pinky key 2 (pitch change -1)
byte RHp3;  // Right Hand pinky key 3 (pitch change -2)
byte OCTup; // Octave switch key (pitch change +12) 


Ok, so we have straightforward rules for how the note number should change when the keys are pressed. With no keys pressed, it should send a C#, so lets put that in there.

byte startNote=61; // set startNote to C# (change this value in steps of 12 to start in other octaves)

And also a variable for the calculated result.

int fingeredNote; // note calculated from fingering (switches)

To get the keys pressed into their dedicated variables we need to read them from the Arduino pins. In the setup part of our sketch, we set their input pins as digital inputs with pullups. That makes them active low. (To use this with other Arduinos or a Teensy, just change the input pins accordingly. Just make sure you have enough digital pins for the project.)

  pinMode(2, INPUT_PULLUP); // Set inputs with pull-up
  pinMode(3, INPUT_PULLUP); 
  pinMode(4, INPUT_PULLUP); 
  pinMode(5, INPUT_PULLUP); 
  pinMode(6, INPUT_PULLUP); 
  pinMode(7, INPUT_PULLUP); 
  pinMode(8, INPUT_PULLUP); 
  pinMode(9, INPUT_PULLUP); 
  pinMode(10, INPUT_PULLUP); 
  pinMode(11, INPUT_PULLUP); 
  pinMode(12, INPUT_PULLUP);  
  pinMode(14, INPUT_PULLUP); 
  pinMode(15, INPUT_PULLUP); 
  pinMode(16, INPUT_PULLUP);

Reading them will give us a HIGH (1) for the keys that are not pressed and a LOW (0) for the keys that are pressed. To get the value TRUE (1) into our variables for the LOW (0) inputs, and FALSE (0) for the HIGH (1) inputs, we just invert the value with the logical operator NOT. A logical NOT in C is simply a '!' in front of the value that is inverted, like this.

void readSwitches(){  
  // Read  switches and put inverted value in variables
  LH1=!digitalRead(2);
  LHb=!digitalRead(3);
  LH2=!digitalRead(4);
  LH3=!digitalRead(5);
  LHp1=!digitalRead(6);
  LHp2=!digitalRead(7);
  RHs=!digitalRead(8);
  RH1=!digitalRead(9);
  RH2=!digitalRead(10);
  RH3=!digitalRead(11);
  RHp1=!digitalRead(12);
  RHp2=!digitalRead(14);
  RHp3=!digitalRead(15);
  OCTup=!digitalRead(16);
}

Now we will have every variable for a pressed key set to a numerical '1' and the variables for keys not pressed to a numerical '0'. Here comes the tricky bit. We want to combine math and logic to get our fingeredNote right. We begin with our startNote.

fingeredNote=startNote;

Then we take the keys from the top down. First we have LH1. It changes the pitch with -2 semitones, so let's subtract two times the value of LH1 from the startNote.

fingeredNote=startNote-2*LH1;

Next up is the bis key, LHb, and already it gets more tricky. In our notes we stated it should do "pitch change -1 unless both LH1 and LH2 are pressed". How do we do that in C? Another logical operator, AND (&&), will help us do this. AND will make the condition true only if both operands are true, otherwise it's false. So, we subtract 1 (TRUE) only if LHb is pressed AND NOT (LH1 AND LH2). Let's add it to our calculation.

fingeredNote=startNote-2*LH1-(LHb && !(LH1 && LH2));

Ok, now we know how to do this, so the next one will be easy. If LH2 is pressed, subtract 1, and if both LH2 AND LH1 are pressed, subtract another 1.

fingeredNote=startNote-2*LH1-(LHb && !(LH1 && LH2))-LH2-(LH2 && LH1);

The following three keys are simple, just add or subtract each key multiplied with the number of semitones.

fingeredNote=startNote-2*LH1-(LHb && !(LH1 && LH2))-LH2-(LH2 && LH1)-2*LH3+LHp1-LHp2;

Next is the right hand side key, RHs. It should subtract two semitones unless LHp1 is pressed. Not too hard now that we know how to use ! and &&.

fingeredNote=startNote-2*LH1-(LHb && !(LH1 && LH2))-LH2-(LH2 && LH1)-2*LH3+LHp1-LHp2+(RHs && !LHp1);

Not many special cases left now. Just this last one. RH1 should subtract one semitone, but if LH3 is also pressed it should subtract two semitones.

fingeredNote=startNote-2*LH1-(LHb && !(LH1 && LH2))-LH2-(LH2 && LH1)-2*LH3+LHp1-LHp2+(RHs && !LHp1)-RH1-(RH1 && LH3);

After that it's easy as pie. Again just add or subtract each key multiplied with the number of semitones.

fingeredNote=startNote-2*LH1-(LHb && !(LH1 && LH2))-LH2-(LH2 && LH1)-2*LH3+LHp1-LHp2+(RHs && !LHp1)-RH1-(RH1 && LH3)-RH2-2*RH3+RHp1-RHp2-2*RHp3+12*OCTup;

Done! We now have one single line of code that deals with all possible fingerings, just like that.

In the context of my MiniWI Arduino sketch it looks like this (I'm removing the fancy stuff like the joysticks for octaves, modulation and pitch bend for now):

/*
NAME:                 MiniWI Lite Ver.
WRITTEN BY:           JOHAN BERGLUND
CREDITS:              State machine from the Gordophone blog by GORDON GOOD
DATE:                 2016-04-13
FILE SAVED AS:        MiniWI.ino
FOR:                  Arduino Pro Mini, ATmega328
CLOCK:                16.00 MHz CRYSTAL                                        
PROGRAMME FUNCTION:   Wind Controller with EWI style key setup, Freescale MPX5010GP breath sensor 
                      and output to 5-pin DIN MIDI 

HARDWARE NOTES:
* For the MIDI connection, attach a MIDI out Female 180 Degree 5-Pin DIN socket to Arduino.
* Socket is seen from solder tags at rear.
* DIN-5 pinout is:                                         _______ 
*    pin 2 - GND                                          /       \
*    pin 4 - 220 ohm resistor to +5V                     | 1     3 |  MIDI jack
*    pin 5 - Arduino Pin 1 (TX) via a 220 ohm resistor   |  4   5  |
*    all other pins - unconnected                         \___2___/
*
* The Freescale MPX5010GP pressure sensor output (V OUT) is connected to Arduino pin A3.
* Sensor pinout
* 1: V OUT (pin with indent)
* 2: GND
* 3: VCC (to 5V)    
* 4: n/c
* 5: n/c
* 6: n/c
*     
*     
* All key switches connect Ardino digital inputs (with internal pullups) to GND
*/

//_______________________________________________________________________________________________ DECLARATIONS

#define ON_Thr 40       // Set threshold level before switching ON
#define ON_Delay   20   // Set Delay after ON threshold before velocity is checked (wait for tounging peak)
#define breath_max 300  // Blowing as hard as you can
#define modsLo_Thr 411  // Low threshold for mod stick center
#define modsHi_Thr 611  // High threshold for mod stick center
#define octsLo_Thr 311  // Low threshold for octave stick center
#define octsHi_Thr 711  // High threshold for octave stick center

// The three states of our state machine

// No note is sounding
#define NOTE_OFF 1

// We've observed a transition from below to above the
// threshold value. We wait a while to see how fast the
// breath velocity is increasing
#define RISE_WAIT 2

// A note is sounding
#define NOTE_ON 3

// Send CC data no more than every CC_INTERVAL
// milliseconds
#define CC_INTERVAL 40 


//variables setup

int state;                         // The state of the state machine
unsigned long ccSendTime = 0L;     // The last time we sent CC values
unsigned long breath_on_time = 0L; // Time when breath sensor value went over the ON threshold
int initial_breath_value;          // The breath value at the time we observed the transition

long lastDebounceTime = 0;         // The last time the fingering was changed
long debounceDelay = 30;           // The debounce time; increase if the output flickers
int lastFingering = 0;             // Keep the last fingering value for debouncing

byte MIDIchannel=0;                // MIDI channel 1

int breathLevel;

int pressureSensor;  // pressure data from breath sensor, for midi breath cc and breath threshold checks
byte velocity;       // remapped midi velocity from breath sensor

int fingeredNote;    // note calculated from fingering (switches) and octave joystick position
byte activeNote;     // note playing
byte startNote=61;   // set startNote to C# (change this value in steps of 12 to start in other octaves)

byte midistatus=0;
byte x;
byte LedPin = 13;    // select the pin for the LED

            // Key variables, TRUE (1) for pressed, FALSE (0) for not pressed
byte LH1;   // Left Hand key 1 (pitch change -2)
byte LHb;   // Left Hand bis key (pitch change -1 unless both LH1 and LH2 are pressed)
byte LH2;   // Left Hand key 2  (with LH1 also pressed pitch change is -2, otherwise -1)
byte LH3;   // Left Hand key 3 (pitch change -2)
byte LHp1;  // Left Hand pinky key 1 (pitch change +1)
byte LHp2;  // Left Hand pinky key 2 (pitch change -1)
byte RHs;   // Right Hand side key  (pitch change -2 unless LHp1 is pressed)
byte RH1;   // Right Hand key 1 (with LH3 also pressed pitch change is -2, otherwise -1)
byte RH2;   // Right Hand key 2 (pitch change -1)
byte RH3;   // Right Hand key 3 (pitch change -2)
byte RHp1;  // Right Hand pinky key 1 (pitch change +1)
byte RHp2;  // Right Hand pinky key 2 (pitch change -1)
byte RHp3;  // Right Hand pinky key 3 (pitch change -2)
byte OCTup; // Octave switch key (pitch change +12)



//_______________________________________________________________________________________________ SETUP

void setup() {
  pinMode(2, INPUT_PULLUP); // Set inputs with pull-up
  pinMode(3, INPUT_PULLUP); 
  pinMode(4, INPUT_PULLUP); 
  pinMode(5, INPUT_PULLUP); 
  pinMode(6, INPUT_PULLUP); 
  pinMode(7, INPUT_PULLUP); 
  pinMode(8, INPUT_PULLUP); 
  pinMode(9, INPUT_PULLUP); 
  pinMode(10, INPUT_PULLUP); 
  pinMode(11, INPUT_PULLUP); 
  pinMode(12, INPUT_PULLUP);  
  pinMode(14, INPUT_PULLUP); 
  pinMode(15, INPUT_PULLUP); 
  pinMode(16, INPUT_PULLUP);
  
  state = NOTE_OFF;  // initialize state machine
  
  pinMode(LedPin,OUTPUT);   // declare the LED's pin as output

  for (x=1; x<=4; x++){  // Do the flashy-flashy to say we are up and running
    digitalWrite( LedPin, HIGH );
    delay(300);
    digitalWrite( LedPin, LOW );
    delay(300);
  }

  Serial.begin(31250);  // start serial with midi baudrate 31250
  Serial.flush();
}

//_______________________________________________________________________________________________ MAIN LOOP

void loop() {
  
  pressureSensor = analogRead(A3); // Get the pressure sensor reading from analog pin A3

  if (state == NOTE_OFF) {
    if (pressureSensor > ON_Thr) {
      // Value has risen above threshold. Move to the ON_Delay
      // state. Record time and initial breath value.
      breath_on_time = millis();
      initial_breath_value = pressureSensor;
      state = RISE_WAIT;  // Go to next state
    }
  } else if (state == RISE_WAIT) {
    if (pressureSensor > ON_Thr) {
      // Has enough time passed for us to collect our second
      // sample?
      if (millis() - breath_on_time > ON_Delay) {
        // Yes, so calculate MIDI note and velocity, then send a note on event
        readSwitches();
     
        //calculate midi note number from pressed keys and octave shifts
        fingeredNote=startNote-2*LH1-(LHb && !(LH1 && LH2))-LH2-(LH2 && LH1)-2*LH3+LHp1-LHp2+(RHs && !LHp1)-RH1-(RH1 && LH3)-RH2-2*RH3+RHp1-RHp2-2*RHp3+12*OCTup;

        // We should be at tonguing peak, so set velocity based on current pressureSensor value        
        // If initial value is greater than value after delay, go with initial value, constrain input to keep mapped output within 7 to 127
        velocity = map(constrain(max(pressureSensor,initial_breath_value),ON_Thr,breath_max),ON_Thr,breath_max,7,127);
        midiSend((0x90 | MIDIchannel), fingeredNote, velocity); // send Note On message for new note
        activeNote=fingeredNote;
        state = NOTE_ON;
      }
    } else {
      // Value fell below threshold before ON_Delay passed. Return to
      // NOTE_OFF state (e.g. we're ignoring a short blip of breath)
      state = NOTE_OFF;
    }
  } else if (state == NOTE_ON) {
    if (pressureSensor < ON_Thr) {
      // Value has fallen below threshold - turn the note off
      midiSend((0x80 | MIDIchannel), activeNote, velocity); //  send Note Off message 
      state = NOTE_OFF;
    } else {
      // Is it time to send more CC data?
      if (millis() - ccSendTime > CC_INTERVAL) {
         // deal with Breath
         breath();
         ccSendTime = millis();
      }
    }
    readSwitches();     
    //calculate midi note number from pressed keys
    fingeredNote=startNote-2*LH1-(LHb && !(LH1 && LH2))-LH2-(LH2 && LH1)-2*LH3+LHp1-LHp2+(RHs && !LHp1)-RH1-(RH1 && LH3)-RH2-2*RH3+RHp1-RHp2-2*RHp3+12*OCTup;

    if (fingeredNote != lastFingering){ //
      // reset the debouncing timer
      lastDebounceTime = millis();
    }
    if ((millis() - lastDebounceTime) > debounceDelay) {
    // whatever the reading is at, it's been there for longer
    // than the debounce delay, so take it as the actual current state
      if (fingeredNote != activeNote) {
        // Player has moved to a new fingering while still blowing.
        // Send a note off for the current note and a note on for
        // the new note.
        midiSend((0x80 | MIDIchannel), activeNote, velocity); // send Note Off message
        activeNote=fingeredNote;
        midiSend((0x90 | MIDIchannel), activeNote, velocity); // send Note On message
      }
    }
  }
  lastFingering=fingeredNote; 
}
//_______________________________________________________________________________________________ FUNCTIONS

//  Send a three byte midi message  
  void midiSend(byte midistatus, byte data1, byte data2) {
  digitalWrite(LedPin,HIGH);  // indicate we're sending MIDI data
  Serial.write(midistatus);
  Serial.write(data1);
  Serial.write(data2);
  digitalWrite(LedPin,LOW);  // indicate we're sending MIDI data
}

//***********************************************************

void breath(){
  breathLevel = analogRead(A3); // read voltage on analog pin A3
  breathLevel = map(constrain(breathLevel,ON_Thr,breath_max),ON_Thr,breath_max,0,127);
  midiSend((0xB0 | MIDIchannel), 2, breathLevel);
}

//***********************************************************

void readSwitches(){  
  // Read switches and put inverted value in variables
  LH1=!digitalRead(2);
  LHb=!digitalRead(3);
  LH2=!digitalRead(4);
  LH3=!digitalRead(5);
  LHp1=!digitalRead(6);
  LHp2=!digitalRead(7);
  RHs=!digitalRead(8);
  RH1=!digitalRead(9);
  RH2=!digitalRead(10);
  RH3=!digitalRead(11);
  RHp1=!digitalRead(12);
  RHp2=!digitalRead(14);
  RHp3=!digitalRead(15);
  OCTup=!digitalRead(16);
}



Supplementary studies

If you prefer brass valve instruments over woodwind, you can do the exact same thing but with the AKAI/Steiner EVI fingerings, only it's easier because there are no conditional value changes for any keys. Resources needed to do that are found here:


Direct links to the EVI 1000 fingering charts including the value changes for each key are here:

There's also info on the EVI fingering mode in the reference manual for the EWI4000s:


The code for my MiniWI project in its complete (and now and then updated) form is available at Github.

5 comments:

  1. Hello Gordon.

    I really enjoyed reading your blog on EWI design & construction. Good stuff! I have been working on a similar project, still a work-in-progress. Whereas you have forged ahead with the software, my software is lagging behind but my hardware design is well advanced. By coincidence, I also chose the MPR121 for touch-pad sensing. (An obvious choice really.) My initial focus was development of a "built-in sound synthesizer" implemented largely in software, which will allow the instrument to be played "stand-alone".

    Where your ideas and mine diverge mostly is in the fingering schemes. I decided to take a radical path moving away from traditional fingerings in favour of a simpler (binary) keying scheme. Musicians already playing a traditional wind instrument might find this idea objectionable, but many novices wanting to learn a new instrument with minimum effort might be more accepting.

    If you're curious, you can find an introductory article on my project here:
    http://www.mjbauer.biz/Build_the_REMI_by_MJB.htm

    I would appreciate your comments, also from other readers of your blog.

    Cheers... Mike

    ReplyDelete
    Replies
    1. Mike, thanks for sharing your work!

      I haven't made it through all of your REMI post yet, but I couldn't refrain from posting about how impressed I am with the physical construction of your instrument. It's really beautiful.

      I would be very interested in hearing your thoughts about fingerings, and how people learn to play musical instruments; e.g. traditional vs. novel fingerings, and so on. Would you be willing to share some of your thoughts in some blog posts, either here or on your own site? It might make for an interesting discussion. Maybe you could, like Johan did, write a "guest post" which I will host on this blog, that includes links to your own site?

      Delete
    2. Hi Gordon. Yes, I'm also very interested to get readers' opinions on EWI fingering schemes. Would they prefer the minimalist "binary" fingering scheme proposed for the generic REMI, or one of the fingerings used in traditional wind instruments (flute, sax, recorder, clarinet, oboe, piccolo, etc)? They're all different, and many musicians play more than one type of instrument, so it seems to me why not introduce yet another fingering for a new instrument? (The REMI's fingering scheme uses fewer keys, covers a greater pitch range, i.e. 7 octaves, and allows the "touch trigger mode" to operate.)

      My website is primitive, doesn't have a comment/feedback facility, so I suggest readers either respond here, or on the official REMI project post on Elektor Labs website: https://www.elektormagazine.com/labs/rudimentary-electronic-musical-instrument-and-midi-controller

      I may well take up your offer to do a Guest Post further down the track, when I have more feedback from REMI makers and observers.

      You and your blog followers might also be interested in the 'BIRL' EWI project at: http://www.snyderphonics.com/birl.htm

      Cheers - Mike

      Delete
    3. After seeing your schematics, Mike, I start feeling dumb with my primitive controller.
      http://i.imgur.com/7UqZVWr.jpg
      Anyway, it does what's needed from it and I'm pretty happy with my "design". :)

      Delete
    4. I've made Telegram group, named "windmidi", so it might be a good idea to gather all people who are instrested in creating their own DIY wind controllers in one place. The link is telegram.me/windmidi. You're welcome!

      Delete