Working on teensy 4.0 + PAJ7620

This commit is contained in:
Neale Pickett 2020-11-11 16:29:09 -07:00
parent 7f867b3018
commit 458b9eb891
3 changed files with 101 additions and 442 deletions

View File

@ -1,249 +0,0 @@
/* Audio Library for Teensy 3.X
* Copyright (c) 2018, Paul Stoffregen, paul@pjrc.com
*
* Development of this audio library was funded by PJRC.COM, LLC by sales of
* Teensy and Audio Adaptor boards. Please support PJRC's efforts to develop
* open source software by purchasing Teensy or other PJRC products.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice, development funding notice, and this permission
* notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#if defined(ADAFRUIT_TRELLIS_M4_EXPRESS)
#include <Arduino.h>
#include "synth_waveform.h"
#include "arm_math.h"
#include "utility/dspinst.h"
void AudioSynthWaveformModulated::update(void)
{
audio_block_t *block, *moddata, *shapedata;
int16_t *bp, *end;
int32_t val1, val2;
int16_t magnitude15;
uint32_t i, ph, index, index2, scale, priorphase;
const uint32_t inc = phase_increment;
moddata = receiveReadOnly(0);
shapedata = receiveReadOnly(1);
// Pre-compute the phase angle for every output sample of this update
ph = phase_accumulator;
priorphase = phasedata[AUDIO_BLOCK_SAMPLES-1];
if (moddata && modulation_type == 0) {
// Frequency Modulation
bp = moddata->data;
for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
int32_t n = (*bp++) * modulation_factor; // n is # of octaves to mod
int32_t ipart = n >> 27; // 4 integer bits
n &= 0x7FFFFFF; // 27 fractional bits
#ifdef IMPROVE_EXPONENTIAL_ACCURACY
// exp2 polynomial suggested by Stefan Stenzel on "music-dsp"
// mail list, Wed, 3 Sep 2014 10:08:55 +0200
int32_t x = n << 3;
n = multiply_accumulate_32x32_rshift32_rounded(536870912, x, 1494202713);
int32_t sq = multiply_32x32_rshift32_rounded(x, x);
n = multiply_accumulate_32x32_rshift32_rounded(n, sq, 1934101615);
n = n + (multiply_32x32_rshift32_rounded(sq,
multiply_32x32_rshift32_rounded(x, 1358044250)) << 1);
n = n << 1;
#else
// exp2 algorithm by Laurent de Soras
// https://www.musicdsp.org/en/latest/Other/106-fast-exp2-approximation.html
n = (n + 134217728) << 3;
n = multiply_32x32_rshift32_rounded(n, n);
n = multiply_32x32_rshift32_rounded(n, 715827883) << 3;
n = n + 715827882;
#endif
uint32_t scale = n >> (14 - ipart);
uint64_t phstep = (uint64_t)inc * scale;
uint32_t phstep_msw = phstep >> 32;
if (phstep_msw < 0x7FFE) {
ph += phstep >> 16;
} else {
ph += 0x7FFE0000;
}
phasedata[i] = ph;
}
release(moddata);
} else if (moddata) {
// Phase Modulation
bp = moddata->data;
for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
// more than +/- 180 deg shift by 32 bit overflow of "n"
uint32_t n = (uint16_t)(*bp++) * modulation_factor;
phasedata[i] = ph + n;
ph += inc;
}
release(moddata);
} else {
// No Modulation Input
for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
phasedata[i] = ph;
ph += inc;
}
}
phase_accumulator = ph;
// If the amplitude is zero, no output, but phase still increments properly
if (magnitude == 0) {
if (shapedata) release(shapedata);
return;
}
block = allocate();
if (!block) {
if (shapedata) release(shapedata);
return;
}
bp = block->data;
// Now generate the output samples using the pre-computed phase angles
switch(tone_type) {
case WAVEFORM_SINE:
for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
ph = phasedata[i];
index = ph >> 24;
val1 = AudioWaveformSine[index];
val2 = AudioWaveformSine[index+1];
scale = (ph >> 8) & 0xFFFF;
val2 *= scale;
val1 *= 0x10000 - scale;
*bp++ = multiply_32x32_rshift32(val1 + val2, magnitude);
}
break;
case WAVEFORM_ARBITRARY:
if (!arbdata) {
release(block);
if (shapedata) release(shapedata);
return;
}
// len = 256
for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
ph = phasedata[i];
index = ph >> 24;
index2 = index + 1;
if (index2 >= 256) index2 = 0;
val1 = *(arbdata + index);
val2 = *(arbdata + index2);
scale = (ph >> 8) & 0xFFFF;
val2 *= scale;
val1 *= 0x10000 - scale;
*bp++ = multiply_32x32_rshift32(val1 + val2, magnitude);
}
break;
case WAVEFORM_PULSE:
if (shapedata) {
magnitude15 = signed_saturate_rshift(magnitude, 16, 1);
for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
uint32_t width = ((shapedata->data[i] + 0x8000) & 0xFFFF) << 16;
if (phasedata[i] < width) {
*bp++ = magnitude15;
} else {
*bp++ = -magnitude15;
}
}
break;
} // else fall through to orginary square without shape modulation
case WAVEFORM_SQUARE:
magnitude15 = signed_saturate_rshift(magnitude, 16, 1);
for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
if (phasedata[i] & 0x80000000) {
*bp++ = -magnitude15;
} else {
*bp++ = magnitude15;
}
}
break;
case WAVEFORM_SAWTOOTH:
for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
*bp++ = signed_multiply_32x16t(magnitude, phasedata[i]);
}
break;
case WAVEFORM_SAWTOOTH_REVERSE:
for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
*bp++ = signed_multiply_32x16t(0xFFFFFFFFu - magnitude, phasedata[i]);
}
break;
case WAVEFORM_TRIANGLE_VARIABLE:
if (shapedata) {
for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
uint32_t width = (shapedata->data[i] + 0x8000) & 0xFFFF;
uint32_t rise = 0xFFFFFFFF / width;
uint32_t fall = 0xFFFFFFFF / (0xFFFF - width);
uint32_t halfwidth = width << 15;
uint32_t n;
ph = phasedata[i];
if (ph < halfwidth) {
n = (ph >> 16) * rise;
*bp++ = ((n >> 16) * magnitude) >> 16;
} else if (ph < 0xFFFFFFFF - halfwidth) {
n = 0x7FFFFFFF - (((ph - halfwidth) >> 16) * fall);
*bp++ = (((int32_t)n >> 16) * magnitude) >> 16;
} else {
n = ((ph + halfwidth) >> 16) * rise + 0x80000000;
*bp++ = (((int32_t)n >> 16) * magnitude) >> 16;
}
ph += inc;
}
break;
} // else fall through to orginary triangle without shape modulation
case WAVEFORM_TRIANGLE:
for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
ph = phasedata[i];
uint32_t phtop = ph >> 30;
if (phtop == 1 || phtop == 2) {
*bp++ = ((0xFFFF - (ph >> 15)) * magnitude) >> 16;
} else {
*bp++ = (((int32_t)ph >> 15) * magnitude) >> 16;
}
}
break;
case WAVEFORM_SAMPLE_HOLD:
for (i=0; i < AUDIO_BLOCK_SAMPLES; i++) {
ph = phasedata[i];
if (ph < priorphase) { // does not work for phase modulation
sample = random(magnitude) - (magnitude >> 1);
}
priorphase = ph;
*bp++ = sample;
}
break;
}
if (tone_offset) {
bp = block->data;
end = bp + AUDIO_BLOCK_SAMPLES;
do {
val1 = *bp;
*bp++ = signed_saturate_rshift(val1 + tone_offset, 16, 0);
} while (bp < end);
}
if (shapedata) release(shapedata);
transmit(block, 0);
release(block);
}
#endif

View File

@ -1,121 +0,0 @@
/** Backport modulated waveform to Adafruit fork
*/
#pragma once
/* Audio Library for Teensy 3.X
* Copyright (c) 2014, Paul Stoffregen, paul@pjrc.com
*
* Development of this audio library was funded by PJRC.COM, LLC by sales of
* Teensy and Audio Adaptor boards. Please support PJRC's efforts to develop
* open source software by purchasing Teensy or other PJRC products.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice, development funding notice, and this permission
* notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#if defined(ADAFRUIT_TRELLIS_M4_EXPRESS)
#include <Arduino.h>
#include <Audio.h>
#include "AudioStream.h"
#include "arm_math.h"
#define WAVEFORM_TRIANGLE_VARIABLE 8
class AudioSynthWaveformModulated : public AudioStream
{
public:
AudioSynthWaveformModulated(void) : AudioStream(2, inputQueueArray),
phase_accumulator(0), phase_increment(0), modulation_factor(32768),
magnitude(0), arbdata(NULL), sample(0), tone_offset(0),
tone_type(WAVEFORM_SINE), modulation_type(0) {
}
void frequency(float freq) {
if (freq < 0.0) {
freq = 0.0;
} else if (freq > AUDIO_SAMPLE_RATE_EXACT / 2) {
freq = AUDIO_SAMPLE_RATE_EXACT / 2;
}
phase_increment = freq * (4294967296.0 / AUDIO_SAMPLE_RATE_EXACT);
if (phase_increment > 0x7FFE0000u) phase_increment = 0x7FFE0000;
}
void amplitude(float n) { // 0 to 1.0
if (n < 0) {
n = 0;
} else if (n > 1.0) {
n = 1.0;
}
magnitude = n * 65536.0;
}
void offset(float n) {
if (n < -1.0) {
n = -1.0;
} else if (n > 1.0) {
n = 1.0;
}
tone_offset = n * 32767.0;
}
void begin(short t_type) {
tone_type = t_type;
}
void begin(float t_amp, float t_freq, short t_type) {
amplitude(t_amp);
frequency(t_freq);
tone_type = t_type;
}
void arbitraryWaveform(const int16_t *data, float maxFreq) {
arbdata = data;
}
void frequencyModulation(float octaves) {
if (octaves > 12.0) {
octaves = 12.0;
} else if (octaves < 0.1) {
octaves = 0.1;
}
modulation_factor = octaves * 4096.0;
modulation_type = 0;
}
void phaseModulation(float degrees) {
if (degrees > 9000.0) {
degrees = 9000.0;
} else if (degrees < 30.0) {
degrees = 30.0;
}
modulation_factor = degrees * (65536.0 / 180.0);
modulation_type = 1;
}
virtual void update(void);
private:
audio_block_t *inputQueueArray[2];
uint32_t phase_accumulator;
uint32_t phase_increment;
uint32_t modulation_factor;
int32_t magnitude;
const int16_t *arbdata;
uint32_t phasedata[AUDIO_BLOCK_SAMPLES];
int16_t sample; // for WAVEFORM_SAMPLE_HOLD
int16_t tone_offset;
uint8_t tone_type;
uint8_t modulation_type;
};
#endif

View File

@ -1,16 +1,17 @@
#include <Audio.h> #include <Audio.h>
#include <Wire.h> #include <Wire.h>
#include <Adafruit_NeoTrellisM4.h> #include <Adafruit_GFX.h>
#include <SFE_MicroOLED.h> #include <Adafruit_SSD1306.h>
#include <SparkFun_Qwiic_Button.h> #include <SparkFun_Qwiic_Button.h>
#include <Adafruit_MPR121.h> #include <Adafruit_MPR121.h>
#include <paj7620.h>
#include "synth.h" #include "synth.h"
#include "patches.h" #include "patches.h"
#include "notes.h" #include "notes.h"
#include "fingering.h" #include "fingering.h"
#define DRONES
#define DEBUG #define DEBUG
#define KNEE_OFFSET 0
#define KEY_OFFSET 2 #define KEY_OFFSET 2
FMVoice Chanter; FMVoice Chanter;
@ -22,17 +23,15 @@ AudioMixer4 mixDrones;
AudioMixer4 mixRegulators; AudioMixer4 mixRegulators;
AudioMixer4 mixL; AudioMixer4 mixL;
AudioMixer4 mixR; AudioMixer4 mixR;
AudioOutputAnalogStereo dacs1; AudioOutputI2S out1;
AudioSynthNoiseWhite noise;
#ifdef DEBUG
AudioSynthNoiseWhite debug;
#endif
AudioConnection FMVoicePatchCords[] = { AudioConnection FMVoicePatchCords[] = {
#ifdef DEBUG //{0, 0, 0, 0}, // For some reason, the first one is ignored
{debug, 0, mixL, 3},
{debug, 0, mixR, 3}, {noise, 0, mixDrones, 3},
#endif {noise, 0, mixL, 3},
{noise, 0, mixR, 3},
{Chanter.outputMixer, 0, biquad1, 0}, {Chanter.outputMixer, 0, biquad1, 0},
{biquad1, 0, mixL, 0}, {biquad1, 0, mixL, 0},
@ -50,56 +49,71 @@ AudioConnection FMVoicePatchCords[] = {
{mixRegulators, 0, mixL, 2}, {mixRegulators, 0, mixL, 2},
{mixRegulators, 0, mixR, 2}, {mixRegulators, 0, mixR, 2},
{mixL, 0, dacs1, 0}, {mixL, 0, out1, 0},
{mixR, 0, dacs1, 1}, {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; int currentPatch = 0;
Adafruit_MPR121 cap = Adafruit_MPR121(); Adafruit_MPR121 cap = Adafruit_MPR121();
Adafruit_NeoTrellisM4 trellis = Adafruit_NeoTrellisM4(); Adafruit_SSD1306 display(128, 32, &Wire, -1);
MicroOLED oled(9, 1);
QwiicButton bag; QwiicButton bag;
bool use_bag;
void blink(bool forever) {
for (;;) {
digitalWrite(LED_BUILTIN, true);
delay(200);
digitalWrite(LED_BUILTIN, false);
delay(200);
if (! forever) break;
}
}
void setup() { void setup() {
setupJustPitches(NOTE_D4, PITCH_D4);
pinMode(LED_BUILTIN, OUTPUT); pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, true);
setupJustPitches(NOTE_D4, PITCH_D4);
// Wire.begin needs a moment // Wire.begin needs a moment
delay(100); delay(100);
Wire.begin(); Wire.begin();
// Initialize OLED display // Initialize gesture/proximity sensor
oled.begin(); if (paj7620Init()) {
oled.clear(ALL); // XXX: Error handling
}
// Initialize display display
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3c)) {
blink(true);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.print("Starting");
display.display();
// Initialize bag // Initialize bag
bag.begin(); bag.begin();
use_bag = bag.isConnected();
// Initialize the Trellis
trellis.begin();
// Initialize touch sensor // Initialize touch sensor
bool blink = true;
while (!cap.begin(0x5A)) { while (!cap.begin(0x5A)) {
oled.clear(PAGE); display.clearDisplay();
oled.setCursor(0, 0); display.setCursor(0, 0);
oled.print("No Pipe?"); display.print("Pipe?");
oled.display(); display.display();
blink(false);
trellis.setPixelColor(0, blink?0xff6666:0);
blink = !blink;
delay(200);
} }
// Set aside some memory for the audio library // Set aside some memory for the audio library
@ -125,17 +139,12 @@ void setup() {
} }
#ifdef DEBUG #ifdef DEBUG
debug.amplitude(0.1); noise.amplitude(0.1);
mixL.gain(3, 0.1); mixL.gain(3, 0.1);
mixR.gain(3, 0.1); mixR.gain(3, 0.1);
#endif #endif
} }
#define BUTTON_UP 0
#define BUTTON_DOWN 8
#define BUTTON_PITCH 24
#define BUTTON_VOLUME 25
#define INIT_PITCH_ADJUST 0 #define INIT_PITCH_ADJUST 0
#define INIT_GAIN 0.7 #define INIT_GAIN 0.7
#define INIT_PATCH 0 #define INIT_PATCH 0
@ -162,7 +171,6 @@ void updateTunables(uint8_t buttons, int note) {
float adj = pow(2, pitchAdjust / 32768.0); float adj = pow(2, pitchAdjust / 32768.0);
setupJustPitches(NOTE_D4, PITCH_D4*adj); setupJustPitches(NOTE_D4, PITCH_D4*adj);
trellis.setPixelColor(BUTTON_PITCH, trellis.ColorHSV(uint16_t(pitchAdjust), 255, 80));
if (!note || (note == NOTE_G4)) { if (!note || (note == NOTE_G4)) {
// Volume adjust if playing G // Volume adjust if playing G
@ -178,20 +186,14 @@ void updateTunables(uint8_t buttons, int note) {
break; break;
} }
} }
for (int i=0; i<3; i++) { for (int i=0; i<3; i++) {
mixL.gain(i, chanterGain); mixL.gain(i, chanterGain);
mixR.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 (!note || (note == NOTE_CS5)) {
if (buttons == 3) { if (buttons == 3) {
patch = INIT_PATCH; patch = INIT_PATCH;
} else if (trellis.justPressed(BUTTON_DOWN)) {
patch -= 1;
} else if (trellis.justPressed(BUTTON_UP)) {
patch += 1;
} }
// wrap // wrap
@ -201,14 +203,13 @@ void updateTunables(uint8_t buttons, int note) {
FMPatch *p = &Bank[patch]; FMPatch *p = &Bank[patch];
FMVoiceLoadPatch(&Chanter, p); FMVoiceLoadPatch(&Chanter, p);
oled.clear(PAGE); display.clearDisplay();
oled.setFontType(0); display.setCursor(0, 0);
oled.setCursor(0, 0); display.print(p->name);
oled.print(p->name); display.setCursor(0, 10);
oled.setCursor(0, 10); display.print("Patch ");
oled.print("Patch "); display.print(patch);
oled.print(patch); display.display();
oled.display();
} }
} }
@ -216,7 +217,8 @@ const uint8_t CLOSEDVAL = 0x30;
const uint8_t OPENVAL = 0x70; const uint8_t OPENVAL = 0x70;
const uint8_t GLISSANDO_STEPS = OPENVAL - CLOSEDVAL; const uint8_t GLISSANDO_STEPS = OPENVAL - CLOSEDVAL;
bool playing = false; uint8_t loopno = 0;
uint8_t last_note = 0;
void loop() { void loop() {
uint8_t keys = 0; uint8_t keys = 0;
@ -225,10 +227,15 @@ void loop() {
uint8_t glissandoNote; uint8_t glissandoNote;
float glissandoOpenness = 0; float glissandoOpenness = 0;
bool silent = false; bool silent = false;
bool knee = cap.filteredData(KNEE_OFFSET) < CLOSEDVAL; uint8_t paj_knee = 127;
uint8_t buttons = trellis.isPressed(BUTTON_DOWN)?1:0 | trellis.isPressed(BUTTON_UP)?2:0; bool knee = false;
trellis.tick(); loopno++;
paj7620ReadReg(0x6c, 1, &paj_knee);
if (paj_knee > 240) {
knee = true;
}
for (int i = 0; i < 8; i++) { for (int i = 0; i < 8; i++) {
uint16_t val = max(cap.filteredData(i+KEY_OFFSET), CLOSEDVAL); uint16_t val = max(cap.filteredData(i+KEY_OFFSET), CLOSEDVAL);
@ -246,9 +253,6 @@ void loop() {
} }
} }
// 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]; note = uilleann_matrix[keys];
@ -265,20 +269,37 @@ void loop() {
silent = true; silent = true;
} }
} }
// Look up the note name
char *note_name = NoteNames[note % 12];
if (silent) {
note_name = "-";
}
// Jump octave if the bag is squished // Jump octave if the bag is squished
//bag = !digitalRead(BAG); //bag = !digitalRead(BAG);
if (bag.isPressed()) { if (use_bag && bag.isPressed()) {
if (keys & bit(7)) { if (keys & bit(7)) {
note += 12; note += 12;
glissandoNote += 12; glissandoNote += 12;
} }
} }
// Read some trellis button states #if 0
if (buttons) { display.clearDisplay();
updateTunables(buttons, note); display.setCursor(0, 0);
} display.print("mem: ");
display.print(AudioMemoryUsageMax());
display.print(" prx: ");
display.print(paj_knee);
display.setCursor(0, 24);
display.print("Note: ");
display.print(note);
display.print(" n: ");
display.print(loopno);
display.display();
return;
#endif
if (silent) { if (silent) {
FMVoiceNoteOff(&Chanter); FMVoiceNoteOff(&Chanter);
@ -305,4 +326,12 @@ void loop() {
FMVoiceNoteOn(&Chanter, pitch); FMVoiceNoteOn(&Chanter, pitch);
} }
} }
if (note != last_note) {
display.clearDisplay();
display.setCursor(0, 0);
display.print(note_name);
display.display();
last_note = note;
}
} }