[Continuation of my prior post]
That seems to work well enough, but one thing I'd really like to improve is the "feel" of the beat and the ability to plug in more complicated sequences. As currently set up, each beat is locked in a static "grid" and I can't easily add additional subdivisions (such as 32nd notes or triplets), or play notes longer than a 16th. Also, the timing might be a bit off since calculations happen between each delay. I can't tell.
So, for my next step I'd like to play a MIDI file, or perhaps a sanitized, simplified version thereof, using a timer instead of delays. I've seen examples of how to accomplish a single task using a timer, but I get confused when trying to apply to a long list of events. In pseudo-code, I'd like to:
0. Load 2D array with a sequence of { int tick, int note, int velocity } lines, with a noteOn and noteOff for each note
1. Loop through the array as a timer counts ticks for each measure.
2. With each array item, check if the tick has passed. If so, trigger the noteOn. If not, wait a bit (1ms?) and check that point in the array again. (I think that triggering within about 10ms of the desired time will be good enough for me.)
3. If the timer count hits 384 (384 = 4 beats x 4 subdivisions x 24 ticks; I think that's a tenth as many as the MIDI file I was working from), reset it to zero and go back to step 1.
Is that a sensible way to set it up? How could I optimize it better to allow more cycles for reading and interpreting input?
I've given a first shot at coding this, but I'm getting errors relating to pointers (which I'm trying to learn about) in the loop() section where I look up the note in the array (2nd field) based on the tick in the array (1st field). Should I be setting this up differently, or would it be a simple syntax tweak to fix this?
#include <NewSoftSerial.h>
NewSoftSerial mySerial(2, 3); //Soft TX on 3, we don't use RX in this code
byte note = 0; //The MIDI note value to be played
byte resetMIDI = 4; //Tied to VS1053 Reset line
byte ledPin = 13; //MIDI traffic indicator
int instrument = 0;
int scale_major[] = {0, 2, 4, 5, 7, 9, 11}; //one octave of major scale
int scale_minpent[] = {0, 2, 3, 5, 6, 7, 10}; //minor pentatonic scale
int scale_consonant[] = {0, 2, 4, 7, 9, 12, 14}; //major pentatonic scale plus octave and 9th (to be same array length as others)
int drum_motown[100][3] = //Tick (out of 384 per bar), note, velocity
{ //This is an attempt to adapt data from an actual MIDI file, with
{ 0,54,94 }, //matching 0-velocity lines in place of noteOff messages
{ 0,48,97 },
{ 8,54,0 },
{ 48,54,75 },
{ 51,48,0 },
{ 54,54,0 },
{ 96,50,106 },
{ 96,54,96 },
{ 103,54,0 },
{ 127,50,0 },
{ 144,54,73 },
{ 153,54,0 },
{ 171,50,92 },
{ 177,50,0 },
{ 192,54,82 },
{ 201,54,0 },
{ 219,48,85 },
{ 240,54,84 },
{ 240,48,0 },
{ 246,54,0 },
{ 267,48,86 },
{ 273,48,0 },
{ 288,54,101 },
{ 288,50,103 },
{ 296,54,0 },
{ 318,50,0 },
{ 336,54,77 },
{ 344,54,0 },
{ 363,50,70 },
{ 368,50,0 }
};
int drumSpot = 0;
int drumSpotTime = 0;
long curTime = 0;
int timeclock96 = 0; //96 ticks per beat, based on 4 subdivs per beat and 24 ticks per subdiv (not 240 like the source midi file)
void setup() {
Serial.begin(57600);
//Setup soft serial for MIDI control
mySerial.begin(31250);
//Reset the VS1053
pinMode(resetMIDI, OUTPUT);
digitalWrite(resetMIDI, LOW);
delay(100);
digitalWrite(resetMIDI, HIGH);
delay(100);
talkMIDI(0xB0, 0x07, 30); //0xB0 is channel message for channel 1 (0), set channel volume to 30
talkMIDI(0xB0, 0, 0x00); //Bank select GM1
talkMIDI(0xB9, 0x07, 50); //0xB0 is channel message for channel 10 (9), set channel volume to 50
talkMIDI(0xB9, 0, 0x78); //Bank select drums
curTime = millis();
}
void loop() {
timeclock96 = (millis() - curTime)/96;
Serial.println(timeclock96);
for (int j = drumSpot; j<100; j++) {
drumSpotTime = (int)drum_motown[drumSpot,0];
if (drumSpotTime < timeclock96) {
noteOn(0, (byte)10, drum_motown[drumSpot,1], drum_motown[drumSpot,2]); //ERROR HERE: invalid conversion from int* to byte
}
if (timeclock96 >= 96) {
curTime = millis();
}
// playNoodling(0, random(48,59), scale_consonant, 180, 0);
}
void playNoodling(byte channel, int key, int scale[], float tempo, int instrument) {
Serial.print(" Instrument: ");
Serial.println(instrument, DEC);
talkMIDI(0xC0, instrument, 0); //Set instrument number. 0xC0 is a 1 data byte command
talkMIDI(0xCA, 30, 0); //Set instrument number. 0xC0 is a 1 data byte command
for (int i = 0; i < 16; i++) {
int note = key + scale[random(0, 6)];
noteOn(1, channel, note, 80);
noteOn(0, 10, drum_pattern1[i], 50);
//1 minute 60 seconds 1000 ms
//--------- x --------- x --------
//120 Beats 1 minute 1 second
delay(60000/tempo);
noteOff(1, channel, note, 0);
noteOff(0, 10, drum_pattern1[i], 0);
}
}
void allOff(byte channel) {
talkMIDI ( (0xB0 | channel), 123, 0);
}
//Send a MIDI note-on message. Like pressing a piano key
//channel ranges from 0-15
void noteOn(boolean instr, byte channel, byte note, byte attack_velocity) {
talkMIDI( (0x90 | channel), note, attack_velocity);
}
//Send a MIDI note-off message. Like releasing a piano key
void noteOff(boolean instr, byte channel, byte note, byte release_velocity) {
talkMIDI( (0x80 | channel), note, release_velocity);
}
//Plays a MIDI note. Doesn't check to see that cmd is greater than 127, or that data values are less than 127
void talkMIDI(byte cmd, byte data1, byte data2) {
digitalWrite(ledPin, HIGH);
// Serial.write(cmd);
// Serial.write(data1);
mySerial.print(cmd, BYTE);
mySerial.print(data1, BYTE);
//Some commands only have one data byte. All cmds less than 0xBn have 2 data bytes
//(sort of: http://253.ccarh.org/handout/midiprotocol/)
if( (cmd & 0xF0) <= 0xB0)
// Serial.write(data2);
mySerial.print(data2, BYTE);
digitalWrite(ledPin, LOW);
}
Many thanks in advance to anyone who can provide a suggestion as to how to improve this code! My apologies that it's so scrappy!