diff --git a/fingering.h b/fingering.h index 1e2de78..e8feab9 100644 --- a/fingering.h +++ b/fingering.h @@ -1,15 +1,22 @@ #pragma once -#include "notes.h" +#include "tuning.h" -#define CCCC NOTE_CS5, NOTE_CS5, NOTE_CS5, NOTE_CS5 -#define CCDD NOTE_CS5, NOTE_CS5, NOTE_D5, NOTE_D5 -#define CDCD NOTE_CS5, NOTE_D5, NOTE_CS5, NOTE_D5 -#define DDDD NOTE_D5, NOTE_D5, NOTE_D5, NOTE_D5 -#define P 0x80 +struct Fingering { + Note note; + bool alt; // Alternate fingering: sounds more choked +}; -uint8_t uilleann_matrix[] = { +#define n(note) {note, false} +#define P(note) {note, true} + +#define CCCC n(NOTE_CS5), n(NOTE_CS5), n(NOTE_CS5), n(NOTE_CS5) +#define CCDD n(NOTE_CS5), n(NOTE_CS5), n(NOTE_D5), n(NOTE_D5) +#define CDCD n(NOTE_CS5), n(NOTE_D5), n(NOTE_CS5), n(NOTE_D5) +#define DDDD n(NOTE_D5), n(NOTE_D5), n(NOTE_D5), n(NOTE_D5) + +struct Fingering uilleann_matrix[] = { // Open Back D - NOTE_CS5, NOTE_CS5, NOTE_CS5, NOTE_D5, // OOO OO.. + n(NOTE_CS5), n(NOTE_CS5), n(NOTE_CS5), n(NOTE_D5), // OOO OO.. CCDD, // OOO OX.. CDCD, // OOO XO.. DDDD, // OOO XX.. @@ -39,40 +46,40 @@ uint8_t uilleann_matrix[] = { DDDD, // XXO XX.. DDDD, // XXX OO.. DDDD, // XXX OX.. - NOTE_D5, NOTE_D5, NOTE_D5, NOTE_D5|P, // XXX XO.. + n(NOTE_D5), n(NOTE_D5), n(NOTE_D5), P(NOTE_D5), // XXX XO.. DDDD, // XXX XX.. // Closed Back D CCCC, // OOO OO... - NOTE_CS5, NOTE_CS5, NOTE_CS5, NOTE_CS5|P, // OOO OX.. + n(NOTE_CS5), n(NOTE_CS5), n(NOTE_CS5), P(NOTE_CS5), // OOO OX.. CCCC, // OOO XO.. CCCC, // OOO XX.. CCCC, // OOX OO.. - NOTE_CS5, NOTE_CS5|P, NOTE_CS5, NOTE_CS5|P, // OOX OX.. + n(NOTE_CS5), P(NOTE_CS5), n(NOTE_CS5), P(NOTE_CS5), // OOX OX.. CCCC, // OOX XO.. CCCC, // OOX XX.. CCCC, // OXO OO.. - NOTE_CS5, NOTE_CS5|P, NOTE_CS5, NOTE_CS5|P, // OXO OX.. + n(NOTE_CS5), P(NOTE_CS5), n(NOTE_CS5), P(NOTE_CS5), // OXO OX.. CCCC, // OXO XO.. CCCC, // OXO XX.. - NOTE_C5|P, NOTE_C5|P, NOTE_C5|P, NOTE_C5|P, // OXX OO.. - NOTE_C5, NOTE_C5, NOTE_C5, NOTE_C5, // OXX OX.. - NOTE_C5, NOTE_C5, NOTE_C5, NOTE_C5|P, // OXX XO.. - NOTE_C5, NOTE_C5, NOTE_C5, NOTE_CS5, // OXX XX.. - NOTE_B4, NOTE_B4, NOTE_B4, NOTE_B4, // XOO OO.. - NOTE_B4|P, NOTE_B4|P, NOTE_B4, NOTE_B4|P, // XOO OX.. - NOTE_AS4, NOTE_B4, NOTE_AS4, NOTE_B4, // XOO XO.. - NOTE_B4, NOTE_B4, NOTE_B4, NOTE_B4, // XOO XX.. - NOTE_B4|P, NOTE_B4|P, NOTE_B4|P, NOTE_B4|P, // XOX OO.. - NOTE_B4|P, NOTE_B4|P, NOTE_B4|P, NOTE_B4|P, // XOX OX.. - NOTE_B4|P, NOTE_B4|P, NOTE_B4|P, NOTE_B4|P, // XOX XO.. - NOTE_B4|P, NOTE_B4|P, NOTE_B4|P, NOTE_B4|P, // XOX XX.. - NOTE_A4, NOTE_A4, NOTE_A4|P, NOTE_A4, // XXO OO.. - NOTE_A4|P, NOTE_A4|P, NOTE_A4|P, NOTE_A4|P, // XXO OX.. - NOTE_GS4, NOTE_GS4|P, NOTE_A4, NOTE_A4, // XXO XO.. - NOTE_A4|P, NOTE_A4|P, NOTE_A4|P, NOTE_A4, // XXO XX.. - NOTE_G4, NOTE_G4, NOTE_G4|P, NOTE_G4, // XXX OO.. - NOTE_G4|P, NOTE_G4|P, NOTE_G4|P, NOTE_G4|P, // XXX OX.. - NOTE_FS4, NOTE_FS4, NOTE_F4, NOTE_FS4|P, // XXX XO.. - NOTE_E4, NOTE_E4|P, NOTE_DS4, NOTE_D4, // XXX XX.. + P(NOTE_C5), P(NOTE_C5), P(NOTE_C5), P(NOTE_C5), // OXX OO.. + n(NOTE_C5), n(NOTE_C5), n(NOTE_C5), n(NOTE_C5), // OXX OX.. + n(NOTE_C5), n(NOTE_C5), n(NOTE_C5), P(NOTE_C5), // OXX XO.. + n(NOTE_C5), n(NOTE_C5), n(NOTE_C5), n(NOTE_CS5), // OXX XX.. + n(NOTE_B4), n(NOTE_B4), n(NOTE_B4), n(NOTE_B4), // XOO OO.. + P(NOTE_B4), P(NOTE_B4), n(NOTE_B4), P(NOTE_B4), // XOO OX.. + n(NOTE_AS4), n(NOTE_B4), n(NOTE_AS4), n(NOTE_B4), // XOO XO.. + n(NOTE_B4), n(NOTE_B4), n(NOTE_B4), n(NOTE_B4), // XOO XX.. + P(NOTE_B4), P(NOTE_B4), P(NOTE_B4), P(NOTE_B4), // XOX OO.. + P(NOTE_B4), P(NOTE_B4), P(NOTE_B4), P(NOTE_B4), // XOX OX.. + P(NOTE_B4), P(NOTE_B4), P(NOTE_B4), P(NOTE_B4), // XOX XO.. + P(NOTE_B4), P(NOTE_B4), P(NOTE_B4), P(NOTE_B4), // XOX XX.. + n(NOTE_A4), n(NOTE_A4), P(NOTE_A4), n(NOTE_A4), // XXO OO.. + P(NOTE_A4), P(NOTE_A4), P(NOTE_A4), P(NOTE_A4), // XXO OX.. + n(NOTE_GS4), P(NOTE_GS4), n(NOTE_A4), n(NOTE_A4), // XXO XO.. + P(NOTE_A4), P(NOTE_A4), P(NOTE_A4), n(NOTE_A4), // XXO XX.. + n(NOTE_G4), n(NOTE_G4), P(NOTE_G4), n(NOTE_G4), // XXX OO.. + P(NOTE_G4), P(NOTE_G4), P(NOTE_G4), P(NOTE_G4), // XXX OX.. + n(NOTE_FS4), n(NOTE_FS4), n(NOTE_F4), P(NOTE_FS4), // XXX XO.. + n(NOTE_E4), P(NOTE_E4), n(NOTE_DS4), n(NOTE_D4), // XXX XX.. }; diff --git a/notes.cpp b/notes.cpp deleted file mode 100644 index e562074..0000000 --- a/notes.cpp +++ /dev/null @@ -1,41 +0,0 @@ -#include -#include "notes.h" - -const char *NoteNames[] { - "C ", "C#", "D ", "Eb", "E ", "F ", "F#", "G ", "Ab", "A ", "Bb", "B ", -}; -float JustPitches[MaxNote + 1]; - -// Hat tip to Kyle Gann -// https://www.kylegann.com/tuning.html -void setupJustPitches(uint8_t baseNote, float basePitch) { - JustPitches[baseNote + 0] = basePitch * 1 / 1; // D - JustPitches[baseNote + 1] = basePitch * 16 / 15; // Eb - JustPitches[baseNote + 2] = basePitch * 9 / 8; // E - JustPitches[baseNote + 3] = basePitch * 6 / 5; // F - JustPitches[baseNote + 4] = basePitch * 5 / 4; // F# - JustPitches[baseNote + 5] = basePitch * 4 / 3; // G - JustPitches[baseNote + 6] = basePitch * 45 / 32; // Ab - JustPitches[baseNote + 7] = basePitch * 3 / 2; // A - JustPitches[baseNote + 8] = basePitch * 8 / 5; // Bb - JustPitches[baseNote + 9] = basePitch * 5 / 3; // B - JustPitches[baseNote + 10] = basePitch * 16 / 9; // C (fourth up from G) - JustPitches[baseNote + 11] = basePitch * 15 / 8; // C# - - // Octaves - for (int note = baseNote; note < baseNote + 12; note++) { - for (int i = 1; i < 9; i++) { - int multiplier = 1<= 0) { - JustPitches[dnNote] = JustPitches[note] / multiplier; - } - } - } -} diff --git a/notes.h b/notes.h deleted file mode 100644 index d8b4435..0000000 --- a/notes.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#define PITCH_D4 293.66 - -enum Notes { - NOTE_C0, NOTE_CS0, NOTE_D0, NOTE_DS0, NOTE_E0, NOTE_F0, NOTE_FS0, NOTE_G0, NOTE_GS0, NOTE_A0, NOTE_AS0, NOTE_B0, - NOTE_C1, NOTE_CS1, NOTE_D1, NOTE_DS1, NOTE_E1, NOTE_F1, NOTE_FS1, NOTE_G1, NOTE_GS1, NOTE_A1, NOTE_AS1, NOTE_B1, - NOTE_C2, NOTE_CS2, NOTE_D2, NOTE_DS2, NOTE_E2, NOTE_F2, NOTE_FS2, NOTE_G2, NOTE_GS2, NOTE_A2, NOTE_AS2, NOTE_B2, - NOTE_C3, NOTE_CS3, NOTE_D3, NOTE_DS3, NOTE_E3, NOTE_F3, NOTE_FS3, NOTE_G3, NOTE_GS3, NOTE_A3, NOTE_AS3, NOTE_B3, - NOTE_C4, NOTE_CS4, NOTE_D4, NOTE_DS4, NOTE_E4, NOTE_F4, NOTE_FS4, NOTE_G4, NOTE_GS4, NOTE_A4, NOTE_AS4, NOTE_B4, - NOTE_C5, NOTE_CS5, NOTE_D5, NOTE_DS5, NOTE_E5, NOTE_F5, NOTE_FS5, NOTE_G5, NOTE_GS5, NOTE_A5, NOTE_AS5, NOTE_B5, - NOTE_C6, NOTE_CS6, NOTE_D6, NOTE_DS6, NOTE_E6, NOTE_F6, NOTE_FS6, NOTE_G6, NOTE_GS6, NOTE_A6, NOTE_AS6, NOTE_B6, - NOTE_C7, NOTE_CS7, NOTE_D7, NOTE_DS7, NOTE_E7, NOTE_F7, NOTE_FS7, NOTE_G7, NOTE_GS7, NOTE_A7, NOTE_AS7, NOTE_B7, - NOTE_C8, NOTE_CS8, NOTE_D8, NOTE_DS8, NOTE_E8, NOTE_F8, NOTE_FS8, NOTE_G8, NOTE_GS8, NOTE_A8, NOTE_AS8, NOTE_B8, -}; -const uint8_t MaxNote = NOTE_B8; - -extern const char *NoteNames[]; -extern float JustPitches[MaxNote + 1]; - -void setupJustPitches(uint8_t baseNote, float basePitch); diff --git a/pipe.cpp b/pipe.cpp index 12dc309..67a93d4 100644 --- a/pipe.cpp +++ b/pipe.cpp @@ -1,4 +1,5 @@ #include "pipe.h" +#include "tuning.h" #include "fingering.h" // Kludge time: this is something I did just to make my breadboard look nicer. @@ -9,6 +10,7 @@ #define GLISSANDO_STEPS (OPENVAL - CLOSEDVAL) Pipe::Pipe() { + keysLast = 0; } bool Pipe::Init() { @@ -44,6 +46,7 @@ void Pipe::Update() { // 0x6c is actually 8 bytes, but all 8 are always the same... paj7620ReadReg(0x6c, 1, &kneeClosedness); + keysLast = keys; keys = 0; glissandoKeys = 0; for (int i=0; i<8; i++) { @@ -63,22 +66,33 @@ void Pipe::Update() { } // Look up notes in the big table - note = uilleann_matrix[keys]; - glissandoNote = uilleann_matrix[glissandoKeys]; + struct Fingering f = uilleann_matrix[keys]; + struct Fingering gf = uilleann_matrix[glissandoKeys]; + + note = f.note; + glissandoNote = gf.note; // Was the high bit set? That indicates "alternate fingering", which sounds different. - altFingering = (note & 0x80); - - note &= 0x7f; - glissandoNote &= 0x7f; + altFingering = f.alt; // If the bag is squished, jump up an octave // But only if the left thumb is down! if (bag && (keys & bit(7))) { - note += 12; - glissandoNote += 12; + note += NOTE_OCTAVE; + glissandoNote += NOTE_OCTAVE; } // All keys closed + knee = no sound silent = ((kneeClosedness > 240) && (keys == 0xff)); } + +bool Pipe::Pressed(uint8_t key) { + return bitRead(keys, key); +} + +bool Pipe::JustPressed(uint8_t key) { + if (bitRead(keys, key)) { + return !bitRead(keysLast, key); + } + return false; +} diff --git a/pipe.h b/pipe.h index 5a307cf..055895c 100644 --- a/pipe.h +++ b/pipe.h @@ -4,6 +4,7 @@ #include #include #include +#include "tuning.h" class Pipe { public: @@ -12,9 +13,16 @@ public: // keys are which keys are being pressed. uint8_t keys; + uint8_t keysLast; // note holds the note being played, according to the fingering chart. - uint8_t note; + Note note; + + // glissandoNote is the note that would be played if partially open keys were fully open. + Note glissandoNote; + + // glissandoOpenness is how "open" the holes are in the direction of the glissandoNote. + float glissandoOpenness; // silent is true if all keys and the knee are closed. bool silent; @@ -26,12 +34,6 @@ public: // This should sound different than the standard fingering. bool altFingering; - // glissandoNote is the note that would be played if partially open keys were fully open. - uint8_t glissandoNote; - - // glissandoOpenness is how "open" the holes are in the direction of the glissandoNote. - float glissandoOpenness; - Pipe(); // Init initializes everything. @@ -44,6 +46,12 @@ public: // It should be run once per loop. void Update(); + // Pressed returns whether the given key is pressed. + bool Pressed(uint8_t key); + + // JustPressed returns whether the given key was just pressed. + bool JustPressed(uint8_t key); + private: Adafruit_MPR121 capSensor; QwiicButton bagSensor; diff --git a/tuning.cpp b/tuning.cpp new file mode 100644 index 0000000..d56f8b5 --- /dev/null +++ b/tuning.cpp @@ -0,0 +1,111 @@ +#include "tuning.h" + +#include + +// 12th root of 2, used for Twelvetone Equal Temperament +#define TET_CONST 1.059463 + +Tuning::Tuning(Note base, float pitch, TuningSystem system) { + Setup(base, pitch, system); +} +Tuning::Tuning(Note base, float pitch) { + Tuning(base, pitch, TUNINGSYSTEM_JUST); +} + +// I like just Intonation. +Tuning::Tuning() { +#if 0 + Tuning(NOTE_D4, PITCH_CONCERT_D4, TUNINGSYSTEM_JUST); +#endif +} + +Note Tuning::GetBaseNote() { return baseNote; } + +void Tuning::SetTuningSystem(TuningSystem system) { + Setup(baseNote, GetPitch(baseNote), system); +} + +TuningSystem Tuning::GetSystem() { return system; } + +// setupOctaves computes the entire tuning frequency chart. +// +// You must call this after setting a full octave chromatic scale, rooted at +// base. +void Tuning::setupOctaves(Note base) { + int multiplier = 1; + for (Note octave = NOTE_ZERO; octave < NOTE_MAX; octave += NOTE_OCTAVE) { + for (Note note = base; note < base + NOTE_OCTAVE; note += NOTE_SEMITONE) { + Note upNote = note + octave; + Note dnNote = note - octave; + + if (upNote < NOTE_MAX) { + pitches[upNote] = pitches[note] * multiplier; + } + if (dnNote >= NOTE_ZERO) { + pitches[dnNote] = pitches[note] / multiplier; + } + } + multiplier <<= 1; + } +} + +// setupEqual sets an even-temperament chromatic scale rooted at base +void Tuning::setupEqual(Note base, float pitch) { + pitches[base] = pitch; + for (int i = 1; i < 12; i++) { + pitches[base + i] = pitches[base + i - 1] * TET_CONST; + } +} + +// setupJust sets a just-temperament chromatic scale rooted at base +void Tuning::setupJust(Note base, float pitch) { + // Diatonic scale + pitches[base + 0] = pitch * 1 / 1; // Unison + pitches[base + 2] = pitch * 9 / 8; // Second + pitches[base + 4] = pitch * 5 / 4; // Third + pitches[base + 5] = pitch * 4 / 3; // Fourth + pitches[base + 7] = pitch * 3 / 2; // Fifth + pitches[base + 9] = pitch * 5 / 3; // Sixth + pitches[base + 11] = pitch * 15 / 8; // Seventh + + // I got this off various Wikipedia pages. + // The main thing here is that the minor seventh works out to be a diatonic + // fourth up from the fourth computed above, since the music I want to play + // frequently wants to play G major on a D major instrument + pitches[base + 1] = pitch * 16 / 15; // min2 + pitches[base + 3] = pitch * 6 / 5; // min3 + pitches[base + 6] = pitch * 10 / 7; // dim5 + pitches[base + 8] = pitch * 8 / 5; // min6 + pitches[base + 10] = pitch * 16 / 9; // min7 = fourth + fourth +} + +void Tuning::Setup(Note base, float pitch, TuningSystem system) { + this->baseNote = base; + this->system = system; + + switch (system) { + case TUNINGSYSTEM_EQUAL: + setupEqual(base, pitch); + break; + case TUNINGSYSTEM_JUST: + default: + setupJust(base, pitch); + break; + } + setupOctaves(base); +} + +void Tuning::Setup(Note base, float pitch) { Setup(base, pitch, system); } + +float Tuning::GetPitch(Note note) { return pitches[note]; } + +Note NearestNote(float pitch) { + return Note(round(log2(pitch / PITCH_CONCERT_C0))); +} + +const char *noteNames[]{ + "C ", "C#", "D ", "Eb", "E ", "F ", "F#", "G ", "Ab", "A ", "Bb", "B ", +}; + +const char *NoteName(Note note) { return noteNames[note % 12]; } + diff --git a/tuning.h b/tuning.h new file mode 100644 index 0000000..5483cdc --- /dev/null +++ b/tuning.h @@ -0,0 +1,99 @@ +#pragma once +#include + +enum TuningSystem { + TUNINGSYSTEM_JUST, + TUNINGSYSTEM_EQUAL, +}; +#define TUNINGSYSTEM_MAX TUNINGSYSTEM_EQUAL + +enum Note { + NOTE_C0, NOTE_CS0, NOTE_D0, NOTE_DS0, NOTE_E0, NOTE_F0, NOTE_FS0, NOTE_G0, NOTE_GS0, NOTE_A0, NOTE_AS0, NOTE_B0, + NOTE_C1, NOTE_CS1, NOTE_D1, NOTE_DS1, NOTE_E1, NOTE_F1, NOTE_FS1, NOTE_G1, NOTE_GS1, NOTE_A1, NOTE_AS1, NOTE_B1, + NOTE_C2, NOTE_CS2, NOTE_D2, NOTE_DS2, NOTE_E2, NOTE_F2, NOTE_FS2, NOTE_G2, NOTE_GS2, NOTE_A2, NOTE_AS2, NOTE_B2, + NOTE_C3, NOTE_CS3, NOTE_D3, NOTE_DS3, NOTE_E3, NOTE_F3, NOTE_FS3, NOTE_G3, NOTE_GS3, NOTE_A3, NOTE_AS3, NOTE_B3, + NOTE_C4, NOTE_CS4, NOTE_D4, NOTE_DS4, NOTE_E4, NOTE_F4, NOTE_FS4, NOTE_G4, NOTE_GS4, NOTE_A4, NOTE_AS4, NOTE_B4, + NOTE_C5, NOTE_CS5, NOTE_D5, NOTE_DS5, NOTE_E5, NOTE_F5, NOTE_FS5, NOTE_G5, NOTE_GS5, NOTE_A5, NOTE_AS5, NOTE_B5, + NOTE_C6, NOTE_CS6, NOTE_D6, NOTE_DS6, NOTE_E6, NOTE_F6, NOTE_FS6, NOTE_G6, NOTE_GS6, NOTE_A6, NOTE_AS6, NOTE_B6, + NOTE_C7, NOTE_CS7, NOTE_D7, NOTE_DS7, NOTE_E7, NOTE_F7, NOTE_FS7, NOTE_G7, NOTE_GS7, NOTE_A7, NOTE_AS7, NOTE_B7, + NOTE_C8, NOTE_CS8, NOTE_D8, NOTE_DS8, NOTE_E8, NOTE_F8, NOTE_FS8, NOTE_G8, NOTE_GS8, NOTE_A8, NOTE_AS8, NOTE_B8, + NOTE_ZERO = 0, + NOTE_SEMITONE = 1, + NOTE_WHOLETONE = 2, + NOTE_OCTAVE = NOTE_C1, + NOTE_MAX = NOTE_B8, +}; + +#define PITCH_CONCERT_C0 16.35 +#define PITCH_CONCERT_A4 440.00 +#define PITCH_CONCERT_D4 293.66 + +class Tuning { +public: + // name contains the name of the current tuning system + const char *name; + + Tuning(Note base, float pitch, TuningSystem system); + Tuning(Note base, float pitch); + Tuning(); + void Setup(Note base, float pitch, TuningSystem system); + void Setup(Note base, float pitch); + void SetTuningSystem(TuningSystem system); + TuningSystem GetSystem(); + Note GetBaseNote(); + float GetPitch(Note note); + +private: + TuningSystem system; + Note baseNote; + float pitches[NOTE_MAX]; + + void setupOctaves(Note base); + void setupJust(Note base, float pitch); + void setupEqual(Note base, float pitch); +}; + +// NearestNote returns the note nearest to pitch. +Note NearestNote(float pitch); + +// NoteName return the name of a note (without octave). +const char *NoteName(Note note); + + +// Make notes support some arithmetic +inline Note toNote(int a) { + if (a < NOTE_ZERO) { + return NOTE_ZERO; + } else if (a > NOTE_MAX) { + return NOTE_MAX; + } else { + return Note(a); + } +} +inline Note operator+(const Note &a, const Note b) { + return toNote(int(a) + int(b)); +} +inline Note &operator+=(Note &a, const Note b) { + return a = a + b; +} +inline Note &operator++(Note &a) { + return a += NOTE_SEMITONE; +} +inline Note operator-(const Note a, const Note b) { + return toNote(int(a) - int(b)); +} +inline Note &operator-=(Note &a, const Note b) { + return a = a - b; +} +inline Note &operator--(Note &a) { + return a -= NOTE_SEMITONE; +} +inline Note operator*(const Note a, const int b) { + return toNote(int(a) * b); +} +inline int operator/(const Note a, const int b) { + return int(a)/b; +} +inline int operator/(const Note a, const Note b) { + return int(a)/b; +} diff --git a/uilleann.ino b/uilleann.ino index 1026df3..37a3eff 100644 --- a/uilleann.ino +++ b/uilleann.ino @@ -1,80 +1,86 @@ -#include -#include #include -#include -#include #include -#include +#include +#include #include -#include "synth.h" +#include +#include +#include +#include + #include "patches.h" -#include "notes.h" #include "pipe.h" +#include "synth.h" +#include "tuning.h" #if defined(ADAFRUIT_TRELLIS_M4_EXPRESS) #include -Adafruit_NeoTrellisM4 trellis;// = Adafruit_NeoTrellisM4(); +Adafruit_NeoTrellisM4 trellis; // = Adafruit_NeoTrellisM4(); #endif -#define DRONES -#define DEBUG false - Pipe pipe; +Tuning tuning = Tuning(NOTE_D4, PITCH_CONCERT_D4, TUNINGSYSTEM_JUST); + Adafruit_SSD1306 display(128, 32, &Wire, -1); int currentPatch = 0; +// Settings +uint8_t intonation = 0; +int16_t pitchAdjust = 0; +uint8_t patch[4] = {0}; +float volume[4] = {0}; +const char *settingNames[4] = {"c", "r", "d", "*"}; + +// Pipes FMVoice Chanter; FMVoice Drones[3]; FMVoice Regulators[3]; -AudioFilterBiquad biquad1; -AudioMixer4 mixDrones; -AudioMixer4 mixRegulators; -AudioMixer4 mixL; -AudioMixer4 mixR; +AudioFilterBiquad biquad1; +AudioMixer4 mixDrones; +AudioMixer4 mixRegulators; +AudioMixer4 mixL; +AudioMixer4 mixR; AudioSynthNoiseWhite noise; #if defined(ADAFRUIT_TRELLIS_M4_EXPRESS) AudioOutputAnalogStereo out1; #else -AudioOutputI2S out1; +AudioOutputI2S out1; #endif AudioControlSGTL5000 sgtl5000; AudioConnection FMVoicePatchCords[] = { - //{0, 0, 0, 0}, // For some reason, the first one is ignored + {noise, 0, mixL, 3}, + {noise, 0, mixR, 3}, -// {noise, 0, mixDrones, 3}, - {noise, 0, mixL, 3}, - {noise, 0, mixR, 3}, + {Chanter.outputMixer, 0, biquad1, 0}, + {biquad1, 0, mixL, 0}, + {biquad1, 0, mixR, 0}, - {Chanter.outputMixer, 0, biquad1, 0}, - {biquad1, 0, mixL, 0}, - {biquad1, 0, mixR, 0}, + {Drones[0].outputMixer, 0, mixDrones, 0}, + {Drones[1].outputMixer, 0, mixDrones, 1}, + {Drones[2].outputMixer, 0, mixDrones, 2}, + {mixDrones, 0, mixL, 1}, + {mixDrones, 0, mixR, 1}, - {Drones[0].outputMixer, 0, mixDrones, 0}, - {Drones[1].outputMixer, 0, mixDrones, 1}, - {Drones[2].outputMixer, 0, mixDrones, 2}, - {mixDrones, 0, mixL, 1}, - {mixDrones, 0, mixR, 1}, + {Regulators[0].outputMixer, 0, mixRegulators, 0}, + {Regulators[1].outputMixer, 0, mixRegulators, 1}, + {Regulators[2].outputMixer, 0, mixRegulators, 2}, + {mixRegulators, 0, mixL, 2}, + {mixRegulators, 0, mixR, 2}, - {Regulators[0].outputMixer, 0, mixRegulators, 0}, - {Regulators[1].outputMixer, 0, mixRegulators, 1}, - {Regulators[2].outputMixer, 0, mixRegulators, 2}, - {mixRegulators, 0, mixL, 2}, - {mixRegulators, 0, mixR, 2}, + {mixL, 0, out1, 0}, + {mixR, 0, out1, 1}, - {mixL, 0, out1, 0}, - {mixR, 0, out1, 1}, - - FMVoiceWiring(Chanter), - FMVoiceWiring(Drones[0]), - FMVoiceWiring(Drones[1]), - FMVoiceWiring(Drones[2]), - FMVoiceWiring(Regulators[0]), - FMVoiceWiring(Regulators[1]), - FMVoiceWiring(Regulators[2]), + FMVoiceWiring(Chanter), + FMVoiceWiring(Drones[0]), + FMVoiceWiring(Drones[1]), + FMVoiceWiring(Drones[2]), + FMVoiceWiring(Regulators[0]), + FMVoiceWiring(Regulators[1]), + FMVoiceWiring(Regulators[2]), }; void blink(bool forever) { @@ -83,13 +89,48 @@ void blink(bool forever) { delay(200); digitalWrite(LED_BUILTIN, false); delay(200); - if (! forever) break; + if (!forever) { + return; + } } } +void diag(const char *fmt, ...) { + va_list args; + char s[80]; + + va_start(args, fmt); + vsnprintf(s, sizeof(s)-1, fmt, args); + va_end(args); + + display.clearDisplay(); + display.drawRect(124, 16, 4, 16, SSD1306_WHITE); + display.setTextColor(SSD1306_WHITE); + display.setTextSize(1); + + display.setCursor(56, 24); + display.print(__DATE__); + +#if 0 + display.setCursor(0, 16); + display.print(fn); + display.print(":"); + display.print(lineno); +#endif + + display.setCursor(0, 0); + display.print(s); + + display.display(); +} + void setup() { - // Wire.begin needs a moment, so let's do some math. - setupJustPitches(NOTE_D4, PITCH_D4); + // Initialize settings + // XXX: Read these from persistent storage later + for (int i = 0; i < 4; i++) { + patch[i] = 0; + volume[i] = 0.75; + } Wire.begin(); @@ -101,131 +142,62 @@ void setup() { if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3c)) { blink(true); } - display.clearDisplay(); - display.setTextSize(1); - display.setTextColor(SSD1306_WHITE); - display.print("Starting"); - display.display(); + diag("Hello!"); #if defined(ADAFRUIT_TRELLIS_M4_EXPRESS) + diag("Trellis..."); trellis.begin(); #endif + diag("Pipe..."); while (!pipe.Init()) { - display.clearDisplay(); - display.setCursor(0, 0); - display.print("Pipe?"); - display.display(); + diag("No pipe. Is it connected?"); blink(false); } - // Set aside some memory for the audio library + diag("Audio..."); + // Set aside some memory for the audio library AudioMemory(20); // Set up the SGTL5000 using I2C sgtl5000.enable(); sgtl5000.volume(0.3); - // initialize tunables - updateTunables(3, 0); - // Initialize processor and memory measurements AudioProcessorUsageMaxReset(); AudioMemoryUsageMaxReset(); + diag("Drones..."); // Turn on drones - for (int i=0; i<3; i++) { + for (int i = 0; i < 3; i++) { + Note note = NOTE_D4 - (NOTE_OCTAVE * i); + float pitch = + tuning.GetPitch(note) * (0.01 * (i - 1)); // Detune just a touch Drones[i].LoadPatch(&Bank[0]); - Drones[i].NoteOn(JustPitches[NOTE_D4 - 12*i] + i); + Drones[i].NoteOn(pitch); } // Turn on all mixer channels - for (int i=0; i<4; i++) { + for (int i = 0; i < 4; i++) { mixL.gain(i, 0.5); mixR.gain(i, 0.6); } - -#ifdef DEBUG - noise.amplitude(0.1); - mixL.gain(3, 0.1); - mixR.gain(3, 0.1); -#endif - display.clearDisplay(); - display.setCursor(0, 0); - display.print("Done!"); - display.display(); + diag("Done!"); } -#define INIT_PITCH_ADJUST 0 -#define INIT_GAIN 0.7 -#define INIT_PATCH 0 - -int16_t pitchAdjust; -float chanterGain; -int patch; - -void updateTunables(uint8_t buttons, int note) { - // Pitch adjust if playing A - if (!note || (note == NOTE_A4)) { - switch (buttons) { - case 3: - pitchAdjust = INIT_PITCH_ADJUST; - break; - case 2: - pitchAdjust += 4; - break; - case 1: - pitchAdjust -= 4; - break; - } - } - - float adj = pow(2, pitchAdjust / 32768.0); - setupJustPitches(NOTE_D4, PITCH_D4*adj); - - if (!note || (note == NOTE_G4)) { - // Volume adjust if playing G - switch (buttons) { - case 3: - chanterGain = INIT_GAIN; - break; - case 2: - chanterGain = min(chanterGain+0.005, 1.0); - break; - case 1: - chanterGain = max(chanterGain-0.005, 0.0); - break; - } - } - for (int i=0; i<3; i++) { - mixL.gain(i, chanterGain); - mixR.gain(i, chanterGain); - } - - if (!note || (note == NOTE_CS5)) { - if (buttons == 3) { - patch = INIT_PATCH; - } - - // wrap - int bankSize = sizeof(Bank) / sizeof(Bank[0]); - patch = (patch + bankSize) % bankSize; - - FMPatch *p = &Bank[patch]; - Chanter.LoadPatch(p); - } -} - - void loop() { pipe.Update(); + diag("loop %d", millis()); #if defined(ADAFRUIT_TRELLIS_M4_EXPRESS) trellis.tick(); trellis.setPixelColor(1, trellis.ColorHSV(millis(), 255, 120)); - trellis.setPixelColor(0, trellis.ColorHSV(64*pipe.kneeClosedness, 255, 120)); + { + uint16_t color = trellis.ColorHSV(64 * pipe.kneeClosedness, 255, 120); + trellis.setPixelColor(0, color); + } #endif // If we're infinitely (for the sensor) off the knee, @@ -240,7 +212,7 @@ void loop() { /** doSetup performs "setup mode" behavior for the pipe. * * Setup mode sets the following new meanings to the buttons: - * + * * key: function [alternate] * C♯: Alt * B♮: Chanter @@ -250,21 +222,66 @@ void loop() { * E♮: Down [- coarse] * E♭: + [+ fine] * D♮: - [- fine] - * + * */ void doSetup() { display.clearDisplay(); - bool alt = bitRead(pipe.keys, 7); + bool alt = pipe.Pressed(7); + + // Draw indicator bar + display.fillRect(126, 0, 2, 32, SSD1306_WHITE); + display.setTextSize(1); + display.setCursor(0, 0); - display.fillRect(0, 0, 40, 32, SSD1306_WHITE); - display.setFont(&FreeSans9pt7b); - display.setCursor(1, 13); - display.setTextColor(SSD1306_BLACK); if (alt) { - display.print("pipe"); + // Show settings for each of Chanter, Regulators, Drones + for (int i = 0; i < 3; i++) { + int p = patch[i]; + int16_t x = 0; + int16_t y = i * 8; + + display.setCursor(x, y); + if (pipe.Pressed(6 - i)) { + display.fillRect(x - 1, y, 8, 8, SSD1306_WHITE); + display.setTextColor(SSD1306_BLACK); + } else { + display.setTextColor(SSD1306_WHITE); + } + display.print(settingNames[i]); + x += 7; + + display.drawRect(x, y + 2, 32, 4, SSD1306_WHITE); + display.fillRect(x, y + 2, 32 * volume[i], 4, SSD1306_WHITE); + + x += 34; + display.setTextColor(SSD1306_WHITE); + display.setCursor(x, y); + if (p < 10) { + display.print(" "); + } + display.print(p); + display.print(" "); + display.print(Bank[p].name); + } } else { - display.print("alt"); + if (pipe.Pressed(6)) { + float freq = PITCH_CONCERT_D4 + pitchAdjust; + Note note = NearestNote(freq); + + display.setCursor(0, 0); + display.print(NoteName(note)); + display.setCursor(6 + 6, 0); + display.print(freq); + } else if (pipe.Pressed(5)) { + display.print("fn2"); + } else if (pipe.Pressed(4)) { + display.print("fn3"); + } else { + display.setCursor(56, 8); + display.setTextSize(2); + display.print("Setup"); + } } display.display(); @@ -278,8 +295,8 @@ void doPlay() { Chanter.NoteOff(); } else { // Calculate pitch, and glissando pitch - uint16_t pitch = JustPitches[pipe.note]; - uint16_t glissandoPitch = JustPitches[pipe.glissandoNote]; + uint16_t pitch = tuning.GetPitch(pipe.note); + uint16_t glissandoPitch = tuning.GetPitch(pipe.glissandoNote); // Bend pitch if fewer than 3 half steps away if (abs(pipe.glissandoNote - pipe.note) < 3) { @@ -287,13 +304,13 @@ void doPlay() { pitch += diff * pipe.glissandoOpenness; } - // Apply a low shelf filter if this is the alternate fingering + // Apply a low shelf filter if this is the alternate fingering if (pipe.altFingering) { biquad1.setLowShelf(0, 2000, 0.2, 1); } else { biquad1.setHighShelf(0, 1000, 1.0, 1); } - + // We've figured out what pitch to play, now we can play it. if (Chanter.playing) { Chanter.SetPitch(pitch); @@ -303,11 +320,11 @@ void doPlay() { } // Look up the note name - const char *note_name = NoteNames[pipe.note % 12]; - if (pipe.silent) { - note_name = "--"; - updateDisplay = true; - } + const char *note_name = NoteName(pipe.note); + if (pipe.silent) { + note_name = "--"; + updateDisplay = true; + } if (pipe.note != last_note) { updateDisplay = true; @@ -319,7 +336,7 @@ void doPlay() { display.setCursor(0, 16); display.setTextSize(2); display.print(Chanter.patch->name); - + display.setCursor(0, 0); display.setTextSize(2); display.print(note_name); @@ -329,6 +346,6 @@ void doPlay() { display.print(pipe.kneeClosedness); display.display(); - last_note = pipe.note; + last_note = pipe.note; } }