Refactored for teensy 4.0, xEvi hardware
- Switched to platformio, ino -> cpp - MPRLS for pressure sensor - Added basic ICM support - Removed widi, battery, other features not supported in xEvi - Removed legacy options/processing - Added LED strip support - Added encoder support - Reworked menu code to use encoders/be more flexible
This commit is contained in:
parent
c58c3f9e46
commit
01d193c9b3
92 changed files with 69119 additions and 73272 deletions
823
NuEVI/src/xEVI.cpp
Normal file
823
NuEVI/src/xEVI.cpp
Normal file
|
@ -0,0 +1,823 @@
|
|||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
#include <SPI.h>
|
||||
#include <EEPROM.h>
|
||||
#include <array>
|
||||
|
||||
#include "globals.h"
|
||||
#include "hardware.h"
|
||||
#include "midi.h"
|
||||
#include "menu.h"
|
||||
#include "config.h"
|
||||
#include "settings.h"
|
||||
#include "led.h"
|
||||
|
||||
/*
|
||||
NAME: xEVI
|
||||
WRITTEN BY: BRIAN HREBEC
|
||||
BASED ON: NuEVI by JOHAN BERGLUND
|
||||
DATE: 2023-8-23
|
||||
FOR: PJRC Teensy 4.0 and 2x MPR121 capactive touch sensor board.
|
||||
Uses an SSD1306 controlled OLED display communicating over I2C and a NeoPixel LED strip for status display
|
||||
ICM20948 for intertial measurement.
|
||||
FUNCTION: EVI Wind Controller using MPRLS pressure sensors and capacitive touch keys. Output to USB MIDI and CV.
|
||||
*/
|
||||
|
||||
#if !defined(USB_MIDI) && !defined(USB_MIDI_SERIAL)
|
||||
#error "USB MIDI not enabled. Please set USB type to 'MIDI' or 'Serial + MIDI'."
|
||||
#endif
|
||||
|
||||
preset_t presets[PRESET_COUNT];
|
||||
instrument_state_t state;
|
||||
preset_t *currentPreset;
|
||||
calibration_t calibration;
|
||||
|
||||
static const int pbDepthList[13] = { 8192, 8192, 4096, 2731, 2048, 1638, 1365, 1170, 1024, 910, 819, 744, 683 };
|
||||
static const float vibDepth[10] = { 0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35, 0.40, 0.45 }; // max pitch bend values (+/-) for the vibrato settings
|
||||
static const short vibMaxBiteList[16] = { 1400, 1200, 1000, 900, 800, 700, 600, 500, 400, 300, 250, 200, 150, 100, 50, 25 };
|
||||
static const short vibMaxList[12] = { 300, 275, 250, 225, 200, 175, 150, 125, 100, 75, 50, 25 };
|
||||
static const int timeDividerList[9] = { 0, 222, 200, 181, 167, 152, 143, 130, 125 }; // For CV vibrato - 222 is 4.5Hz, 200 is 5Hz, 181 is 5.5Hz 167 is 6Hz, 152 is 6.5Hz, 143 is 7Hz, 130 is 7.5Hz, 125 is 8Hz
|
||||
|
||||
static const unsigned short curveM4[] = { 0, 4300, 7000, 8700, 9900, 10950, 11900, 12600, 13300, 13900, 14500, 15000, 15450, 15700, 16000, 16250, 16383 };
|
||||
static const unsigned short curveM3[] = { 0, 2900, 5100, 6650, 8200, 9500, 10550, 11500, 12300, 13100, 13800, 14450, 14950, 15350, 15750, 16150, 16383 };
|
||||
static const unsigned short curveM2[] = { 0, 2000, 3600, 5000, 6450, 7850, 9000, 10100, 11100, 12100, 12900, 13700, 14400, 14950, 15500, 16000, 16383 };
|
||||
static const unsigned short curveM1[] = { 0, 1400, 2850, 4100, 5300, 6450, 7600, 8700, 9800, 10750, 11650, 12600, 13350, 14150, 14950, 15650, 16383 };
|
||||
const unsigned short curveIn[] = { 0, 1023, 2047, 3071, 4095, 5119, 6143, 7167, 8191, 9215, 10239, 11263, 12287, 13311, 14335, 15359, 16383 };
|
||||
static const unsigned short curveP1[] = { 0, 600, 1350, 2150, 2900, 3800, 4700, 5600, 6650, 7700, 8800, 9900, 11100, 12300, 13500, 14850, 16383 };
|
||||
static const unsigned short curveP2[] = { 0, 400, 800, 1300, 2000, 2650, 3500, 4300, 5300, 6250, 7400, 8500, 9600, 11050, 12400, 14100, 16383 };
|
||||
static const unsigned short curveP3[] = { 0, 200, 500, 900, 1300, 1800, 2350, 3100, 3800, 4600, 5550, 6550, 8000, 9500, 11250, 13400, 16383 };
|
||||
static const unsigned short curveP4[] = { 0, 100, 200, 400, 700, 1050, 1500, 1950, 2550, 3200, 4000, 4900, 6050, 7500, 9300, 12100, 16383 };
|
||||
static const unsigned short curveS1[] = { 0, 600, 1350, 2150, 2900, 3800, 4700, 6000, 8700, 11000, 12400, 13400, 14300, 14950, 15500, 16000, 16383 };
|
||||
static const unsigned short curveS2[] = { 0, 600, 1350, 2150, 2900, 4000, 6100, 9000, 11000, 12100, 12900, 13700, 14400, 14950, 15500, 16000, 16383 };
|
||||
// static const unsigned short curveS3[] = {0,600,1350,2300,3800,6200,8700,10200,11100,12100,12900,13700,14400,14950,15500,16000,16383};
|
||||
// static const unsigned short curveS4[] = {0,600,1700,4000,6600,8550,9700,10550,11400,12200,12900,13700,14400,14950,15500,16000,16383};
|
||||
|
||||
static const unsigned short curveZ1[] = { 0, 1400, 2100, 2900, 3200, 3900, 4700, 5600, 6650, 7700, 8800, 9900, 11100, 12300, 13500, 14850, 16383 };
|
||||
static const unsigned short curveZ2[] = { 0, 2000, 3200, 3800, 4096, 4800, 5100, 5900, 6650, 7700, 8800, 9900, 11100, 12300, 13500, 14850, 16383 };
|
||||
|
||||
const std::array<const unsigned short*, 13> curves = {
|
||||
curveM4, curveM3, curveM2, curveM1, curveIn, curveP1, curveP2,
|
||||
curveP3, curveP4, curveS1, curveS2, curveZ1, curveZ2 };
|
||||
|
||||
static int waveformsTable[maxSamplesNum] = {
|
||||
// Sine wave
|
||||
0x7ff, 0x86a, 0x8d5, 0x93f, 0x9a9, 0xa11, 0xa78, 0xadd, 0xb40, 0xba1,
|
||||
0xbff, 0xc5a, 0xcb2, 0xd08, 0xd59, 0xda7, 0xdf1, 0xe36, 0xe77, 0xeb4,
|
||||
0xeec, 0xf1f, 0xf4d, 0xf77, 0xf9a, 0xfb9, 0xfd2, 0xfe5, 0xff3, 0xffc,
|
||||
0xfff, 0xffc, 0xff3, 0xfe5, 0xfd2, 0xfb9, 0xf9a, 0xf77, 0xf4d, 0xf1f,
|
||||
0xeec, 0xeb4, 0xe77, 0xe36, 0xdf1, 0xda7, 0xd59, 0xd08, 0xcb2, 0xc5a,
|
||||
0xbff, 0xba1, 0xb40, 0xadd, 0xa78, 0xa11, 0x9a9, 0x93f, 0x8d5, 0x86a,
|
||||
0x7ff, 0x794, 0x729, 0x6bf, 0x655, 0x5ed, 0x586, 0x521, 0x4be, 0x45d,
|
||||
0x3ff, 0x3a4, 0x34c, 0x2f6, 0x2a5, 0x257, 0x20d, 0x1c8, 0x187, 0x14a,
|
||||
0x112, 0xdf, 0xb1, 0x87, 0x64, 0x45, 0x2c, 0x19, 0xb, 0x2,
|
||||
0x0, 0x2, 0xb, 0x19, 0x2c, 0x45, 0x64, 0x87, 0xb1, 0xdf,
|
||||
0x112, 0x14a, 0x187, 0x1c8, 0x20d, 0x257, 0x2a5, 0x2f6, 0x34c, 0x3a4,
|
||||
0x3ff, 0x45d, 0x4be, 0x521, 0x586, 0x5ed, 0x655, 0x6bf, 0x729, 0x794 };
|
||||
|
||||
const int rollerHarmonic[2][7] = { {0, 7, 12, 16, 19, 24, 26}, // F horn 2,3,4,5,6,8,9 hrm
|
||||
{7, 12, 16, 19, 24, 26, 31} }; // Bb horn 3,4,5,6,8,9,12 hrm
|
||||
|
||||
const int trumpetHarmonic[2][7] = { {0, 7, 12, 16, 19, 26, 31}, //! K4: hrm 8->9, 10->12
|
||||
{0, 7, 12, 16, 19, 24, 28} }; // trumpet 2,3,4,5,6,8,10 hrm
|
||||
|
||||
bool configManagementMode = false;
|
||||
bool testMode = false;
|
||||
|
||||
//_______________________________________________________________________________________________ SETUP
|
||||
|
||||
// MIDI note value check with out of range octave repeat
|
||||
inline int noteValueCheck(int note) {
|
||||
if (note > 127) {
|
||||
note = 115 + (note - 127) % 12;
|
||||
} else if (note < 0) {
|
||||
note = 12 - abs(note) % 12;
|
||||
}
|
||||
return note;
|
||||
}
|
||||
|
||||
//***********************************************************
|
||||
|
||||
void port(int portCC) {
|
||||
if (portCC == state.portamentoVal) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentPreset->portamentoMode == PortamentoMode::PON || currentPreset->portamentoMode == PortamentoMode::PGLIDE_ONLY) {
|
||||
if (state.portamentoVal > 0 && portCC == 0) {
|
||||
midiSendControlChange(CCN_PortOnOff, 0);
|
||||
} else if (state.portamentoVal == 0 && portCC > 0) {
|
||||
midiSendControlChange(CCN_PortOnOff, 127);
|
||||
}
|
||||
}
|
||||
|
||||
midiSendControlChange(CCN_Port, portCC);
|
||||
state.portamentoVal = portCC;
|
||||
}
|
||||
|
||||
// Update CV output pin, run from timer.
|
||||
void cvUpdate() {
|
||||
static byte cvPortaTuneCount = 0;
|
||||
uint32_t currentTime = millis();
|
||||
int cvPressure = readPressure();
|
||||
analogWrite(cvBreathPin, cvPressure);
|
||||
state.targetPitch = (state.activeNote - 24) * 42;
|
||||
state.targetPitch += map(state.pitchBend, 0, 16383, -84, 84);
|
||||
state.targetPitch -= state.quarterToneTrigger * 21;
|
||||
if (state.portamentoVal > 0) {
|
||||
if (state.targetPitch > state.cvPitch) {
|
||||
if (!cvPortaTuneCount) {
|
||||
state.cvPitch += 1 + (127 - state.portamentoVal) / 4;
|
||||
} else {
|
||||
cvPortaTuneCount++;
|
||||
if (cvPortaTuneCount > CVPORTATUNE)
|
||||
cvPortaTuneCount = 0;
|
||||
}
|
||||
if (state.cvPitch > state.targetPitch)
|
||||
state.cvPitch = state.targetPitch;
|
||||
} else if (state.targetPitch < state.cvPitch) {
|
||||
if (!cvPortaTuneCount) {
|
||||
state.cvPitch -= 1 + (127 - state.portamentoVal) / 4;
|
||||
} else {
|
||||
cvPortaTuneCount++;
|
||||
if (cvPortaTuneCount > CVPORTATUNE)
|
||||
cvPortaTuneCount = 0;
|
||||
}
|
||||
if (state.cvPitch < state.targetPitch)
|
||||
state.cvPitch = state.targetPitch;
|
||||
} else {
|
||||
state.cvPitch = state.targetPitch;
|
||||
}
|
||||
} else {
|
||||
state.cvPitch = state.targetPitch;
|
||||
}
|
||||
|
||||
if (currentPreset->cvVibRate) {
|
||||
int timeDivider = timeDividerList[currentPreset->cvVibRate];
|
||||
int cvVib = map(((waveformsTable[map(currentTime % timeDivider, 0, timeDivider, 0, maxSamplesNum - 1)] - 2047)), -259968, 259969, -11, 11);
|
||||
state.cvPitch += cvVib;
|
||||
}
|
||||
int cvPitchTuned = 2 * (currentPreset->cvTune) + map(state.cvPitch, 0, 4032, 0, 4032 + 2 * (currentPreset->cvScale));
|
||||
analogWrite(cvPitchPin, constrain(cvPitchTuned, 0, 4095));
|
||||
}
|
||||
|
||||
//**************************************************************
|
||||
// non linear mapping function (http://playground.arduino.cc/Main/MultiMap)
|
||||
// note: the _in array should have increasing values
|
||||
unsigned int multiMap(unsigned short val, const unsigned short* _in, const unsigned short* _out, uint8_t size) {
|
||||
// take care the value is within range
|
||||
// val = constrain(val, _in[0], _in[size-1]);
|
||||
if (val <= _in[0])
|
||||
return _out[0];
|
||||
if (val >= _in[size - 1])
|
||||
return _out[size - 1];
|
||||
|
||||
// search right interval
|
||||
uint8_t pos = 1; // _in[0] allready tested
|
||||
while (val > _in[pos])
|
||||
pos++;
|
||||
|
||||
// this will handle all exact "points" in the _in array
|
||||
if (val == _in[pos])
|
||||
return _out[pos];
|
||||
|
||||
// interpolate in the right segment for the rest
|
||||
return (val - _in[pos - 1]) * (_out[pos] - _out[pos - 1]) / (_in[pos] - _in[pos - 1]) + _out[pos - 1];
|
||||
}
|
||||
|
||||
|
||||
|
||||
//**************************************************************
|
||||
|
||||
// map breath values to selected curve
|
||||
unsigned int breathCurve(unsigned int inputVal) {
|
||||
if (currentPreset->breathCurve >= curves.size())
|
||||
return inputVal;
|
||||
return multiMap(inputVal, curveIn, curves[currentPreset->breathCurve], 17);
|
||||
}
|
||||
|
||||
//**************************************************************
|
||||
|
||||
int patchLimit(int value) {
|
||||
return constrain(value, 1, 128);
|
||||
}
|
||||
|
||||
//**************************************************************
|
||||
|
||||
int breath() {
|
||||
static int oldbreath = 0;
|
||||
static unsigned int oldbreathhires = 0;
|
||||
|
||||
int breathCCval, breathCCvalFine;
|
||||
unsigned int breathCCvalHires;
|
||||
breathCCvalHires = breathCurve(mapConstrain(state.breathSignal, state.breathThrVal, state.breathMaxVal, 0, 16383));
|
||||
breathCCval = (breathCCvalHires >> 7) & 0x007F;
|
||||
breathCCvalFine = breathCCvalHires & 0x007F;
|
||||
if (breathCCval != oldbreath) { // only send midi data if breath has changed from previous value
|
||||
if (currentPreset->breathCC) {
|
||||
// send midi cc
|
||||
midiSendControlChange(currentPreset->breathCC, breathCCval);
|
||||
}
|
||||
if (currentPreset->breathMode == BreathMode::BREATH_AT || currentPreset->breathMode == BreathMode::BREATH_LSB_AT) {
|
||||
// send aftertouch
|
||||
midiSendAfterTouch(breathCCval);
|
||||
}
|
||||
oldbreath = breathCCval;
|
||||
}
|
||||
|
||||
if (breathCCvalHires != oldbreathhires
|
||||
&& (currentPreset->breathMode == BreathMode::BREATH_LSB || currentPreset->breathMode == BreathMode::BREATH_LSB_AT)) {
|
||||
midiSendControlChange(currentPreset->breathCC + 32, breathCCvalFine);
|
||||
}
|
||||
|
||||
oldbreathhires = breathCCvalHires;
|
||||
|
||||
return breathCCval;
|
||||
}
|
||||
|
||||
//**************************************************************
|
||||
|
||||
void pitch_bend() {
|
||||
// handle input from pitchbend touchpads and
|
||||
// on-pcb variable capacitor for vibrato.
|
||||
static int oldpb = 0;
|
||||
int vibMax;
|
||||
int vibMaxBite;
|
||||
int calculatedPBdepth;
|
||||
byte pbTouched = 0;
|
||||
int vibRead = 0;
|
||||
int vibReadBite = 0;
|
||||
state.pbUpSignal = readTouchUtil(pbUpPin); // PCB PIN "Pu"
|
||||
state.pbDnSignal = readTouchUtil(pbDnPin); // PCB PIN "Pd"
|
||||
bool halfPitchBendKey = (currentPreset->pinkySetting == PBD) && state.pinkyKey; // hold pinky key for 1/2 pitchbend value
|
||||
state.quarterToneTrigger = (currentPreset->pinkySetting == QTN) && state.pinkyKey; // pinky key for a quarter tone down using pitch bend (assuming PB range on synth is set to 2 semitones)
|
||||
|
||||
calculatedPBdepth = pbDepthList[currentPreset->PBdepth];
|
||||
if (halfPitchBendKey)
|
||||
calculatedPBdepth = calculatedPBdepth * 0.5;
|
||||
|
||||
vibMax = vibMaxList[currentPreset->vibSens - 1];
|
||||
|
||||
float calculatedDepth = 0;
|
||||
if (currentPreset->vibratoMode == VibratoMode::VSTART_DOWN) {
|
||||
calculatedDepth = calculatedPBdepth * vibDepth[currentPreset->vibratoDepth];
|
||||
} else {
|
||||
calculatedDepth = (0 - calculatedPBdepth * vibDepth[currentPreset->vibratoDepth]);
|
||||
}
|
||||
|
||||
if (ExtraControl::VIBRATO == currentPreset->biteControl) { // bite vibrato
|
||||
vibMaxBite = vibMaxBiteList[currentPreset->vibSens - 1];
|
||||
vibReadBite = readTouchUtil(bitePin); // get sensor data, do some smoothing - SENSOR PIN 17 - PCB PINS LABELED "BITE" (GND left, sensor pin right)
|
||||
|
||||
if (vibReadBite < state.vibThrBite) {
|
||||
state.vibSignal = (state.vibSignal + mapConstrain(
|
||||
vibReadBite, (state.vibZeroBite - vibMaxBite), state.vibThrBite, calculatedDepth, 0)
|
||||
) / 2;
|
||||
} else if (vibReadBite > state.vibThrBiteLo) {
|
||||
state.vibSignal = (state.vibSignal + mapConstrain(
|
||||
vibReadBite, (state.vibZeroBite + vibMaxBite), state.vibThrBite, calculatedDepth, 0)
|
||||
) / 2;
|
||||
} else {
|
||||
state.vibSignal = state.vibSignal / 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (ExtraControl::VIBRATO == currentPreset->leverControl) { // lever vibrato
|
||||
vibRead = readTouchUtil(vibratoPin);
|
||||
if (vibRead < state.vibThr) {
|
||||
state.vibSignal = (state.vibSignal +
|
||||
mapConstrain(vibRead, (state.vibZero - vibMax), state.vibThr, calculatedDepth, 0)
|
||||
) / 2;
|
||||
} else if (vibRead > state.vibThrLo) {
|
||||
state.vibSignal = (state.vibSignal +
|
||||
mapConstrain(vibRead, (state.vibZero + vibMax), state.vibThr, calculatedDepth, 0)
|
||||
) / 2;
|
||||
} else {
|
||||
state.vibSignal = state.vibSignal / 2;
|
||||
}
|
||||
}
|
||||
|
||||
switch (currentPreset->vibRetn) { // moving baseline
|
||||
case 0:
|
||||
// keep vibZero value
|
||||
break;
|
||||
case 1:
|
||||
state.vibZero = state.vibZero * 0.95 + vibRead * 0.05;
|
||||
state.vibZeroBite = state.vibZeroBite * 0.95 + vibReadBite * 0.05;
|
||||
break;
|
||||
case 2:
|
||||
state.vibZero = state.vibZero * 0.9 + vibRead * 0.1;
|
||||
state.vibZeroBite = state.vibZeroBite * 0.9 + vibReadBite * 0.1;
|
||||
break;
|
||||
case 3:
|
||||
state.vibZero = state.vibZero * 0.8 + vibRead * 0.2;
|
||||
state.vibZeroBite = state.vibZeroBite * 0.8 + vibReadBite * 0.2;
|
||||
break;
|
||||
case 4:
|
||||
state.vibZero = state.vibZero * 0.6 + vibRead * 0.4;
|
||||
state.vibZeroBite = state.vibZeroBite * 0.6 + vibReadBite * 0.4;
|
||||
}
|
||||
state.vibThr = state.vibZero - currentPreset->vibSquelch;
|
||||
state.vibThrLo = state.vibZero + currentPreset->vibSquelch;
|
||||
state.vibThrBite = state.vibZeroBite - currentPreset->vibSquelch;
|
||||
state.vibThrBiteLo = state.vibZeroBite + currentPreset->vibSquelch;
|
||||
int pbPos = mapConstrain(state.pbUpSignal, calibration.pbUpMaxVal, calibration.pbUpThrVal, calculatedPBdepth, 0);
|
||||
int pbNeg = mapConstrain(state.pbDnSignal, calibration.pbDnMaxVal, calibration.pbDnThrVal, calculatedPBdepth, 0);
|
||||
int pbSum = 8193 + pbPos - pbNeg;
|
||||
int pbDif = abs(pbPos - pbNeg);
|
||||
|
||||
if ((state.pbUpSignal < calibration.pbUpThrVal || state.pbDnSignal < calibration.pbDnThrVal) && currentPreset->PBdepth) {
|
||||
if (pbDif < 10) {
|
||||
state.pitchBend = 8192;
|
||||
} else {
|
||||
state.pitchBend = state.pitchBend * 0.6 + 0.4 * pbSum;
|
||||
}
|
||||
pbTouched = 1;
|
||||
}
|
||||
if (!pbTouched) {
|
||||
state.pitchBend = state.pitchBend * 0.6 + 8192 * 0.4; // released, so smooth your way back to zero
|
||||
if ((state.pitchBend > 8187) && (state.pitchBend < 8197))
|
||||
state.pitchBend = 8192; // 8192 is 0 pitch bend, don't miss it bc of smoothing
|
||||
}
|
||||
|
||||
state.pitchBend = state.pitchBend + state.vibSignal;
|
||||
state.pitchBend = constrain(state.pitchBend, 0, 16383);
|
||||
|
||||
state.pbSend = state.pitchBend - state.quarterToneTrigger * calculatedPBdepth * 0.25;
|
||||
state.pbSend = constrain(state.pbSend, 0, 16383);
|
||||
|
||||
if (state.pbSend != oldpb) { // only send midi data if pitch bend has changed from previous value
|
||||
midiSendPitchBend(state.pbSend);
|
||||
oldpb = state.pbSend;
|
||||
}
|
||||
}
|
||||
|
||||
//***********************************************************
|
||||
|
||||
void portamento_() {
|
||||
if (currentPreset->portamentoMode == PortamentoMode::POFF) {
|
||||
port(0); // ensure it's off
|
||||
return;
|
||||
}
|
||||
|
||||
int portSumCC = 0;
|
||||
if (currentPreset->pinkySetting == GLD) {
|
||||
if (state.pinkyKey) {
|
||||
portSumCC += currentPreset->portamentoLimit;
|
||||
}
|
||||
}
|
||||
if (ExtraControl::GLIDE == currentPreset->biteControl) {
|
||||
// Portamento is controlled with the bite sensor in the mouthpiece
|
||||
state.biteSignal = readTouchUtil(bitePin);
|
||||
if (state.biteSignal >= calibration.biteThrVal) { // if we are enabled and over the threshold, send portamento
|
||||
portSumCC += mapConstrain(state.biteSignal, calibration.biteThrVal, calibration.biteMaxVal, 0, currentPreset->portamentoLimit);
|
||||
}
|
||||
}
|
||||
|
||||
if (ExtraControl::GLIDE == currentPreset->leverControl) {
|
||||
// Portamento is controlled with thumb lever
|
||||
state.leverSignal = readTouchUtil(vibratoPin);
|
||||
if (((3000 - state.leverSignal) >= calibration.leverThrVal)) { // if we are enabled and over the threshold, send portamento
|
||||
portSumCC += mapConstrain((3000 - state.leverSignal), calibration.leverThrVal, calibration.leverMaxVal, 0, currentPreset->portamentoLimit);
|
||||
}
|
||||
}
|
||||
|
||||
port(constrain(portSumCC, 0, currentPreset->portamentoLimit)); // Total output glide rate limited to glide max setting
|
||||
}
|
||||
|
||||
//***********************************************************
|
||||
|
||||
void biteCC_() {
|
||||
int biteVal = 0;
|
||||
if (ExtraControl::CC == currentPreset->biteControl) {
|
||||
state.biteSignal = readTouchUtil(bitePin); // get sensor data, do some smoothing - SENSOR PIN 17 - PCB PINS LABELED "BITE" (GND left, sensor pin right)
|
||||
if (state.biteSignal >= calibration.biteThrVal) { // we are over the threshold, calculate CC value
|
||||
biteVal = mapConstrain(state.biteSignal, calibration.biteThrVal, calibration.biteMaxVal, 0, 127);
|
||||
}
|
||||
|
||||
if (biteVal != state.biteVal) {
|
||||
midiSendControlChange(currentPreset->biteCC, biteVal);
|
||||
}
|
||||
state.biteVal = biteVal;
|
||||
}
|
||||
}
|
||||
|
||||
void autoCal() {
|
||||
state.vibZero = state.vibZeroBite = 0;
|
||||
for(int i = 1 ; i <= CALIBRATE_SAMPLE_COUNT; ++i) {
|
||||
state.breathZero += readPressure();
|
||||
state.breathAltZero += readAltPressure();
|
||||
state.vibZero += readTouchUtil(vibratoPin);
|
||||
state.vibZeroBite += readTouchUtil(bitePin);
|
||||
}
|
||||
|
||||
state.breathZero /= CALIBRATE_SAMPLE_COUNT;
|
||||
state.breathAltZero /= CALIBRATE_SAMPLE_COUNT;
|
||||
state.vibZero /= CALIBRATE_SAMPLE_COUNT;
|
||||
state.vibZeroBite /= CALIBRATE_SAMPLE_COUNT;
|
||||
|
||||
state.vibThr = state.vibZero - currentPreset->vibSquelch;
|
||||
state.vibThrLo = state.vibZero + currentPreset->vibSquelch;
|
||||
state.vibThrBite = state.vibZeroBite - currentPreset->vibSquelch;
|
||||
state.vibThrBiteLo = state.vibZeroBite + currentPreset->vibSquelch;
|
||||
|
||||
state.breathThrVal = state.breathZero + calibration.breathThrValOffset;
|
||||
state.breathMaxVal = state.breathThrVal + calibration.breathMaxValOffset;
|
||||
state.breathAltThrVal = state.breathAltZero + calibration.breathAltThrValOffset;
|
||||
state.breathAltMaxVal = state.breathAltThrVal + calibration.breathAltMaxValOffset;
|
||||
|
||||
}
|
||||
|
||||
void fullAutoCal() {
|
||||
int calRead;
|
||||
int calReadNext;
|
||||
autoCal();
|
||||
|
||||
// Lever
|
||||
calRead = 3000 - readTouchUtil(vibratoPin);
|
||||
calibration.leverThrVal = constrain(calRead + 60, LEVER_LO_LIMIT, LEVER_HI_LIMIT);
|
||||
calibration.leverMaxVal = constrain(calRead + 120, LEVER_LO_LIMIT, LEVER_HI_LIMIT);
|
||||
|
||||
// Bite sensor
|
||||
calRead = readTouchUtil(bitePin);
|
||||
calibration.biteThrVal = constrain(calRead + 100, BITE_LO_LIMIT, BITE_HI_LIMIT);
|
||||
calibration.biteMaxVal = constrain(calibration.biteThrVal + 300, BITE_LO_LIMIT, BITE_HI_LIMIT);
|
||||
|
||||
// Touch sensors
|
||||
calRead = CTOUCH_HI_LIMIT;
|
||||
for (byte i = 0; i < 12; i++) {
|
||||
calReadNext = readTouchKey(i);
|
||||
if (calReadNext < calRead)
|
||||
calRead = calReadNext; // use lowest value
|
||||
}
|
||||
|
||||
calibration.ctouchThrVal = calRead - 20;
|
||||
}
|
||||
|
||||
//***********************************************************
|
||||
|
||||
/*
|
||||
* Read octave and return offset
|
||||
*/
|
||||
int readOctave() {
|
||||
static byte lastOctaveR = 0;
|
||||
|
||||
// Roller modes
|
||||
// 1: Highest touched roller, release memory
|
||||
// 2: Highest touched roller, extend range on top/bottom release
|
||||
// 3: Touched roller pair
|
||||
// 4: Touched roller pair, extend range
|
||||
// 5: Touched roller, pair = 5th partial
|
||||
// 6: Touched roller, pair = 5th partial, extend range
|
||||
RollerMode rollerMode = currentPreset->rollerMode;
|
||||
byte extend = rollerMode == RollerMode::HIGHEST_EXTEND || rollerMode == RollerMode::HIGHEST_PAIR_EXTEND || rollerMode == RollerMode::PARTIAL_EXTEND ? 1 : 0;
|
||||
|
||||
byte rollers[6];
|
||||
uint16_t ctouchThrVal = calibration.ctouchThrVal;
|
||||
rollers[0] = readTouchUtil(R1Pin) < ctouchThrVal;
|
||||
rollers[1] = readTouchUtil(R2Pin) < ctouchThrVal;
|
||||
rollers[2] = readTouchUtil(R3Pin) < ctouchThrVal;
|
||||
rollers[3] = readTouchUtil(R4Pin) < ctouchThrVal;
|
||||
rollers[4] = readTouchUtil(R5Pin) < ctouchThrVal;
|
||||
rollers[5] = readTouchUtil(R6Pin) < ctouchThrVal;
|
||||
|
||||
byte offset = 0;
|
||||
byte octaveR = 0;
|
||||
if (rollerMode == RollerMode::HIGHEST || rollerMode == RollerMode::HIGHEST_EXTEND) {
|
||||
for (int i = 5; i >= 0; i--) {
|
||||
if (rollers[i]) {
|
||||
octaveR = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (rollerMode == RollerMode::HIGHEST_PAIR || rollerMode == RollerMode::HIGHEST_PAIR_EXTEND) {
|
||||
for (int i = 5; i >= 1; i--) {
|
||||
if (rollers[i] && rollers[i - 1]) {
|
||||
octaveR = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Allow top/bottom single rollers in extended mode
|
||||
if (octaveR == 0 && extend) {
|
||||
if (rollers[0]) {
|
||||
octaveR = 1;
|
||||
} else if (rollers[5]) {
|
||||
octaveR = 6;
|
||||
}
|
||||
}
|
||||
} else if (rollerMode == RollerMode::PARTIAL || rollerMode == RollerMode::PARTIAL_EXTEND) {
|
||||
for (int i = 5; i >= 0; i--) {
|
||||
if (rollers[i]) {
|
||||
octaveR = i + 1;
|
||||
if (i > 0 && rollers[i - 1]) {
|
||||
offset = -5;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Go up/down a partial on top/bottom release
|
||||
if (extend && octaveR == 0) {
|
||||
if (lastOctaveR == 1) {
|
||||
offset = -5;
|
||||
} else if (lastOctaveR == 6) {
|
||||
offset = 7;
|
||||
}
|
||||
|
||||
octaveR = lastOctaveR;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle extended release
|
||||
if (extend && octaveR == 0) {
|
||||
if (rollerMode == RollerMode::HIGHEST_EXTEND && lastOctaveR == 6) {
|
||||
octaveR = 7;
|
||||
} else if (rollerMode == RollerMode::PARTIAL_EXTEND && lastOctaveR == 7) {
|
||||
octaveR = 8;
|
||||
} else if (lastOctaveR == 1) {
|
||||
octaveR = 0;
|
||||
} else {
|
||||
octaveR = lastOctaveR;
|
||||
}
|
||||
}
|
||||
|
||||
lastOctaveR = octaveR;
|
||||
|
||||
FingeringMode fingering = currentPreset->fingering;
|
||||
byte K4 = readTouchKey(K4Pin) < calibration.ctouchThrVal;
|
||||
if (FingeringMode::TPT == fingering) { // TPT fingering
|
||||
return 24 + offset + trumpetHarmonic[K4][octaveR]; // roller harmonics
|
||||
} else if (FingeringMode::HRN == fingering) { // HRN fingering
|
||||
return 12 + offset + rollerHarmonic[K4][octaveR]; // roller harmonics
|
||||
} else if (FingeringMode::EVR == fingering) { // HRN fingering
|
||||
return 12 * (6 - octaveR) + offset;
|
||||
} else { // EVI
|
||||
return 12 * octaveR + offset;
|
||||
}
|
||||
}
|
||||
|
||||
int readSwitches() {
|
||||
// Keep the last fingering value for debouncing
|
||||
static int lastFingering = 0;
|
||||
static int fingeredNote = 0;
|
||||
static unsigned long lastDeglitchTime = 0; // The last time the fingering was changed
|
||||
|
||||
// Read touch pads (MPR121), compare against threshold value
|
||||
bool touchKeys[12];
|
||||
for (byte i = 0; i < 12; i++) {
|
||||
touchKeys[i] = readTouchKey(i) < calibration.ctouchThrVal;
|
||||
}
|
||||
|
||||
// Valves and trill keys, TRUE (1) for pressed, FALSE (0) for not pressed
|
||||
byte K1 = touchKeys[K1Pin]; // Valve 1 (pitch change -2)
|
||||
byte K2 = touchKeys[K2Pin]; // Valve 2 (pitch change -1)
|
||||
byte K3 = touchKeys[K3Pin]; // Valve 3 (pitch change -3)
|
||||
byte K4 = touchKeys[K4Pin]; // Left Hand index finger (pitch change -5)
|
||||
byte K5 = touchKeys[K5Pin]; // Trill key 1 (pitch change +2)
|
||||
byte K6 = touchKeys[K6Pin]; // Trill key 2 (pitch change +1)
|
||||
byte K7 = touchKeys[K7Pin]; // Trill key 3 (pitch change +4)
|
||||
|
||||
state.pinkyKey = touchKeys[K8Pin];
|
||||
|
||||
int qTransp = (state.pinkyKey && (currentPreset->pinkySetting < 25)) ? currentPreset->pinkySetting - 12 : 0;
|
||||
|
||||
// Calculate midi note number from pressed keys
|
||||
int fingeredNoteUntransposed = 0;
|
||||
if (EVI == currentPreset->fingering) { // EVI fingering
|
||||
fingeredNoteUntransposed = START_NOTE - 2 * K1 - K2 - 3 * K3 //"Trumpet valves"
|
||||
- 5 * K4 // Fifth key
|
||||
+ 2 * K5 + K6 + currentPreset->trill3_interval * K7; // Trill keys. 3rd trill key interval controlled by setting
|
||||
} else if (EVR == currentPreset->fingering) { // EVR fingering
|
||||
fingeredNoteUntransposed = START_NOTE - 2 * K1 - K2 - 3 * K3 //"Trumpet valves"
|
||||
- 5 * K4 // Fifth key
|
||||
+ 2 * K5 + K6 + currentPreset->trill3_interval * K7; // Trill keys. 3rd trill key interval controlled by setting
|
||||
} else if (TPT == currentPreset->fingering) { // TPT fingering
|
||||
fingeredNoteUntransposed = START_NOTE - 2 * K1 - K2 - 3 * K3 //"Trumpet valves"
|
||||
- 2 // Trumpet in B flat
|
||||
+ 2 * K5 + K6 + currentPreset->trill3_interval * K7; // Trill keys. 3rd trill key interval controlled by setting
|
||||
} else if (HRN == currentPreset->fingering) { // HRN fingering
|
||||
fingeredNoteUntransposed = START_NOTE - 2 * K1 - K2 - 3 * K3 //"Trumpet valves"
|
||||
+ 5 * K4 // Switch to Bb horn
|
||||
+ 5 // Horn in F
|
||||
+ 2 * K5 + K6 + currentPreset->trill3_interval * K7; // Trill keys. 3rd trill key interval controlled by setting
|
||||
}
|
||||
|
||||
if (K3 && K7) {
|
||||
if (4 == currentPreset->trill3_interval)
|
||||
fingeredNoteUntransposed += 2;
|
||||
else
|
||||
fingeredNoteUntransposed += 4;
|
||||
}
|
||||
|
||||
int fingeredNoteRead = fingeredNoteUntransposed + state.transpose - 12 + qTransp;
|
||||
|
||||
if (fingeredNoteRead != lastFingering) { //
|
||||
// reset the debouncing timer
|
||||
lastDeglitchTime = millis();
|
||||
}
|
||||
|
||||
if ((millis() - lastDeglitchTime) > currentPreset->deglitch) {
|
||||
// whatever the reading is at, it's been there for longer
|
||||
// than the debounce delay, so take it as the actual current state
|
||||
fingeredNote = fingeredNoteRead;
|
||||
}
|
||||
|
||||
lastFingering = fingeredNoteRead;
|
||||
|
||||
return fingeredNote;
|
||||
}
|
||||
|
||||
void noteOn(int fingeredNote, float pressureSensor, int initial_breath_value) {
|
||||
// Yes, so calculate MIDI note and velocity, then send a note on event
|
||||
// We should be at tonguing peak, so set velocity based on current pressureSensor value unless fixed velocity is set
|
||||
state.breathSignal = constrain(max(pressureSensor, initial_breath_value), state.breathThrVal, state.breathMaxVal);
|
||||
byte velocitySend;
|
||||
if (!currentPreset->fixedVelocity) {
|
||||
unsigned int breathValHires = breathCurve(mapConstrain(state.breathSignal, state.breathThrVal, state.breathMaxVal, 0, 16383));
|
||||
velocitySend = (breathValHires >> 7) & 0x007F;
|
||||
velocitySend = constrain(velocitySend + velocitySend * .1 * currentPreset->velBias, 1, 127);
|
||||
} else {
|
||||
velocitySend = currentPreset->fixedVelocity;
|
||||
}
|
||||
|
||||
breath(); // send breath data
|
||||
midiSendNoteOn(fingeredNote, velocitySend); // send Note On message for new note
|
||||
state.activeNote = fingeredNote;
|
||||
}
|
||||
|
||||
void handleOffStateActions() {
|
||||
if (state.activeMIDIchannel != currentPreset->MIDIchannel) {
|
||||
state.activeMIDIchannel = currentPreset->MIDIchannel; // only switch channel if no active note
|
||||
midiSetChannel(state.activeMIDIchannel);
|
||||
}
|
||||
if ((state.activePatch != state.patch) && state.doPatchUpdate) {
|
||||
state.activePatch = state.patch;
|
||||
midiSendProgramChange(state.activePatch);
|
||||
state.doPatchUpdate = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Initialize the main instrument state
|
||||
*/
|
||||
void initState() {
|
||||
state.activePatch = 0;
|
||||
state.mainState = NOTE_OFF; // initialize main state machine
|
||||
|
||||
state.activeMIDIchannel = currentPreset->MIDIchannel;
|
||||
midiInitialize(currentPreset->MIDIchannel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send CC data when needed
|
||||
*/
|
||||
void sendCCs() {
|
||||
static unsigned long ccBreathSendTime = 0L; // The last time we sent breath CC values
|
||||
static unsigned long ccSendTime = 0L; // The last time we sent CC values
|
||||
static unsigned long ccSendTime2 = 0L; // The last time we sent CC values 2 (slower)
|
||||
static unsigned long ccSendTime3 = 0L; // The last time we sent CC values 3 (and slower)
|
||||
|
||||
// Is it time to send more CC data?
|
||||
uint32_t currentTime = millis();
|
||||
if ((currentTime - ccBreathSendTime) > (currentPreset->breathInterval - 1u)) {
|
||||
breath();
|
||||
ccBreathSendTime = currentTime;
|
||||
}
|
||||
if (currentTime - ccSendTime > CC_INTERVAL_PRIMARY) {
|
||||
// deal with Pitch Bend, Modulation, etc.
|
||||
pitch_bend();
|
||||
biteCC_();
|
||||
ccSendTime = currentTime;
|
||||
}
|
||||
if (currentTime - ccSendTime2 > CC_INTERVAL_PORT) {
|
||||
portamento_();
|
||||
ccSendTime2 = currentTime;
|
||||
}
|
||||
if (currentTime - ccSendTime3 > CC_INTERVAL_OTHER) {
|
||||
updateSensorLEDs();
|
||||
ccSendTime3 = currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main instrument state machine
|
||||
*/
|
||||
void runStateMachine() {
|
||||
static unsigned long breath_on_time = 0L; // Time when breath sensor value went over the ON threshold
|
||||
static int initial_breath_value = 0; // The breath value at the time we observed the transition
|
||||
|
||||
int fingeredNote = noteValueCheck(readSwitches() + readOctave());
|
||||
if (state.mainState == NOTE_OFF) {
|
||||
handleOffStateActions();
|
||||
if ((state.breathSignal > state.breathThrVal)) {
|
||||
// Value has risen above threshold. Move to the RISE_WAIT
|
||||
// state. Record time and initial breath value.
|
||||
breath_on_time = millis();
|
||||
initial_breath_value = state.breathSignal;
|
||||
state.mainState = RISE_WAIT; // Go to next state
|
||||
}
|
||||
} else if (state.mainState == RISE_WAIT) {
|
||||
if ((state.breathSignal > state.breathThrVal)) {
|
||||
// Has enough time passed for us to collect our second sample?
|
||||
if ((millis() - breath_on_time > currentPreset->velSmpDl) || (0 == currentPreset->velSmpDl) || currentPreset->fixedVelocity) {
|
||||
noteOn(fingeredNote, state.breathSignal, initial_breath_value);
|
||||
state.mainState = NOTE_ON;
|
||||
}
|
||||
} else {
|
||||
// Value fell below threshold before velocity sample delay time passed. Return to NOTE_OFF state
|
||||
state.mainState = NOTE_OFF;
|
||||
}
|
||||
} else if (state.mainState == NOTE_ON) {
|
||||
if ((state.breathSignal < state.breathThrVal)) {
|
||||
// Value has fallen below threshold - turn the note off
|
||||
midiSendNoteOff(state.activeNote); // send Note Off message
|
||||
state.breathSignal = 0;
|
||||
state.mainState = NOTE_OFF;
|
||||
} else {
|
||||
if (fingeredNote != state.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.
|
||||
noteOn(fingeredNote, state.breathSignal, 0);
|
||||
delayMicroseconds(2000); // delay for midi recording fix
|
||||
midiSendNoteOff(state.activeNote); // send Note Off message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setup() {
|
||||
if (checkButtonState(DEBUG_CONFIG)) {
|
||||
Serial.begin(9600); // debug
|
||||
Serial.println("Debug Startup");
|
||||
}
|
||||
|
||||
bool factoryReset = checkButtonState(STARTUP_FACTORY_RESET);
|
||||
configManagementMode = checkButtonState(STARTUP_CONFIG);
|
||||
testMode = checkButtonState(TEST_CONFIG);
|
||||
|
||||
initDisplay(); // Start up display and show logo
|
||||
initHardware();
|
||||
|
||||
// If going into config management mode, stop here before we even touch the EEPROM.
|
||||
if (configManagementMode) {
|
||||
configModeSetup();
|
||||
return;
|
||||
}
|
||||
|
||||
// Read eeprom data into global vars
|
||||
readEEPROM(factoryReset);
|
||||
|
||||
statusLedFlash(500);
|
||||
statusLedOff();
|
||||
|
||||
if (factoryReset) {
|
||||
// Full calibration
|
||||
fullAutoCal();
|
||||
} else {
|
||||
// Minimal startup calibration (atmo pressure)
|
||||
autoCal();
|
||||
}
|
||||
|
||||
showVersion();
|
||||
delay(1000);
|
||||
|
||||
initState(); // Set up midi/etc
|
||||
statusLedOn(); // Switch on the onboard LED to indicate power on/ready
|
||||
}
|
||||
|
||||
//_______________________________________________________________________________________________ MAIN LOOP
|
||||
|
||||
void loop() {
|
||||
static unsigned long pixelUpdateTime = 0;
|
||||
static const unsigned long pixelUpdateInterval = 80;
|
||||
|
||||
// If in config mgmt loop, do that and nothing else
|
||||
if (configManagementMode) {
|
||||
configModeLoop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (testMode) {
|
||||
|
||||
}
|
||||
|
||||
state.breathSignal = constrain(readPressure(), BREATH_LO_LIMIT, BREATH_HI_LIMIT); // Get the filtered pressure sensor reading from analog pin A0, input from sensor MP3V5004GP
|
||||
|
||||
runStateMachine();
|
||||
sendCCs();
|
||||
|
||||
// cvUpdate();
|
||||
midiDiscardInput();
|
||||
|
||||
if (millis() - pixelUpdateTime > pixelUpdateInterval) {
|
||||
// even if we just alter a pixel, the whole display is redrawn (35ms of MPU lockup) and we can't do that all the time
|
||||
// this is one of the big reasons the display is for setup use only
|
||||
// TODO: is this still true on teensy 4?
|
||||
pixelUpdateTime = millis();
|
||||
handleMenu(true);
|
||||
} else {
|
||||
handleMenu(false);
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue