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:
Brian Hrebec 2023-08-27 11:52:08 -05:00
parent c58c3f9e46
commit 01d193c9b3
92 changed files with 69119 additions and 73272 deletions

238
NuEVI/src/FilterOnePole.cpp Normal file
View file

@ -0,0 +1,238 @@
// Copyright 2014 Jonathan Driscoll
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "FilterOnePole.h"
#include <Arduino.h>
FilterOnePole::FilterOnePole( FILTER_TYPE ft, float fc, float initialValue ) {
setFilter( ft, fc, initialValue );
}
void FilterOnePole::setFilter( FILTER_TYPE ft, float fc, float initialValue ) {
FT = ft;
setFrequency( fc );
Y = initialValue;
Ylast = initialValue;
X = initialValue;
LastUS = micros();
}
float FilterOnePole::input( float inVal ) {
long time = micros();
ElapsedUS = float(time - LastUS); // cast to float here, for math
LastUS = time; // update this now
// shift the data values
Ylast = Y;
X = inVal; // this is now the most recent input value
// filter value is controlled by a parameter called X
// tau is set by the user in microseconds, but must be converted to samples here
TauSamps = TauUS / ElapsedUS;
float ampFactor;
#ifdef ARM_FLOAT
ampFactor = expf( -1.0 / TauSamps ); // this is 1 if called quickly
#else
ampFactor = exp( -1.0 / TauSamps ); // this is 1 if called quickly
#endif
Y = (1.0-ampFactor)*X + ampFactor*Ylast; // set the new value
return output();
}
void FilterOnePole::setFrequency( float newFrequency ) {
setTau( 1.0/(TWO_PI*newFrequency ) ); // τ=1/ω
}
void FilterOnePole::setTau( float newTau ) {
TauUS = newTau * 1e6;
}
float FilterOnePole::output() {
// figure out which button to read
switch (FT) {
case LOWPASS:
// return the last value
return Y;
break;
case INTEGRATOR:
// using a lowpass, but normaize
return Y * (TauUS/1.0e6);
break;
case HIGHPASS:
// highpass is the _difference_
return X-Y;
break;
case DIFFERENTIATOR:
// like a highpass, but normalize
return (X-Y)/(TauUS/1.0e6);
break;
default:
// should never get to here, return 0 just in case
return 0;
}
}
void FilterOnePole::print() {
Serial.println("");
Serial.print(" Y: "); Serial.print( Y );
Serial.print(" Ylast: "); Serial.print( Ylast );
Serial.print(" X "); Serial.print( X );
Serial.print(" ElapsedUS "); Serial.print( ElapsedUS );
Serial.print(" TauSamps: "); Serial.print( TauSamps );
//Serial.print(" ampFactor " ); Serial.print( ampFactor );
Serial.print(" TauUS: "); Serial.print( TauUS );
Serial.println("");
}
void FilterOnePole::test() {
float tau = 10;
float updateInterval = 1;
float nextupdateTime = millis()*1e-3;
float inputValue = 0;
FilterOnePole hp( HIGHPASS, tau, inputValue );
FilterOnePole lp( LOWPASS, tau, inputValue );
while( true ) {
float now = millis()*1e-3;
// switch input values on a 20 second cycle
if( round(now/20.0)-(now/20.0) < 0 )
inputValue = 0;
else
inputValue = 100;
hp.input(inputValue);
lp.input(inputValue);
if( now > nextupdateTime ) {
nextupdateTime += updateInterval;
Serial.print("inputValue: "); Serial.print( inputValue );
Serial.print("\t high-passed: "); Serial.print( hp.output() );
Serial.print("\t low-passed: "); Serial.print( lp.output() );
Serial.println();
}
}
}
void FilterOnePole::setToNewValue( float newVal ) {
Y = Ylast = X = newVal;
}
// stuff for filter2 (lowpass only)
// should be able to set a separate fall time as well
FilterOnePoleCascade::FilterOnePoleCascade( float riseTime, float initialValue ) {
setRiseTime( riseTime );
setToNewValue( initialValue );
}
void FilterOnePoleCascade::setRiseTime( float riseTime ) {
float tauScale = 3.36; // found emperically, by running test();
Pole1.setTau( riseTime / tauScale );
Pole2.setTau( riseTime / tauScale );
}
float FilterOnePoleCascade::input( float inVal ) {
Pole2.input( Pole1.input( inVal ));
return output();
}
// clears out the values in the filter
void FilterOnePoleCascade::setToNewValue( float newVal ) {
Pole1.setToNewValue( newVal );
Pole2.setToNewValue( newVal );
}
float FilterOnePoleCascade::output() {
return Pole2.output();
}
void FilterOnePoleCascade::test() {
// make a filter, how fast does it run:
float rise = 1.0;
FilterOnePoleCascade myFilter( rise );
// first, test the filter speed ...
long nLoops = 1000;
Serial.print( "testing filter with a rise time of ");
Serial.print( rise ); Serial.print( "s" );
Serial.print( "\n running filter speed loop ... ");
float startTime, stopTime;
startTime = millis()*1e-3;
for( long i=0; i<nLoops; ++i ) {
myFilter.input( PI ); // use pi, so it will actually do a full calculation
}
stopTime = millis()*1e-3;
Serial.print( "done, filter runs at " );
Serial.print( float(nLoops) / (stopTime - startTime) );
Serial.print( " hz " );
Serial.print( "\n filter value: " ); Serial.print( myFilter.output() );
myFilter.setToNewValue( 0.0 );
Serial.print( "\n after reset to 0: "); Serial.print( myFilter.output() );
Serial.print( "\n testing rise time (10% to 90%) ...");
bool crossedTenPercent = false;
while( myFilter.output() < 0.9 ) {
myFilter.input( 1.0 );
if( myFilter.output() > 0.1 && !crossedTenPercent ) {
// filter first crossed the 10% point
startTime = millis()*1e-3;
crossedTenPercent = true;
}
}
stopTime = millis()*1e-3;
Serial.print( "done, rise time: " ); Serial.print( stopTime-startTime );
Serial.print( "testing attenuation at f = 1/risetime" );
myFilter.setToNewValue( 0.0 );
float maxVal = 0;
float valWasOutputThisCycle = true;
while( true ) {
float now = 1e-3*millis();
float currentFilterVal = myFilter.input( sin( TWO_PI*now) );
if( currentFilterVal < 0.0 ) {
if( !valWasOutputThisCycle ) {
// just crossed below zero, output the max
Serial.print( maxVal*100 ); Serial.print( " %\n" );
valWasOutputThisCycle = true;
}
}
}
}

89
NuEVI/src/FilterOnePole.h Normal file
View file

@ -0,0 +1,89 @@
// Copyright 2014 Jonathan Driscoll
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// FilterOnePole has been copied from https://github.com/JonHub/Filters
#ifndef FilterOnePole_h
#define FilterOnePole_h
enum FILTER_TYPE {
HIGHPASS,
LOWPASS,
INTEGRATOR,
DIFFERENTIATOR
};
// the recursive filter class implements a recursive filter (low / pass / highpass
// note that this must be updated in a loop, using the most recent acquired values and the time acquired
// Y = a0*X + a1*Xm1
// + b1*Ylast
struct FilterOnePole {
FILTER_TYPE FT;
float TauUS; // decay constant of the filter, in US
float TauSamps; // tau, measued in samples (this changes, depending on how long between input()s
// filter values - these are public, but should not be set externally
float Y; // most recent output value (gets computed on update)
float Ylast; // prevous output value
float X; // most recent input value
// elapsed times are kept in long, and will wrap every
// 35 mins, 47 seconds ... however, the wrap does not matter,
// because the delta will still be correct (always positive and small)
float ElapsedUS; // time since last update
long LastUS; // last time measured
FilterOnePole( FILTER_TYPE ft=LOWPASS, float fc=1.0, float initialValue=0 );
// sets or resets the parameters and state of the filter
void setFilter( FILTER_TYPE ft, float tauS, float initialValue );
void setFrequency( float newFrequency );
void setTau( float newTau );
float input( float inVal );
float output();
void print();
void test();
void setToNewValue( float newVal ); // resets the filter to a new value
};
// two pole filter, these are very useful
struct FilterOnePoleCascade {
FilterOnePole Pole1;
FilterOnePole Pole2;
FilterOnePoleCascade( float riseTime=1.0, float initialValue=0 ); // rise time to step function, 10% to 90%
// rise time is 10% to 90%, for a step input
void setRiseTime( float riseTime );
void setToNewValue( float newVal );
float input( float inVal );
float output();
void test();
};
#endif

8
NuEVI/src/TODO Normal file
View file

@ -0,0 +1,8 @@
1. LED abstraction code
2. Encoder code
3. Menu refactor
4. Refactor note play behavior into module
5. Refactor CV behavior into module
6. 9dof sensor code
7. Alternate fingerings
8. Encoder midi

175
NuEVI/src/adjustmenu.cpp Normal file
View file

@ -0,0 +1,175 @@
#include <array>
#include <Arduino.h>
#include <Adafruit_SSD1306.h>
#include "menu.h"
#include "globals.h"
#include "config.h"
#include "hardware.h"
#include "settings.h"
//***********************************************************
extern Adafruit_SSD1306 display;
extern Adafruit_MPR121 touchSensorUtil;
extern Adafruit_MPR121 touchSensorKeys;
extern Adafruit_MPRLS pressureSensorMain;
extern Adafruit_MPRLS pressureSensorAlt;
extern byte cursorNow;
int16_t ctouchVal = 0;
// Track pixels for faster redrawing
struct AdjustDrawing {
int row;
int thrX;
int maxX;
int valX;
};
struct AdjustValue {
const char *title;
const int16_t &value;
int16_t &thrVal;
int16_t &maxVal;
const int16_t limitLow;
const int16_t limitHigh;
// If not null, thr and max are relative to zeroPoint
const int16_t *zeroPoint;
};
template<size_t N>
class AdjustMenuScreen : public MenuScreen {
public:
AdjustMenuScreen(const char* title, std::array<AdjustValue, N> entries) : _title(title), _entries(entries) { }
void update(InputState input, bool redraw) {
bool redrawIndicators = false;
if (input.changed) {
if (input.knobMenu) {
_selectedEntry = _selectedEntry + input.knobMenu;
redraw = true;
}
AdjustValue value = _entries[_selectedEntry];
if (input.knobVal1) {
value.thrVal += input.knobVal1;
redrawIndicators = true;
}
if (input.knobVal2) {
value.maxVal += input.knobVal2;
redrawIndicators = true;
}
} else {
draw(redrawIndicators, redraw);
}
}
const char *title() {
return _title;
}
private:
void draw(bool redrawIndicators, bool redraw) {
for (size_t i = 0; i < N; i++) {
if (redraw) {
drawAdjustRow(_entries[i], _rowDrawings[i], i == _selectedEntry);
} else if (redrawIndicators && i == _selectedEntry) {
drawAdjustIndicators(_entries[i], _rowDrawings[i]);
} else {
drawAdjustValues(_entries[i], _rowDrawings[i]);
}
}
}
const char* _title;
size_t _selectedEntry = 0;
std::array<AdjustValue, N> _entries;
std::array<AdjustDrawing, N> _rowDrawings;
};
std::array<AdjustValue, 8> adjustValues = {{
{"BREATH", state.breathSignal, calibration.breathThrValOffset, calibration.breathMaxValOffset,
BREATH_LO_LIMIT, BREATH_HI_LIMIT, &state.breathZero},
{"BR ALT", state.breathAltSignal, calibration.breathAltThrValOffset, calibration.breathAltMaxValOffset,
BREATH_LO_LIMIT, BREATH_HI_LIMIT, &state.breathAltZero},
{"BITE",state.biteSignal, calibration.biteThrVal, calibration.biteMaxVal, BITE_LO_LIMIT, BITE_HI_LIMIT, NULL},
{"PB DOWN",state.pbDnSignal, calibration.pbDnThrVal, calibration.pbDnMaxVal, PITCHB_LO_LIMIT, PITCHB_HI_LIMIT, NULL},
{"PB UP", state.pbUpSignal, calibration.pbUpThrVal, calibration.pbUpMaxVal, PITCHB_LO_LIMIT, PITCHB_HI_LIMIT, NULL},
{"EXTRA", state.extraSignal, calibration.extraThrVal, calibration.extraMaxVal, EXTRA_LO_LIMIT, EXTRA_HI_LIMIT, NULL},
{"LEVER", state.leverSignal, calibration.leverThrVal, calibration.leverMaxVal, LEVER_LO_LIMIT, LEVER_HI_LIMIT, NULL},
{"TOUCH", ctouchVal, calibration.ctouchThrVal, calibration.ctouchThrVal, CTOUCH_LO_LIMIT, CTOUCH_HI_LIMIT, NULL},
}};
const MenuScreen adjustMenu = AdjustMenuScreen<8>("ADJUST", adjustValues);
void autoCalSelected() {
}
//***********************************************************
static void drawIndicator(int x, int row, int color) {
display.fillTriangle(x-2, row+1, x+2, row+1, x, row+3, color);
display.fillTriangle(x-2, row+10, x+2, row+10, x, row+7, color);
}
static void drawAdjustIndicators(const AdjustValue &value, AdjustDrawing &drawing) {
const int thrX = mapConstrain(value.thrVal, value.limitLow, value.limitHigh, 1, 127);
const int maxX = mapConstrain(value.maxVal, value.limitLow, value.limitHigh, 1, 127);
if (drawing.maxX != maxX) {
drawIndicator(drawing.thrX, drawing.row, BLACK);
drawIndicator(thrX, drawing.row, WHITE);
drawing.maxX = maxX;
}
if (drawing.thrX != thrX) {
drawIndicator(drawing.thrX, drawing.row, BLACK);
drawIndicator(thrX, drawing.row, WHITE);
drawing.thrX = thrX;
}
}
static void drawAdjustTitle(const AdjustValue &value, AdjustDrawing &drawing, bool highlight) {
display.setTextSize(1);
if (highlight) {
display.setTextColor(BLACK, WHITE);
} else {
display.setTextColor(WHITE, BLACK);
}
display.setCursor(0, drawing.row);
display.println(value.title);
}
static void drawAdjustValues(const AdjustValue &value, AdjustDrawing &drawing) {
char buffer[13];
snprintf(buffer, 13, "%d>%d<%d", value.thrVal, value.value, value.maxVal);
display.setTextSize(1);
display.setCursor(128 - 6 * strlen(buffer), drawing.row);
display.println(buffer);
const int valX = mapConstrain(value.value, value.limitLow, value.limitHigh, 1, 127);
if (drawing.valX != valX) {
display.drawFastVLine(drawing.valX, drawing.row+4, 4, BLACK);
display.drawFastVLine(valX, drawing.row+4, 4, WHITE);
drawing.valX = valX;
}
}
static void drawAdjustFrame(int line) {
display.drawLine(25,line,120,line,WHITE); // Top line
display.drawLine(25,line+12,120,line+12,WHITE); // Bottom line
display.drawLine(25,line+1,25,line+2,WHITE);
display.drawLine(120,line+1,120,line+2,WHITE);
display.drawLine(120,line+10,120,line+11,WHITE);
display.drawLine(25,line+10,25,line+11,WHITE);
}
static void drawAdjustRow(const AdjustValue &value, AdjustDrawing &drawing, bool highlight) {
display.fillRect(0, drawing.row, 128, 21, BLACK);
drawAdjustFrame(drawing.row);
drawAdjustTitle(value, drawing, highlight);
drawAdjustValues(value, drawing);
drawAdjustIndicators(value, drawing);
}

57
NuEVI/src/config.h Normal file
View file

@ -0,0 +1,57 @@
#ifndef __CONFIG_H
#define __CONFIG_H
// Compile options, comment/uncomment to change
#define FIRMWARE_VERSION "0.0.1" // FIRMWARE VERSION NUMBER HERE <<<<<<<<<<<<<<<<<<<<<<<
#define ON_Delay 20 // Set Delay after ON threshold before velocity is checked (wait for tounging peak)
#define CCN_Port 5 // Controller number for portamento level
#define CCN_PortOnOff 65// Controller number for portamento on/off
#define START_NOTE 24 // set startNote to C (change this value in steps of 12 to start in other octaves)
#define FILTER_FREQ 30.0
#define CAP_SENS_ABSOLUTE_MAX 1000 // For inverting capacitive sensors
#define PRESSURE_SENS_MULTIPLIER 10 // Multiply pressure sens so it's not a float
#define CALIBRATE_SAMPLE_COUNT 4
// Statup buttons
#define STARTUP_FACTORY_RESET 0x3
#define STARTUP_CONFIG 0xC
#define TEST_CONFIG 0xA
#define DEBUG_CONFIG 0x1
// Buttons
#define BTN_MENU 0x1
#define BTN_VAL1 0x2
#define BTN_VAL2 0x4
#define BTN_PRESET 0x8
// Send breath CC data no more than every CC_BREATH_INTERVAL
// milliseconds
#define CC_BREATH_INTERVAL 5
#define SLOW_MIDI_ADD 7
#define CC_INTERVAL_PRIMARY 9
#define CC_INTERVAL_PORT 13
#define CC_INTERVAL_OTHER 37
#define LVL_TIMER_INTERVAL 15
#define CVPORTATUNE 2
#define maxSamplesNum 120
#define BREATH_LO_LIMIT 8000
#define BREATH_HI_LIMIT 10000
#define BITE_LO_LIMIT 0
#define BITE_HI_LIMIT 1000
#define PITCHB_LO_LIMIT 0
#define PITCHB_HI_LIMIT 1000
#define EXTRA_LO_LIMIT 0
#define EXTRA_HI_LIMIT 1000
#define CTOUCH_LO_LIMIT 0
#define CTOUCH_HI_LIMIT 1000
#define LEVER_LO_LIMIT 0
#define LEVER_HI_LIMIT 1000
#endif

139
NuEVI/src/globals.h Normal file
View file

@ -0,0 +1,139 @@
#ifndef __GLOBALS_H
#define __GLOBALS_H
#include "wiring.h"
#include <array>
// The three states of our main 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
enum PinkyMode : uint8_t {
PBD = 12,
GLD = 25,
MOD = 26,
QTN = 27,
};
enum FingeringMode : uint8_t {
EVI = 0,
EVR = 1,
TPT = 2,
HRN = 3,
};
enum RollerMode : uint8_t {
HIGHEST = 1,
HIGHEST_EXTEND = 2,
HIGHEST_PAIR = 3,
HIGHEST_PAIR_EXTEND = 4, // Releasing the roller from the highest/lowest moves
PARTIAL = 5,
PARTIAL_EXTEND = 6,
};
enum VibratoMode : uint8_t {
VSTART_DOWN = 0,
VSTART_UP = 1,
};
enum BreathMode : uint8_t {
BREATH_STD = 0,
BREATH_LSB = 1,
BREATH_AT = 2,
BREATH_LSB_AT = 3,
};
enum ExtraControl : uint8_t {
OFF = 0,
VIBRATO = 1,
GLIDE = 2,
CC = 3,
};
enum PolySelect : uint8_t {
EHarmonizerOff = 0,
EDuo = 1,
EChord = 2,
};
enum PortamentoMode : uint8_t {
POFF = 0,
PON = 1,
PSWITCH_ONLY = 2,
PGLIDE_ONLY = 3,
};
struct instrument_state_t {
int mainState; // The state of the main state machine
uint8_t patch; // 1-128
byte activeMIDIchannel = 1; // MIDI channel
byte activeNote = 0; // note playing
byte activePatch = 0;
byte doPatchUpdate = 0;
int8_t transpose = 0;
uint8_t octave = 0;
PolySelect polyMode = PolySelect::EHarmonizerOff;
// Raw sensor signals
int16_t breathSignal = 0; // breath level (smoothed) not mapped to CC value
int16_t breathAltSignal = 0;
int16_t biteSignal = 0; // capacitance data from bite sensor, for midi cc and threshold checks
int16_t leverSignal = 0;
int16_t pbUpSignal = 0;
int16_t pbDnSignal = 0;
int16_t extraSignal = 0;
int16_t vibSignal = 0;
// MIDI values
int breathCCVal = 0;
byte biteVal = 0; // keep track and make sure we send CC with 0 value when off threshold
byte portamentoVal = 0; // keep track and make sure we send CC with 0 value when off threshold
byte extraVal = 0; // keep track and make sure we send CC with 0 value when off threshold
byte leverVal = 0; // keep track and make sure we send CC with 0 value when off threshold
int pitchBend = 8192;
int pbSend = 8192; // Pitch bend actually sent, modified by vibrato, etc
// Key states
byte quarterToneTrigger;
byte pinkyKey = 0;
// CV values
int cvPitch;
int targetPitch;
// Calibration
int16_t breathZero; // this gets auto calibrated in setup
int16_t breathThrVal; // this gets auto calibrated in setup
int16_t breathMaxVal; // this gets auto calibrated in setup
int16_t breathAltZero; // this gets auto calibrated in setup
int16_t breathAltThrVal; // this gets auto calibrated in setup
int16_t breathAltMaxVal; // this gets auto calibrated in setup
int16_t vibThr; // this gets auto calibrated in setup
int16_t vibThrLo;
int16_t vibZero;
int16_t vibZeroBite;
int16_t vibThrBite;
int16_t vibThrBiteLo;
};
extern instrument_state_t state;
extern const std::array<const unsigned short*, 13> curves;
extern const unsigned short curveIn[];
extern unsigned int multiMap(unsigned short val, const unsigned short * _in, const unsigned short * _out, uint8_t size);
#define mapConstrain(val, in_lo, in_hi, out_lo, out_hi) map(constrain(val, in_lo, in_hi), in_lo, in_hi, out_lo, out_hi)
#endif

131
NuEVI/src/hardware.cpp Normal file
View file

@ -0,0 +1,131 @@
#include <Wire.h>
#include "hardware.h"
#define ENCODER_OPTIMIZE_INTERRUPTS
#include <Encoder.h>
#include "menu.h"
#include "config.h"
#include "FilterOnePole.h" // for the breath signal low-pass filtering, from https://github.com/JonHub/Filters
FilterOnePole breathFilter;
FilterOnePole breathAltFilter;
Adafruit_MPR121 touchSensorKeys = Adafruit_MPR121();
Adafruit_MPR121 touchSensorUtil = Adafruit_MPR121();
Adafruit_MPRLS pressureSensorMain = Adafruit_MPRLS();
Adafruit_MPRLS pressureSensorAlt = Adafruit_MPRLS();
byte drawingMemory[numLeds*3]; // 3 bytes per LED
DMAMEM byte displayMemory[numLeds*12]; // 12 bytes per LED
WS2812Serial ledStrip(numLeds, displayMemory, drawingMemory, ledStripPin, WS2812_GRB);
Adafruit_ICM20948 icmSensor;
Adafruit_Sensor *accelSensor;
Encoder knobs[] = {
Encoder(e1aPin, e1bPin),
Encoder(e2aPin, e2bPin),
Encoder(e3aPin, e3bPin),
Encoder(e4aPin, e4bPin),
};
void errorWait();
void initHardware() {
MainI2CBus.setClock(1000000);
AuxI2CBus.setClock(1000000);
pinMode(statusLedPin,OUTPUT); // Teensy onboard LED
// Buttons
pinMode(b1Pin,INPUT_PULLUP);
pinMode(b2Pin,INPUT_PULLUP);
pinMode(b3Pin,INPUT_PULLUP);
pinMode(b4Pin,INPUT_PULLUP);
breathFilter.setFilter(LOWPASS, FILTER_FREQ, 0.0); // create a one pole (RC) lowpass filter
breathAltFilter.setFilter(LOWPASS, FILTER_FREQ, 0.0); // create a one pole (RC) lowpass filter
ledStrip.begin();
if (!touchSensorKeys.begin(KeysI2CAddr, &MainI2CBus)) {
displayError("Keys touch error");
errorWait();
}
if (!touchSensorUtil.begin(UtilI2CAddr, &MainI2CBus)) {
displayError("Roller/Util touch error");
errorWait();
}
if (!pressureSensorMain.begin(MPRLS_DEFAULT_ADDR, &MainI2CBus)) {
displayError("Main pressure sensor error");
errorWait();
}
if (!pressureSensorAlt.begin(MPRLS_DEFAULT_ADDR, &AuxI2CBus)) {
displayError("Alt pressure sensor error");
errorWait();
}
if (!icmSensor.begin_I2C(ICM20948_I2CADDR_DEFAULT, &MainI2CBus)) {
displayError("ICM sensor error");
errorWait();
}
}
/*
Return true if the given button bitmask is pressed
*/
bool checkButtonState(uint8_t mask) {
return buttonState() & mask;
}
/**
* Read the state of the switches (note that they are active low, so we invert the values)
*/
uint8_t buttonState() {
return 0x0f ^(digitalRead(b1Pin)
| (digitalRead(b2Pin) << 1)
| (digitalRead(b3Pin) << 2)
| (digitalRead(b4Pin) << 3));
}
void errorWait() {
while (1) {
if (!digitalRead(b1Pin)) {
_reboot_Teensyduino_(); // reboot to program mode if b1 pressed
}
if (!digitalRead(b4Pin)) {
break; // Continue if b4 pressed
}
delay(10);
}
}
int readKnob(uint8_t n) {
return knobs[n].readAndReset();
}
int readTouchKey(uint8_t n) {
return CAP_SENS_ABSOLUTE_MAX - touchSensorKeys.filteredData(n);
}
int readTouchUtil(uint8_t n) {
return CAP_SENS_ABSOLUTE_MAX - touchSensorUtil.filteredData(n);
}
uint16_t keysTouched() {
return touchSensorKeys.touched();
}
uint16_t utilTouched() {
return touchSensorKeys.touched();
}
int readPressure() {
return breathFilter.input(pressureSensorMain.readPressure()) * PRESSURE_SENS_MULTIPLIER;
}
int readAltPressure() {
return breathAltFilter.input(pressureSensorAlt.readPressure()) * PRESSURE_SENS_MULTIPLIER;
}

96
NuEVI/src/hardware.h Normal file
View file

@ -0,0 +1,96 @@
#ifndef __HARDWARE_H
#define __HARDWARE_H
#include <Adafruit_MPR121.h>
#include <Adafruit_MPRLS.h>
#include <Adafruit_ICM20X.h>
#include <WS2812Serial.h>
#include <Adafruit_ICM20948.h>
#include <stdint.h>
// Hardware sensor definitions
// TODO: remove these
extern WS2812Serial ledStrip;
extern Adafruit_Sensor *accelSensor;
extern Adafruit_ICM20948 icmSensor;
void initHardware();
bool checkButtonState(uint8_t mask); // return true if the given buttons are pressed
uint8_t buttonState(); // return true if the given buttons are pressed
int readKnob(uint8_t n);
int readTouchKey(uint8_t n);
int readTouchUtil(uint8_t n);
uint16_t keysTouched();
uint16_t utilTouched();
int readPressure();
int readAltPressure();
// xEVI hardware setup
// I2C
#define MainI2CBus Wire1
#define AuxI2CBus Wire
#define KeysI2CAddr 0x5B
#define UtilI2CAddr 0x5A
// Digital pins for encoder buttons
#define b1Pin 0
#define b2Pin 2
#define b3Pin 3
#define b4Pin 4
// Digital pins for encoder quadrature
#define e1aPin 5
#define e2aPin 6
#define e3aPin 7
#define e4aPin 8
#define e1bPin 20
#define e2bPin 21
#define e3bPin 22
#define e4bPin 23
// CV pins
#define cvGatePin 9
#define cvPitchPin 10
#define cvBreathPin 11
#define cvBitePin 12
//Output pins for LEDs
#define statusLedPin 13
#define ledStripPin 1
#define numLeds 8
// Key pins
// RH keys
#define K1Pin 0
#define K2Pin 1
#define K3Pin 2
#define K4Pin 3
#define K5Pin 4
#define K6Pin 5
#define K7Pin 6
#define K8Pin 7
// LH keys
#define K9Pin 8
#define K10Pin 9
#define K11Pin 10
#define K12Pin 11
// Octave roller pins
#define R1Pin 0
#define R2Pin 1
#define R3Pin 2
#define R4Pin 3
#define R5Pin 4
#define R6Pin 5
// Additional pins
#define bitePin 6
#define pbUpPin 7
#define pbDnPin 8
#define vibratoPin 9
#endif

49
NuEVI/src/led.cpp Normal file
View file

@ -0,0 +1,49 @@
#include <Arduino.h>
#include "hardware.h"
#include "globals.h"
#include "config.h"
void singleLED(int n, int color) {
}
void ledFullMeter(byte indicatedValue, int color){
}
void ledHalfMeter(int n, byte indicatedValue, int color){
}
void ledQuarterMeter(int n, byte indicatedValue, int color){
}
void statusLedOn() {
digitalWrite(statusLedPin, HIGH);
}
void statusLedOff() {
digitalWrite(statusLedPin, LOW);
}
void statusLed(bool state) {
digitalWrite(statusLedPin, state);
}
void statusLedFlip() {
digitalWrite(statusLedPin, !digitalRead(statusLedPin));
}
void statusLedFlash(uint16_t delayTime) {
statusLedOff();
delay(delayTime/2);
statusLedOn();
delay(delayTime/2);
}
void statusLedBlink() {
statusLedFlash(300);
statusLedFlash(300);
}
void updateSensorLEDs() {
ledHalfMeter(1, state.breathCCVal, 0x00FF00);
ledQuarterMeter(3, state.biteVal, 0x0000FF);
}

13
NuEVI/src/led.h Normal file
View file

@ -0,0 +1,13 @@
#ifndef __LED_H
#define __LED_H
void statusLedOn();
void statusLedOff();
void statusLedFlip();
void statusLed(bool state);
void statusLedFlash(uint16_t delayTime);
void statusLedBlink();
void updateSensorLEDs();
void ledMeter(byte indicatedValue);
#endif

755
NuEVI/src/menu.cpp Normal file
View file

@ -0,0 +1,755 @@
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_MPR121.h>
#include <Arduino.h>
#include <array>
#include <cstdio>
#include "menu.h"
#include "hardware.h"
#include "config.h"
#include "settings.h"
#include "globals.h"
#include "midi.h"
#include "led.h"
// constants
const unsigned long debounceDelay = 30; // the debounce time; increase if the output flickers
const unsigned long buttonRepeatInterval = 50;
const unsigned long buttonRepeatDelay = 400;
const unsigned long cursorBlinkInterval = 300; // the cursor blink toggle interval time
const unsigned long patchViewTimeUp = 2000; // ms until patch view shuts off
const unsigned long menuTimeUp = 60000; // menu shuts off after one minute of button inactivity
static unsigned long menuTime = 0;
static unsigned long patchViewTime = 0;
unsigned long cursorBlinkTime = 0; // the last time the cursor was toggled
std::array<const char *, 128> CC_NAMES = {
"Bank Select", // 0
"Mod Wheel", // 1
"Breath", // 2
"Undefined", // 3
"Foot Pedal", // 4
"Port. Time", // 5
"Data Entry", // 6
"Volume", // 7
"Balance", // 8
"Undefined", // 9
"Pan", // 10
"Expression", // 11
"Effect 1", // 12
"Effect 2", // 13
"Undefined", // 14
"Undefined", // 15
"GP 1", // 16
"GP 2", // 17
"GP 3", // 18
"GP 3", // 19
"Undefined", // 20
"Undefined", // 21
"Undefined", // 22
"Undefined", // 23
"Undefined", // 24
"Undefined", // 25
"Undefined", // 26
"Undefined", // 27
"Undefined", // 28
"Undefined", // 29
"Undefined", // 30
"Undefined", // 31
"LSB 0", // 32
"LSB 1", // 33
"LSB 2", // 34
"LSB 3", // 35
"LSB 4", // 36
"LSB 5", // 37
"LSB 6", // 38
"LSB 7", // 39
"LSB 8", // 40
"LSB 9", // 41
"LSB 10", // 42
"LSB 11", // 43
"LSB 12", // 44
"LSB 13", // 45
"LSB 14", // 46
"LSB 15", // 47
"LSB 16", // 48
"LSB 17", // 49
"LSB 18", // 50
"LSB 19", // 51
"LSB 20", // 52
"LSB 21", // 53
"LSB 22", // 54
"LSB 23", // 55
"LSB 24", // 56
"LSB 25", // 57
"LSB 26", // 58
"LSB 27", // 59
"LSB 28", // 60
"LSB 29", // 61
"LSB 30", // 62
"LSB 31", // 63
"Sustain", // 64
"Portamento", // 65
"Sostenuto", // 66
"Soft Pedal", // 67
"Legato", // 68
"Hold 2", // 69
"Variation", // 70
"Resonance", // 71
"Release", // 72
"Attack", // 73
"Cutoff", // 74
"Sound 6", // 75
"Sound 7", // 76
"Sound 8", // 77
"Sound 9", // 78
"Sound 10", // 79
"Decay", // 80
"Hi Pass", // 81
"GP Button 3", // 82
"GP Button 4", // 83
"Port. Amount", // 84
"Undefined", // 85
"Undefined", // 86
"Undefined", // 87
"Undefined", // 88
"Undefined", // 89
"Undefined", // 90
"Reverb", // 91
"Tremolo", // 92
"Chorus", // 93
"Detune", // 94
"Phaser", // 95
"Data Inc", // 96
"Data Dec", // 97
"NRPN LSB", // 98
"NRPN MSB", // 99
"RP LSB", // 100
"RP MSB", // 101
"Undefined", // 102
"Undefined", // 103
"Undefined", // 104
"Undefined", // 105
"Undefined", // 106
"Undefined", // 107
"Undefined", // 108
"Undefined", // 109
"Undefined", // 110
"Undefined", // 111
"Undefined", // 112
"Undefined", // 113
"Undefined", // 114
"Undefined", // 115
"Undefined", // 116
"Undefined", // 117
"Undefined", // 118
"Undefined", // 119
"All Sound Off", // 120
"All CCC Off", // 121
"Keyboard On", // 122
"All Notes Off", // 123
"Omni Mode Off", // 124
"Omni Mode On", // 125
"Mono", // 126
"Poly Mode", // 127
};
// 'NuEVI' or 'NuRAD' logo
#define LOGO16_GLCD_WIDTH 128
#define LOGO16_GLCD_HEIGHT 64
static const unsigned char PROGMEM nuevi_logo_bmp[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xe3, 0x60, 0x00, 0x07, 0x73, 0x60, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xff, 0xff, 0xe3, 0x60, 0x00, 0x0e, 0xe3, 0x60, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x03, 0x60, 0x00, 0x1d, 0xc3, 0x60, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xbf, 0xff, 0xff, 0xe3, 0x60, 0x00, 0x3b, 0x83, 0x60, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xbf, 0xff, 0xff, 0xe3, 0x60, 0x00, 0x77, 0x03, 0x60, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xb0, 0x00, 0x00, 0x03, 0x60, 0x00, 0xee, 0x03, 0x60, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xb0, 0x00, 0x00, 0x03, 0x60, 0x01, 0xdc, 0x03, 0x60, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xb0, 0x00, 0x00, 0x03, 0x60, 0x03, 0xb8, 0x03, 0x60, 0x00,
0x00, 0x00, 0x00, 0x20, 0x00, 0x01, 0xb0, 0x00, 0x00, 0x03, 0x60, 0x07, 0x70, 0x03, 0x60, 0x00,
0x00, 0x00, 0x00, 0x60, 0x00, 0x01, 0xbf, 0xff, 0xff, 0xe3, 0x60, 0x0e, 0xe0, 0x03, 0x60, 0x00,
0x00, 0x00, 0x00, 0x60, 0x00, 0x01, 0xbf, 0xff, 0xff, 0xe3, 0x60, 0x1d, 0xc0, 0x03, 0x60, 0x00,
0x00, 0x03, 0x00, 0x60, 0x00, 0x01, 0x80, 0x00, 0x00, 0x03, 0x60, 0x3b, 0x80, 0x03, 0x60, 0x00,
0x00, 0x03, 0x00, 0xe0, 0x00, 0x01, 0xbf, 0xff, 0xff, 0xe3, 0x60, 0x77, 0x00, 0x03, 0x60, 0x00,
0x00, 0x03, 0x00, 0xc0, 0x00, 0x01, 0xbf, 0xff, 0xff, 0xe3, 0x60, 0xee, 0x00, 0x03, 0x60, 0x00,
0x00, 0x03, 0x80, 0xc0, 0x00, 0x01, 0xb0, 0x00, 0x00, 0x03, 0x61, 0xdc, 0x00, 0x03, 0x60, 0x00,
0x00, 0x07, 0x80, 0xc0, 0x00, 0x01, 0xb0, 0x00, 0x00, 0x03, 0x63, 0xb8, 0x00, 0x03, 0x60, 0x00,
0x00, 0x07, 0xc0, 0xc0, 0x00, 0x01, 0xb0, 0x00, 0x00, 0x03, 0x67, 0x70, 0x00, 0x03, 0x60, 0x00,
0x00, 0x06, 0xc0, 0xc0, 0x00, 0x01, 0xb0, 0x00, 0x00, 0x03, 0x6e, 0xe0, 0x00, 0x03, 0x60, 0x00,
0x00, 0x06, 0x60, 0xc1, 0x01, 0x01, 0xb0, 0x00, 0x00, 0x03, 0x7d, 0xc0, 0x00, 0x03, 0x60, 0x00,
0x00, 0x06, 0x30, 0xc3, 0x03, 0x01, 0xbf, 0xff, 0xff, 0xe3, 0x7b, 0x80, 0x00, 0x03, 0x60, 0x00,
0x00, 0x0c, 0x30, 0xc3, 0x07, 0x01, 0xbf, 0xff, 0xff, 0xe3, 0x77, 0x00, 0x00, 0x03, 0x60, 0x00,
0x00, 0x0c, 0x1c, 0xc3, 0x06, 0x01, 0x80, 0x00, 0x00, 0x03, 0x0e, 0x00, 0x00, 0x03, 0x60, 0x00,
0x00, 0x0c, 0x0c, 0xc2, 0x0e, 0x01, 0xff, 0xff, 0xff, 0xe3, 0xfc, 0x00, 0x00, 0x03, 0x60, 0x00,
0x00, 0x0c, 0x0e, 0xc6, 0x1e, 0x01, 0xff, 0xff, 0xff, 0xe3, 0xf8, 0x00, 0x00, 0x03, 0x60, 0x00,
0x00, 0x0c, 0x07, 0xc6, 0x1e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0c, 0x03, 0xc6, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0c, 0x01, 0xc7, 0xe6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0c, 0x00, 0xc7, 0xc6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0c, 0x00, 0x03, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
extern void readSwitches(void);
#define OLED_RESET 4
Adafruit_SSD1306 display(128, 64, &Wire1, OLED_RESET,1000000,1000000);
MenuScreen *currentMenu = NULL;
void plotSubOption(const char* label, const char* unit = nullptr) {
int text_x, unit_x;
int label_pixel_width = strlen(label)*12;
if(unit == nullptr) {
text_x = 96 - (label_pixel_width/2);
} else {
int unit_pixel_width = strlen(unit)*6;
int halfSum = (label_pixel_width + unit_pixel_width)/2;
text_x = 96 - halfSum;
unit_x = 96 + halfSum - unit_pixel_width;
display.setCursor(unit_x,40);
display.setTextSize(1);
display.println(unit);
}
display.setTextSize(2);
display.setCursor(text_x,33);
display.println(label);
}
template<size_t N>
static void plotMenuEntries(std::array<MenuScreen, N> entries, size_t cursorPos) {
display.fillRect(0, MENU_HEADER_OFFSET, 63, 64-MENU_HEADER_OFFSET, BLACK);
display.setTextSize(1);
size_t scrollPos = 0;
if (entries.size() >= MENU_NUM_ROWS) {
if ((cursorPos - scrollPos) > (MENU_NUM_ROWS-2) ) {
scrollPos = cursorPos - (MENU_NUM_ROWS-2);
} else if( (cursorPos - scrollPos) < 1) {
scrollPos = cursorPos - 1;
}
scrollPos = constrain(scrollPos, 0, entries.size() - MENU_NUM_ROWS);
}
int row = 0;
int end = constrain(scrollPos + MENU_NUM_ROWS, 0, entries.size());
for (size_t i = scrollPos; i < end; i++, row++) {
int rowPixel = (row)*MENU_ROW_HEIGHT + MENU_HEADER_OFFSET;
display.setCursor(0,rowPixel);
if (cursorPos == i) {
display.setTextColor(BLACK, WHITE);
} else {
display.setTextColor(WHITE);
}
display.println(entries[i].title());
}
}
class AboutMenu : public MenuScreen {
const char *title() {
return "ABOUT";
}
void update(InputState input, bool redraw) {
if (redraw) {
display.clearDisplay();
display.setCursor(49,0);
display.setTextColor(WHITE);
display.setTextSize(0);
display.println("xEVI");
display.setCursor(16,12);
display.print("firmware v.");
display.println(FIRMWARE_VERSION);
display.print("eeprom v.");
display.println(EEPROM_VERSION);
}
}
};
template<size_t N>
class MainMenu : public MenuScreen {
public:
MainMenu(std::array<MenuScreen, N> entries) : _entries(entries) { }
void update(InputState input, bool redraw) {
if (input.changed && input.btnMenu) {
_inSubMenu = !_inSubMenu;
input.changed = false;
redraw = true;
}
if (_inSubMenu) {
_entries[_cursorPos].update(input, redraw);
return;
}
if (input.changed) {
_cursorPos = (_cursorPos + input.knobMenu) % _entries.size();
}
draw(redraw);
};
const char *title() {
return "MENU";
}
private:
void draw(bool redraw) {
if (redraw) {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0,0);
display.drawLine(0, MENU_ROW_HEIGHT, 127, MENU_ROW_HEIGHT, WHITE);
display.println("MENU");
}
plotMenuEntries(_entries, _cursorPos);
}
bool _inSubMenu = false;
size_t _cursorPos;
std::array<MenuScreen, N> _entries;
};
template<size_t N>
class SubMenu : public MenuScreen {
public:
SubMenu(const char *title, std::array<MenuScreen, N> entries) : _title(title), _entries(entries) {}
void update(InputState input, bool redraw) {
bool redrawValue = false;
if (input.changed && input.knobMenu != 0) {
_cursorPos = (_cursorPos + input.knobMenu) % _entries.size();
draw(false);
input.changed = false;
redrawValue = true;
}
if (redraw) {
draw(redraw);
}
_entries[_cursorPos].update(input, redrawValue);
};
const char *title() {
return _title;
}
private:
void draw(bool redraw) {
if (redraw) {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0,0);
display.drawLine(0, MENU_ROW_HEIGHT, 127, MENU_ROW_HEIGHT, WHITE);
display.println(_title);
}
plotMenuEntries(_entries, _cursorPos);
}
const char *_title;
size_t _cursorPos;
std::array<MenuScreen, N> _entries;
};
template<
size_t L,
typename T
>
class ValueMenu : public MenuScreen {
public:
ValueMenu(
const char * title, T &value,
const T min, const T max, const bool wrap = false,
const std::array<const char *, L> labels = {}
) : _title(title), _value(value), _min(min), _max(max), _wrap(wrap), _labels(labels) {}
void update(InputState input, bool redraw) {
if (input.knobMenu) {
_value = (_value + input.knobMenu);
if (_value > _max) {
_value = _min;
} else if (_value < _min) {
_value = _max;
}
draw(redraw);
} else if (redraw) {
draw(redraw);
}
}
private:
T &_value;
const T _min;
const T _max;
const bool _wrap;
const std::array<const char *, L> _labels;
const char *_title;
void draw(bool redraw) {
if (redraw) {
display.fillRect(63,11,64,52,BLACK);
display.drawRect(63,11,64,52,WHITE);
display.setTextSize(1);
int len = strlen(this->_title);
display.setCursor(95-len*3,15);
display.println(this->_title);
}
char buffer[12];
snprintf(buffer, 12, "%+d", _value);
size_t label_idx = _value - _min;
if (_labels.size() > 0 && label_idx >= 0 && label_idx <= _labels.size()) {
plotSubOption(_labels[label_idx], buffer);
} else {
plotSubOption(buffer);
}
}
};
void drawFlash(int x, int y){
display.drawLine(x+5,y,x,y+6,WHITE);
display.drawLine(x,y+6,x+5,y+6,WHITE);
display.drawLine(x,y+12,x+5,y+6,WHITE);
}
void initDisplay() {
// by default, we'll generate the high voltage from the 3.3v line internally! (neat!)
display.begin(SSD1306_SWITCHCAPVCC, 0x3D); // initialize with the I2C addr 0x3D (for the 128x64)
// Show image buffer on the display hardware.
// Since the buffer is intialized with an Adafruit splashscreen
// internally, this will display the splashscreen.
display.clearDisplay();
#if defined(NURAD)
display.drawBitmap(0,0,nurad_logo_bmp,LOGO16_GLCD_WIDTH,LOGO16_GLCD_HEIGHT,1);
#else
display.drawBitmap(0,0,nuevi_logo_bmp,LOGO16_GLCD_WIDTH,LOGO16_GLCD_HEIGHT,1);
#endif
display.display();
}
void displayError(const char *error) {
display.clearDisplay();
display.setTextColor(WHITE);
display.setTextSize(1);
display.setCursor(0,0);
display.println(error);
display.display();
Serial.print("ERROR: ");
Serial.println(error);
}
void showVersion() {
display.setTextColor(WHITE);
display.setTextSize(1);
display.setCursor(85,52);
display.print("v.");
display.println(FIRMWARE_VERSION);
display.display();
}
static void clearSub(){
display.fillRect(63,11,64,52,BLACK);
}
static void clearSubValue() {
display.fillRect(65, 24, 60, 37, BLACK);
}
//***********************************************************
const MenuScreen breathModeMenu = ValueMenu<4, BreathMode>("BREATH MODE", currentPreset->breathMode, 0, 1, true, {{ "STANDARD", "LSB", "AT", "LSB_AT" }});
const MenuScreen breathCCMenu = ValueMenu<128, uint8_t>("BREATH CC", currentPreset->breathCC, 0, 127, true, CC_NAMES);
const MenuScreen velocityMenu = ValueMenu<1, uint8_t>("VELOCITY", currentPreset->fixedVelocity, 0, 127, true, {{ "DYN" }});
const MenuScreen curveMenu = ValueMenu<0, uint8_t>("CURVE", currentPreset->breathCurve, 0, 12); // TODO: curve display
const MenuScreen velSmpDlMenu = ValueMenu<1, uint8_t>("VEL DELAY", currentPreset->velSmpDl, 0, 30, true, { "OFF" }); // TODO: unit ms
const MenuScreen velBiasMenu = ValueMenu<1, uint8_t>("VEL BOOST", currentPreset->velBias, 0, 30, true, { "OFF" });
const MenuScreen breathIntervalMenu = ValueMenu<0, uint8_t>("BR INTERV", currentPreset->breathInterval, 0, 30, true);
const MenuScreen trill3Menu = ValueMenu<0, int8_t>("TRILL3", currentPreset->trill3_interval, 3, 4, true, {});
const MenuScreen cvTuneMenu = ValueMenu<0, int8_t>("CV Tune", currentPreset->cvTune, -100, 100, false, {});
const MenuScreen cvVibMenu = ValueMenu<0, uint8_t>("CV Vib LFO", currentPreset->cvVibRate, 0, 8, false, {});
const MenuScreen cvScaleMenu = ValueMenu<0, int8_t>("CV SCALING", currentPreset->cvScale, -100, 100, false, {});
const std::array<const char *, 25> transposeLabels = {
"C>", "C#>", "D>", "D#>", "E>", "F>", "F#>", "G>", "G#>", "A>", "Bb>", "B>",
">C<", "<C#", "<D", "<D#", "<E", "<F", "<F#", "<G", "<G#", "<A", "<Bb", "<B", "<C"
};
const MenuScreen transposeMenu = ValueMenu<25, int8_t>("TRANSPOSE", state.transpose, -12, 12, true, transposeLabels);
const MenuScreen octaveMenu = ValueMenu<0, uint8_t>("OCTAVE", state.octave, -3, 3, true, {});
const MenuScreen midiMenu = ValueMenu<0, byte>("MIDI CH", currentPreset->MIDIchannel, 1, 16, false, {});
const MenuScreen vibDepthMenu = ValueMenu<1, uint8_t>("DEPTH", currentPreset->vibratoDepth, 0, 9, true, {"OFF"});
const MenuScreen vibRetnMenu = ValueMenu<1, uint8_t>("RETURN", currentPreset->vibRetn, 0, 4, true, {"OFF"});
const MenuScreen vibSenseMenu = ValueMenu<0, uint8_t>("SENSE LVR", currentPreset->vibSens, 0, 12);
const MenuScreen vibSquelchMenu = ValueMenu<0, uint8_t>("SQUELCH LVR", currentPreset->vibSquelch, 0, 12);
const MenuScreen vibDirMenu = ValueMenu<2, VibratoMode>("DIRECTION", currentPreset->vibratoMode, VSTART_DOWN, VSTART_UP, true, { "START DOWN", "START UP"});
const MenuScreen biteCtlMenu = ValueMenu<4, ExtraControl>("BITE CTL", currentPreset->biteControl, 0, 3, true, {{
"OFF",
"VIBRATO",
"GLIDE",
"CC"
}});
const MenuScreen biteCCMenu = ValueMenu<128, uint8_t>("BITE CC", currentPreset->biteCC, 0, 127, true, CC_NAMES);
const MenuScreen leverCtlMenu = ValueMenu<4, ExtraControl>("LEVER CTL", currentPreset->leverControl, 0, 3, true, {
"OFF",
"VIBRATO",
"GLIDE",
"CC"
});
const MenuScreen leverCCMenu = ValueMenu<128, uint8_t>("LEVER CC", currentPreset->leverCC, 0, 127, true, CC_NAMES);
const MenuScreen portMenu = ValueMenu<4, PortamentoMode>("GLIDE MOD", currentPreset->portamentoMode, 0, 3, true, {
"OFF",
"ON",
"SWITCH_ONLY",
"GLIDE_ONLY",
});
const MenuScreen portLimitMenu = ValueMenu<0, uint8_t>("GLIDE LMT", currentPreset->portamentoLimit, 1, 127, true);
const MenuScreen pitchBendMenu = ValueMenu<0, uint8_t>("PITCHBEND", currentPreset->PBdepth, 0, 12, true);
const MenuScreen extraCtlMenu = ValueMenu<4, ExtraControl>("EXCT CC A", currentPreset->extraControl, 0,4, true, {
"OFF",
"ON",
"SWITCH_ONLY",
"GLIDE_ONLY",
});
const MenuScreen extraCCMenu = ValueMenu<128, uint8_t>("EXCT CC", currentPreset->extraCC, 0,4, true, CC_NAMES);
const MenuScreen deglitchMenu = ValueMenu<1, uint8_t>("DEGLITCH", currentPreset->deglitch, 0, 70, true, {"OFF"});
const MenuScreen pinkyMenu = ValueMenu<29, uint8_t>("PINKY KEY", currentPreset->pinkySetting, 0, 29, true, {
"-12", "-11", "-10", "-9", "-8", "-7", "-6", "-5", "-4", "-3", "-2", "-1",
"PB/2",
"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12",
"PBD", "GLD", "MOD", "QTN"
});
const MenuScreen fingeringMenu = ValueMenu<4, FingeringMode>("FINGERING", currentPreset->fingering, 0, 3, true, {
"EVI",
"EVR",
"TPT",
"HRN",
});
const MenuScreen rollerMenu = ValueMenu<6, RollerMode>("ROLLRMODE", currentPreset->rollerMode, 1, 6, true, {
"HIGHEST",
"HIGHEST_EXTEND",
"HIGHEST_PAIR",
"HIGHEST_PAIR_EXTEND",
"PARTIAL",
"PARTIAL_EXTEND",
});
std::array<MenuScreen, 7> breathMenuEntries = {
breathModeMenu,
breathCCMenu,
velocityMenu,
curveMenu,
velSmpDlMenu,
velBiasMenu,
breathIntervalMenu,
};
const MenuScreen breathMenu = SubMenu<7>("BREATH SETUP", breathMenuEntries);
const std::array<MenuScreen, 13> controlMenuEntries = {
fingeringMenu,
rollerMenu,
biteCtlMenu,
biteCCMenu,
leverCtlMenu,
leverCCMenu,
extraCtlMenu,
extraCCMenu,
portMenu,
portLimitMenu,
deglitchMenu,
pinkyMenu,
pitchBendMenu
};
const MenuScreen controlMenu = SubMenu<13>("CONTROL SETUP", controlMenuEntries);
const std::array<MenuScreen, 5> vibratoMenuEntries = {
vibDepthMenu,
vibRetnMenu,
vibDirMenu,
vibSenseMenu,
vibSquelchMenu,
};
const MenuScreen vibratoMenu = SubMenu<5>("VIBRATO", vibratoMenuEntries);
const MenuScreen aboutMenu = AboutMenu();
std::array <MenuScreen, 4> extrasMenuEntries = {
trill3Menu,
cvTuneMenu,
cvScaleMenu,
cvVibMenu,
};
const MenuScreen extrasMenu = SubMenu<4>("EXTRAS", extrasMenuEntries);
// Top-level screens
const std::array<MenuScreen, 9> mainMenuEntries = {
transposeMenu,
octaveMenu,
midiMenu,
breathMenu,
controlMenu,
vibratoMenu,
adjustMenu,
extrasMenu,
aboutMenu,
};
const MenuScreen mainMenuPage = MainMenu<9>(mainMenuEntries);
// const MenuScreen patchPage
// const MenuScreen presetPage
// const MenuScreen ccPage
static void curveCustomDraw() {
const char* curveMenuLabels[] = {"-4", "-3", "-2", "-1", "LIN", "+1", "+2",
"+3", "+4", "S1", "S2", "Z1", "Z2" };
int y0 = 0, x0 = 0;
int scale = ((1<<14)-1)/60;
for(int x = x0; x < 60; x+=1) {
int y = multiMap(x*scale, curveIn, curves[currentPreset->breathCurve], 17);
y = (y*37) / ((1<<14)-1);
display.drawLine(x0 + 65, 60 - y0, x + 65, 60 - y, WHITE);
x0 = x; y0 = y;
}
display.setCursor(125 - 3*6, 60-8 );
display.setTextSize(0);
display.print(curveMenuLabels[currentPreset->breathCurve]);
}
static bool updateSensorPixelsFlag = false;
void drawSensorPixels() {
updateSensorPixelsFlag = true;
}
//***********************************************************
static InputState readInput(uint32_t timeNow) {
static uint32_t lastDebounceTime = 0; // the last time the output pin was toggled
static uint32_t buttonRepeatTime = 0;
static uint32_t buttonPressedTime = 0;
static uint8_t lastDeumButtons = 0;
static uint8_t deumButtonState = 0;
static int lastKnobs[] = {0, 0, 0, 0};
InputState input;
uint8_t deumButtons = buttonState();
// check to see if you just pressed the button
// (i.e. the input went from LOW to HIGH), and you've waited long enough
// since the last press to ignore any noise:
// If the switch changed, due to noise or pressing:
if (deumButtons != lastDeumButtons) {
// reset the debouncing timer
lastDebounceTime = timeNow;
}
if ((timeNow - 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 the button state has changed:
if (deumButtons != deumButtonState) {
// keys.current = deumButtons;
input.btnMenu = deumButtons & BTN_MENU;
input.btnVal1 = deumButtons & BTN_VAL1;
input.btnVal2 = deumButtons & BTN_VAL2;
input.btnPreset = deumButtons & BTN_PRESET;
input.changed = true;
deumButtonState = deumButtons;
menuTime = timeNow;
buttonPressedTime = timeNow;
}
}
for (int i = 0; i < 4; i++) {
int val = readKnob(i);
if (val != lastKnobs[i]) {
input.changed = 1;
switch (i) {
case 0:
input.knobMenu = val;
break;
case 1:
input.knobVal1 = val;
break;
case 2:
input.knobVal2 = val;
break;
case 3:
input.knobPreset = val;
break;
}
}
}
// save the reading. Next time through the loop, it'll be the lastButtonState:
lastDeumButtons = deumButtons;
return input;
}
void handleMenu(bool draw) {
unsigned long timeNow = millis();
InputState input = readInput(timeNow);
// shut off menu system if not used for a while (changes not stored by exiting a setting manually will not be stored in EEPROM)
if (currentMenu && ((timeNow - menuTime) > menuTimeUp)) {
display.ssd1306_command(SSD1306_DISPLAYOFF);
display.clearDisplay();
currentMenu = NULL;
}
if (currentMenu && (draw || input.changed)) {
currentMenu->update(input, false);
}
}

44
NuEVI/src/menu.h Normal file
View file

@ -0,0 +1,44 @@
#ifndef __MENU_H
#define __MENU_H
#include "wiring.h"
#define MENU_ROW_HEIGHT 9
#define MENU_HEADER_OFFSET 12
#define MENU_NUM_ROWS 6
#define ADJUST_NUM_ROWS 3
#define ADJUST_ROW_HEIGHT 21
extern const unsigned long debounceDelay; // the debounce time; increase if the output flickers
extern const unsigned long buttonRepeatInterval;
extern const unsigned long buttonRepeatDelay;
extern const unsigned long cursorBlinkInterval; // the cursor blink toggle interval time
extern const unsigned long patchViewTimeUp; // ms until patch view shuts off
extern const unsigned long menuTimeUp; // menu shuts off after one minute of button inactivity
struct InputState {
bool changed = false;
bool btnMenu = false;
bool btnVal1 = false;
bool btnVal2 = false;
bool btnPreset = false;
int knobMenu = 0;
int knobVal1 = 0;
int knobVal2 = 0;
int knobPreset = 0;
};
struct MenuScreen {
MenuScreen() {};
virtual const char *title() { return ""; };
virtual void update(InputState input, bool redraw) {};
virtual ~MenuScreen() {};
};
extern const MenuScreen adjustMenu;
void initDisplay();
void showVersion();
void displayError(const char *error);
void handleMenu(bool draw);
#endif

232
NuEVI/src/midi.cpp Normal file
View file

@ -0,0 +1,232 @@
#include <Arduino.h>
#include "midi.h"
#include "hardware.h"
#include "globals.h"
int midiChannel;
void midiSetChannel(uint8_t channel) {
midiChannel = constrain(channel, 1, 16);
}
byte midiGetChannel() {
return midiChannel;
}
void midiSendProgramChange(uint8_t patch) {
usbMIDI.sendProgramChange(patch-1, midiChannel);
dinMIDIsendProgramChange(patch-1, midiChannel-1);
}
void midiSendControlChange(uint8_t ccParam, uint8_t ccValue) {
usbMIDI.sendControlChange(ccParam, ccValue, midiChannel);
dinMIDIsendControlChange(ccParam, ccValue, midiChannel - 1);
}
void midiSendNoteOn(uint8_t note, uint8_t velocity) {
usbMIDI.sendNoteOn(note, velocity, midiChannel);
dinMIDIsendNoteOn(note, velocity, midiChannel - 1);
}
void midiSendNoteOff(uint8_t note) {
//Always send velocity 0 on note off to avoid confusing some synthesizers
usbMIDI.sendNoteOn(note, 0, midiChannel);
dinMIDIsendNoteOn(note, 0, midiChannel - 1);
}
void midiSendAfterTouch(uint8_t value) {
usbMIDI.sendAfterTouch(value, midiChannel);
dinMIDIsendAfterTouch(value, midiChannel - 1);
}
void midiSendPitchBend(uint16_t value) {
#if (TEENSYDUINO >= 141)
usbMIDI.sendPitchBend(value-8192, midiChannel); // newer teensyduino "pitchBend-8192" older just "pitchBend"... strange thing to change
#else
usbMIDI.sendPitchBend(value, midiChannel);
#endif
dinMIDIsendPitchBend(value, midiChannel - 1);
}
void midiDiscardInput()
{
while (usbMIDI.read()) {
// read & ignore incoming messages
}
}
void midiReset() { // reset controllers
midiSendControlChange(7, 100);
midiSendControlChange(11, 127);
}
void midiPanic() { // all notes off
midiSendControlChange(123, 0);
for (int i = 0; i < 128; i++){
midiSendNoteOff(i);
delay(2);
}
}
void midiInitialize(uint8_t channel) {
/*
MIDI_SERIAL.begin(31250); // start serial with midi baudrate 31250
MIDI_SERIAL.flush();
if(widiJumper){
WIDI_SERIAL.begin(31250); // start serial with midi baudrate 31250
WIDI_SERIAL.flush();
}
*/
midiSetChannel(channel);
}
//Serial midi functions
// Send a three byte din midi message
void midiSend3B(uint8_t midistatus, uint8_t data1, uint8_t data2) {
/*
MIDI_SERIAL.write(midistatus);
MIDI_SERIAL.write(data1);
MIDI_SERIAL.write(data2);
if (widiJumper && widiOn){
WIDI_SERIAL.write(midistatus);
WIDI_SERIAL.write(data1);
WIDI_SERIAL.write(data2);
}
*/
}
//**************************************************************
// Send a two byte din midi message
void midiSend2B(uint8_t midistatus, uint8_t data) {
/*
MIDI_SERIAL.write(midistatus);
MIDI_SERIAL.write(data);
if (widiJumper && widiOn){
WIDI_SERIAL.write(midistatus);
WIDI_SERIAL.write(data);
}
*/
}
//**************************************************************
// Send din pitchbend
void dinMIDIsendPitchBend(uint16_t pb, uint8_t ch) {
int pitchLSB = pb & 0x007F;
int pitchMSB = (pb >>7) & 0x007F;
midiSend3B((0xE0 | ch), pitchLSB, pitchMSB);
}
//**************************************************************
// Send din control change
void dinMIDIsendControlChange(uint8_t ccNumber, uint8_t cc, uint8_t ch) {
midiSend3B((0xB0 | ch), ccNumber, cc);
}
//**************************************************************
// Send din note on
void dinMIDIsendNoteOn(uint8_t note, uint8_t vel, uint8_t ch) {
midiSend3B((0x90 | ch), note, vel);
}
//**************************************************************
// Send din note off
void dinMIDIsendNoteOff(uint8_t note, uint8_t vel, uint8_t ch) {
midiSend3B((0x80 | ch), note, vel);
}
//**************************************************************
// Send din aftertouch
void dinMIDIsendAfterTouch(uint8_t value, uint8_t ch) {
midiSend2B((0xD0 | ch), value);
}
//**************************************************************
// Send din program change
void dinMIDIsendProgramChange(uint8_t value, uint8_t ch) {
midiSend2B((0xC0 | ch), value);
}
// Send sysex commands to wireless module
void dinMIDIsendSysex(const uint8_t data[], const uint8_t length) {
//MIDI_SERIAL.write(0xF0); //Sysex command
for(int i=0; i<length; ++i) {
//MIDI_SERIAL.write(data[i]);
}
//MIDI_SERIAL.write(0xF7); //Sysex end
}
void sendWLPower(const uint8_t level) {
uint8_t buf[6] = {
0x00, 0x21, 0x11, //Manufacturer id
0x02, //TX02
0x02, //Set power level
0x00 //Power level value (0-3)
};
if(level>3) return; //Don't send invalid values
buf[5] = level;
dinMIDIsendSysex(buf, 6);
}
void sendWLChannel(const uint8_t channel) {
uint8_t buf[6] = {
0x00, 0x21, 0x11, //Manufacturer id
0x02, //TX02
0x05, //Set channel
0x04 //Channel value (4-80)
};
if(channel<4 || channel>80) return; //Don't send invalid values
buf[5] = channel;
dinMIDIsendSysex(buf, 6);
}
//Translate between "midi data" (only use 7 LSB per byte, big endian) and "teensy data" (little endian)
//Only 14 LSB of int value are used (2MSB are discarded), so only works for unsigned data 0-16383
//NOTE: This assumes code is running on a little-endian CPU, both for real device (Teensy) and simulator.
uint16_t convertToMidiValue(const uint16_t realdata) {
return (realdata & 0x3F80) >>7 | (realdata & 0x007F) <<8;
}
uint16_t convertFromMidiValue(const uint16_t mididata) {
return (mididata & 0x7F00) >> 8 | (mididata & 0x007F) <<7 ;
}
//Read from a memory location, such as MIDI receive buffer
uint16_t convertFromMidiValue(const uint8_t* mididata) {
uint8_t msb = *mididata;
uint8_t lsb = *(mididata+1);
return (msb & 0x007F) <<7 | (lsb & 0x007F);
}
//This is a bit different. MSB of each byte is just discarded (instead of discarding MSB for whole value). Just used for CRC (easier to compare)
uint32_t convertToMidiCRC(const uint32_t realdata) {
uint8_t* p = (uint8_t*)&realdata;
uint32_t r=0;
for(int i=0; i<4; ++i) {
r = r<<8 | (p[i] & 0x7F);
}
return r;
}

46
NuEVI/src/midi.h Normal file
View file

@ -0,0 +1,46 @@
#ifndef __MIDI_H
#define __MIDI_H
//This is a completely made up "European" SysEx manufacturer ID.
static const char sysex_id[] = { 0x00, 0x3e, 0x7f };
//Enable use of USB and serial MIDI
#define USE_MIDI_USB
#define USE_MIDI_SERIAL
//Set / get current midi channel
void midiSetChannel(uint8_t channel);
uint8_t midiGetChannel();
void midiSendProgramChange(uint8_t patch);
void midiSendControlChange(uint8_t ccParam, uint8_t ccValue);
void midiSendNoteOn(uint8_t note, uint8_t velocity);
void midiSendNoteOff(uint8_t note);
void midiSendAfterTouch(uint8_t value);
void midiSendPitchBend(uint16_t value);
void midiDiscardInput(void);
void midiReset(); // reset controllers
void midiPanic(); // turn all notes off
void midiInitialize(uint8_t channel=1);
void dinMIDIsendControlChange(uint8_t ccNumber, uint8_t cc, uint8_t ch);
void dinMIDIsendNoteOn(uint8_t note, uint8_t vel, uint8_t ch);
void dinMIDIsendNoteOff(uint8_t note, uint8_t vel, uint8_t ch);
void dinMIDIsendAfterTouch(uint8_t value, uint8_t ch);
void dinMIDIsendProgramChange(uint8_t value, uint8_t ch);
void dinMIDIsendPitchBend(uint16_t pb, uint8_t ch);
void dinMIDIsendSysex(const uint8_t data[], const uint8_t length);
void sendWLPower(const uint8_t level);
void sendWLChannel(const uint8_t channel);
//Convert things between "regular data" and MIDI data (byte order and 7-bits-per-byte)
uint16_t convertToMidiValue(const uint16_t realdata);
uint16_t convertFromMidiValue(const uint16_t mididata);
uint16_t convertFromMidiValue(const uint8_t* mididata);
uint32_t convertToMidiCRC(const uint32_t realdata);
#endif

16
NuEVI/src/name.c Normal file
View file

@ -0,0 +1,16 @@
// To give your project a unique name, this code must be
// placed into a .c file (its own tab). It can not be in
// a .cpp file or your main sketch (the .ino file).
#include "usb_names.h"
#define MIDI_NAME {'x','E','V','I',' ','M','I','D','I'}
#define MIDI_NAME_LEN 9
// Do not change this part. This exact format is required by USB.
struct usb_string_descriptor_struct usb_string_product_name = {
2 + MIDI_NAME_LEN * 2,
3,
MIDI_NAME
};

342
NuEVI/src/settings.cpp Normal file
View file

@ -0,0 +1,342 @@
#include <Arduino.h>
#include <EEPROM.h>
#include <Adafruit_SSD1306.h>
#include "settings.h"
#include "globals.h"
#include "menu.h"
#include "hardware.h"
#include "config.h"
#include "midi.h"
#include "led.h"
//Read and write EEPROM data
void writeInt(const uint16_t address, const uint16_t value) {
union {
uint8_t v[2];
uint16_t val;
} data;
data.val = value;
EEPROM.update(address, data.v[0]);
EEPROM.update(address+1, data.v[1]);
}
uint16_t readInt(uint16_t address) {
union {
uint8_t v[2];
uint16_t val;
} data;
data.v[0] = EEPROM.read(address);
data.v[1] = EEPROM.read(address+1);
return data.val;
}
void writeCalibration() {
EEPROM.put(SETTINGS_OFFSET, calibration);
}
void readCalibration() {
EEPROM.get(SETTINGS_OFFSET, calibration);
}
void writePreset(uint8_t preset) {
EEPROM.put(SETTINGS_OFFSET + preset * sizeof(preset_t), presets[preset]);
}
void writePresets() {
EEPROM.put(SETTINGS_OFFSET + CALIBRATION_MAX_SIZE, presets);
}
void readPresets() {
EEPROM.get(SETTINGS_OFFSET + CALIBRATION_MAX_SIZE, presets);
}
//Functions to send and receive config (and other things) via USB MIDI SysEx messages
uint32_t crc32(const uint8_t *message, const size_t length) {
size_t pos=0;
uint32_t crc=0xFFFFFFFF;
while (pos<length) {
crc ^= message[pos++]; //Get next byte and increment position
for (uint8_t j=0; j<8; ++j) { //Mask off 8 next bits
crc = (crc >> 1) ^ (0xEDB88320 & -(crc & 1));
}
}
return ~crc;
}
/*
Send EEPROM config dump as sysex message. Message format is structured like this:
+------------------------------------------------------------------------------------+
| vendor(3) | "NuEVIc01" (8) | Payload size (2) | EEPROM data (variable) | crc32 (4) |
+------------------------------------------------------------------------------------+
Payload size is for the EEPROM data chunk (not including anything else before or after
CRC32 covers the entire buffer up to and including the eeprom data (but not the checksum itself)
This currently operates under the assumption that the whole EEPROM chunk only consists of unsigned 16 bit ints, only using the range 0-16383
*/
void sendSysexSettings() {
const char *header = "NuEVIc01"; //NuEVI config dump 01
//Build a send buffer of all the things
size_t sysex_size = 3 + strlen(header) + 2 + EEPROM_SIZE + 4;
uint8_t *sysex_data = (uint8_t*)malloc(sysex_size);
//Positions (offsets) of parts in send buffer
int header_pos = 3;
int size_pos = header_pos + strlen(header);
int payload_pos = size_pos + 2;
int checksum_pos = payload_pos + EEPROM_SIZE;
//SysEX manufacturer ID
memcpy(sysex_data, sysex_id, 3);
//Header with command code
memcpy(sysex_data+header_pos, header, strlen(header));
//Payload length
*(uint16_t*)(sysex_data+size_pos) = convertToMidiValue(EEPROM_SIZE);
//Config data
uint16_t* config_buffer_start = (uint16_t*)(sysex_data+payload_pos);
//Read one settings item at a time, change data format, and put in send buffer
readPresets();
uint16_t *preset_buffer = (uint16_t*)presets;
for(uint16_t idx=0; idx<EEPROM_SIZE/2; idx++) {
uint16_t eepromval = preset_buffer[idx];
config_buffer_start[idx] = convertToMidiValue(eepromval);
}
uint32_t checksum = crc32(sysex_data, checksum_pos);
*(uint32_t*)(sysex_data+checksum_pos) = convertToMidiCRC(checksum);
usbMIDI.sendSysEx(sysex_size, sysex_data);
free(sysex_data);
}
//Send a simple 3-byte message code as sysex
void sendSysexMessage(const char* messageCode) {
char sysexMessage[] = "vvvNuEVIccc"; //Placeholders for vendor and code
memcpy(sysexMessage, sysex_id, 3);
memcpy(sysexMessage+8, messageCode, 3);
usbMIDI.sendSysEx(11, (const uint8_t *)sysexMessage);
}
bool receiveSysexSettings(const uint8_t* data, const uint16_t length) {
//Expected size of data (vendor+NuEVIc02+len+payload+crc32)
uint16_t expected_size = 3 + 8 + 2 + EEPROM_SIZE + 4;
//Positions (offsets) of parts in buffer
int size_pos = 11;
int payload_pos = size_pos + 2;
int checksum_pos = payload_pos + EEPROM_SIZE;
//Make sure length of receive buffer is enough to read all we need to. We can accept extra junk at the end though.
if(length<expected_size) {
configShowMessage("Invalid config format");
return false;
}
//No need to verify vendor or header/command, already done before we get here.
//Calculate checksum of stuff received (everything before checksum), transform to midi format
//(being a one-way operation, we can't do the reverse anyway)
uint32_t crc=convertToMidiCRC(crc32(data, checksum_pos));
uint32_t crc_rcv;
memcpy(&crc_rcv, data+checksum_pos, 4);
if(crc != crc_rcv && crc_rcv != NO_CHECKSUM) {
configShowMessage("Invalid checksum");
return false;
}
//Verify that payload size matches the size of our EEPROM config
uint16_t payload_size = convertFromMidiValue(data+size_pos);
if(payload_size != EEPROM_SIZE) {
configShowMessage("Invalid config size");
return false;
}
uint16_t eeprom_version_rcv = convertFromMidiValue(data+(payload_pos+EEPROM_VERSION_ADDR));
if(eeprom_version_rcv != EEPROM_VERSION) {
configShowMessage("Invalid config version");
return false;
}
//Grab all the items in payload and save to EEPROM
uint16_t *preset_buffer = (uint16_t*)presets;
for(uint16_t i=0; i<payload_size/2; i++) {
uint16_t addr = i*2;
uint16_t val;
preset_buffer[addr] = convertFromMidiValue(data+(payload_pos+addr));
}
writePresets();
//All went well
return true;
}
//Send EEPROM and firmware versions
void sendSysexVersion() {
char sysexMessage[] = "vvvNuEVIc04eevvvvvvvv"; //Placeholders for vendor and code
uint8_t fwStrLen = min(strlen(FIRMWARE_VERSION), 8); //Limit firmware version string to 8 bytes
memcpy(sysexMessage, sysex_id, 3);
memcpy(sysexMessage+13, FIRMWARE_VERSION, fwStrLen);
*(uint16_t*)(sysexMessage+11) = convertToMidiValue(EEPROM_VERSION);
uint8_t message_length = 13+fwStrLen;
usbMIDI.sendSysEx(message_length, (const uint8_t *)sysexMessage);
}
extern Adafruit_SSD1306 display;
void configShowMessage(const char* message) {
display.fillRect(0,32,128,64,BLACK);
display.setCursor(0,32);
display.setTextColor(WHITE);
display.print(message);
display.display();
}
uint8_t* sysex_rcv_buffer = NULL;
uint16_t sysex_buf_size = 0;
void handleSysexChunk(const uint8_t *data, uint16_t length, bool last) {
uint16_t pos;
if(!sysex_rcv_buffer) {
//Start out with an empty buffer
pos = 0;
sysex_buf_size = length;
sysex_rcv_buffer = (uint8_t *)malloc(sysex_buf_size);
} else {
//Increase size of current buffer
pos = sysex_buf_size;
sysex_buf_size += length;
sysex_rcv_buffer = (uint8_t *)realloc(sysex_rcv_buffer, sysex_buf_size);
}
//Append this chunk to buffer
memcpy(sysex_rcv_buffer + pos, data, length);
//If it's the last one, call the regular handler to process it
if(last) {
handleSysex(sysex_rcv_buffer, sysex_buf_size);
//Discard the buffer
free(sysex_rcv_buffer);
sysex_rcv_buffer = NULL;
sysex_buf_size = 0;
}
}
void handleSysex(uint8_t *data, unsigned int length) {
//Note: Sysex data as received here contains sysex start and end markers (0xF0 and 0xF7)
//Too short to even contain a 3-byte vendor id is not for us.
if(length<4) return;
//Verify vendor
if(strncmp((char*)(data+1), sysex_id, 3)) return; //Silently ignore different vendor id
//Verify header. Min length is 3+5+3 bytes (vendor+header+message code)
if(length<12 || strncmp((char*)(data+4), "NuEVI", 5)) {
configShowMessage("Invalid message.");
sendSysexMessage("e00");
return;
}
//Get message code
char messageCode[3];
strncpy(messageCode, (char*)(data+9), 3);
if(!strncmp(messageCode, "c00", 3)) { //Config dump request
configShowMessage("Sending config...");
sendSysexSettings();
configShowMessage("Config sent.");
} else if(!strncmp(messageCode, "c03", 3)) { //Version info request
configShowMessage("Sending version.");
sendSysexVersion();
} else if(!strncmp(messageCode, "c02", 3)) { //New config incoming
configShowMessage("Receiving config...");
//Tell receiveSysexSettings about what's between sysex start and end markers
if(receiveSysexSettings(data+1, length-2)) configShowMessage("New config saved.");
} else {
configShowMessage("Unknown message.");
sendSysexMessage("e01"); //Unimplemented message code
}
}
void configModeSetup() {
statusLedFlash(500);
display.clearDisplay();
display.setCursor(0,0);
display.setTextColor(WHITE);
display.setTextSize(0);
display.println("Config mgmt");
display.println("Power off NuEVI");
display.println("to exit");
display.display();
usbMIDI.setHandleSystemExclusive(handleSysexChunk);
statusLedFlash(500);
sendSysexVersion(); //Friendly hello
configShowMessage("Ready.");
}
//"Main loop". Just sits and wait for midi messages and lets the sysex handler do all the work.
void configModeLoop() {
usbMIDI.read();
}
//Read settings from eeprom. Returns wether or not anything was written (due to factory reset or upgrade)
void readEEPROM(const bool factoryReset) {
// if stored settings are not for current version, or Enter+Menu are pressed at startup, they are replaced by factory settings
uint16_t settings_version = readInt(EEPROM_VERSION_ADDR);
// blank eeprom will be 0xFFFF. For a full reset, call it "version 0" so everything gets overwritten.
if (factoryReset || settings_version == 0xffffu) {
settings_version = 0;
} else {
readPresets();
readCalibration();
}
if(settings_version != EEPROM_VERSION) {
// Add default settings here
writePresets();
writeCalibration();
writeInt(EEPROM_VERSION_ADDR, EEPROM_VERSION);
}
}

123
NuEVI/src/settings.h Normal file
View file

@ -0,0 +1,123 @@
#ifndef __SETTINGS_H
#define __SETTINGS_H
#include <stdint.h>
#include "globals.h"
#define EEPROM_VERSION 1
#define EEPROM_VERSION_ADDR 0
#define SETTINGS_OFFSET 2
#define PRESET_MAX_SIZE 128 // Leave extra space for future settings
#define PRESET_COUNT 8
#define CALIBRATION_MAX_SIZE 54 // Leave extra space for future settings
#define EEPROM_SIZE 1080 // Cannot exceed this amount of EEPROM space
static_assert(SETTINGS_OFFSET + PRESET_MAX_SIZE * PRESET_COUNT + CALIBRATION_MAX_SIZE <= EEPROM_SIZE,
"Not enough EEPROM");
/**
* Sensor calibration is global across presets
*/
struct calibration_t {
int16_t breathThrValOffset = 5;
int16_t breathMaxValOffset = 1500;
int16_t breathAltThrValOffset = 5;
int16_t breathAltMaxValOffset = 1500;
int16_t biteThrVal = 50;
int16_t biteMaxVal = 150;
int16_t pbDnThrVal = 50;
int16_t pbDnMaxVal = 150;
int16_t pbUpThrVal = 50;
int16_t pbUpMaxVal = 150;
int16_t leverThrVal = 50;
int16_t leverMaxVal = 150;
int16_t extraThrVal = 50;
int16_t extraMaxVal = 150;
int16_t ctouchThrVal = 80;
uint8_t _reserved[24];
};
static_assert(sizeof(calibration_t) == CALIBRATION_MAX_SIZE, "calibration data wrong size");
/**
* Per-preset config
*/
struct preset_t {
uint8_t MIDIchannel = 1; // Midi channel to send on
uint8_t breathCC = 2; // breath CC selection
uint8_t altBreathCC = 70; // breath CC selection
uint8_t extraCC = 1; // extra CC selection
uint8_t leverCC = 7; // "lever" CC selection
uint8_t biteCC = 11; // bite CC selection
uint8_t fixedVelocity = 0; // Zero = not fixed
uint8_t portamentoLimit = 127; // 1-127 - max portamento level
uint8_t PBdepth = 1; // OFF:1-12 divider
uint8_t deglitch = 20; // debounes time for key/roller inputs
uint8_t breathCurve = 4; // breath curve selection
uint8_t velSmpDl = 20; // velocity sample delay
uint8_t velBias = 0; // velocity bias
uint8_t pinkySetting = 12; // 0 - 11 (QuickTranspose -12 to -1), 12 (pb/2), 13 - 24 (QuickTranspose +1 to +12), 25 (EC2), 26 (ECSW), 27 (LVL), 28 (LVLP)
uint8_t breathInterval; // 3-15
int8_t trill3_interval = 4;
uint8_t vibSquelch = 12; // vibrato signal squelch
uint8_t cvVibRate = 0; // OFF, 1 - 8 CV extra controller LFO vibrato rate 4.5Hz to 8Hz
int8_t cvTune = 0;
int8_t cvScale = 0;
PortamentoMode portamentoMode;
BreathMode breathMode = BreathMode::BREATH_LSB_AT;
FingeringMode fingering = FingeringMode::EVI;
RollerMode rollerMode = RollerMode::HIGHEST;
ExtraControl biteControl = ExtraControl::GLIDE;
ExtraControl leverControl = ExtraControl::VIBRATO;
ExtraControl extraControl = ExtraControl::CC;
VibratoMode vibratoMode = VSTART_DOWN; // direction of first vibrato wave UPWD or DNWD
uint8_t vibratoDepth = 1; // OFF:1-9
uint8_t vibSens = 2; // vibrato sensitivity
uint8_t vibRetn = 2; // vibrato return speed
uint8_t knob1CC = 71;
uint8_t knob2CC = 72;
uint8_t knob3CC = 73;
uint8_t knob4CC = 74;
uint8_t icmAccelMode;
uint8_t icmAccelCC;
uint8_t icmTiltMode;
uint8_t icmTiltCC;
uint8_t icmRotationMode;
uint8_t icmRotationCC;
uint8_t _reserved[87];
};
static_assert(sizeof(preset_t) == PRESET_MAX_SIZE, "preset_t must be 128 bytes");
extern preset_t presets[PRESET_COUNT];
extern calibration_t calibration;
extern preset_t *currentPreset;
#define NO_CHECKSUM 0x7F007F00
void readEEPROM(const bool factoryReset);
void writePreset(uint8_t preset);
void writeCalibration();
//Functions for config management mode
void sendSysexSettings();
void sendSysexMessage(const char* messageCode);
void sendSysexVersion();
void handleSysex(uint8_t *data, unsigned int length);
void handleSysexChunk(const uint8_t *data, uint16_t length, bool last);
uint32_t crc32(const uint8_t *message, const size_t length);
void configInitScreen();
void configShowMessage(const char* message);
void configModeSetup();
void configModeLoop();
#endif

57
NuEVI/src/test.cpp Normal file
View file

@ -0,0 +1,57 @@
#include "hardware.h"
uint8_t oldButtons = 0;
uint16_t oldKeys = 0;
uint16_t oldUtil = 0;
bool plotCap = false;
void handleTestMode() {
uint8_t buttons = buttonState();
if (buttons != oldButtons) {
oldButtons = buttons;
Serial.print("Buttons:");
Serial.println(buttons, HEX);
}
for (int i = 0; i < 4; i++) {
int k = readKnob(i);
if (k != 0) {
Serial.print("Knob");
Serial.print(i);
Serial.print(":");
Serial.println(k);
}
}
uint16_t keys = keysTouched();
if (keys != oldKeys) {
Serial.print("Keys:");
Serial.println(keys, HEX);
}
uint16_t util = utilTouched();
if (util != oldUtil) {
Serial.print("Util:");
Serial.println(util, HEX);
}
if (buttons == 0x01) {
plotCap = !plotCap;
}
if (plotCap) {
for (int i = 0; i < 12; i++) {
Serial.print(">key");
Serial.print(i);
Serial.print(":");
Serial.println(readTouchKey(i));
}
for (int i = 0; i < 12; i++) {
Serial.print(">util");
Serial.print(i);
Serial.print(":");
Serial.println(readTouchUtil(i));
}
}
}

6
NuEVI/src/test.h Normal file
View file

@ -0,0 +1,6 @@
#ifndef __TEST_H
#define __TEST_H
void handleTestMode();
#endif

823
NuEVI/src/xEVI.cpp Normal file
View 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);
}
}