Thursday, February 11, 2010

Switch-Based Overtone Selector

My latest experiment with overtone selection on the trombone controller is to use four momentary switches, played with the left hand, to select overtones. I used some Radio Shack lever switches and epoxied them to a 1/2" by 1/2" piece of scrap wood I had, then tie-wrapped it to the handle of the instrument (hey, I'm just prototyping).

The four switches are wired to pull Arduino digital pins 2, 3, 4, and 5 to ground when pressed, and I coded up my sketch to give the following overtones for the given switch selections:

Switch
3210 Overtone
0000 OT_1 (B flat)
0001 OT_2 (F)
0011 OT_3 (B flat)
0111 OT_4 (D)
1111 OT_5 (F)
1110 OT_7 (A flat*)
1100 OT_8 (B flat)
1000 OT_9 (C)

Switch 0 is under the index finger, and switch 3 is under the pinky. Here's a short video showing how it is played:




In terms of playability, it feels pretty good. I can more or less play a scale and the fingers of the left hand will generally do the right thing.

Here's the sketch:



/*

Prototype sketch for a trombone-like MIDI controller based on the Arduino hardware.

Hardware:

- An set of four switches used to select an overtone. We use "chording" to allow
the 4 switches to select overtones. I'm not sure what the most natural method
of chording is, but let's try the following:

Switch
3210 Overtone
0000 OT_1
0001 OT_2
0011 OT_3
0111 OT_4
1111 OT_5
1110 OT_7
1100 OT_8
1000 OT_8

Switches 0-3 are wired to pull Arduino digital input pins 2-5 low when
pressed.

- A "slide". Currently, this produces pitch bend information, and is implemented
with a 500mm SpectraSymbol SoftPot linear resistance strip.

- A volume controller, implemented with a FreeScale pressure sensor. The player
blows into a tube that goes to a "T" - one leg goes to the pressure sensor, and
the other is open (a "dump tube") so that the player can put air through the
instrument.

Feb 9, 2010
Gordon Good (velo27 yahoo com)

*/
#include <MidiUart.h>
#include <Midi.h>

MidiClass Midi;

// If DEBUG == true, then the sketch will print to the serial port what
// it would send on the MIDI bus.
const boolean DEBUG = false;
//const boolean DEBUG = true;

const int BREATH_PIN = 0; // Breath sensor on analog pin 0
const int SLIDE_LPOT_PIN = 1; // Slide sensor on analog pin 1

const int OT_SW_0_PIN = 2; // Overtone switch 0
const int OT_SW_1_PIN = 3; // Overtone switch 1
const int OT_SW_2_PIN = 4; // Overtone switch 2
const int OT_SW_3_PIN = 5; // Overtone switch 3

const int PANIC_PIN = 6; // MIDI all notes off momentary switch on digital I/O 6

// The overtone series this instrument will produce
const int FUNDAMENTAL = 36; // MIDI note value of our fundamental
const int OT_1 = 48; // First overtone (B flat)
const int OT_2 = 55; // Second overtone (F)
const int OT_3 = 60; // Third overtone (B flat)
const int OT_4 = 64; // Fourth overtone (D)
const int OT_5 = 67; // Fifth overtone (F)
const int OT_6 = 70; // Sixth overtone (A flat - not in tune - need to tweak pitch bend)
const int OT_7 = 72; // Seventh overtone (B flat)
const int OT_8 = 74; // Eighth overtone (C)
const int OT_9 = 76; // Ninth overtone (D)
const int OT_NONE = -1; // No overtone key pressed (not possible with ribbon)

// All overtones for this instrument
const int overtones[10] = {FUNDAMENTAL, OT_1, OT_2, OT_3, OT_4, OT_5, OT_6, OT_7, OT_8, OT_9};
// Switch values for given overtones. 0xff means that overtone can't be selected.
const int overtone_sw_values[10] = {0xff, 0x00, 0x01, 0x03, 0x07, 0x0f, 0x0e, 0x0c, 0x08, 0xff};

const int MIDI_VOLUME_CC = 7; // The controller number for MIDI volume data
const int MIDI_BREATH_CC = 2; // The controller number for MIDI breath controller data

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);
const int PB_SEND_THRESHOLD = 10; // Only send pitch bend if it's this much different than the current value
const int VOLUME_SEND_THRESHOLD = 1; // Only send volume change if it's this much differnt that the current value
const int NOTE_ON_VOLUME_THRESHOLD = 50; // Raw sensor value required to turn on a note

// If a value larger than this is read from a SoftPot, treat it as if the player is not touching it.
// Note: for some reason, the two SoftPots interact, e.g. just actuating the slide pot gives me
// no-touch values all above 1000, but when also touching the overtone pot, the values can go
// as low as 999. I suspect I may be taxing the 5v supply line.
const int LPOT_NO_TOUCH_VALUE = 1010;

int currentNote = -1; // The MIDI note currently sounding
int currentPitchBend = 8192; // The current pitch bend
int currentVolume = 0; // The current volume

void setup() {
enableDigitalInput(OT_SW_0_PIN, true);
enableDigitalInput(OT_SW_1_PIN, true);
enableDigitalInput(OT_SW_2_PIN, true);
enableDigitalInput(OT_SW_3_PIN, true);
enableDigitalInput(PANIC_PIN, true);
enableAnalogInput(BREATH_PIN, false);
enableAnalogInput(SLIDE_LPOT_PIN, true);

if (DEBUG) {
Serial.begin(9600);
} else {
MidiUart.init(); // Initialize MIDI
}
}

/**
* Enable a pin for analog input, and set its internal pullup.
*/
void enableAnalogInput(int pin, boolean enablePullup) {
pinMode(pin, INPUT);
digitalWrite(pin + 14, enablePullup ? HIGH : LOW);
}

/**
* Enable a pin for digital input, and set its internal pullup.
*/
void enableDigitalInput(int pin, boolean enablePullup) {
pinMode(pin, INPUT);
digitalWrite(pin, enablePullup ? HIGH : LOW);
}


/**
* Read the slide pot and return a pitch bend value. The values
* returned are all bends down from the base pitch being played,
* and are in the range 8192 (no bend) to 0 (maximum bend down).
* This means that the synth patch needs to be adjusted to provide
* a maximum pitch bend of seven semitones, if you want it to
* behave like a trombone.
*
* Return -1 if the player is not touching the sensor.
*/
int getPitchBendFromLinearPot() {
// Get the raw value from the linear pot
int pbRawVal = analogRead(SLIDE_LPOT_PIN);
if (pbRawVal > LPOT_NO_TOUCH_VALUE) {
return -1;
} else {
return map(pbRawVal, 0, LPOT_NO_TOUCH_VALUE, 0, 16383 / 2);
}
}

int getPitchBend() {
return getPitchBendFromLinearPot();
}

/**
* Read the overtone switches and return the appropriate overtone.
* If an invalid key combination is found, return -1. Note that
* we invert the values from digitalRead, since these switches
* pull to ground, so switch enabled = digital 0.
*/
int getOvertoneFromOvertoneSwitches() {
unsigned char val = !digitalRead(OT_SW_3_PIN);
val = val << 1 | !digitalRead(OT_SW_2_PIN);
val = val << 1 | !digitalRead(OT_SW_1_PIN);
val = val << 1 | !digitalRead(OT_SW_0_PIN);
// now select the appropriate overtone
for (int i = 0; i < sizeof(overtone_sw_values); i++) {
if (val == overtone_sw_values[i]) {
return i;
}
}
return -1;
}

int getMIDINote() {
int ot = getOvertoneFromOvertoneSwitches();
if (-1 == ot) {
return currentNote;
} else {
return overtones[ot];
}
}

/**
* Read the breath sensor and map it to a volume level. For now,
* this maps to the range 0 - 127 so we can generate MIDI
* continuous controller information.
*/
int getVolumeFromBreathSensor() {
int volRawVal = analogRead(BREATH_PIN);
if (volRawVal < NOTE_ON_VOLUME_THRESHOLD) {
return 0;
} else {
return map(constrain(volRawVal, 30, 500), 30, 500, 0, 127);
}
}

int getVolume() {
return getVolumeFromBreathSensor();
}

void sendNoteOn(int note, int vel, byte chan, boolean debug) {
if (debug) {
Serial.print("ON ");
Serial.println(note);
} else {
MidiUart.sendNoteOn(chan, note, vel);
}
}

void sendNoteOff(int note, int vel, byte chan, boolean debug) {
if (debug) {
Serial.print("OFF ");
Serial.println(note);
} else {
MidiUart.sendNoteOff(chan, note, vel);
}
}

void sendPitchBend(int pitchBend, boolean debug) {
if (-1 != pitchBend) {
if (abs(currentPitchBend - pitchBend) > PB_SEND_THRESHOLD) {
currentPitchBend = pitchBend;
if (debug) {
Serial.print("BEND ");
Serial.println(pitchBend);
} else {
MidiUart.sendPitchBend(pitchBend);
}
}
}
}

void sendVolume(int volume, byte chan, boolean debug) {
if (abs(currentVolume - volume) > VOLUME_SEND_THRESHOLD) {
currentVolume = volume;
if (debug) {
Serial.print("VOL ");
Serial.println(volume);
} else {
//midi.sendControlChange(chan, MIDI_VOLUME_CC, volume);
MidiUart.sendCC(chan, MIDI_VOLUME_CC, 100 );
}
}
}

void sendBreathController(int volume, byte chan, boolean debug) {
if (abs(currentVolume - volume) > VOLUME_SEND_THRESHOLD) {
if (debug) {
Serial.print("BC ");
Serial.println(volume);
} else {
MidiUart.sendCC(chan, MIDI_BREATH_CC, volume );
}
}
}

void allNotesOff() {
for (int i = 0; i < 128; i++) {
sendNoteOff(i, 0, 1, DEBUG);
}
}

void loop() {

if (digitalRead(PANIC_PIN) == 0) {
allNotesOff();
}

int pb = getPitchBend();
int note = getMIDINote();
int volume = getVolume();

if ((-1 != currentNote) && (0 == volume)) {
// Breath stopped, so send a note off
sendNoteOff(currentNote, 0, 1, DEBUG);
currentNote = -1;
} else if ((-1 == currentNote) && (0 != volume) && (-1 != note)) {
// No note was playing, and we have breath and a valid overtone, so send a note on
sendNoteOn(note, 127, 1, DEBUG);
currentNote = note;
} else if ((-1 != currentNote) && (note != currentNote)) {
// A note was playing, but the player has moved to a different note.
// Turn off the old note and turn on the new one.
sendNoteOff(currentNote, 0, 1, DEBUG);
sendPitchBend(pb, DEBUG);
sendBreathController(volume, 1, DEBUG);
sendNoteOn(note, 127, 1, DEBUG);
currentNote = note;
} else if (-1 != currentNote) {
// Send updated breath controller and pitch bend values.
if (millis() > ccSendTime + MIN_CC_INTERVAL) {
sendPitchBend(pb, DEBUG);
sendBreathController(volume, 1, DEBUG);
ccSendTime = millis();
}
}
delay(50);
}

No comments:

Post a Comment