Hi, sorry I don't know how to use GIT, so here is code I've written
that you can incorporate and I can help you test out results if you
need.
**********
// ------------------------------------------------------------------------------------------------------------------------------------------------------------
// START MICROTONAL FUNCTIONS
// these amounts should get recalculated once whenever the tuning
system (number of tones, transposition,
// key note, or tuning of A4) changes. i do not know how that works
with menus and such so i simply
// defined these as functions to return the correct value given the
necessary variables as inputs.
//
// Disclosure -- i'm NOT an experienced C programmer, so i don't know
how to handle low level
// memory and pointer concerns. please feel free to edit my code as
much as needed for it to work.
//
#if ModelNumber == 1
const byte overflowBtn[5] = {99,128,119,138,139};
#elif ModelNumber == 2
const byte overflowBtn[5] = {90,121,110,131,130};
#endif
// general interval shift expressed as a vector, also called a "Monzo"
// http://en.xen.wiki/w/Monzo
// only working through the 11 limit, so the structure is defined as:
// {+/-oct, +/-oct&fifth, +/-2oct&maj3, +/-2oct&flat7, +/-3oct&harmonic11}
// common regular intervals can all be defined in this way.
// folks playing music in 12-tone can get by with just the first 2 or 3 factors.
// note that there's usually a bunch of octave adjustments just cuz of
how the math works.
// example:
// up by a perfect fifth = {-1,1,0,0,0}
// up by a pure minor 3rd = {1,1,-1,0,0}
// down a whole step = {3,-2,0,0,0}
// up by a harmonic 7th = {-2,0,0,1,0}
// up by a harmonic 11th = {-3,0,0,0,1}
// up by a "pythagorean" sharp/flat = {-11,7,0,0,0} (7 steps on the
circle of fifths)
// up by a minor 2nd = {8,-5,0,0,0} (5 steps backwards on circle of
fifths -- same in 12 but not in others)
struct monzo {
int pwr2;
int pwr3;
int pwr5;
int pwr7;
int pwr11;
};
// what you need to define a layout: 1) where's the root, 2) what's
the pattern, 3) which way is up?
struct layoutDef {
bool isPortrait;
byte rootHex;
struct monzo acrossInterval;
struct monzo dnLeftInterval;
};
// Wicki-hayden; place C one to the left of center, across is a whole
step, DL is down a fifth
struct layoutDef layoutWickiHayden = {1, 64, {-3,2,0,0,0}, {1,-1,0,0,0}};
// 5-limit harmonic table has pure 5th and 3rds. DR is down a fifth,
UL is up a min3
struct layoutDef layoutHarmonicTable = {0, 75, {1,-1,0,0,0}, {1,1,-1,0,0}};
// generalized Bosanquet steps are semitones DL, and #/b's across
struct layoutDef layoutBosanquet = {0, 65, {11,-7,0,0,0}, {-8,5,0,0,0}};
// assuming intention was to create nice major and minor chords
struct layoutDef layoutGerhard = {0, 65, {-3,-1,2,0,0}, {-1,-1,1,0,0}};
// these were designed for 12EDO only, might get weird results in
different tunings
struct layoutDef layoutAccordionC = {1, 75, {-1,2,0,0,0}, {0,-1,1,0,0}};
struct layoutDef layoutAccordionB = {1, 64, {-7,3,1,0,0}, {0,-1,1,0,0}};
// MTS tuning format is three bytes with 7 significant binary digits
each (0-127)
struct MIDItuning {
byte mByte[3];
};
// these arrays store the tuning and mapping of the selected layout
// they can broadly replace layout[] and pitches[] respectively.
int currentLayoutSteps[elementCount]; // given hex number, return EDO steps
int currentLayoutButtonCode[elementCount]; // given hex number, return
button code
float currentPitchMap[128]; // given button code, return MIDI pitch
float currentFreqMap[128]; // given button code, return buzzer
frequency -- if you change Tone.cpp to take frequency as "float" it
should still work.
// assume that the GEM menu passes along:
// layout selection (struct)
// EDO tuning (integer)
// concert pitch (A4)
// key root (in EDO steps away from A4)
// transpose (also in EDO steps, only difference is that transpose
does NOT affect color of hexes)
// run this any time one of these four changes via option or otherwise
//
void makeLayout(struct layoutDef selectedLayout, int EDO, float
concertPitch, int keyspanRoot, int transpose) {
// step 1 -- calculate number of EDO steps for each hex
// output should be an array of each hex with number of EDOsteps away
from the root
// every down-left movement, in a new row. "tens digit" and shift
where there are zero across effects
int rootNoteRow = selectedLayout.rootHex / 10; // if root on hex 65, row 6
int rootNoteCol = selectedLayout.rootHex % 10; // if root on hex 65, col 5
int rowShift;
int colShift;
int rootNoteDiagonal; // depending on what row you iterate, the
column on the same diagonal will change
for (byte r = 0; r < rowCount; r++) {
rowShift = r - rootNoteRow;
rootNoteDiagonal = rootNoteCol + (rowShift + 1 - (rowShift % 2)) / 2;
for (byte c = 0; c < columnCount; c++) {
colShift = c - rootNoteDiagonal;
currentLayoutSteps[10*r+c] = stepsFromMonzo(
{selectedLayout.acrossInterval.pwr2 * colShift +
selectedLayout.dnLeftInterval.pwr2 * rowShift,
selectedLayout.acrossInterval.pwr3 * colShift +
selectedLayout.dnLeftInterval.pwr3 * rowShift,
selectedLayout.acrossInterval.pwr5 * colShift +
selectedLayout.dnLeftInterval.pwr5 * rowShift,
selectedLayout.acrossInterval.pwr7 * colShift +
selectedLayout.dnLeftInterval.pwr7 * rowShift,
selectedLayout.acrossInterval.pwr11* colShift +
selectedLayout.dnLeftInterval.pwr11* rowShift}
,EDO);
};
};
// TBD: if model number 1, flip it
if (ModelNumber == 1) {
int temp;
for (byte r = 0; r < rowCount; r++) {
for (byte c = 0; c < columnCount/2; c++) {
temp = currentLayoutSteps[10*r+c];
currentLayoutSteps[10*r+c] = currentLayoutSteps[10*r+(10-c)];
currentLayoutSteps[10*r+(10-c)] = temp;
};
};
};
// step 2 -- map layout (0 to 139) to button code (0 to 127, UNUSED, CMDB_x)
// enumerate the hexes 0 thru 127 to make the bulk tuning dump and
// ensure the map between hex and re-tuned note is correct for this layout
// basic logic -- go through the hexes and assign a new value if there isn't
// already a hex with the same number of EDO steps calculated.
// if you run out of hexes you are assigned 128 thru 132 which are the
UNUSED keys
// i don't know a faster algorithm for this, it runs at I think ~O(N^2)
// fortunately, at N=128, should be manageable
//
// also performs step 3 and 4 -- calculate the MIDI pitch and frequency for each
// unique pitch. when done in-line with the population of the button layout,
// it saves having to do a lookup search later.
//
int order[elementCount-7];
byte x = 0;
for (byte i = 0; i < elementCount-7; i++) {
if (i != overflowBtn[0] && i != overflowBtn[1] && i != overflowBtn[2]
&& i != overflowBtn[3] && i != overflowBtn[4] && i != cmdBtn1 &&
i != cmdBtn2
&& i != cmdBtn3 && i != cmdBtn4 && i != cmdBtn5 && i != cmdBtn6
&& i != cmdBtn7) {
order[x] = i;
x++;
};
};
for (byte i = 0; i < 5; i++) {
order[128+i] = overflowBtn[i];
};
currentLayoutButtonCode[order[0]] = 0;
currentPitchMap[0] = stepsToMIDI(concertPitch, EDO,
currentLayoutSteps[order[0]] + keyspanRoot + transpose);
currentFreqMap[0] = MIDItoFreq(currentPitchMap[0]);
x = 1;
bool uniquePitch;
for (byte i = 1; i < elementCount-7; i++) {
uniquePitch = 1;
for (byte j = 0; j < i; j++) {
if (currentLayoutSteps[order[i]] == currentLayoutSteps[order[j]]) {
currentLayoutButtonCode[order[i]] = currentLayoutButtonCode[order[j]];
uniquePitch = 0;
break;
};
};
if (uniquePitch) {
if (x > 127) {
currentLayoutButtonCode[order[i]] = UNUSED;
} else {
currentLayoutButtonCode[order[i]] = x;
currentPitchMap[x] = stepsToMIDI(concertPitch, EDO,
currentLayoutSteps[order[i]] + keyspanRoot + transpose);
currentFreqMap[x] = MIDItoFreq(currentPitchMap[x]);
x++;
};
};
};
currentLayoutButtonCode[cmdBtn1] = CMDB_1;
currentLayoutButtonCode[cmdBtn2] = CMDB_2;
currentLayoutButtonCode[cmdBtn3] = CMDB_3;
currentLayoutButtonCode[cmdBtn4] = CMDB_4;
currentLayoutButtonCode[cmdBtn5] = CMDB_5;
currentLayoutButtonCode[cmdBtn6] = CMDB_6;
currentLayoutButtonCode[cmdBtn7] = CMDB_7;
// step 5, construct and send the MTS Bulk Tuning message to MIDI synth
// String MTSmessage = ""; // TBD, not sure how to send
// for (byte i = 0; i < 128; i++) {
// MTSmessage = MTSmessage + formatPitchForMTS(currentPitchMap[i]);
// };
// this whole thing needs an expert's touch!
}
struct MIDItuning formatPitchForMTS(float pitch) {
struct MIDItuning x;
// byte 1 should be the pitch rounded down to a semitone
x.mByte[0] = floor(pitch);
// byte 2 should be the fractional part rounded down to the 2^7th (128th)
// byte 3 should be the remaining fractional part rounded down to
the 2^14th (16384th)
// byte 2's precision is important; byte 3's is not, so allowing
errors to accumulate there is OK
float microTone = ldexp(pitch-x.mByte[0], 7);
x.mByte[1] = floor(microTone);
// i do not know the bitwise function to ensure that the bytes are
capped at 127 but we could add that here
x.mByte[2] = ldexp(microTone-x.mByte[1], 7);
return x;
};
// implementation of Kite Giedraitis's notation guide for EDOs 5 thru 72.
// https://tallkite.com/misc_files/notation%20guide%20for%20edos%205-72.pdf
//
// i would use this function to populate the choices of key center
// perhaps in a routine with a loop like
// for (i = 0; i < EDO, i++) {
// keyOffset = i;
// keyName = noteSpelling(EDO, i);
// }
//
String noteSpelling(int EDO, int stepsFromA) {
// start by spelling the number of steps to get the "normal" notes
around a circle of fifths
// going fifth-ward = D A E B F# C# G# D# A# -- ignore the ones
afterwards: E# B# Fx etc.
// going fourth-ward = D G C F Bb Eb Ab Db Gb -- ignore the ones
afterwards: Cb Fb Bbb etc,
// the preferred spellings start at D, so that there is a minimum of
#s and bs.
String fifthwardNames[8] = {"A", "E", "B", "F#", "C#", "G#", "D#", "A#"};
String fourthwardNames[8] = {"G", "C", "F", "Bb", "Eb", "Ab", "Db", "Gb"};
int fifth = stepsFromMonzo({-1,1,0,0,0}, EDO);
int fifthwardKeyspans[8];
int fourthwardKeyspans[8];
// calculate number of steps to get to each normal note
// the input is the # of steps from A since that note is constant
regardless of EDO.
// hence Keyspan[0] is 0/-2 away, [1] is 1/-3 away, etc.
for (int i = 0; i < 8; i++) {
fifthwardKeyspans[i] = nearMod(i * fifth, EDO);
fourthwardKeyspans[i] = nearMod((i+2) * -fifth, EDO);
}
// find which note is closest to the desired one
int bestMatch = 0;
int closestDistance = nearMod(stepsFromA - fifth, EDO);
for (int i = 0; i < 8; i++) {
int distanceFifthward = nearMod(stepsFromA - fifthwardKeyspans[i], EDO);
if (abs(distanceFifthward) < abs(closestDistance)) {
bestMatch = i;
closestDistance = distanceFifthward;
}
int distanceFourthward = nearMod(stepsFromA - fourthwardKeyspans[i], EDO);
if (abs(distanceFourthward) < abs(closestDistance)) {
bestMatch = -i;
closestDistance = distanceFourthward;
}
}
// negative index = fourthward; positive = fifthward
String baseNote = "D";
if (bestMatch < 0) {baseNote = fourthwardNames[-bestMatch];};
if (bestMatch > 0) {baseNote = fifthwardNames[ bestMatch];};
// negative distance = need to add down arrows; positive = up arrows
if (closestDistance != 0) {
String arrow;
if (closestDistance > 0) {
arrow = "^";
} else {
arrow = "v";
}
for (int i = 0; i < abs(closestDistance); i++) {
baseNote = arrow + baseNote;
}
}
return baseNote;
};
// MOD function that returns in the range of [-b/2...+b/2]
int nearMod(int a, int b) {return (((a % b) + b/2) % b) - b/2;};
// calculate the number of steps to get from A4 to C4, so you can identify
// the pitch of the starting hex of the layout which I think is typically
// going to be defined as middle C
int stepsFromA440ToMiddleC(int EDO) {
return stepsFromMonzo({4,-3,0,0,0},EDO);
}
int stepsFromMonzo(struct monzo m, int EDO) {
return harmonicToEDOstep(2, EDO) * m.pwr2
+ harmonicToEDOstep(3, EDO) * m.pwr3
+ harmonicToEDOstep(5, EDO) * m.pwr5
+ harmonicToEDOstep(7, EDO) * m.pwr7
+ harmonicToEDOstep(11, EDO) * m.pwr11;
}
// pre-load values of log2(x)
// 3 1.584962501 ~ 25968 / 16384
// 5 2.321928095 ~ 38042 / 16384
// 7 2.807354922 ~ 45996 / 16384
// 11 3.459431619 ~ 56679 / 16384
int harmonicToEDOstep(int H, int EDO) {
if (H == 2) {return EDO;}
int logTwo;
switch (H) {
case 3: logTwo = 25968; break;
case 5: logTwo = 38042; break;
case 7: logTwo = 45996; break;
case 11: logTwo = 56679; break;
};
return round(ldexp(EDO*logTwo,-14));
}
// by default, A4 is 440 Hz. if you want you could let the user change
this in a menu.
// i.e. float userTuningA4 = 432.0;
// given EDO (# of tones = divisions of the octave), and given # of
steps away from A4,
// return the MIDI note associated
float stepsToMIDI(float concertA, int EDO, int stepsFromA) {
return freqToMIDI(concertA) + 12.0 * (float)stepsFromA / (float)EDO;
}
// formula to convert from MIDI note to Hz and vice versa
float freqToMIDI(float Hz) {return 69.0 + 12.0 * log2f(Hz / 440.0);};
float MIDItoFreq(float MIDI) {return 440.0 * exp2((MIDI - 69.0)/12.0);};
// END MICROTONAL FUNCTIONS
// ------------------------------------------------------------------------------------------------------------------------------------------------------------