~earboxer/HexBoard

Microtonal functions

Details
Message ID
<CAJrt5V235LKACyed_9whA7VpAO0oRYaxB8PvMabMEeK1DFFghg@mail.gmail.com>
DKIM signature
pass
Download raw message
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
// ------------------------------------------------------------------------------------------------------------------------------------------------------------
Reply to thread Export thread (mbox)