Neale Pickett
·
2026-02-10
concertina.ino
1#include <U8g2lib.h>
2#include <Adafruit_Keypad.h>
3#include <Adafruit_DotStar.h>
4#include "layouts.h"
5#include "ruby.h"
6
7#define VS1053_RESET 2 // This is the pin that connects to the RESET pin on VS1053
8
9// See http://www.vlsi.fi/fileadmin/datasheets/vs1053.pdf Pg 31
10#define VS1053_BANK_DEFAULT 0x00
11#define VS1053_BANK_DRUMS1 0x78
12#define VS1053_BANK_DRUMS2 0x7F
13#define VS1053_BANK_MELODY 0x79
14
15// See http://www.vlsi.fi/fileadmin/datasheets/vs1053.pdf Pg 32 for more!
16#define VS1053_GM1_OCARINA 21
17
18#define MIDI_NOTE_ON 0x90
19#define MIDI_NOTE_OFF 0x80
20#define MIDI_CHAN_MSG 0xB0
21#define MIDI_CHAN_BANK 0x00
22#define MIDI_CHAN_VOLUME 0x07
23#define MIDI_CHAN_PROGRAM 0xC0
24
25// Serial1 on the ItsyBitsy M0 Express is marked on the silkscreen
26#define VS1053_MIDI Serial1
27
28// There's only one DotStar
29Adafruit_DotStar DotStar(DOTSTAR_NUM, PIN_DOTSTAR_DATA, PIN_DOTSTAR_CLK, DOTSTAR_BRG);
30
31// An itsy bitsy OLED display
32U8G2_SSD1306_64X32_1F_F_HW_I2C Display(U8G2_R0);
33
34/* Maps grid positions to layout positions.
35
36 It's in octal: "0%d%d%d" % (side, row, col)
37
38 There are four possible sides: left, right, meta, and undefined.
39 There are eight possible rows.
40 There are eight possible columns.
41*/
42#define ROWS 6
43#define COLS 6
44byte keyMap[ROWS][COLS] = {
45 { 0020, 0010, 0000, 0104, 0114, 0124 },
46 { 0021, 0011, 0001, 0103, 0113, 0123 },
47 { 0022, 0012, 0002, 0102, 0112, 0122 },
48 { 0023, 0013, 0003, 0101, 0111, 0121 },
49 { 0024, 0014, 0004, 0100, 0110, 0120 },
50 { 0200, 0201, 0202, 0203, 0204, 0205 },
51};
52byte rowPins[ROWS] = {14, 15, 16, 17, 18, 19};
53byte colPins[COLS] = { 5, 7, 9, 10, 11, 12};
54Adafruit_Keypad Keypad((byte *)keyMap, rowPins, colPins, ROWS, COLS);
55
56int currentLayoutIdx = 0;
57layout_t currentLayout;
58
59// Load a layout into our button mapping
60void nextLayout(int adj) {
61 int nlayouts = sizeof(layouts) / sizeof(*layouts);
62 currentLayoutIdx = (currentLayoutIdx + nlayouts + adj) % nlayouts;
63 currentLayout = layouts[currentLayoutIdx];
64}
65
66void error() {
67 bool led = false;
68 for (;;) {
69 led = !led;
70 digitalWrite(LED_BUILTIN, led);
71 delay(250);
72 }
73}
74
75void setup() {
76 Serial.begin(9600);
77 Serial.println("Ruby MIDI Concertina");
78
79 pinMode(LED_BUILTIN, OUTPUT);
80 pinMode(VS1053_RESET, OUTPUT);
81
82 // Reset VS1053
83 digitalWrite(VS1053_RESET, LOW);
84 delay(10);
85 digitalWrite(VS1053_RESET, HIGH);
86 delay(10);
87 VS1053_MIDI.begin(31250); // MIDI uses a 'strange baud rate'
88
89 midiSetChannelBank(0, VS1053_BANK_MELODY);
90 midiSetInstrument(0, VS1053_GM1_OCARINA);
91 midiSetChannelVolume(0, 127);
92
93 // Set up keyboard
94 Keypad.begin();
95 nextLayout(0);
96
97 // Use the display
98 Display.begin();
99 Display.firstPage();
100 Display.setFont(u8g2_font_helvR08_tf);
101 Display.drawUTF8(0, 10, currentLayout.name);
102 Display.nextPage();
103
104 // Turn off the DotStar
105 DotStar.begin();
106 DotStar.clear();
107 DotStar.show();
108}
109
110void displayNoteName(int8_t note) {
111 char base = 0;
112 int accidental = 0;
113 char out[32] = {};
114 int pos = 0;
115
116 switch (note % 12) {
117 case 10:
118 accidental = -1;
119 case 11:
120 base = 'B';
121 break;
122 case 9:
123 base = 'A';
124 break;
125 case 8:
126 accidental = 1;
127 case 7:
128 base = 'G';
129 break;
130 case 6:
131 accidental = 1;
132 case 5:
133 base = 'F';
134 break;
135 case 4:
136 base = 'E';
137 break;
138 case 3:
139 accidental = 1;
140 case 2:
141 base = 'D';
142 break;
143 case 1:
144 accidental = 1;
145 case 0:
146 base = 'C';
147 break;
148 }
149
150 if (note > 71) {
151 base += 32;
152 }
153 out[pos++] = base;
154
155 switch (accidental) {
156 case -1:
157 out[pos++] = '_';
158 break;
159 case 1:
160 out[pos++] = '^';
161 break;
162 }
163
164 while (note > 83) {
165 out[pos++] = '\'';
166 note -= 12;
167 }
168 while (note < 60) {
169 out[pos++] = ',';
170 note += 12;
171 }
172 out[pos++] = ' ';
173
174 Display.print((char *)out);
175}
176
177void loop() {
178 Keypad.tick();
179
180 if (Keypad.available()) {
181 int push = Keypad.isPressed(0205) ? 1 : 0;
182
183 Display.setFont(u8g2_font_helvR08_tr);
184 Display.setDrawColor(0);
185 Display.drawBox(0, 10, 64, 12);
186 Display.setDrawColor(1);
187 Display.setCursor(0, 20);
188 while (Keypad.available()) {
189 keypadEvent k = Keypad.read();
190
191 int side = (k.bit.KEY >>6) & 0b11;
192 int row = (k.bit.KEY >> 3) & 0b111;
193 int col = (k.bit.KEY >> 0) & 0b111;
194 bool pressed = (k.bit.EVENT == KEY_JUST_PRESSED);
195
196 // Now look up the note
197 int8_t note = currentLayout.notes[side][row][col][push];
198
199 Serial.printf("%03o %d,%d,%d %d %d\n", k.bit.KEY, side, col, row, note, k.bit.EVENT);
200 //Display.fillRect(0, 16, 40, 8, BLACK);
201 if (pressed) {
202 displayNoteName(note);
203 }
204 VS1053_MIDI.write(pressed ? MIDI_NOTE_ON : MIDI_NOTE_OFF);
205 VS1053_MIDI.write(note);
206 VS1053_MIDI.write(127);
207 }
208 Display.nextPage();
209 }
210}
211
212void midiSetInstrument(uint8_t chan, uint8_t inst) {
213 if (chan > 15) return;
214 inst --; // page 32 has instruments starting with 1 not 0 :(
215 if (inst > 127) return;
216
217 VS1053_MIDI.write(MIDI_CHAN_PROGRAM | chan);
218 VS1053_MIDI.write(inst);
219}
220
221
222void midiSetChannelVolume(uint8_t chan, uint8_t vol) {
223 if (chan > 15) return;
224 if (vol > 127) return;
225
226 VS1053_MIDI.write(MIDI_CHAN_MSG | chan);
227 VS1053_MIDI.write(MIDI_CHAN_VOLUME);
228 VS1053_MIDI.write(vol);
229}
230
231void midiSetChannelBank(uint8_t chan, uint8_t bank) {
232 if (chan > 15) return;
233 if (bank > 127) return;
234
235 VS1053_MIDI.write(MIDI_CHAN_MSG | chan);
236 VS1053_MIDI.write((uint8_t)MIDI_CHAN_BANK);
237 VS1053_MIDI.write(bank);
238}
239
240void midiNoteOn(uint8_t chan, uint8_t n, uint8_t vel) {
241 if (chan > 15) return;
242 if (n > 127) return;
243 if (vel > 127) return;
244
245 VS1053_MIDI.write(MIDI_NOTE_ON | chan);
246 VS1053_MIDI.write(n);
247 VS1053_MIDI.write(vel);
248}
249
250void midiNoteOff(uint8_t chan, uint8_t n, uint8_t vel) {
251 if (chan > 15) return;
252 if (n > 127) return;
253 if (vel > 127) return;
254
255 VS1053_MIDI.write(MIDI_NOTE_OFF | chan);
256 VS1053_MIDI.write(n);
257 VS1053_MIDI.write(vel);
258}