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.
Thursday, February 25, 2016
Shout-Outs
Here are some links to some great wind controller projects, found by Onyx Ashanti:Open Horn MIDI System (OHMS)
Gwen's MIDI Controller
Note Selection Basics
In my previous posts, I've focused on detecting note-on and note-off transitions from the breath controller, and on mapping the breath intensity to a MIDI continuous controller to control expressive parameters. But the circuits and code have never been able to play more than one note. In this post, I'll discuss how to read a set of finger inputs, like the keys on a woodwind instrument, or the valves of a trumpet.
To keep things simple, I'll start with 3 finger inputs, like found on a trumpet. For a B-flat trumpet, the 3 fingers can be used to play 7 different notes:
Index
|
Middle
|
Ring
|
Note Produced
|
Up
|
Up
|
Up
|
B-flat
|
Up
|
Down
|
Up
|
A
|
Down
|
Up
|
Up
|
A-flat
|
Down
|
Down
|
Up
|
G
|
Up
|
Down
|
Down
|
G-flat
|
Down
|
Up
|
Down
|
F
|
Down
|
Down
|
Down
|
E
|
Readers familiar with binary numbers may be saying, "wait, with 3 binary digits, you should be able to produce 2^3, or 8 distinct finger combinations." And you would be right, except that the fingering combination [Index=up, Middle=Up, Ring=Down] produces an "in-between-pitch" note in the lower registers of the trumpet, and is not used.
A Real Instrument
For those of you unfamiliar with how a trumpet works, most have 3 piston valves which re-route the flow of air through the instrument to change the effective length, and therefore the resonant frequency, of the instrument.
A natural way to simulate the operation of a piston valve like this is to use a momentary switch, so let's wire something up. I've used a small tactile switch like the following:
(Photo: Wikimedia Commons: https://upload.wikimedia.org/wikipedia/commons/3/35/BUTA-06-X-STAN-01.jpg)
These tactile switches are widely used in consumer electronics, and are very inexpensive.e.g. $0.20 each. They are available from all the major parts suppliers.
I've wired up three of these on a breadboard, spaced approximately finger-width apart:
Now, to be clear, no actual trumpet player would find this thing pleasant to play, as the "feel" of the tactile switches is very different from the feel of an actual piston valve. But it will let us start to experiment with how we can simulate real-world musical instrument properties like trumpet valves and woodwind keys.
To connect these switches to your microcontroller, first figure out which pins are connected when the switch is pressed. If you are using the tactile switches like I am, there are 4 pins on each one, arranged in two pairs. If you find that the switch appears to be always pressed, you may need pull the switch out of the breadboard, rotate it 90 degrees, and re-insert it.
Once the switches are attached to the breadboard, connect one side of each switch to a common wire (this will attach to the ground on your microcontroller). The other side of each switch should be brought out on a separate wire. In total, there will be 4 connections to the microcontroller, like this:
Once you have these wired up, attach Common to the Ground (GND) pin on your microcontroller, and the Index, Middle, and Ring (finger) switch wires to three digital input pins. I've attached mine to digital inputs 2, 3, and 4 on my Teensy microcontroller. If you have yours attached to different input pins, you'll need to modify the code examples below accordingly.
One thing to be aware of is that we're pulling the digital inputs low when the buttons are pressed. So the "valve" will read HIGH when the button is not pressed and LOW when pressed.
Mapping Inputs to MIDI Notes
Now that we have our "trumpet valves" plugged into our microcontroller, how do we get it to send different MIDI notes to our synthesizers according to which switches are pressed?One approach might be to use a bunch of if/then statements to decide which note to select. It might look like:
if (switch one pressed) {
if (switch two pressed) {
if (switch three pressed) {
send E
} else {
send G
}
} else {
// switch two is NOT pressed
if (switch three pressed) {
send F
} else {
send A-flat
}
etc etc
As you can see, this is pretty hard to read, and it's pretty easy to make a mistake. Plus, imagine how horrifying this code would be if you were trying to build a woodwind controller, which could have as many as 14 or 15 switches.
A better approach is to use binary numbers to map any combination of the switch on/off states to a number. For our trumpet, we only need 3 bits, so all the possible key combinations, and the corresponding MIDI note, can be represented in a new column in our fingering table
Index
|
Middle
|
Ring
|
Note Produced
|
Binary (Decimal) Value
|
Up
|
Up
|
Up
|
B-flat
|
000 (0)
|
Up
|
Down
|
Up
|
A
|
010 (2)
|
Down
|
Up
|
Up
|
A-flat
|
011 (3)
|
Down
|
Down
|
Up
|
G
|
001 (1)
|
Up
|
Down
|
Down
|
G-flat
|
100 (4)
|
Down
|
Up
|
Down
|
F
|
101 (5)
|
Down
|
Down
|
Down
|
E
|
111 (7)
|
Now that ugly code for deciding which note to play can be replaced with:
if (value = 0) {
send B-flat
} else if (value == 2) {
send A
} else if (value == 3) {
send A-flat
} else if (value == 1) {
send G
} else if (value == 4) {
send G-flat
} else if (value == 5) {
send F
} else if (alue == 7) {
send E
}
We'll actually use a C-language case statement in the sample code, and we'll express the numbers in binary instead of decimal, like this:
switch (value) {
case 0:
send B-flat;
break;
case 2:
send A;
break
and so on.
So let's go ahead and write the sketch for our trumpet. To do that, we'll replace the get_note() function with a new one:
int get_note() {
// This routine reads the three buttons that are our "trumpet
// valves", and returns a MIDI note.
int value = (digitalRead(2) << 2) +
(digitalRead(3) << 1) +
digitalRead(4);
// Since the digital input pins are pulled low when the
// button is pressed, the "all valves open" value will
// be 111 (binary) or 7 (decimal). Full table:
// v1 v2 v3 note value (decimal)
// open open open C 7
// open closed open B 5
// closed open open B-flat/A-sharp 3
// closed closed open A 1
// open closed closed A-flat/G-sharp 4
// closed open closed G 2
// closed closed closed G-flat/F-sharp 0
// open open close not used 6
//
#define BASE 60 // The base MIDI note value
switch (value) {
case 7:
return BASE;
break;
case 5:
return BASE - 1;
break;
case 3:
return BASE - 2;
break;
case 1:
return BASE - 3;
break;
case 4:
return BASE - 4;
break;
case 2:
return BASE - 5;
break;
case 0:
return BASE - 6;
break;
default:
// If invalid fingering, ignore and return the
// currently sounding note.
return noteSounding;
}
}
What's going on here?
int value = (digitalRead(2) << 2) +
(digitalRead(3) << 1) +
digitalRead(4);
Here, we're reading the values of the three buttons into a single integer value, using bit shift operators. Let's break it down:
First, we declare a variable named "value". For all practical purposes, that starts off as zero, and in binary that is:
00000000
which is 8 bits of binary zeros.
(Yes, nerds, it's actually a 32-bit signed integer, but that's not important to this discussion)
When we do digitalRead(2) << 2
we're saying "read digital input two and shift the whole value to the left two bits. So if we read 00000001, we'd shift that two bits to the left, so we end up with:
00000100
Then we read digital input three. Suppose we also read a HIGH, and shift left one bit, that ends up being:
00000010
Finally we read input 4, but don't shift it at all. So if we get a HIGH on pins 4, we have:
00000001
Now, we add the following three binary numbers:
00000100
00000010
00000001
and we get:
00000111
So that means the three digital I/O pin we're using are giving the value 1. As I mentioned before, we're taking the digital I/O pins low with button presses, so this binary value 00000111 means no buttons are pressed.
In order to make pulling I/O pins low to detect button pressed, we also need to add pullups to the digitnal I/O pins. The Teensy has support for that built-in (otherwise we'd need to attach some resistors between the digital pin and the power rail). We'll add the following code (in blue) to the setup() function:
void setup() {
state = NOTE_OFF; // initialize state machine
pinMode(2, INPUT_PULLUP);
pinMode(3, INPUT_PULLUP);
pinMode(4, INPUT_PULLUP);
}
We'll also need to change the main loop a little bit to handle the situation where the player chooses a new fingering but keeps blowing. In that case we need to turn off the old note and turn on the new one. So we'll add the following code in the block where we're handing the case where a note is on (new code in blue):
void loop() {
// read the input on analog pin 0
sensorValue = analogRead(A0);
if (state == NOTE_OFF) {
if (sensorValue > NOTE_ON_THRESHOLD) {
// Value has risen above threshold. Move to the RISE_TIME
// state. Record time and initial breath value.
breath_on_time = millis();
initial_breath_value = sensorValue;
state = RISE_WAIT; // Go to next state
}
} else if (state == RISE_WAIT) {
if (sensorValue > NOTE_ON_THRESHOLD) {
// Has enough time passed for us to collect our second
// sample?
if (millis() - breath_on_time > RISE_TIME) {
// Yes, so calculate MIDI note and velocity, then send a note on event
noteSounding = get_note();
int velocity = get_velocity(initial_breath_value, sensorValue, RISE_TIME);
usbMIDI.sendNoteOn(noteSounding, velocity, MIDI_CHANNEL);
state = NOTE_ON;
}
} else {
// Value fell below threshold before RISE_TIME 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 (sensorValue < NOTE_ON_THRESHOLD) {
// Value has fallen below threshold - turn the note off
usbMIDI.sendNoteOff(noteSounding, 100, MIDI_CHANNEL);
state = NOTE_OFF;
} else {
// Is it time to send more aftertouch data?
if (millis() - atSendTime > AT_INTERVAL) {
// Map the sensor value to the aftertouch range 0-127
atVal = map(sensorValue, NOTE_ON_THRESHOLD, 1023, 0, 127);
usbMIDI.sendAfterTouch(atVal, MIDI_CHANNEL);
atSendTime = millis();
}
}
int newNote = get_note();
if (newNote != noteSounding) {
// Player has moved to a new fingering while still blowing.
// Send a note off for the current node and a note on for
// the new note.
usbMIDI.sendNoteOff(noteSounding, 100, MIDI_CHANNEL);
noteSounding = newNote;
int velocity = get_velocity(initial_breath_value, sensorValue, RISE_TIME);
usbMIDI.sendNoteOn(noteSounding, velocity, MIDI_CHANNEL);
}
}
}
Here's the entire sketch, available at https://github.com/ggood/NoteSelectionTutorial
switch (value) {
case 0:
send B-flat;
break;
case 2:
send A;
break
and so on.
So let's go ahead and write the sketch for our trumpet. To do that, we'll replace the get_note() function with a new one:
int get_note() {
// This routine reads the three buttons that are our "trumpet
// valves", and returns a MIDI note.
int value = (digitalRead(2) << 2) +
(digitalRead(3) << 1) +
digitalRead(4);
// Since the digital input pins are pulled low when the
// button is pressed, the "all valves open" value will
// be 111 (binary) or 7 (decimal). Full table:
// v1 v2 v3 note value (decimal)
// open open open C 7
// open closed open B 5
// closed open open B-flat/A-sharp 3
// closed closed open A 1
// open closed closed A-flat/G-sharp 4
// closed open closed G 2
// closed closed closed G-flat/F-sharp 0
// open open close not used 6
//
#define BASE 60 // The base MIDI note value
switch (value) {
case 7:
return BASE;
break;
case 5:
return BASE - 1;
break;
case 3:
return BASE - 2;
break;
case 1:
return BASE - 3;
break;
case 4:
return BASE - 4;
break;
case 2:
return BASE - 5;
break;
case 0:
return BASE - 6;
break;
default:
// If invalid fingering, ignore and return the
// currently sounding note.
return noteSounding;
}
}
What's going on here?
int value = (digitalRead(2) << 2) +
(digitalRead(3) << 1) +
digitalRead(4);
Here, we're reading the values of the three buttons into a single integer value, using bit shift operators. Let's break it down:
First, we declare a variable named "value". For all practical purposes, that starts off as zero, and in binary that is:
00000000
which is 8 bits of binary zeros.
(Yes, nerds, it's actually a 32-bit signed integer, but that's not important to this discussion)
When we do digitalRead(2) << 2
we're saying "read digital input two and shift the whole value to the left two bits. So if we read 00000001, we'd shift that two bits to the left, so we end up with:
00000100
Then we read digital input three. Suppose we also read a HIGH, and shift left one bit, that ends up being:
00000010
Finally we read input 4, but don't shift it at all. So if we get a HIGH on pins 4, we have:
00000001
Now, we add the following three binary numbers:
00000100
00000010
00000001
and we get:
00000111
So that means the three digital I/O pin we're using are giving the value 1. As I mentioned before, we're taking the digital I/O pins low with button presses, so this binary value 00000111 means no buttons are pressed.
In order to make pulling I/O pins low to detect button pressed, we also need to add pullups to the digitnal I/O pins. The Teensy has support for that built-in (otherwise we'd need to attach some resistors between the digital pin and the power rail). We'll add the following code (in blue) to the setup() function:
void setup() {
state = NOTE_OFF; // initialize state machine
pinMode(2, INPUT_PULLUP);
pinMode(3, INPUT_PULLUP);
pinMode(4, INPUT_PULLUP);
}
We'll also need to change the main loop a little bit to handle the situation where the player chooses a new fingering but keeps blowing. In that case we need to turn off the old note and turn on the new one. So we'll add the following code in the block where we're handing the case where a note is on (new code in blue):
void loop() {
// read the input on analog pin 0
sensorValue = analogRead(A0);
if (state == NOTE_OFF) {
if (sensorValue > NOTE_ON_THRESHOLD) {
// Value has risen above threshold. Move to the RISE_TIME
// state. Record time and initial breath value.
breath_on_time = millis();
initial_breath_value = sensorValue;
state = RISE_WAIT; // Go to next state
}
} else if (state == RISE_WAIT) {
if (sensorValue > NOTE_ON_THRESHOLD) {
// Has enough time passed for us to collect our second
// sample?
if (millis() - breath_on_time > RISE_TIME) {
// Yes, so calculate MIDI note and velocity, then send a note on event
noteSounding = get_note();
int velocity = get_velocity(initial_breath_value, sensorValue, RISE_TIME);
usbMIDI.sendNoteOn(noteSounding, velocity, MIDI_CHANNEL);
state = NOTE_ON;
}
} else {
// Value fell below threshold before RISE_TIME 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 (sensorValue < NOTE_ON_THRESHOLD) {
// Value has fallen below threshold - turn the note off
usbMIDI.sendNoteOff(noteSounding, 100, MIDI_CHANNEL);
state = NOTE_OFF;
} else {
// Is it time to send more aftertouch data?
if (millis() - atSendTime > AT_INTERVAL) {
// Map the sensor value to the aftertouch range 0-127
atVal = map(sensorValue, NOTE_ON_THRESHOLD, 1023, 0, 127);
usbMIDI.sendAfterTouch(atVal, MIDI_CHANNEL);
atSendTime = millis();
}
}
int newNote = get_note();
if (newNote != noteSounding) {
// Player has moved to a new fingering while still blowing.
// Send a note off for the current node and a note on for
// the new note.
usbMIDI.sendNoteOff(noteSounding, 100, MIDI_CHANNEL);
noteSounding = newNote;
int velocity = get_velocity(initial_breath_value, sensorValue, RISE_TIME);
usbMIDI.sendNoteOn(noteSounding, velocity, MIDI_CHANNEL);
}
}
}
Here's a short video of the sketch in action:
Here's the entire sketch, available at https://github.com/ggood/NoteSelectionTutorial
Subscribe to:
Posts (Atom)



