Once I found out about the Arduino and the various types of sensors that are available cheaply, it seemed like the right time to get serious about designing a trombone controller.
My current working plans are to use three sensors:
- A breath sensor that controls articulation and volume.
- A position sensor of some kind that can detect the slide position.
- A set of switches, or a position sensor that controls which overtone is selected. I envision that this will be actuated with the player's left thumb, where the F-attachment lever is usually found.
There are a lot of different possibilities for what kind of sensors to use for each of these, and I'll need to do some experimentation to find what works best. But I don't need to figure that out before I can start working on the code - the Arduino lets me abstract away those issues. So, really, we have:
- An analog input for the breath sensor.,
- an analog input for the slide position, and
- a set of switches for selecting the overtone.
- Make the whole thing a MIDI controller, and have the Arduino send MIDI to a synthesizer (or computer + softsynth, etc).
- Use something like Max/MSP or pd to read the controllers directly and control synths implemented in software on the Mac/PC.
The advantages of #1 are:
- You can take advantage of any existing synth patch, although you do have the problem every wind controller player has in finding/creating patches that respond well.
- This approach is faster for me to implement, since I don't know Max/MSP.
- You have more control of how the synth responds to the instrument.
- You aren't limited by MIDI's coarse-grained values (if those are in fact a problem).
So I decided to start down the MIDI path. Just to get started, I built the following circuit to prove some concepts:
The "slide" is a Sharp Infrared sensor, the "breath controller" is a force sensing resistor, and the overtone selector is 5 separate momentary pushbutton switches.
I wrote a sketch that reads the values of each sensor/switch in a loop. For this first iteration, I decided to focus on making the overtone selector and the slide work well, and not worry so much about when note on/off messages are sent. So this thing is always "on", and a new note is only started when a different register button is pressed.
I also opted in this iteration to use pitch bend messages exclusively for pitch within a given overtone. In other words, the only note on values we ever send are the fundamental, the first overtone, the second overtone, etc. All the notes in between are produced by sending a downward pitch bend message. This requires that the synth be set so that maximum bend is six semitones, corresponding to the seven positions of the trombone slide.
On a breadboard, obviously, you can't twiddle all these controllers at once, so I first verified that the overtone selector worked properly. I then checked that tapping/holding/squeezing the force sensor produced something like a reasonable articulation and volume changes. Then I just set up the firmware to send a constant value instead of reading the force sensor (I only have two hands) and verified that the "slide" did something reasonable.
Here's a short sample:
In the first part, I'm moving up and down the overtone series with the slide stationary, and in the second part I "glissando" down from three diffrerent overtones.
In summary, this all looks promising. Next steps:
- Find a pressure sensor to use for a breath controller. The first one I tried required far too much pressure.
- Figure out how to physically make the slide, and experiment with different sensors. Some other ideas include using nichrome wire as a sensor, and position-sensing resistors (these are expensive in the size I need - about 2 feet long).
Here's the sketch.
/*
Prototype sketch for a trombone-like MIDI controller based on the Arduino hardware.
Gordon Good (velo27yahoo com)
Hardware:
- An overtone selector. At this point, it's a series of discrete on-off pushbottons.
This replaces the act of blowing overtones an a physical instrument.
- A "slide". Currently, this produces pitch bend information, and is implemented
using an infrared distance sensor.
- A volume controller, intended to be actuated by the player's breath.
*/
#include "Midi.h"
const boolean DEBUG = false;
//const boolean DEBUG = true;
const int FSR_PIN = 0;
const int IR_PIN = 1;
const int OT_0_PIN = 2;
const int OT_1_PIN = 3;
const int OT_2_PIN = 4;
const int OT_3_PIN = 5;
const int OT_4_PIN = 6;
const int FUNDAMENTAL = 36; // MIDI note value of our fundamental
const int OT_1 = 48; // First overtone
const int OT_2 = 55; // Second overtone
const int OT_3 = 60; // Third overtone
const int OT_4 = 64; // Fourth overtone
const int OT_NONE = -1; // No overtone key pressed
const int MIDI_VOLUME_CC = 7;
long ccSendTime = 0; // Last time we sent continuous data (volume, pb);
const int MIN_CC_INTERVAL = 10; // Send CC data no more often than this (in milliseconds);
Midi midi(Serial);
int playingNote = -1;
void setup() {
if (DEBUG) {
Serial.begin(9600);
} else {
midi.begin(0); // Initialize MIDI
}
}
int getPitchBendFromForceSensor() {
// Get the raw value from the force sensor
int pbRawVal = analogRead(FSR_PIN);
// Constrain it to values we're willing to handle
int pbConstrainedVal = constrain(pbRawVal, 0, 1023);
// Then map those values to the MIDI pitch bend range values.
return map(pbConstrainedVal, 0, 1023, 0, 16383);
}
int getPitchBendFromIRSensor() {
// Get the raw value from the IR sensor
int pbRawVal = analogRead(IR_PIN);
// Constrain it to values we're willing to handle
int pbConstrainedVal = constrain(pbRawVal, 50, 640);
// Then map those values to the MIDI pitch bend range values.
// NOTE: We stick our head in a hole here and assume the IR
// sensor response is linear. In fact, it's not, so we need to
// come back and improve this. The current implementation will
// result in a trombone slide that is non-linear.
return map(pbConstrainedVal, 640, 50, 16383 / 2, 0);
}
int getNeutralPitchBend() {
return 8192;
}
int getPitchBend() {
return getPitchBendFromIRSensor();
}
int getMIDINote() {
// middle C = 60, so let's use two octaves below middle c as the fundamental of this instrument for now
if (digitalRead(OT_4_PIN) == 1) {
return OT_4;
} else if (digitalRead(OT_3_PIN) == 1) {
return OT_3;
} else if (digitalRead(OT_2_PIN) == 1) {
return OT_2;
} else if (digitalRead(OT_1_PIN) == 1) {
return OT_1;
} else if (digitalRead(OT_0_PIN) == 1) {
return FUNDAMENTAL;
} else {
// No overtone key pressed - return -1 so caller can know
return OT_NONE;
}
}
int getFixedVolume() {
return 127;
}
int getVolumeFromFSR() {
// Temporary code, since I don't have a good breath
// sensor yet. However, initial tests with a force sensing resistor
// indicate that it's possible to do decent articulation with the
// following code.
int volRawVal = analogRead(FSR_PIN);
return map(volRawVal, 0, 970, 0, 127);
}
int getVolume() {
return getFixedVolume();
}
void sendNoteOn(int note, int vel, byte chan, boolean debug) {
if (debug) {
Serial.print("ON ");
Serial.println(note);
} else {
midi.sendNoteOn(chan, note, vel);
}
}
void sendNoteOff(int note, int vel, byte chan, boolean debug) {
if (debug) {
Serial.print("OFF ");
Serial.println(note);
} else {
midi.sendNoteOff(chan, note, vel);
}
}
void sendPitchBend(int val, boolean debug) {
if (debug) {
Serial.print("BEND ");
Serial.println(val);
} else {
midi.sendPitchChange(val);
}
}
void sendVolume(int volume, byte chan, boolean debug) {
if (debug) {
Serial.print("VOL ");
Serial.println(volume);
} else {
midi.sendControlChange(chan, MIDI_VOLUME_CC, volume);
}
}
void loop() {
int pb = getPitchBend();
int note = getMIDINote();
int volume = getVolume();
if (-1 != note && note != playingNote) {
sendNoteOff(playingNote, 0, 1, DEBUG);
sendPitchBend(pb, DEBUG);
sendVolume(volume, 1, DEBUG);
sendNoteOn(note, 127, 1, DEBUG);
playingNote = note;
delay(50);
} else {
if (millis() > ccSendTime + MIN_CC_INTERVAL) {
sendPitchBend(pb, DEBUG);
sendVolume(volume, 1, DEBUG);
ccSendTime = millis();
}
}
}