-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathLSDJmi.ino
560 lines (442 loc) · 16.3 KB
/
LSDJmi.ino
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
//
// LSDJmi. Minimal support for the MIDI out mode of "Arduinoboy" version of LSDJ.
// Copyright (C) 2018, Aleh Dzenisiuk.
//
// See the actual "Arduinoboy" project at https://github.com/trash80/Arduinoboy
// which supports other Gameboy programs and modes of operation.
//
#include <a21.hpp>
#include <avr/eeprom.h>
using namespace a21;
/**
* Our main code: reads messages from LSDJ and generates corresponding MIDI events.
*/
template<typename pinCLK, typename pinOUT, typename pinMIDI, typename led>
class LSDJmi {
private:
/** CC messages can be treated differently depending on the settings of the current channel. */
enum ChannelCCMode : uint8_t {
/** In this mode there is a single CC associated with a Gameboy channel.
* The value of the X command will be scaled from 00-6F to 00-7F range of the control's value. */
ChannelCCModeSingle = 0,
/** In this mode the first nibble of the X command argument selects one of 7 CCs associated with a Gameboy.
* The second nibble is scaled from 0-F range to 00-7F range of the control's value. */
ChannelCCModeScaled
};
/** Settings per Gameboy channel adjustable at run time by the user. */
struct ChannelConfig {
// MIDI channel that should be used for all MIDI messages trigerred by LSDJ commands in this Gameboy channel.
uint8_t midiChannel;
// How to treat CC messages for this channel.
ChannelCCMode ccMode;
// In the 'single' mode only the first element is used as a CC number for any Xnn command.
// In the 'scaled' CC mode however these are MIDI CC numbers to associate with the high nibble in Xnn command.
uint8_t ccNumbers[7];
// Default MIDI velocity for the notes in this channel.
uint8_t velocity;
};
// User-adjustable settings per Gameboy channel, like what MIDI channel to use, etc.
ChannelConfig channels[4];
/** In case we receive Gameboy channel config changes, then we need to keep track of what's coming next; these are the possibilities. */
enum ChannelConfigState : uint8_t {
// Not in the channel config mode at the moment.
ChannelConfigStateIdle = 0,
// Just got a channel config start command, waiting for the first CC mode and MIDI channel command.
ChannelConfigStateCCModeAndMIDIChannel,
// Expect one or more definitions of MIDI CCs.
ChannelConfigStateCC1,
ChannelConfigStateCC2,
ChannelConfigStateCC3,
ChannelConfigStateCC4,
ChannelConfigStateCC5,
ChannelConfigStateCC6,
ChannelConfigStateCC7,
// Expecting the default note velocity.
ChannelConfigStateVelocity
};
/** State info we store per channel. */
struct ChannelState {
// The most recent note we've triggered from this channel.
uint8_t currentNote;
// In case the user has just started configuring the channel, then here we keep track of the commands to expect.
ChannelConfigState configState;
// The total number of CC numbers we expect to receive.
uint8_t configStateTotalCCs;
};
// Our own per channel runtime state.
ChannelState channelStates[4];
static bool readByte(uint8_t& b) {
b = 0;
const int d1 = 80;
delayMicroseconds(d1);
pinCLK::setLow();
delayMicroseconds(d1);
const int d2 = 2;
pinCLK::setHigh();
delayMicroseconds(d2);
if (!pinOUT::read())
return false;
for (uint8_t i = 7; i > 0; i--) {
pinCLK::setLow();
delayMicroseconds(d2);
pinCLK::setHigh();
delayMicroseconds(d2);
b <<= 1;
if (pinOUT::read())
b |= 1;
}
return true;
}
/**
* State of our protocol parser.
*
* The protocol is nice and simple. A byte stream is coming from the Gameboy (we poll it for the next byte)
* where bit 7 of all the incoming bytes is set to 1, so effectively we deal with 7 bit values.
*
* The following 1 byte general messages can appear in this stream:
*
* 1111 1111 — Clock tick. Not used in this project.
* 1111 1110 - Stop. The user has stopped playback of a pattern/chain/song.
* 1111 1101 - Start. The user has started playback of a pattern/chain/song.
* 1111 1100 - Not used.
*
* The are also 2 byte messages corresponding to special LSDJ commands (cc is Gameboy channel number
* the command was played in):
*
* 1111 00cc 1ddd dddd - Note on/off. N or Q command in LSDJ. The data bits correspond to a MIDI note with 0 meaning "off".
* 1111 01cc 1ddd dddd - Control Change. X command in LSDJ. The data bits define both the control and its value (depends on settings).
* 1111 10cc 1ddd dddd - Program Change. Y command in LSDJ. The data bits define the program number.
*
* That's it!
*/
enum ReceiverState : uint8_t {
/** The next byte received is expected to be a command. */
ReceiverStateCommand,
/** The current command expects a data byte. */
ReceiverStateData
} state = ReceiverStateCommand;
/** Codes of LSDJ's special MIDI commands. */
enum LSDJCommand : uint8_t {
/** N or Q command. Note on/off. */
LSDJCommandN = 0,
/** X command. Control Change. */
LSDJCommandX = 1,
/** Y command. Program Change. */
LSDJCommandY = 2
} command;
/** Gameboy's channel the current LSDJ command applies to. */
enum LSDJChannel : uint8_t {
LSDJChannelPU1 = 0,
LSDJChannelPU2 = 1,
LSDJChannelWAV = 2,
LSDJChannelNOI = 3
} channel;
/** The data byte for the current command. */
uint8_t data;
/** True, if we've seen the start event. Refuse to send notes otherwise. */
bool started = false;
/** Our MIDI OUT pin, software serial here. */
SerialTx<pinMIDI, 31250> midiOut;
void stopCurrentNote(LSDJChannel channel) {
ChannelState& channelState = channelStates[channel];
if (channelState.currentNote) {
ChannelConfig& channelConfig = channels[channel];
midiOut.write(0x80 | channelConfig.midiChannel);
midiOut.write(channelState.currentNote);
midiOut.write(0x40);
channelState.currentNote = 0;
}
}
void resetConfigState(LSDJChannel channel) {
ChannelState& channelState = channelStates[channel];
if (channelState.configState != ChannelConfigStateIdle) {
channelState.configState = ChannelConfigStateIdle;
led::clear();
}
}
void stopAllNotes() {
for (uint8_t channel = LSDJChannelPU1; channel <= LSDJChannelNOI; channel++) {
stopCurrentNote((LSDJChannel)channel);
resetConfigState((LSDJChannel)channel);
}
}
public:
// Invalid MIDI CC number to use 'no value' instead of 0 (since 0 can be the actual CC number).
const uint8_t NoCC = 0xFF;
LSDJmi()
: channels({
{
.midiChannel = 0,
.ccMode = ChannelCCModeSingle,
.ccNumbers = { NoCC, NoCC, NoCC, NoCC, NoCC, NoCC, NoCC },
.velocity = 0x3F
},
{
.midiChannel = 0,
.ccMode = ChannelCCModeSingle,
.ccNumbers = { NoCC, NoCC, NoCC, NoCC, NoCC, NoCC, NoCC },
.velocity = 0x3F
},
{
.midiChannel = 0,
.ccMode = ChannelCCModeSingle,
.ccNumbers = { NoCC, NoCC, NoCC, NoCC, NoCC, NoCC, NoCC },
.velocity = 0x3F
},
{
.midiChannel = 0,
.ccMode = ChannelCCModeSingle,
.ccNumbers = { NoCC, NoCC, NoCC, NoCC, NoCC, NoCC, NoCC },
.velocity = 0x3F
},
}),
channelStates({{0},{0},{0},{0}})
{
}
void begin() {
led::begin();
pinCLK::setOutput();
pinCLK::setHigh();
pinOUT::setInput(false);
midiOut.begin();
}
void check() {
uint8_t b;
if (!readByte(b))
return;
if (b >= 0x70) {
// No matter the current state, if we see a byte that looks like a command, then we start checking it.
switch (b) {
case 0x7D:
// The user has started playing the current pattern/chain/song.
if (!started) {
//~ Serial.println("Start");
started = true;
}
break;
case 0x7E:
// The user has stopped playing the current pattern/chain/song.
if (started) {
//~ Serial.println("Stop");
started = false;
stopAllNotes();
}
break;
case 0x7C:
case 0x7F:
// Unused.
break;
default:
channel = (LSDJChannel)(b & 0x3);
command = (LSDJCommand)((b >> 2) & 0x3);
state = ReceiverStateData;
break;
}
} else if (state == ReceiverStateData) {
data = b;
state = ReceiverStateCommand;
if (!started) {
return;
}
led::set();
ChannelConfig& channelConfig = channels[channel];
ChannelState& channelState = channelStates[channel];
switch (command) {
case LSDJCommandN:
// Note on/off.
resetConfigState(channel);
stopCurrentNote(channel);
if (data != 0) {
channelState.currentNote = data;
midiOut.write(0x90 | channelConfig.midiChannel);
midiOut.write(data);
midiOut.write((uint16_t)channelConfig.velocity * 0x7F / 0x6F);
}
break;
case LSDJCommandX:
// Control Change (CC)
switch (channelState.configState) {
// Not in the channel config mode, just a normal CC.
case ChannelConfigStateIdle:
{
uint8_t value;
uint8_t ccNumber;
switch (channelConfig.ccMode) {
case ChannelCCModeSingle:
value = (uint16_t)data * 0x7F / 0x6F;
ccNumber = channelConfig.ccNumbers[0];
break;
case ChannelCCModeScaled:
value = (uint16_t)(data & 0x0F) * 0x7F / 0xF;
ccNumber = channelConfig.ccNumbers[(data >> 4) & 0x0F];
break;
}
if (ccNumber != NoCC) {
midiOut.write(0xB0 | channelConfig.midiChannel);
midiOut.write(ccNumber);
midiOut.write(value);
}
}
break;
case ChannelConfigStateCCModeAndMIDIChannel:
{
uint8_t newMIDIChannel = (data & 0xF);
if (channelConfig.midiChannel != newMIDIChannel) {
// Let's make sure we have everything stopped in the old MIDI channel if it's changing.
stopCurrentNote(channel);
channelConfig.midiChannel = newMIDIChannel;
}
// Number of CC configStateTotalCCs to expect.
channelState.configStateTotalCCs = (data >> 4) & 0xF;
// Reset the current CC map so any commands still referring them won't produce unexpected results.
for (uint8_t i = 0; i < 7; i++) {
channelConfig.ccNumbers[i] = NoCC;
}
if (channelState.configStateTotalCCs > 0) {
channelConfig.ccMode = (channelState.configStateTotalCCs == 1) ? ChannelCCModeSingle : ChannelCCModeScaled;
channelState.configState = (ChannelConfigState)(ChannelConfigStateCC1 + channelState.configStateTotalCCs - 1);
} else {
// If no CCs is expected, then jump into the next state whatever it is.
channelState.configState = (ChannelConfigState)(ChannelConfigStateCC7 + 1);
}
}
break;
case ChannelConfigStateCC1:
case ChannelConfigStateCC2:
case ChannelConfigStateCC3:
case ChannelConfigStateCC4:
case ChannelConfigStateCC5:
case ChannelConfigStateCC6:
case ChannelConfigStateCC7:
{
uint8_t left = channelState.configState - ChannelConfigStateCC1;
channelConfig.ccNumbers[channelState.configStateTotalCCs - 1 - left] = data;
if (left > 0) {
channelState.configState = (ChannelConfigState)(channelState.configState - 1);
} else {
channelState.configState = ChannelConfigStateVelocity;
}
}
break;
case ChannelConfigStateVelocity:
channelConfig.velocity = data;
resetConfigState(channel);
break;
}
break;
case LSDJCommandY:
resetConfigState(channel);
if (data == 0x6F) {
// Special patch number, treating it as "enter channel configuration mode".
channelState.configState = ChannelConfigStateCCModeAndMIDIChannel;
led::set();
} else {
// Program Change
midiOut.write(0xC0 | channelConfig.midiChannel);
midiOut.write(data);
break;
}
}
led::clear();
}
}
};
/**
* The LED is not necessary in this project, see below on how to disable it.
* We use it only to blink occasionally to not spend much power on the LED but still to allow
* the user to see when the device is turned on.
* We also toggle it when sending MIDI messages, so it is possible to see that they at least leave the device.
*/
template<typename pinLED, uint32_t onTime = 10, uint32_t offTime = 2000>
class LED {
typedef LED<pinLED, onTime, offTime> Self;
static inline Self& getSelf() { static Self s; return s; }
uint32_t nextBlinkTime;
bool blinkState;
uint8_t toggleState;
static void update() {
Self& self = getSelf();
pinLED::write((self.toggleState > 0) ^ self.blinkState);
}
public:
static void begin() {
pinLED::setOutput();
pinLED::setLow();
Self& self = getSelf();
self.nextBlinkTime = millis();
self.blinkState = false;
self.toggleState = 0;
check();
}
static void check() {
Self& self = getSelf();
if (millis() >= self.nextBlinkTime) {
self.blinkState = !self.blinkState;
self.nextBlinkTime = millis() + (self.blinkState ? onTime : offTime);
update();
}
}
static void set() {
getSelf().toggleState++;
update();
}
static void clear() {
getSelf().toggleState--;
update();
}
};
/**
* When a dummy pin is used for a LED, then we don't need to do anything at all.
* I.e. passing UnusedPin<> should eliminate any LED related code.
*/
template<>
class LED< UnusedPin<> > {
public:
static void begin() {}
static void check() {}
static void set() {}
static void clear() {}
};
//
//
//
// Change the pin to match your board.
typedef LED< FastPin<2> > led;
//! If you don't want to use a LED, then use this definition instead (will save ~200 bytes):
//! typedef LED< UnusedPin<> > led;
// Our receiver of LSDJ messages. The pins are Gameboy's CLK, Gameboy's OUT (our input) and a MIDI out.
//
// Gameboy's Link Port pin-out (looking at the Gameboy):
// ___
// ______/ \______
// / \
// | 5 CLK 3 IN 1 +5V |
// | 6 GND 4 ? 2 OUT |
// |_______________________|
//
// One half of the link cable that I use (the part with a small plug) is missing both Vcc and GND wires. To make it work:
// - IN is tied to the GND of our board;
// - OUT is pulled up to the board's Vcc via a 1K resistor.
//
LSDJmi< FastPin<4>, FastPin<3>, FastPin<1>, led > mi;
bool readCalibrationByte(uint8_t& b) {
b = eeprom_read_byte((uint8_t*)0);
return b == (uint8_t)(~eeprom_read_byte((uint8_t*)1));
}
void setup() {
// If you use your microcontroler without a crystal, like I do with an ATtiny85, then you'll need to use your calibration byte.
// Here I assume it is stored in EEPROM at address 0 by a calibration f/w, but this can be different in your setup.
uint8_t calibrationByte;
if (readCalibrationByte(calibrationByte)) {
OSCCAL = calibrationByte;
} else {
// Alternatively you can manually find a suitable value and hardcode it.
OSCCAL = 0xEF;
}
mi.begin();
}
void loop() {
led::check();
mi.check();
delayMicroseconds(10);
}