Merge branch 'master' of https://github.com/nealey/uilleann
This commit is contained in:
commit
b071f58a2d
|
@ -0,0 +1,3 @@
|
||||||
|
BasedOnStyle: Chromium
|
||||||
|
ColumnLimit: 0
|
||||||
|
PointerAlignment: Right
|
9
Makefile
9
Makefile
|
@ -1,5 +1,6 @@
|
||||||
FQBN = adafruit:samd:adafruit_trellis_m4
|
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
|
default: build/uilleann.ino.uf2
|
||||||
install: build/uilleann.ino.uf2
|
install: build/uilleann.ino.uf2
|
||||||
|
@ -21,11 +22,11 @@ build/%.bin: % *.cpp *.h
|
||||||
-core-api-version 10813 \
|
-core-api-version 10813 \
|
||||||
-fqbn $(FQBN) \
|
-fqbn $(FQBN) \
|
||||||
-hardware ~/.arduino15/packages \
|
-hardware ~/.arduino15/packages \
|
||||||
-tools /app/Arduino/tools-builder \
|
-tools $(ARDUINO_DIR)/tools-builder \
|
||||||
-tools ~/.arduino15/packages \
|
-tools ~/.arduino15/packages \
|
||||||
-hardware /app/Arduino/hardware \
|
-hardware $(ARDUINO_DIR)/hardware \
|
||||||
-hardware ~/.arduino15/packages \
|
-hardware ~/.arduino15/packages \
|
||||||
-built-in-libraries /app/Arduino/libraries \
|
-built-in-libraries $(ARDUINO_DIR)/libraries \
|
||||||
-libraries ~/Arduino/libraries \
|
-libraries ~/Arduino/libraries \
|
||||||
-compile \
|
-compile \
|
||||||
$<
|
$<
|
||||||
|
|
78
fingering.h
78
fingering.h
|
@ -1,12 +1,24 @@
|
||||||
#define CCCC NOTE_CS5, NOTE_CS5, NOTE_CS5, NOTE_CS5
|
#pragma once
|
||||||
#define CCDD NOTE_CS5, NOTE_CS5, NOTE_D5, NOTE_D5
|
#include "tuning.h"
|
||||||
#define CDCD NOTE_CS5, NOTE_D5, NOTE_CS5, NOTE_D5
|
|
||||||
#define DDDD NOTE_D5, NOTE_D5, NOTE_D5, NOTE_D5
|
|
||||||
#define P 0x80
|
|
||||||
|
|
||||||
uint8_t uilleann_matrix[] = {
|
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
|
// 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..
|
CCDD, // OOO OX..
|
||||||
CDCD, // OOO XO..
|
CDCD, // OOO XO..
|
||||||
DDDD, // OOO XX..
|
DDDD, // OOO XX..
|
||||||
|
@ -36,40 +48,44 @@ uint8_t uilleann_matrix[] = {
|
||||||
DDDD, // XXO XX..
|
DDDD, // XXO XX..
|
||||||
DDDD, // XXX OO..
|
DDDD, // XXX OO..
|
||||||
DDDD, // XXX OX..
|
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..
|
DDDD, // XXX XX..
|
||||||
|
|
||||||
// Closed Back D
|
// Closed Back D
|
||||||
CCCC, // OOO OO...
|
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 XO..
|
||||||
CCCC, // OOO XX..
|
CCCC, // OOO XX..
|
||||||
CCCC, // OOX OO..
|
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 XO..
|
||||||
CCCC, // OOX XX..
|
CCCC, // OOX XX..
|
||||||
CCCC, // OXO OO..
|
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 XO..
|
||||||
CCCC, // OXO XX..
|
CCCC, // OXO XX..
|
||||||
NOTE_C5|P, NOTE_C5|P, NOTE_C5|P, NOTE_C5|P, // OXX OO..
|
P(NOTE_C5), P(NOTE_C5), P(NOTE_C5), P(NOTE_C5), // OXX OO..
|
||||||
NOTE_C5, NOTE_C5, NOTE_C5, NOTE_C5, // OXX OX..
|
n(NOTE_C5), n(NOTE_C5), n(NOTE_C5), n(NOTE_C5), // OXX OX..
|
||||||
NOTE_C5, NOTE_C5, NOTE_C5, NOTE_C5|P, // OXX XO..
|
n(NOTE_C5), n(NOTE_C5), n(NOTE_C5), P(NOTE_C5), // OXX XO..
|
||||||
NOTE_C5, NOTE_C5, NOTE_C5, NOTE_CS5, // OXX XX..
|
n(NOTE_C5), n(NOTE_C5), n(NOTE_C5), n(NOTE_Cs5), // OXX XX..
|
||||||
NOTE_B4, NOTE_B4, NOTE_B4, NOTE_B4, // XOO OO..
|
n(NOTE_B4), n(NOTE_B4), n(NOTE_B4), n(NOTE_B4), // XOO OO..
|
||||||
NOTE_B4|P, NOTE_B4|P, NOTE_B4, NOTE_B4|P, // XOO OX..
|
P(NOTE_B4), P(NOTE_B4), n(NOTE_B4), P(NOTE_B4), // XOO OX..
|
||||||
NOTE_AS4, NOTE_B4, NOTE_AS4, NOTE_B4, // XOO XO..
|
n(NOTE_As4), n(NOTE_B4), n(NOTE_As4), n(NOTE_B4), // XOO XO..
|
||||||
NOTE_B4, NOTE_B4, NOTE_B4, NOTE_B4, // XOO XX..
|
n(NOTE_B4), n(NOTE_B4), n(NOTE_B4), n(NOTE_B4), // XOO XX..
|
||||||
NOTE_B4|P, NOTE_B4|P, NOTE_B4|P, NOTE_B4|P, // XOX OO..
|
P(NOTE_B4), P(NOTE_B4), P(NOTE_B4), P(NOTE_B4), // XOX OO..
|
||||||
NOTE_B4|P, NOTE_B4|P, NOTE_B4|P, NOTE_B4|P, // XOX OX..
|
P(NOTE_B4), P(NOTE_B4), P(NOTE_B4), P(NOTE_B4), // XOX OX..
|
||||||
NOTE_B4|P, NOTE_B4|P, NOTE_B4|P, NOTE_B4|P, // XOX XO..
|
P(NOTE_B4), P(NOTE_B4), P(NOTE_B4), P(NOTE_B4), // XOX XO..
|
||||||
NOTE_B4|P, NOTE_B4|P, NOTE_B4|P, NOTE_B4|P, // XOX XX..
|
P(NOTE_B4), P(NOTE_B4), P(NOTE_B4), P(NOTE_B4), // XOX XX..
|
||||||
NOTE_A4, NOTE_A4, NOTE_A4|P, NOTE_A4, // XXO OO..
|
n(NOTE_A4), n(NOTE_A4), P(NOTE_A4), n(NOTE_A4), // XXO OO..
|
||||||
NOTE_A4|P, NOTE_A4|P, NOTE_A4|P, NOTE_A4|P, // XXO OX..
|
P(NOTE_A4), P(NOTE_A4), P(NOTE_A4), P(NOTE_A4), // XXO OX..
|
||||||
NOTE_GS4, NOTE_GS4|P, NOTE_A4, NOTE_A4, // XXO XO..
|
n(NOTE_Gs4), P(NOTE_Gs4), n(NOTE_A4), n(NOTE_A4), // XXO XO..
|
||||||
NOTE_A4|P, NOTE_A4|P, NOTE_A4|P, NOTE_A4, // XXO XX..
|
P(NOTE_A4), P(NOTE_A4), P(NOTE_A4), n(NOTE_A4), // XXO XX..
|
||||||
NOTE_G4, NOTE_G4, NOTE_G4|P, NOTE_G4, // XXX OO..
|
n(NOTE_G4), n(NOTE_G4), P(NOTE_G4), n(NOTE_G4), // XXX OO..
|
||||||
NOTE_G4|P, NOTE_G4|P, NOTE_G4|P, NOTE_G4|P, // XXX OX..
|
P(NOTE_G4), P(NOTE_G4), P(NOTE_G4), P(NOTE_G4), // XXX OX..
|
||||||
NOTE_FS4, NOTE_FS4, NOTE_F4, NOTE_FS4|P, // XXX XO..
|
n(NOTE_Fs4), n(NOTE_Fs4), n(NOTE_F4), P(NOTE_Fs4), // XXX XO..
|
||||||
NOTE_E4, NOTE_E4|P, NOTE_DS4, NOTE_D4, // XXX XX..
|
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];
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
41
notes.cpp
41
notes.cpp
|
@ -1,41 +0,0 @@
|
||||||
#include <Arduino.h>
|
|
||||||
#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<<i;
|
|
||||||
int shift = i*12;
|
|
||||||
int upNote = note + shift;
|
|
||||||
int dnNote = note - shift;
|
|
||||||
|
|
||||||
if (upNote <= MaxNote) {
|
|
||||||
JustPitches[upNote] = JustPitches[note] * multiplier;
|
|
||||||
}
|
|
||||||
if (dnNote >= 0) {
|
|
||||||
JustPitches[dnNote] = JustPitches[note] / multiplier;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
21
notes.h
21
notes.h
|
@ -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);
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "algorithms.h"
|
#include "algorithms.h"
|
||||||
|
#include "synth.h"
|
||||||
|
|
||||||
// Waveform, offset, multiplier, delay, attack, holdAmp, hold, decay, sustainAmp, release
|
// Waveform, offset, multiplier, delay, attack, holdAmp, hold, decay, sustainAmp, release
|
||||||
FMPatch Bank[] = {
|
FMPatch Bank[] = {
|
||||||
|
@ -60,3 +61,5 @@ FMPatch Bank[] = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const int PATCH_MAX = sizeof(Bank) / sizeof(Bank[0]);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Adafruit_MPR121.h>
|
||||||
|
#include <Adafruit_VL6180X.h>
|
||||||
|
#include <SparkFun_Qwiic_Button.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#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);
|
||||||
|
};
|
60
synth.cpp
60
synth.cpp
|
@ -1,47 +1,55 @@
|
||||||
#include "synth.h"
|
#include "synth.h"
|
||||||
#include "synth_waveform.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; i<NUM_OPERATORS; i++) {
|
for (int i=0; i<NUM_OPERATORS; i++) {
|
||||||
FMOperator op = p->operators[i];
|
FMOperator op = p->operators[i];
|
||||||
|
|
||||||
v->oscillators[i].frequencyModulation(1);
|
this->oscillators[i].frequencyModulation(1);
|
||||||
v->oscillators[i].begin(op.waveform);
|
this->oscillators[i].begin(op.waveform);
|
||||||
v->envelopes[i].delay(op.delayTime);
|
this->envelopes[i].delay(op.delayTime);
|
||||||
v->envelopes[i].attack(op.attackTime);
|
this->envelopes[i].attack(op.attackTime);
|
||||||
v->oscillators[i].amplitude(op.holdAmplitude);
|
this->oscillators[i].amplitude(op.holdAmplitude);
|
||||||
v->envelopes[i].hold(op.holdTime);
|
this->envelopes[i].hold(op.holdTime);
|
||||||
v->envelopes[i].decay(op.decayTime);
|
this->envelopes[i].decay(op.decayTime);
|
||||||
v->envelopes[i].sustain(op.sustainAmplitude / op.holdAmplitude);
|
this->envelopes[i].sustain(op.sustainAmplitude / op.holdAmplitude);
|
||||||
v->envelopes[i].release(op.releaseTime);
|
this->envelopes[i].release(op.releaseTime);
|
||||||
|
|
||||||
// This feels wasteful 🙁
|
// This feels wasteful 🙁
|
||||||
for (int j=0; j<NUM_OPERATORS; j++) {
|
for (int j=0; j<NUM_OPERATORS; j++) {
|
||||||
v->mixers[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;
|
this->patch = p;
|
||||||
}
|
if (playing) {
|
||||||
|
NoteOn(pitch);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void FMVoiceNoteOn(FMVoice *v, float freq) {
|
void FMVoice::SetPitch(float freq) {
|
||||||
FMVoiceSetPitch(v, freq);
|
|
||||||
for (int i=0; i<4; i++) {
|
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++) {
|
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;
|
||||||
}
|
}
|
||||||
|
|
77
synth.h
77
synth.h
|
@ -73,21 +73,66 @@ typedef struct FMOperator {
|
||||||
* can be accomplished by patching an operator into itself.
|
* can be accomplished by patching an operator into itself.
|
||||||
*/
|
*/
|
||||||
typedef struct FMPatch {
|
typedef struct FMPatch {
|
||||||
char *name;
|
const char *name;
|
||||||
float gains[NUM_OPERATORS][NUM_OPERATORS+1];
|
float gains[NUM_OPERATORS][NUM_OPERATORS+1];
|
||||||
FMOperator operators[NUM_OPERATORS];
|
FMOperator operators[NUM_OPERATORS];
|
||||||
} FMPatch;
|
} FMPatch;
|
||||||
|
|
||||||
/** FMVoice sets up all the Audio objects for a voice.
|
/** FMVoice sets up all the Audio objects for a voice.
|
||||||
*/
|
*/
|
||||||
typedef struct 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];
|
AudioMixer4 mixers[NUM_OPERATORS];
|
||||||
AudioSynthWaveformModulated oscillators[NUM_OPERATORS];
|
AudioSynthWaveformModulated oscillators[NUM_OPERATORS];
|
||||||
AudioEffectEnvelope envelopes[NUM_OPERATORS];
|
AudioEffectEnvelope envelopes[NUM_OPERATORS];
|
||||||
AudioMixer4 outputMixer;
|
AudioMixer4 outputMixer;
|
||||||
FMPatch *patch;
|
FMPatch *patch;
|
||||||
|
float pitch;
|
||||||
bool playing;
|
bool playing;
|
||||||
} FMVoice;
|
};
|
||||||
|
|
||||||
/** FMOperatorWiring outputs AudioConnection initializers to wire one FM Operator
|
/** FMOperatorWiring outputs AudioConnection initializers to wire one FM Operator
|
||||||
*/
|
*/
|
||||||
|
@ -108,30 +153,4 @@ typedef struct FMVoice {
|
||||||
FMOperatorWiring(name, 2), \
|
FMOperatorWiring(name, 2), \
|
||||||
FMOperatorWiring(name, 3)
|
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);
|
|
||||||
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
#include "tuning.h"
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
#pragma once
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
381
uilleann.ino
381
uilleann.ino
|
@ -1,37 +1,56 @@
|
||||||
|
#include <Adafruit_GFX.h>
|
||||||
|
#include <Adafruit_SSD1306.h>
|
||||||
#include <Audio.h>
|
#include <Audio.h>
|
||||||
#include <Wire.h>
|
#include <Fonts/FreeSans9pt7b.h>
|
||||||
#include <Adafruit_NeoTrellisM4.h>
|
#include <stdio.h>
|
||||||
#include <SFE_MicroOLED.h>
|
|
||||||
#include <SparkFun_Qwiic_Button.h>
|
|
||||||
#include <Adafruit_MPR121.h>
|
|
||||||
#include "synth.h"
|
|
||||||
#include "patches.h"
|
#include "patches.h"
|
||||||
#include "notes.h"
|
#include "pipe.h"
|
||||||
#include "fingering.h"
|
#include "synth.h"
|
||||||
|
#include "tuning.h"
|
||||||
|
|
||||||
//#define DEBUG
|
const char *buildDate = __DATE__;
|
||||||
#define KNEE_OFFSET 0
|
|
||||||
#define KEY_OFFSET 2
|
|
||||||
|
|
||||||
|
#if defined(ADAFRUIT_TRELLIS_MAdafruit_SSD1306EXPRESS)
|
||||||
|
#include <Adafruit_NeoTrellisM4.h>
|
||||||
|
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 Chanter;
|
||||||
FMVoice Drones[3];
|
FMVoice Drones[NUM_DRONES];
|
||||||
FMVoice Regulators[3];
|
FMVoice Regulators[NUM_REGULATORS];
|
||||||
|
|
||||||
AudioFilterBiquad biquad1;
|
AudioFilterBiquad biquad1;
|
||||||
AudioMixer4 mixDrones;
|
AudioMixer4 mixDrones;
|
||||||
AudioMixer4 mixRegulators;
|
AudioMixer4 mixRegulators;
|
||||||
AudioMixer4 mixL;
|
AudioMixer4 mixL;
|
||||||
AudioMixer4 mixR;
|
AudioMixer4 mixR;
|
||||||
AudioOutputAnalogStereo dacs1;
|
AudioSynthNoiseWhite noise;
|
||||||
|
|
||||||
AudioSynthNoiseWhite debug;
|
#if defined(ADAFRUIT_TRELLIS_M4_EXPRESS)
|
||||||
|
AudioOutputAnalogStereo out1;
|
||||||
|
#else
|
||||||
|
AudioOutputI2S out1;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
AudioControlSGTL5000 sgtl5000;
|
||||||
|
|
||||||
AudioConnection FMVoicePatchCords[] = {
|
AudioConnection FMVoicePatchCords[] = {
|
||||||
{debug, 0, mixR, 3}, // Don't know why, but the first one is ignored
|
{noise, 0, mixL, 3},
|
||||||
{debug, 0, mixL, 3},
|
{noise, 0, mixR, 3},
|
||||||
|
|
||||||
{mixL, 0, dacs1, 0},
|
|
||||||
{mixR, 0, dacs1, 1},
|
|
||||||
|
|
||||||
{Chanter.outputMixer, 0, biquad1, 0},
|
{Chanter.outputMixer, 0, biquad1, 0},
|
||||||
{biquad1, 0, mixL, 0},
|
{biquad1, 0, mixL, 0},
|
||||||
|
@ -49,255 +68,173 @@ AudioConnection FMVoicePatchCords[] = {
|
||||||
{mixRegulators, 0, mixL, 2},
|
{mixRegulators, 0, mixL, 2},
|
||||||
{mixRegulators, 0, mixR, 2},
|
{mixRegulators, 0, mixR, 2},
|
||||||
|
|
||||||
|
{mixL, 0, out1, 0},
|
||||||
|
{mixR, 0, out1, 1},
|
||||||
|
|
||||||
FMVoiceWiring(Chanter),
|
FMVoiceWiring(Chanter),
|
||||||
FMVoiceWiring(Drones[0]),
|
FMVoiceWiring(Drones[0]),
|
||||||
FMVoiceWiring(Drones[1]),
|
FMVoiceWiring(Drones[1]),
|
||||||
FMVoiceWiring(Drones[2]),
|
FMVoiceWiring(Drones[2]),
|
||||||
FMVoiceWiring(Drones[3]),
|
|
||||||
FMVoiceWiring(Regulators[0]),
|
FMVoiceWiring(Regulators[0]),
|
||||||
FMVoiceWiring(Regulators[1]),
|
FMVoiceWiring(Regulators[1]),
|
||||||
FMVoiceWiring(Regulators[2]),
|
FMVoiceWiring(Regulators[2]),
|
||||||
FMVoiceWiring(Regulators[3]),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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();
|
void diag(const char *fmt, ...) {
|
||||||
Adafruit_NeoTrellisM4 trellis = Adafruit_NeoTrellisM4();
|
va_list args;
|
||||||
MicroOLED oled(9, 1);
|
char s[80];
|
||||||
QwiicButton bag;
|
|
||||||
|
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() {
|
void setup() {
|
||||||
setupJustPitches(NOTE_D4, PITCH_D4);
|
// PREPARE TO BLINK
|
||||||
pinMode(LED_BUILTIN, OUTPUT);
|
pinMode(LED_BUILTIN, OUTPUT);
|
||||||
|
digitalWrite(LED_BUILTIN, true);
|
||||||
|
|
||||||
// Wire.begin needs a moment
|
// Set up I2C. Apparently this needs a bit of startup delay.
|
||||||
delay(100);
|
|
||||||
Wire.begin();
|
Wire.begin();
|
||||||
|
|
||||||
// Initialize OLED display
|
// Initialize display
|
||||||
oled.begin();
|
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3c)) {
|
||||||
oled.clear(ALL);
|
blink(true);
|
||||||
|
}
|
||||||
|
digitalWrite(LED_BUILTIN, false);
|
||||||
|
diag("Hello!");
|
||||||
|
|
||||||
// Initialize bag
|
#if defined(ADAFRUIT_TRELLIS_M4_EXPRESS)
|
||||||
bag.begin();
|
diag("Trellis...");
|
||||||
|
|
||||||
// Initialize the Trellis
|
|
||||||
trellis.begin();
|
trellis.begin();
|
||||||
|
#endif
|
||||||
|
|
||||||
// Initialize touch sensor
|
diag("Pipe...");
|
||||||
bool blink = true;
|
while (!pipe.Init()) {
|
||||||
while (!cap.begin(0x5A)) {
|
diag("Pipe connected?");
|
||||||
oled.clear(PAGE);
|
blink(false);
|
||||||
oled.setCursor(0, 0);
|
|
||||||
oled.print("No Pipe?");
|
|
||||||
oled.display();
|
|
||||||
|
|
||||||
trellis.setPixelColor(0, blink?0xff6666:0);
|
|
||||||
blink = !blink;
|
|
||||||
delay(200);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set aside some memory for the audio library
|
diag("Audio...");
|
||||||
AudioMemory(120);
|
AudioMemory(120);
|
||||||
|
|
||||||
// initialize tunables
|
|
||||||
updateTunables(3, 0);
|
|
||||||
|
|
||||||
// Initialize processor and memory measurements
|
|
||||||
AudioProcessorUsageMaxReset();
|
AudioProcessorUsageMaxReset();
|
||||||
AudioMemoryUsageMaxReset();
|
AudioMemoryUsageMaxReset();
|
||||||
|
sgtl5000.enable();
|
||||||
|
sgtl5000.volume(volume[4]);
|
||||||
|
|
||||||
// Turn on drones
|
diag("Synth...");
|
||||||
for (int i=0; i<3; i++) {
|
loadPatch(0);
|
||||||
float detune = (1-i) * 0.002;
|
loadPatch(1);
|
||||||
FMVoiceLoadPatch(&Drones[i], &Bank[0]);
|
loadPatch(2);
|
||||||
FMVoiceNoteOn(&Drones[i], JustPitches[NOTE_D4 - 12*i] * (1 + detune));
|
noise.amplitude(1.0);
|
||||||
}
|
|
||||||
|
|
||||||
|
diag("Mixer...");
|
||||||
// Turn on all mixer channels
|
// Turn on all mixer channels
|
||||||
for (int i = 0; i < 4; i++) {
|
for (int i = 0; i < 4; i++) {
|
||||||
mixL.gain(i, 0.5);
|
mixL.gain(i, volume[i]);
|
||||||
mixR.gain(i, 0.6);
|
mixR.gain(i, volume[i]);
|
||||||
|
}
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
debug.amplitude(0.1);
|
void loadPatch(uint8_t where) {
|
||||||
mixL.gain(3, 0);
|
FMPatch *p = &Bank[where];
|
||||||
mixR.gain(3, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#define BUTTON_UP 0
|
switch (where) {
|
||||||
#define BUTTON_DOWN 8
|
case 0:
|
||||||
#define BUTTON_PITCH 24
|
Chanter.LoadPatch(p);
|
||||||
#define BUTTON_VOLUME 25
|
|
||||||
|
|
||||||
#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;
|
break;
|
||||||
case 1:
|
case 1:
|
||||||
pitchAdjust -= 4;
|
for (int i = 0; i < NUM_REGULATORS; ++i) {
|
||||||
break;
|
Regulators[i].LoadPatch(p);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
chanterGain = min(chanterGain+0.005, 1.0);
|
for (int i = 0; i < NUM_DRONES; ++i) {
|
||||||
|
Drones[i].LoadPatch(p);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 1:
|
default:
|
||||||
chanterGain = max(chanterGain-0.005, 0.0);
|
|
||||||
break;
|
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() {
|
void loop() {
|
||||||
uint8_t keys = 0;
|
static bool upSetting = true; // GET IT?
|
||||||
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;
|
|
||||||
|
|
||||||
|
pipe.Update();
|
||||||
|
|
||||||
|
#if defined(ADAFRUIT_TRELLIS_M4_EXPRESS)
|
||||||
trellis.tick();
|
trellis.tick();
|
||||||
|
#endif
|
||||||
|
|
||||||
for (int i = 0; i < 8; i++) {
|
// If we're infinitely (for the sensor) off the knee,
|
||||||
uint16_t val = max(cap.filteredData(i+KEY_OFFSET), CLOSEDVAL);
|
// we might be in setup mode.
|
||||||
float openness = ((val - CLOSEDVAL) / float(GLISSANDO_STEPS));
|
if (pipe.KneeClosedness == 0) {
|
||||||
|
// We only enter into setup mode if no keys are pressed.
|
||||||
// keys = all keys which are at least touched
|
// This hopefully avoids accidentally entering setup while playing.
|
||||||
// glissandoKeys = all keys which are fully closed
|
// Like say you're playing a jaunty tune and suddenly there's an earthquake.
|
||||||
// The glissando operation computes the difference.
|
// You ought to be able to finish the tune off before your pipe goes into setup mode.
|
||||||
if (openness < 1.0) {
|
if (upSetting || (pipe.Keys == 0)) {
|
||||||
glissandoOpenness = max(glissandoOpenness, openness);
|
doSetup();
|
||||||
bitSet(keys, i);
|
upSetting = true;
|
||||||
|
return;
|
||||||
if (openness == 0.0) {
|
|
||||||
bitSet(glissandoKeys, i);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// print key states
|
doPlay(upSetting);
|
||||||
//trellis.setPixelColor(7 - i, trellis.ColorHSV(65536/12, 255, 120*openness));
|
upSetting = false;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue