diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..ce4f900 --- /dev/null +++ b/.clang-format @@ -0,0 +1,3 @@ +BasedOnStyle: Chromium +ColumnLimit: 0 +PointerAlignment: Right diff --git a/Makefile b/Makefile index c2ee968..150d86c 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ FQBN = adafruit:samd:adafruit_trellis_m4 -UF2_MOUNT = /mnt/chromeos/removable/TRELM4BOOT +UF2_MOUNT = /media/neale/TRELM4BOOT +ARDUINO_DIR = /opt/arduino-1.8.13 default: build/uilleann.ino.uf2 install: build/uilleann.ino.uf2 @@ -21,11 +22,11 @@ build/%.bin: % *.cpp *.h -core-api-version 10813 \ -fqbn $(FQBN) \ -hardware ~/.arduino15/packages \ - -tools /app/Arduino/tools-builder \ + -tools $(ARDUINO_DIR)/tools-builder \ -tools ~/.arduino15/packages \ - -hardware /app/Arduino/hardware \ + -hardware $(ARDUINO_DIR)/hardware \ -hardware ~/.arduino15/packages \ - -built-in-libraries /app/Arduino/libraries \ + -built-in-libraries $(ARDUINO_DIR)/libraries \ -libraries ~/Arduino/libraries \ -compile \ $< diff --git a/algorithms.h b/algorithms.h index e4cf919..f15084c 100644 --- a/algorithms.h +++ b/algorithms.h @@ -50,7 +50,7 @@ #define ALG_DX9_2(feedback) \ { \ {0, 1, 0, 0, 1}, \ - {0, 0, 1, 1, 0}, \ + {0, 0, 1, 1, 0}, \ {0, 0, 0, 0, 0}, \ {0, 0, 0, feedback, 0}, \ } diff --git a/fingering.h b/fingering.h index 6658aa5..3e0900f 100644 --- a/fingering.h +++ b/fingering.h @@ -1,75 +1,91 @@ -#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 +#pragma once +#include "tuning.h" -uint8_t uilleann_matrix[] = { - // Open Back D - NOTE_CS5, NOTE_CS5, NOTE_CS5, NOTE_D5, // OOO OO.. - CCDD, // OOO OX.. - CDCD, // OOO XO.. - DDDD, // OOO XX.. - CDCD, // OOX OO.. - DDDD, // OOX OX.. - CDCD, // OOX XO.. - DDDD, // OOX XX.. - CCDD, // OXO OO.. - CCDD, // OXO OX.. - DDDD, // OXO XO.. - DDDD, // OXO XX.. - DDDD, // OXX OO.. - DDDD, // OXX OX.. - DDDD, // OXX XO.. - DDDD, // OXX XX.. - CDCD, // XOO OO.. - DDDD, // XOO OX.. - CDCD, // XOO XO.. - DDDD, // XOO XX.. - CDCD, // XOX OO.. - DDDD, // XOX OX.. - CDCD, // XOX XO.. - DDDD, // XOX XX.. - DDDD, // XXO OO.. - DDDD, // XXO OX.. - DDDD, // XXO XO.. - DDDD, // XXO XX.. - DDDD, // XXX OO.. - DDDD, // XXX OX.. - NOTE_D5, NOTE_D5, NOTE_D5, NOTE_D5|P, // XXX XO.. - DDDD, // XXX XX.. - - // Closed Back D - CCCC, // OOO OO... - NOTE_CS5, NOTE_CS5, NOTE_CS5, NOTE_CS5|P, // OOO OX.. - CCCC, // OOO XO.. - CCCC, // OOO XX.. - CCCC, // OOX OO.. - NOTE_CS5, NOTE_CS5|P, NOTE_CS5, NOTE_CS5|P, // OOX OX.. - CCCC, // OOX XO.. - CCCC, // OOX XX.. - CCCC, // OXO OO.. - NOTE_CS5, NOTE_CS5|P, NOTE_CS5, NOTE_CS5|P, // 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.. +struct Fingering { + Note note; + bool alt; // Alternate fingering: sounds more choked }; + +#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 + n(NOTE_Cs5), n(NOTE_Cs5), n(NOTE_Cs5), n(NOTE_D5), // OOO OO.. + CCDD, // OOO OX.. + CDCD, // OOO XO.. + DDDD, // OOO XX.. + CDCD, // OOX OO.. + DDDD, // OOX OX.. + CDCD, // OOX XO.. + DDDD, // OOX XX.. + CCDD, // OXO OO.. + CCDD, // OXO OX.. + DDDD, // OXO XO.. + DDDD, // OXO XX.. + DDDD, // OXX OO.. + DDDD, // OXX OX.. + DDDD, // OXX XO.. + DDDD, // OXX XX.. + CDCD, // XOO OO.. + DDDD, // XOO OX.. + CDCD, // XOO XO.. + DDDD, // XOO XX.. + CDCD, // XOX OO.. + DDDD, // XOX OX.. + CDCD, // XOX XO.. + DDDD, // XOX XX.. + DDDD, // XXO OO.. + DDDD, // XXO OX.. + DDDD, // XXO XO.. + DDDD, // XXO XX.. + DDDD, // XXX OO.. + DDDD, // XXX OX.. + n(NOTE_D5), n(NOTE_D5), n(NOTE_D5), P(NOTE_D5), // XXX XO.. + DDDD, // XXX XX.. + + // Closed Back D + CCCC, // OOO OO... + n(NOTE_Cs5), n(NOTE_Cs5), n(NOTE_Cs5), P(NOTE_Cs5), // OOO OX.. + CCCC, // OOO XO.. + CCCC, // OOO XX.. + CCCC, // OOX OO.. + n(NOTE_Cs5), P(NOTE_Cs5), n(NOTE_Cs5), P(NOTE_Cs5), // OOX OX.. + CCCC, // OOX XO.. + CCCC, // OOX XX.. + CCCC, // OXO OO.. + n(NOTE_Cs5), P(NOTE_Cs5), n(NOTE_Cs5), P(NOTE_Cs5), // OXO OX.. + CCCC, // OXO XO.. + CCCC, // OXO 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.. +}; + +inline Fingering FingeredNote(uint16_t keys) { + return uilleann_matrix[keys & 0xff]; +} diff --git a/main-play.h b/main-play.h new file mode 100644 index 0000000..71944b5 --- /dev/null +++ b/main-play.h @@ -0,0 +1,73 @@ +#pragma once + +void playDrones() { + for (int i = 0; i < NUM_DRONES; ++i) { + float pitch = tuning.GetPitch(NOTE_D3); + pitch /= 1 << i; // Take down the appropriate number of octaves + pitch *= (i - 1) / 1000; // Detune ever so AudioProcessorUsageMaxReset(); + Drones[i].NoteOn(pitch); + } +} + +void doPlay(bool forceDisplayUpdate) { + static Note last_note = NOTE_ZERO; + bool updateDisplay = forceDisplayUpdate; + + if (updateDisplay) { + display.clearDisplay(); + display.fillRect(0, 0, 2, 2, SSD1306_WHITE); + display.display(); + } + + if (pipe.Silent) { + Chanter.NoteOff(); + } else { + // Calculate pitch, and glissando pitch + float pitch = tuning.GetPitch(pipe.CurrentNote); + float glissandoPitch = tuning.GetPitch(pipe.GlissandoNote); + + // Bend pitch if fewer than 3 half steps away + if (abs(pipe.GlissandoNote - pipe.CurrentNote) < 3) { + float diff = glissandoPitch - pitch; + pitch = glissandoPitch - (diff * pipe.GlissandoPressure); + } + + // 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); + } else { + Chanter.NoteOn(pitch); + } + } + + if (pipe.CurrentNote != last_note) { + updateDisplay = true; + } + +#if 0 + if (updateDisplay) { + // Look up the note name + const char *noteName = NoteName(pipe.CurrentNote); + if (pipe.Silent) { + noteName = "--"; + updateDisplay = true; + } + + display.clearDisplay(); + display.setFont(&FreeSans9pt7b); + + display.setCursor(0, 16); + display.print(noteName); + + display.display(); + last_note = pipe.CurrentNote; + } +#endif +} diff --git a/main-setup.h b/main-setup.h new file mode 100644 index 0000000..f4f869c --- /dev/null +++ b/main-setup.h @@ -0,0 +1,192 @@ +#pragma once + +#define QUELL_DURATION 200 +#define ADJ_TYPEMATIC_DELAY 500 +#define ADJ_TYPEMATIC_REPEAT 33 + +const char *settingNames[4] = {"c", "r", "d", "*"}; + +// quellUntil can be set to give the user time to get their fingers off the continuous adjustment buttons. +unsigned long quellUntil = 0; +void quell(unsigned long ms) { + quellUntil = millis() + ms; +} +void quell() { + quell(QUELL_DURATION); +} + +void setupVolume() { + Adjust volAdjust = pipe.ReadAdjust(2, 3, 0, ADJ_TYPEMATIC_REPEAT); + Adjust patchAdjust = pipe.ReadAdjust(0, 1, 0, 500); + + for (int i = 0; i < 3; i++) { + int16_t x = 1; + 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); + switch (volAdjust) { + case ADJUST_BOTH: + volume[i] = VOLUME_INITIAL; + quell(); + break; + case ADJUST_UP: + case ADJUST_DOWN: + { + float vol = volume[i] + float(volAdjust)*0.02; + volume[i] = max(min(vol, 1.0), 0.0); + } + break; + default: + break; + } + switch (patchAdjust) { + case ADJUST_BOTH: + patch[i] = 0; + quell(); + loadPatch(i); + break; + case ADJUST_UP: + case ADJUST_DOWN: + patch[i] = (patch[i] + PATCH_MAX + int(patchAdjust)) % PATCH_MAX; + loadPatch(i); + break; + default: + break; + } + mixL.gain(i, volume[i]); + mixR.gain(i, volume[i]); + } 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); + display.print(patch[i]); + display.print(" "); + display.print(Bank[patch[i]].name); + } +} + +void setupTuning() { + Adjust noteAdjust = pipe.ReadAdjust(2, 3, ADJ_TYPEMATIC_DELAY, ADJ_TYPEMATIC_REPEAT); + Adjust pitchAdjust = pipe.ReadAdjust(0, 1, 0, ADJ_TYPEMATIC_REPEAT); + TuningSystem system = tuning.GetTuningSystem(); + float freq = tuning.GetPitch(NOTE_D4); + Note note = NearestNote(freq); + + if (noteAdjust != ADJUST_NONE) { + // Set up even temperament to pick a concert pitch + tuning.Setup(NOTE_A4, PITCH_CONCERT_A4, TUNINGSYSTEM_EQUAL); + switch (noteAdjust) { + case ADJUST_BOTH: + ++system; + break; + case ADJUST_UP: + case ADJUST_DOWN: + note += int(noteAdjust); + freq = tuning.GetPitch(note); + break; + default: + break; + } + // Now retune + tuning.Setup(NOTE_D4, freq, system); + } + + if (pitchAdjust != ADJUST_NONE) { + switch (pitchAdjust) { + case ADJUST_BOTH: + freq = PITCH_CONCERT_D4; + quell(); + break; + case ADJUST_UP: + freq *= 1.001; + break; + case ADJUST_DOWN: + freq /= 1.001; + break; + default: + break; + } + tuning.Setup(NOTE_D4, freq); + note = NearestNote(freq); + } + + display.setFont(&FreeSans9pt7b); + display.setCursor(0, 12); + display.print(NoteName(note)); + display.setCursor(24, 12); + display.print(NoteOctave(note)); + display.setCursor(48, 12); + display.print(freq); + + display.setCursor(0, 27); + display.print(TuningSystemName(system)); +} + +void setupInfo() { + display.setFont(&FreeSans9pt7b); + display.setCursor(64, 18); + display.print("Setup"); + + display.setFont(); + display.setTextSize(1); + display.setCursor(0, 16); + display.print("FC-1"); + display.setCursor(0, 24); + display.print(buildDate); + display.setCursor(0, 0); + display.print("M:"); + display.print(AudioMemoryUsageMax()); +} + +/** 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 + * A♮: Regulators + * G♮: Drones + * F♯: Up [+ coarse] + * E♮: Down [- coarse] + * E♭: + [+ fine] + * D♮: - [- fine] + * + */ +void doSetup() { + if (millis() < quellUntil) { + return; + } + + // Draw setup indicator bar + display.clearDisplay(); + display.fillRect(126, 0, 2, 32, SSD1306_WHITE); + display.setFont(0); + display.setTextSize(1); + display.setCursor(0, 0); + + if (pipe.Pressed(7)) { // Volume + setupVolume(); + } else if (pipe.Pressed(4)) { + display.print("fn3"); + } else if (pipe.Pressed(5)) { + display.print("fn2"); + } else if (pipe.Pressed(6)) { // Tuning + setupTuning(); + } else { + setupInfo(); + } + + display.display(); +} 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/patches.h b/patches.h index 6ab0470..82b8c15 100644 --- a/patches.h +++ b/patches.h @@ -2,6 +2,7 @@ #pragma once #include "algorithms.h" +#include "synth.h" // Waveform, offset, multiplier, delay, attack, holdAmp, hold, decay, sustainAmp, release FMPatch Bank[] = { @@ -60,3 +61,5 @@ FMPatch Bank[] = { }, }, }; + +const int PATCH_MAX = sizeof(Bank) / sizeof(Bank[0]); diff --git a/pipe.cpp b/pipe.cpp new file mode 100644 index 0000000..97a3e75 --- /dev/null +++ b/pipe.cpp @@ -0,0 +1,141 @@ +#include "pipe.h" +#include "fingering.h" +#include "tuning.h" + + +#define CLOSEDVAL 0x30 +#define OPENVAL 0x70 +#define GLISSANDO_STEPS (OPENVAL - CLOSEDVAL) + +Pipe::Pipe() { + KeysLast = 0; +} + +bool Pipe::Init() { + // Capacative touch sensor + if (!capSensor.begin(0x5A)) { + return false; + } + + // Knee sensor + if (!kneeSensor.begin()) { + return false; + } + + // Bag button + bagSensor.begin(); + // This library takes the entire program out if you poll it 5-40 times without anything connected + bag_enabled = bagSensor.isConnected(); + + return true; +} + +void Pipe::Update() { + uint8_t glissandoKeys = 0; + + KeysLast = Keys; + Keys = 0; + + // Read the bag state, if there's a bag. + // if there isn't a bag, don't try, or this library will crash the program. + if (bag_enabled) { + Bag = bagSensor.isPressed(); + } else { + Bag = false; + } + + // 0x6c is actually 8 bytes, but all 8 are always the same... + KneeClosedness = 255 - kneeSensor.readRange(); + + for (int i = 0; i < NUM_KEYS; ++i) { + uint16_t sensorReading = capSensor.filteredData(i); + uint16_t val = OPENVAL - min(max(sensorReading, CLOSEDVAL), OPENVAL); + KeyPressure[i] = val / float(GLISSANDO_STEPS); + + // keys = all keys which are at least touched + // glissandoKeys = all keys which are fully closed + // The glissando operation computes the difference. + if (KeyPressure[i] > 0.0) { + bitSet(Keys, i); + } + if (KeyPressure[i] == 1.0) { + bitSet(glissandoKeys, i); + } + } + + // Compute glissando amount + GlissandoPressure = 1.0; + for (int i = 0; i < 8; ++i) { + if (KeyPressure[i] > 0) { + GlissandoPressure = min(GlissandoPressure, KeyPressure[i]); + } + } + + // Look up notes in the big table + struct Fingering f = FingeredNote(Keys); + struct Fingering gf = FingeredNote(glissandoKeys); + + CurrentNote = f.note; + GlissandoNote = gf.note; + + // Was the high bit set? That indicates "alternate fingering", which sounds different. + 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))) { + CurrentNote += 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; +} + +bool Pipe::typematicEvent(uint8_t key, uint16_t delay, uint16_t repeat) { + if (Pressed(key)) { + unsigned long now = millis(); + + if (JustPressed(key)) { + nextRepeat[key] = now + max(delay, repeat); + return true; + } + if (now >= nextRepeat[key]) { + nextRepeat[key] = now + repeat; + return true; + } + } + return false; +} + +Adjust Pipe::ReadAdjust(uint8_t keyUp, uint8_t keyDown, uint16_t delay, uint16_t repeat) { + bool eventUp = typematicEvent(keyUp, delay, repeat); + bool eventDown = typematicEvent(keyDown, delay, repeat); + + if (Pressed(keyUp) && Pressed(keyDown)) { + unsigned long nr = max(nextRepeat[keyUp], nextRepeat[keyDown]); + + nextRepeat[keyUp] = nr; + nextRepeat[keyDown] = nr; + } + + if (eventUp && eventDown) { + return ADJUST_BOTH; + } else if (eventUp) { + return ADJUST_UP; + } else if (eventDown) { + return ADJUST_DOWN; + } + return ADJUST_NONE; +} diff --git a/pipe.h b/pipe.h new file mode 100644 index 0000000..6ef4955 --- /dev/null +++ b/pipe.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include +#include +#include "tuning.h" + +#define NUM_KEYS 12 + +enum Adjust { + ADJUST_DOWN = -1, + ADJUST_NONE = 0, + ADJUST_UP = 1, + ADJUST_BOTH, +}; + +class Pipe { + public: + // kneeClosedness indicates how "closed" the knee sensor is. 0 = wide open. + uint8_t KneeClosedness; + + // keys are which keys are being pressed. + uint16_t Keys; + uint16_t KeysLast; + float KeyPressure[NUM_KEYS]; + + // note holds the note being played, according to the fingering chart. + Note CurrentNote; + + // glissandoNote is the note that would be played if partially open keys were fully open. + Note GlissandoNote; + + // glissandoPressure is how "closed" the holes are in the direction away from the glissandoNote. + float GlissandoPressure; + + // silent is true if all keys and the knee are closed. + bool Silent; + + // bag is true if the bag is being squished. + bool Bag; + + // altFingering is true if the "alternate fingering" is being played. + // This should sound different than the standard fingering. + bool AltFingering; + + Pipe(); + + // Init initializes everything. + // + // Returns true if it all worked. You can run it again if it didn't. + bool Init(); + + // Update reads sensors and updates pipe state. + // + // 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); + + // ReadAdjust returns the input for two keys paired as up/down. + // + // delay is the number of milliseconds to wait before repeating a key + // repeat is the number of milliseconds to wait between repeated keystrokes + Adjust ReadAdjust(uint8_t upKey, uint8_t downKey, uint16_t delay, uint16_t repeat); + + private: + Adafruit_MPR121 capSensor; + Adafruit_VL6180X kneeSensor; + QwiicButton bagSensor; + bool bag_enabled; + unsigned long nextRepeat[NUM_KEYS]; + bool typematicEvent(uint8_t key, uint16_t delay, uint16_t repeat); +}; diff --git a/synth.cpp b/synth.cpp index b4e2488..64ca9e4 100644 --- a/synth.cpp +++ b/synth.cpp @@ -1,47 +1,55 @@ #include "synth.h" #include "synth_waveform.h" -void FMVoiceLoadPatch(FMVoice *v, FMPatch *p) { +void FMVoice::LoadPatch(FMPatch *p) { + bool playing = this->playing; + float pitch = this->pitch; + + NoteOff(); for (int i=0; ioperators[i]; - v->oscillators[i].frequencyModulation(1); - v->oscillators[i].begin(op.waveform); - v->envelopes[i].delay(op.delayTime); - v->envelopes[i].attack(op.attackTime); - v->oscillators[i].amplitude(op.holdAmplitude); - v->envelopes[i].hold(op.holdTime); - v->envelopes[i].decay(op.decayTime); - v->envelopes[i].sustain(op.sustainAmplitude / op.holdAmplitude); - v->envelopes[i].release(op.releaseTime); + this->oscillators[i].frequencyModulation(1); + this->oscillators[i].begin(op.waveform); + this->envelopes[i].delay(op.delayTime); + this->envelopes[i].attack(op.attackTime); + this->oscillators[i].amplitude(op.holdAmplitude); + this->envelopes[i].hold(op.holdTime); + this->envelopes[i].decay(op.decayTime); + this->envelopes[i].sustain(op.sustainAmplitude / op.holdAmplitude); + this->envelopes[i].release(op.releaseTime); // This feels wasteful 🙁 for (int j=0; jmixers[i].gain(j, p->gains[i][j]); + this->mixers[i].gain(j, p->gains[i][j]); } - v->outputMixer.gain(i, p->gains[i][NUM_OPERATORS]); + this->outputMixer.gain(i, p->gains[i][NUM_OPERATORS]); } - v->patch = p; -} - -void FMVoiceSetPitch(FMVoice *v, float freq) { - for (int i=0; i<4; i++) { - FMOperator op = v->patch->operators[i]; - v->oscillators[i].frequency(op.offset + (freq * op.multiplier)); + this->patch = p; + if (playing) { + NoteOn(pitch); } } -void FMVoiceNoteOn(FMVoice *v, float freq) { - FMVoiceSetPitch(v, freq); +void FMVoice::SetPitch(float freq) { for (int i=0; i<4; i++) { - v->envelopes[i].noteOn(); + FMOperator op = this->patch->operators[i]; + this->oscillators[i].frequency(op.offset + (freq * op.multiplier)); } - v->playing = true; + this->pitch = freq; } -void FMVoiceNoteOff(FMVoice *v) { +void FMVoice::NoteOn(float freq) { + SetPitch(freq); for (int i=0; i<4; i++) { - v->envelopes[i].noteOff(); + this->envelopes[i].noteOn(); } - v->playing = false; + this->playing = true; +} + +void FMVoice::NoteOff() { + for (int i=0; i<4; i++) { + this->envelopes[i].noteOff(); + } + this->playing = false; } diff --git a/synth.h b/synth.h index 8c35942..e14cfb0 100644 --- a/synth.h +++ b/synth.h @@ -73,21 +73,66 @@ typedef struct FMOperator { * can be accomplished by patching an operator into itself. */ typedef struct FMPatch { - char *name; + const char *name; float gains[NUM_OPERATORS][NUM_OPERATORS+1]; FMOperator operators[NUM_OPERATORS]; } FMPatch; /** FMVoice sets up all the Audio objects for a voice. */ -typedef struct FMVoice { - AudioMixer4 mixers[NUM_OPERATORS]; - AudioSynthWaveformModulated oscillators[NUM_OPERATORS]; - AudioEffectEnvelope envelopes[NUM_OPERATORS]; - AudioMixer4 outputMixer; - FMPatch *patch; - bool playing; -} FMVoice; +class FMVoice { + public: + /** LoadPatch loads a patch into a voice. + */ + void LoadPatch(FMPatch *p); + + /** SetPitch sets the pitch (Hz) of a voice. + * + * This does not signal the envelope in any way. + * You would use this for a glissando, portamento, or pitch bend. + * In my bagpipe, this prevents "reed noise" when changing notes. + */ + void SetPitch(float pitch); + + /** GetPitch returns the pitch (Hz) of a voice. + */ + float GetPitch(); + + /** SetModulation sets the modulation amount of a voice. + * + * What this means depends on the loaded patch. + * For a "normal" bagpipe patch, this would adjust the intensity of + * of a filter, or set the level of an oscillator. + * In an old-school keyboard patch, this would set the + * intensity of a Low Frequency Oscillator to set a vibrato. + */ + void setModulation(float level); + + /** NoteOn sets the pitch (Hz) of a voice, and starts in playing. + * + * This tells the envelope generators to begin. + * On a piano, this is what you would use when a key is pressed. + * In my bagpipe, this triggers "reed noise". + */ + void NoteOn(float pitch); + + /** NoteOff stops a note from playing. + * + * This turns the voice "off" by shutting down all the envelope generators. + * On a piano, this is what you would use when a key is released. + * In my bagpipe, this corresponds to all holes being closed. + */ + void NoteOff(); + + + AudioMixer4 mixers[NUM_OPERATORS]; + AudioSynthWaveformModulated oscillators[NUM_OPERATORS]; + AudioEffectEnvelope envelopes[NUM_OPERATORS]; + AudioMixer4 outputMixer; + FMPatch *patch; + float pitch; + bool playing; +}; /** FMOperatorWiring outputs AudioConnection initializers to wire one FM Operator */ @@ -108,30 +153,4 @@ typedef struct FMVoice { FMOperatorWiring(name, 2), \ FMOperatorWiring(name, 3) -/** FMVoiceLoadPatch loads a patch into a voice. - */ -void FMVoiceLoadPatch(FMVoice *v, FMPatch *p); -/** FMVoiceSetPitch sets the pitch (Hz) of a voice. - * - * This does not signal the envelope in any way. - * You would use this for a glissando, portamento, or pitch bend. - * In my bagpipe, this prevents "reed noise" when changing notes. - */ -void FMVoiceSetPitch(FMVoice *v, float pitch); - -/** FMVoiceNoteOn sets the pitch (Hz) of a voice, and starts in playing. - * - * This tells the envelope generators to begin. - * On a piano, this is what you would use when a key is pressed. - * In my bagpipe, this triggers "reed noise". - */ -void FMVoiceNoteOn(FMVoice *v, float pitch); - -/** FMVoiceNoteOff stops a note from playing. - * - * This turns the voice "off" by shutting down all the envelope generators. - * On a piano, this is what you would use when a key is released. - * In my bagpipe, this corresponds to all holes being closed. - */ -void FMVoiceNoteOff(FMVoice *v); diff --git a/tuning.cpp b/tuning.cpp new file mode 100644 index 0000000..be9826c --- /dev/null +++ b/tuning.cpp @@ -0,0 +1,140 @@ +#include "tuning.h" + +#include + +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() { + Tuning(NOTE_D4, PITCH_CONCERT_D4, TUNINGSYSTEM_JUST); +} + +Note Tuning::GetBaseNote() { + return baseNote; +} + +void Tuning::SetTuningSystem(TuningSystem system) { + Setup(baseNote, GetPitch(baseNote), system); +} + +TuningSystem Tuning::GetTuningSystem() { + 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_SEMITONE_MULTIPLIER; + } +} + +// 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(log(pitch / PITCH_CONCERT_C0) / log(TET_SEMITONE_MULTIPLIER))); +} + +const char *noteNames[]{ + "C", + "C#", + "D", + "Eb", + "E", + "F", + "F#", + "G", + "Ab", + "A", + "Bb", + "B", +}; + +const char *NoteName(Note note) { + return noteNames[note % 12]; +} + +int NoteOctave(Note note) { + return int(note / NOTE_OCTAVE); +} + +const char *TuningSystemName(TuningSystem system) { + switch (system) { + case TUNINGSYSTEM_EQUAL: + return "Equal"; + case TUNINGSYSTEM_JUST: + default: + return "Just"; + } +} diff --git a/tuning.h b/tuning.h new file mode 100644 index 0000000..6a08a23 --- /dev/null +++ b/tuning.h @@ -0,0 +1,130 @@ +#pragma once +#include + +enum TuningSystem { + TUNINGSYSTEM_JUST, + TUNINGSYSTEM_EQUAL, + TUNINGSYSTEM_MAX = TUNINGSYSTEM_EQUAL, +}; + +// Twelve-Tone Note (one chromatic scale) +#define NOTE_TT(o) NOTE_C##o, NOTE_Cs##o, NOTE_Db##o = NOTE_Cs##o, \ + NOTE_D##o, NOTE_Ds##o, NOTE_Eb##o = NOTE_Ds##o, \ + NOTE_E##o, \ + NOTE_F##o, NOTE_Fs##o, NOTE_Gb##o = NOTE_Fs##o, \ + NOTE_G##o, NOTE_Gs##o, NOTE_Ab##o = NOTE_Gs##o, \ + NOTE_A##o, NOTE_As##o, NOTE_Bb##o = NOTE_As##o, \ + NOTE_B##o + +enum Note { + NOTE_TT(0), + NOTE_TT(1), + NOTE_TT(2), + NOTE_TT(3), + NOTE_TT(4), + NOTE_TT(5), + NOTE_TT(6), + NOTE_TT(7), + NOTE_TT(8), + 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 + +// Twelvetone Equal Temperament semitone multiplier +// Take any frequency and multiply it by this magic number to get a semitone higher! +// Divide to get a semitone lower! +// This is an approximation of exp(2, 1/12), +// which was worked out in around the 1500s. +#define TET_SEMITONE_MULTIPLIER 1.059463 + +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 GetTuningSystem(); + 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); + +// NoteOctave returns which octave the note is in +int NoteOctave(Note note); + +// NoteName returns the name of a note (without octave). +const char *NoteName(Note note); + +// TuningSystemName returns the name of a tuning system. +const char *TuningSystemName(TuningSystem system); + +// 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 int b) { + return toNote(int(a) + b); +} +inline Note operator+(const Note &a, const Note b) { + return a + int(b); +} +inline Note &operator+=(Note &a, const int b) { + return a = a + 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; +} + +inline TuningSystem operator++(TuningSystem &a) { + return a = TuningSystem((int(a) + 1) % int(TUNINGSYSTEM_MAX + 1)); +} diff --git a/uilleann.ino b/uilleann.ino index cdc4bd9..e14b679 100644 --- a/uilleann.ino +++ b/uilleann.ino @@ -1,303 +1,240 @@ +#include +#include #include -#include -#include -#include -#include -#include -#include "synth.h" +#include +#include + #include "patches.h" -#include "notes.h" -#include "fingering.h" +#include "pipe.h" +#include "synth.h" +#include "tuning.h" -//#define DEBUG -#define KNEE_OFFSET 0 -#define KEY_OFFSET 2 +const char *buildDate = __DATE__; +#if defined(ADAFRUIT_TRELLIS_MAdafruit_SSD1306EXPRESS) +#include +Adafruit_NeoTrellisM4 trellis; // = Adafruit_NeoTrellisM4(); +#endif + +Pipe pipe; +Tuning tuning = Tuning(NOTE_D4, PITCH_CONCERT_D4, TUNINGSYSTEM_JUST); + +Adafruit_SSD1306 display(128, 32, &Wire, -1); + +// Settings +#define VOLUME_INITIAL 0.8 +uint8_t patch[4] = {0}; +float volume[5] = {VOLUME_INITIAL, VOLUME_INITIAL, VOLUME_INITIAL, VOLUME_INITIAL, 0.5}; + +// Pipes +#define NUM_DRONES 3 +#define NUM_REGULATORS 3 FMVoice Chanter; -FMVoice Drones[3]; -FMVoice Regulators[3]; +FMVoice Drones[NUM_DRONES]; +FMVoice Regulators[NUM_REGULATORS]; -AudioFilterBiquad biquad1; -AudioMixer4 mixDrones; -AudioMixer4 mixRegulators; -AudioMixer4 mixL; -AudioMixer4 mixR; -AudioOutputAnalogStereo dacs1; +AudioFilterBiquad biquad1; +AudioMixer4 mixDrones; +AudioMixer4 mixRegulators; +AudioMixer4 mixL; +AudioMixer4 mixR; +AudioSynthNoiseWhite noise; -AudioSynthNoiseWhite debug; +#if defined(ADAFRUIT_TRELLIS_M4_EXPRESS) +AudioOutputAnalogStereo out1; +#else +AudioOutputI2S out1; +#endif + +AudioControlSGTL5000 sgtl5000; AudioConnection FMVoicePatchCords[] = { - {debug, 0, mixR, 3}, // Don't know why, but the first one is ignored - {debug, 0, mixL, 3}, + {noise, 0, mixL, 3}, + {noise, 0, mixR, 3}, - {mixL, 0, dacs1, 0}, - {mixR, 0, dacs1, 1}, + {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}, - FMVoiceWiring(Chanter), - FMVoiceWiring(Drones[0]), - FMVoiceWiring(Drones[1]), - FMVoiceWiring(Drones[2]), - FMVoiceWiring(Drones[3]), - FMVoiceWiring(Regulators[0]), - FMVoiceWiring(Regulators[1]), - FMVoiceWiring(Regulators[2]), - FMVoiceWiring(Regulators[3]), + FMVoiceWiring(Chanter), + FMVoiceWiring(Drones[0]), + FMVoiceWiring(Drones[1]), + FMVoiceWiring(Drones[2]), + FMVoiceWiring(Regulators[0]), + FMVoiceWiring(Regulators[1]), + FMVoiceWiring(Regulators[2]), }; -int currentPatch = 0; +void blink(bool forever) { + for (;;) { + digitalWrite(LED_BUILTIN, true); + delay(200); + digitalWrite(LED_BUILTIN, false); + delay(200); + if (!forever) { + return; + } + } +} -Adafruit_MPR121 cap = Adafruit_MPR121(); -Adafruit_NeoTrellisM4 trellis = Adafruit_NeoTrellisM4(); -MicroOLED oled(9, 1); -QwiicButton bag; +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.setFont(); + display.setTextSize(1); + + display.setCursor(56, 24); + display.print(buildDate); + +#if 0 + display.setCursor(0, 16); + display.print(fn); + display.print(":"); + display.print(lineno); +#endif + + display.setCursor(0, 0); + display.print(s); + + display.display(); +} + +// The right way to do this would be to make a Uilleann object, +// and pass that around. +// The Auido library makes this sort of a pain, +// and honestly, is anybody other than me going to use this? +#include "main-play.h" +#include "main-setup.h" void setup() { - setupJustPitches(NOTE_D4, PITCH_D4); + // PREPARE TO BLINK pinMode(LED_BUILTIN, OUTPUT); + digitalWrite(LED_BUILTIN, true); - // Wire.begin needs a moment - delay(100); + // Set up I2C. Apparently this needs a bit of startup delay. Wire.begin(); - // Initialize OLED display - oled.begin(); - oled.clear(ALL); + // Initialize display + if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3c)) { + blink(true); + } + digitalWrite(LED_BUILTIN, false); + diag("Hello!"); - // Initialize bag - bag.begin(); - - // Initialize the Trellis +#if defined(ADAFRUIT_TRELLIS_M4_EXPRESS) + diag("Trellis..."); trellis.begin(); +#endif - // Initialize touch sensor - bool blink = true; - while (!cap.begin(0x5A)) { - oled.clear(PAGE); - oled.setCursor(0, 0); - oled.print("No Pipe?"); - oled.display(); - - trellis.setPixelColor(0, blink?0xff6666:0); - blink = !blink; - delay(200); + diag("Pipe..."); + while (!pipe.Init()) { + diag("Pipe connected?"); + blink(false); } - // Set aside some memory for the audio library + diag("Audio..."); AudioMemory(120); - - // initialize tunables - updateTunables(3, 0); - - // Initialize processor and memory measurements AudioProcessorUsageMaxReset(); AudioMemoryUsageMaxReset(); + sgtl5000.enable(); + sgtl5000.volume(volume[4]); - // Turn on drones - for (int i=0; i<3; i++) { - float detune = (1-i) * 0.002; - FMVoiceLoadPatch(&Drones[i], &Bank[0]); - FMVoiceNoteOn(&Drones[i], JustPitches[NOTE_D4 - 12*i] * (1 + detune)); - } + diag("Synth..."); + loadPatch(0); + loadPatch(1); + loadPatch(2); + noise.amplitude(1.0); + diag("Mixer..."); // Turn on all mixer channels - for (int i=0; i<4; i++) { - mixL.gain(i, 0.5); - mixR.gain(i, 0.6); + for (int i = 0; i < 4; i++) { + mixL.gain(i, volume[i]); + mixR.gain(i, volume[i]); } - - debug.amplitude(0.1); - mixL.gain(3, 0); - mixR.gain(3, 0); + for (int i = 0; i < NUM_REGULATORS; ++i) { + mixRegulators.gain(i, 1); + } + for (int i = 0; i < NUM_DRONES; ++i) { + mixDrones.gain(i, 1); + } + biquad1.setNotch(0, PITCH_CONCERT_A4, 0.001); + + diag("Drones..."); + playDrones(); + + diag("Done!"); + display.dim(true); } -#define BUTTON_UP 0 -#define BUTTON_DOWN 8 -#define BUTTON_PITCH 24 -#define BUTTON_VOLUME 25 +void loadPatch(uint8_t where) { + FMPatch *p = &Bank[where]; -#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; + switch (where) { + case 0: + Chanter.LoadPatch(p); break; case 1: - pitchAdjust -= 4; - break; - } - } - - float adj = pow(2, pitchAdjust / 32768.0); - setupJustPitches(NOTE_D4, PITCH_D4*adj); - trellis.setPixelColor(BUTTON_PITCH, trellis.ColorHSV(uint16_t(pitchAdjust), 255, 80)); - - if (!note || (note == NOTE_G4)) { - // Volume adjust if playing G - switch (buttons) { - case 3: - chanterGain = INIT_GAIN; + for (int i = 0; i < NUM_REGULATORS; ++i) { + Regulators[i].LoadPatch(p); + } break; case 2: - chanterGain = min(chanterGain+0.005, 1.0); + for (int i = 0; i < NUM_DRONES; ++i) { + Drones[i].LoadPatch(p); + } break; - case 1: - chanterGain = max(chanterGain-0.005, 0.0); + default: break; - } - } - - for (int i=0; i<3; i++) { - mixL.gain(i, chanterGain); - mixR.gain(i, chanterGain); - } - trellis.setPixelColor(BUTTON_VOLUME, trellis.ColorHSV(uint16_t(chanterGain * 65535), 255, 80)); - - if (!note || (note == NOTE_CS5)) { - if (buttons == 3) { - patch = INIT_PATCH; - } else if (trellis.justPressed(BUTTON_DOWN)) { - patch -= 1; - } else if (trellis.justPressed(BUTTON_UP)) { - patch += 1; - } - - // wrap - int bankSize = sizeof(Bank) / sizeof(Bank[0]); - patch = (patch + bankSize) % bankSize; - - FMPatch *p = &Bank[patch]; - FMVoiceLoadPatch(&Chanter, p); - - oled.clear(PAGE); - oled.setFontType(0); - oled.setCursor(0, 0); - oled.print(p->name); - oled.setCursor(0, 10); - oled.print("Patch "); - oled.print(patch); - oled.display(); } } -const uint8_t CLOSEDVAL = 0x30; -const uint8_t OPENVAL = 0x70; -const uint8_t GLISSANDO_STEPS = OPENVAL - CLOSEDVAL; - -bool playing = false; void loop() { - uint8_t keys = 0; - uint8_t note; - uint8_t glissandoKeys = 0; - uint8_t glissandoNote; - float glissandoOpenness = 0; - bool silent = false; - bool knee = cap.filteredData(KNEE_OFFSET) < CLOSEDVAL; - uint8_t buttons = trellis.isPressed(BUTTON_DOWN)?1:0 | trellis.isPressed(BUTTON_UP)?2:0; + static bool upSetting = true; // GET IT? + pipe.Update(); + +#if defined(ADAFRUIT_TRELLIS_M4_EXPRESS) trellis.tick(); +#endif - for (int i = 0; i < 8; i++) { - uint16_t val = max(cap.filteredData(i+KEY_OFFSET), CLOSEDVAL); - float openness = ((val - CLOSEDVAL) / float(GLISSANDO_STEPS)); - - // keys = all keys which are at least touched - // glissandoKeys = all keys which are fully closed - // The glissando operation computes the difference. - if (openness < 1.0) { - glissandoOpenness = max(glissandoOpenness, openness); - bitSet(keys, i); - - if (openness == 0.0) { - bitSet(glissandoKeys, i); - } - } - - // print key states - //trellis.setPixelColor(7 - i, trellis.ColorHSV(65536/12, 255, 120*openness)); - trellis.setPixelColor(7 - i, trellis.ColorHSV(22222*openness, 255, 40)); - } - - note = uilleann_matrix[keys]; - glissandoNote = uilleann_matrix[glissandoKeys]; - - bool alt = note & 0x80; - bool galt = glissandoNote & 0x80; - note = note & 0x7f; - glissandoNote = glissandoNote & 0x7f; - - // All keys closed + knee = no sound - if (knee) { - if (keys == 0xff) { - silent = true; + // If we're infinitely (for the sensor) off the knee, + // we might be in setup mode. + if (pipe.KneeClosedness == 0) { + // We only enter into setup mode if no keys are pressed. + // This hopefully avoids accidentally entering setup while playing. + // Like say you're playing a jaunty tune and suddenly there's an earthquake. + // You ought to be able to finish the tune off before your pipe goes into setup mode. + if (upSetting || (pipe.Keys == 0)) { + doSetup(); + upSetting = true; + return; } } - // Jump octave if the bag is squished - //bag = !digitalRead(BAG); - if (bag.isPressed()) { - if (keys & bit(7)) { - note += 12; - glissandoNote += 12; - } - } - - // Read some trellis button states - if (buttons) { - updateTunables(buttons, note); - } - - if (silent) { - FMVoiceNoteOff(&Chanter); - } else { - // Calculate pitch, and glissando pitch - uint16_t pitch = JustPitches[note]; - uint16_t glissandoPitch = JustPitches[glissandoNote]; - - if (alt) { - biquad1.setLowShelf(0, 2000, 0.2, 1); - } else { - biquad1.setHighShelf(0, 1000, 1.0, 1); - } - - // Bend pitch if fewer than 3 half steps away - if (abs(glissandoNote - note) < 3) { - float diff = glissandoPitch - pitch; - pitch += diff * glissandoOpenness; - } - - if (Chanter.playing) { - FMVoiceSetPitch(&Chanter, pitch); - } else { - FMVoiceNoteOn(&Chanter, pitch); - } - } + doPlay(upSetting); + upSetting = false; }