Hi all,
Hopefully some of you have some experience with FL Studio and maybe have encountered this problem. I've checked Image Line forums and people have had this issue before on many versions of FL Studio going back to version 11, but they were rarely resolved. When they were, it was about some MIDI service that needed to be disabled which was related to the midi input instruments they had purchased. I don't have any midi input instruments, just Teensy MIDI playback instruments. Anyways, my issue is below:
I've had an issue with FL Studio interrupting playback of music by refreshing MIDI devices. This forces playback to stop for the duration that it is refreshing (less than 1 second duration) and then restart from the beginning of the song. It refreshes 3 times in quick succesion; the same number of times as I have Teensy devices acting as Native USB MIDI devices.
I use my MIDI devices as playback devices rather than input devices. My channel rack on all my files consists solely of "MIDI Out" Channels. My teensy MIDI devices use MIDI Events to trigger interrupts to actuate some instruments that I've made. The devices are plugged into a powered USB 3.0 port and the port goes to the front panel USB 2.0 port on my PC. I have a PCIE USB 3.0 port that I had used in place of the front panel, but I encountered the problem there as well.
The timing on when this issue occurs is seemingly random. It doesn't occur at specific points within specific songs. I've gone several songs at times without encountering the issue. Often enough to be problematic though, I can't go more than 1 minute 30 seconds from start of playback to encounter it.
Here's the source code and compile settings for my instruments. My coding practices are probably shoddy; I'm not a professional.
Musical Floppy Drives:
Board - Teensy 3.6
USB Type - MIDI
CPU Speed - 180 MHz
Optimize - "Faster"
Other stepper motor actuated instruments
Board - Teensy 3.6
USB Type - MIDI
CPU Speed - 180 MHz
Optimize - "Faster"
Xylophone
Other stepper motor actuated instruments
Board - Teensy 3.2
USB Type - MIDI
CPU Speed - 96 MHz (overclocked)
Optimize - "Faster"
Teensy 3.2
Hopefully some of you have some experience with FL Studio and maybe have encountered this problem. I've checked Image Line forums and people have had this issue before on many versions of FL Studio going back to version 11, but they were rarely resolved. When they were, it was about some MIDI service that needed to be disabled which was related to the midi input instruments they had purchased. I don't have any midi input instruments, just Teensy MIDI playback instruments. Anyways, my issue is below:
I've had an issue with FL Studio interrupting playback of music by refreshing MIDI devices. This forces playback to stop for the duration that it is refreshing (less than 1 second duration) and then restart from the beginning of the song. It refreshes 3 times in quick succesion; the same number of times as I have Teensy devices acting as Native USB MIDI devices.
I use my MIDI devices as playback devices rather than input devices. My channel rack on all my files consists solely of "MIDI Out" Channels. My teensy MIDI devices use MIDI Events to trigger interrupts to actuate some instruments that I've made. The devices are plugged into a powered USB 3.0 port and the port goes to the front panel USB 2.0 port on my PC. I have a PCIE USB 3.0 port that I had used in place of the front panel, but I encountered the problem there as well.
The timing on when this issue occurs is seemingly random. It doesn't occur at specific points within specific songs. I've gone several songs at times without encountering the issue. Often enough to be problematic though, I can't go more than 1 minute 30 seconds from start of playback to encounter it.
Here's the source code and compile settings for my instruments. My coding practices are probably shoddy; I'm not a professional.
Musical Floppy Drives:
Board - Teensy 3.6
USB Type - MIDI
CPU Speed - 180 MHz
Optimize - "Faster"
Code:
#include <TimerOne.h>
//Conditions and constraints
unsigned int period[60] = { 30402, 28696 , 27085 , 25565 , 24130 , 22776 , 21497 , 20291 , 19152 , 18077 , 17063 , 16105,
15201 , 14348 , 13543 , 12782 , 12065 , 11388 , 10749 , 10145 , 9576 , 9039 , 8531 , 8052,
7600 , 7174 , 6771 , 6391 , 6033 , 5694 , 5374 , 5073 , 4788 , 4519 , 4266 , 4026,
3800 , 3587 , 3386 , 3196 , 3016 , 2847 , 2687 , 2536 , 2394 , 2260 , 2133 , 2013,
1900 , 1793 , 1693 , 1598 , 1508 , 1423 , 1344 , 1268 , 1197 , 1130 , 1066 , 1007
// Uncorrected frequencies. I find that pulsing at 20 cents above the nominal frequency gets each note better tuned.
// https://docs.google.com/spreadsheets/d/12MbrmTmzfQR1kzVH3Orp17z25sn0WuJCodh59aa5hxo/edit?usp=sharing
//30581 , 28864 , 27244 , 25715 , 24272 , 22909 , 21624 , 20410 , 19264 , 18183 , 17163 , 16199 ,
// 15290 , 14432 , 13622 , 12857 , 12136 , 11454 , 10812 , 10205 , 9632 , 9091 , 8581 , 8099 ,
// 7645 , 7216 , 6811 , 6428 , 6068 , 5727 , 5406 , 5102 , 4816 , 4545 , 4290 , 4049 ,
// 3822 , 3608 , 3405 , 3214 , 3034 , 2863 , 2703 , 2551 , 2408 , 2272 , 2145 , 2024,
// 1911 , 1804 , 1702 , 1607 , 1517 , 1431 , 1351 , 1275 , 1204 , 1136 , 1072 , 1012 ,
};
float pitchfactor[164] = {0.500 , 0.504 , 0.509 , 0.513 , 0.517 , 0.522 , 0.526 , 0.531 , 0.535 , 0.540 , 0.544 , 0.549 , 0.554 , 0.558 , 0.563 , 0.568 , 0.573 , 0.578 , 0.583 , 0.588 ,
0.593 , 0.598 , 0.603 , 0.608 , 0.613 , 0.618 , 0.624 , 0.629 , 0.634 , 0.640 , 0.645 , 0.651 , 0.656 , 0.662 , 0.668 , 0.673 , 0.679 , 0.685 , 0.691 , 0.697 ,
0.703 , 0.709 , 0.715 , 0.721 , 0.727 , 0.733 , 0.739 , 0.746 , 0.752 , 0.759 , 0.765 , 0.772 , 0.778 , 0.785 , 0.791 , 0.798 , 0.805 , 0.812 , 0.819 , 0.826 ,
0.833 , 0.840 , 0.847 , 0.854 , 0.862 , 0.869 , 0.876 , 0.884 , 0.892 , 0.899 , 0.907 , 0.915 , 0.922 , 0.930 , 0.938 , 0.946 , 0.954 , 0.962 , 0.971 , 0.979 ,
0.987 , 1.000 , 1.004 , 1.013 , 1.021 , 1.030 , 1.039 , 1.048 , 1.057 , 1.066 , 1.075 , 1.084 , 1.093 , 1.103 , 1.112 , 1.122 , 1.131 , 1.141 , 1.151 , 1.160 ,
1.170 , 1.180 , 1.190 , 1.201 , 1.211 , 1.221 , 1.232 , 1.242 , 1.253 , 1.263 , 1.274 , 1.285 , 1.296 , 1.307 , 1.318 , 1.330 , 1.341 , 1.352 , 1.364 , 1.376 ,
1.387 , 1.399 , 1.411 , 1.423 , 1.435 , 1.448 , 1.460 , 1.473 , 1.485 , 1.498 , 1.511 , 1.523 , 1.536 , 1.550 , 1.563 , 1.576 , 1.590 , 1.603 , 1.617 , 1.631 ,
1.645 , 1.659 , 1.673 , 1.687 , 1.702 , 1.716 , 1.731 , 1.746 , 1.760 , 1.775 , 1.791 , 1.806 , 1.821 , 1.837 , 1.853 , 1.868 , 1.884 , 1.901 , 1.917 , 1.933 ,
1.950 , 1.966 , 1.983 , 2.000
};
const byte RESOLUTION = 25;
const byte MAX_POSITION = 158;
// data
byte currentPosition[17] = {LOW, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
int basePeriod[17] = {LOW, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
bool currentStateStep[17] = {0 , LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW};
bool currentStateDir[17] = {0 , LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW, LOW};
int currentTick[] = {LOW, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
byte note2note;
byte note2octave;
float pitchShift[17];
byte currentNote[] = {LOW, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
byte lvar = 0;
//pin assignments
const byte STEPPIN[17] = {LOW, // Teensy MIDI channels are given as channels 1-16, not 0-15
2, //Device 1
4, //Device 2
6, //Device 3
8, //Device 4
11, //Device 5
25, //Device 6
27, //Device 7
29, //Device 8
14, //Device 9
16, //Device 10
18, //Device 11
20, //Device 12
22, //Device 13
34, //Device 14
36, //Device 15
38, //Device 16
};
const byte DIRPIN[] = { LOW,
3, //Device 1
5, //Device 2
7, //Device 3
9, //Device 4
10, //Device 5
24, //Device 6
26, //Device 7
28, //Device 8
15, //Device 9
17, //Device 10
19, //Device 11
21, //Device 12
23, //Device 13
35, //Device 14
37, //Device 15
39, //Device 16
};
const byte relay[] = {30, 31, 32};
void setup() {
for (int channel = 1; channel <= 16; channel++) {
pinMode(STEPPIN[channel], OUTPUT);
pinMode(DIRPIN[channel], OUTPUT);
}
for (int setup_relays = 0; setup_relays <= 2; setup_relays++) {
pinMode(relay[setup_relays], OUTPUT);
}
for (int i = 0; i<17; i++){
pitchShift[i] = 1.000;
}
//period_lib();
//pitchbend_Calculate();
usbMIDI.setHandleNoteOff(OnNoteOff);
usbMIDI.setHandleNoteOn(OnNoteOn);
usbMIDI.setHandlePitchChange(OnPitchChange);
Timer1.initialize(RESOLUTION);
Timer1.attachInterrupt(processNotes);
Serial.begin(250000);
}
void loop() {
usbMIDI.read();
}
void OnNoteOn(byte channel, byte note, byte velocity) {
currentNote[channel] = note;
note2note = note % 12;
note2octave = note / 12;
if (note2octave < 4) { //37-48
basePeriod[channel] = period[note2note];
}
else if (note2octave < 5) { //49-60
basePeriod[channel] = period[12 + note2note];
}
else if (note2octave < 6) { //61-72
basePeriod[channel] = period[24 + note2note];
}
else if (note2octave < 7) { //73-84
basePeriod[channel] = period[36 + note2note];
}
else if (note2octave < 8) { //85-96
basePeriod[channel] = period[48 + note2note];
}
else if (note2octave < 10) { //109-120
relayFlip();
Serial.println(note);
}
else if (note == 127) {
resetDrives();
}
// Serial.print(note); Serial.print(" "); Serial.println(basePeriod[channel]);
}
void OnNoteOff(byte channel, byte note, byte velocity) {
if (currentNote[channel] == note) {
basePeriod[channel] = 0;
}
}
void OnPitchChange(byte channel, int pitch) {
pitch = pitch / 100;
pitchShift[channel] = pitchfactor[pitch+81];
Serial.println(pitch);
}
void processNotes() {
for (byte channel = 1; channel <= 16; channel++) {
if (basePeriod[channel]) { // if currentPeriod of a channel is nonzero
if (currentTick[channel] >= basePeriod[channel] / pitchShift[channel]) { //and if the current Period has past its period
currentTick[channel] = 0; //reset the tick
if (currentPosition[channel] >= MAX_POSITION) { // check if direction needs to change due to read carriage bounds
currentPosition[channel] = 0;
currentStateDir[channel] = !currentStateDir[channel];
digitalWrite(DIRPIN[channel], currentStateDir[channel]) ;
}
currentPosition[channel]++;
currentStateStep[channel] = !currentStateStep[channel]; //Step by changing the pin state
digitalWrite(STEPPIN[channel], currentStateStep[channel]) ;
}
else currentTick[channel] += RESOLUTION;
}
}
}
void period_lib() { // generates the periods used. Equation derived from site: http://www.phy.mtu.edu/~suits/NoteFreqCalcs.html
float c0 = 16.35;
float a = 1.059463094359;
long period_data[128];
for (int i = 0; i < 128; i++) {
period_data[i] = long(1000000 / (c0 * pow(a, i)));
if ((i % 12) < 1) Serial.println();
Serial.print(period_data[i]);
Serial.print(" , ");
}
}
void pitchbend_Calculate() {
for (int pitchbend = 0; pitchbend <= 163; pitchbend++) {
pitchfactor[pitchbend] = pow(2.0, (pitchbend - 81.5) / 81.5); //calculates ratio f_new/f_old
if ((pitchbend % 20) < 1) Serial.println();
Serial.print(pitchfactor[pitchbend], 3);
Serial.print(" , ");
}
}
void resetDrives() {
//Serial.println("resetDrives activated");
Timer1.detachInterrupt(); //Turn Interrupt off
for (byte channel = 1; channel <= 16; channel++) { // Set all drives direction to backwards
currentStateDir[channel] = HIGH;
digitalWrite(DIRPIN[channel], currentStateDir[channel]);
}
for (byte t = 0; t < 200; t++) { // All drives go backwards 200 ticks (max is 158)
for (byte channel = 1; channel <= 16; channel++) {
currentStateStep[channel] = !currentStateStep[channel];
digitalWrite(STEPPIN[channel], currentStateStep[channel]);
}
delayMicroseconds(7645); // add delay
}
for (byte channel = 1; channel <= 16; channel++) { // Set all drives direction to forwards
currentStateDir[channel] = LOW;
digitalWrite(DIRPIN[channel], currentStateDir[channel]);
currentPosition[channel] = 0;
}
Timer1.attachInterrupt(processNotes); // Turn interrupt back on
}
void relayFlip() {
lvar += 1;
switch (lvar) {
case 1:
digitalWrite(relay[0], HIGH);
break;
case 2:
digitalWrite(relay[0], LOW);
break;
case 3:
digitalWrite(relay[1], HIGH);
break;
case 4:
digitalWrite(relay[1], LOW);
break;
case 5:
digitalWrite(relay[2], HIGH);
break;
case 6:
digitalWrite(relay[2], LOW);
lvar = 0;
break;
default:
lvar = 0;
}
}
Other stepper motor actuated instruments
Board - Teensy 3.6
USB Type - MIDI
CPU Speed - 180 MHz
Optimize - "Faster"
Code:
IntervalTimer mainTimer;
IntervalTimer modTimer[3];
elapsedMicros phaseTimer[17];
/*
This code recieves MIDI data and plays the music on instruments
Instruments:
xylophone
scanner 1
scanner 2
receipt printers 1 & 2 (3 channels (stepper motors each){
1 channel for melody
1 channel for back and forth gear chatter percussion
1 channel for droning paper cutter sound
}
Stepper choir (4 channels)
viola (1 channel)
Total channels: (1 + 1 + 1 + 3 + 3 + 4 + 1 = 14
It achieves this through use of the following libraries
https://www.pjrc.com/teensy/td_midi.html
https://www.pjrc.com/teensy/td_libs_SPI.html
https://www.pjrc.com/teensy/td_timing_IntervalTimer.html
https://www.pjrc.com/teensy/interrupts.html
*/
//Constants
const int RESOLUTION = 15; // Microseconds between calls to NoteProcessing; Set to difference in periods for a low period note with pitch factor at finest difference. 4050 uS * (0.504 - 0.5) = 16.2
//----------------------------------Lookup Tables----------------------------------//
const unsigned int microPeriods[] = {
30578, 28861, 27242, 25713, 24270, 22909, 21622, 20409, 19263, 18182, 17161, 16198, //C0 - B0
30578, 28861, 27242, 25713, 24270, 22909, 21622, 20409, 19263, 18182, 17161, 16198, //C1 - B1
30578, 28861, 27242, 25713, 24270, 22909, 21622, 20409, 19263, 18182, 17161, 16198, //C2 - B2
15289, 14436, 13621, 12856, 12135, 11454, 10811, 10205, 9632, 9091, 8581, 8099, //C3 - B3
7645, 7218, 6811, 6428, 6068, 5727, 5406, 5103, 4816, 4546, 4291, 4050, //C4 - B4
3800 , 3587 , 3386 , 3196 , 3016 , 2847 , 2687 , 2536 , 2394 , 2260 , 2133 , 2013, //C5 - B5
1900 , 1793 , 1693 , 1598 , 1508 , 1423 , 1344 , 1268 , 1197 , 1130 , 1066 , 1007, //C6 - B6
950, 897, 847, 799, 754, 712, 672, 634, 599, 565, 533, 504, //C7 - B7
475, 449, 424, 400, 377, 356, 336, 317, 300,283, 267, 252, //C8 - B8
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //C9 - B9
};
const float pitchfactor[164] = {0.500 , 0.504 , 0.509 , 0.513 , 0.517 , 0.522 , 0.526 , 0.531 , 0.535 , 0.540 , 0.544 , 0.549 , 0.554 , 0.558 , 0.563 , 0.568 , 0.573 , 0.578 , 0.583 , 0.588 ,
0.593 , 0.598 , 0.603 , 0.608 , 0.613 , 0.618 , 0.624 , 0.629 , 0.634 , 0.640 , 0.645 , 0.651 , 0.656 , 0.662 , 0.668 , 0.673 , 0.679 , 0.685 , 0.691 , 0.697 ,
0.703 , 0.709 , 0.715 , 0.721 , 0.727 , 0.733 , 0.739 , 0.746 , 0.752 , 0.759 , 0.765 , 0.772 , 0.778 , 0.785 , 0.791 , 0.798 , 0.805 , 0.812 , 0.819 , 0.826 ,
0.833 , 0.840 , 0.847 , 0.854 , 0.862 , 0.869 , 0.876 , 0.884 , 0.892 , 0.899 , 0.907 , 0.915 , 0.922 , 0.930 , 0.938 , 0.946 , 0.954 , 0.962 , 0.971 , 0.979 ,
0.987 , 1.000 , 1.000 , 1.013 , 1.021 , 1.030 , 1.039 , 1.048 , 1.057 , 1.066 , 1.075 , 1.084 , 1.093 , 1.103 , 1.112 , 1.122 , 1.131 , 1.141 , 1.151 , 1.160 ,
1.170 , 1.180 , 1.190 , 1.201 , 1.211 , 1.221 , 1.232 , 1.242 , 1.253 , 1.263 , 1.274 , 1.285 , 1.296 , 1.307 , 1.318 , 1.330 , 1.341 , 1.352 , 1.364 , 1.376 ,
1.387 , 1.399 , 1.411 , 1.423 , 1.435 , 1.448 , 1.460 , 1.473 , 1.485 , 1.498 , 1.511 , 1.523 , 1.536 , 1.550 , 1.563 , 1.576 , 1.590 , 1.603 , 1.617 , 1.631 ,
1.645 , 1.659 , 1.673 , 1.687 , 1.702 , 1.716 , 1.731 , 1.746 , 1.760 , 1.775 , 1.791 , 1.806 , 1.821 , 1.837 , 1.853 , 1.868 , 1.884 , 1.901 , 1.917 , 1.933 ,
1.950 , 1.966 , 1.983 , 2.000 ,
};
const float vibrato[] = {0.97, 0.98, 0.99, 1.000, 1.01, 1.02, 1.03, 1.02, 1.01, 1.00, 0.99, 0.98, 0.97};
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
const int MAX_POSITION[] = {0, 0, 1000, -5, 0, 0, 0, 1000, -5, 0, 0, 0, 0, 0, 0, 0, 0}; // positive indicates back and forth stepper travel motion, negative indicates vibration back and forth, zero indicates continuous unidirectional travel
//----------------------------------Pins----------------------------------//
//For each channel, there is a stepper able to play different notes. This keeps track of that
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
const unsigned int channel2drive[] = {40, 40, 14, 16, 18, 24, 7, 34, 36, 38, 40, 40, 40, 40, 40, 40, 40};
const byte dinkChannel1 = 4; //interupt pin for dink channel 1
const byte dinkChannel2 = 9; //interupt pin for dink channel 2
// direction switches
const byte sc1sw1 = 27;
const byte sc1sw2 = 28;
const byte sc2sw1 = 10;
const byte sc2sw2 = 11;
//---------------------------------Pitch Shifting variable----------------------------//
float pitchShift [17] = {1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000, 1.000}; //pull numbers from pitchfactor to here to modulate pitch
//----------------------------------Vibrato Variables---------------------------------//
int modPeriod[] = {6000, 6000, 6000}; //map pan control to change the mod Period of up to 3 instruments
byte vchannelElmt = 0; //element of vchannel to look at
unsigned int vibratoSpeed = 6000; //
byte vchannel[3];
bool modTimerOn[3];
byte v0, v1, v2;
//----------------------------------dink variables----------------------------------//
long beat[dinkChannel2 + 1];
long phaseError[dinkChannel2 + 1];
//----------------------------------sleeping----------------------------------//
bool awake;
const byte sleepPinLength = 4;
const byte sleepPins[] = {26, 9, 13, 33};
//----------------------------------Stepper Motor Notes ----------------------------------//
int stepperNote[17] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}; // what note is this drive currently playing. (if negative, it's not playing)
unsigned int stepperPeriod[17]; // what period is this drive currently playing (if 0, it's not playing)
int Position[17];
unsigned int stepperTick[17]; //Current tick keeps track of where a note is in it's period
bool stepperState[33];
//Current tick keeps track of where a note is in it's period
void wakeywakey() {
awake = HIGH;
digitalWrite(26, HIGH);
digitalWrite(9, HIGH);
digitalWrite(13, LOW);
digitalWrite(33, LOW);
Serial.println("should be awake");
}
void goToSleep(){
awake = LOW;
digitalWrite(26, LOW);
digitalWrite(9, LOW);
digitalWrite(13, HIGH);
digitalWrite(33, HIGH);
Serial.println("should be asleep");
}
void myNoteOn(byte channel, byte note, byte velocity) {
//Serial.println("Note On");
if (velocity > 0) {
if (channel > 1 && channel < 14) { // channels 2 through 13 are steppers
if (!awake) wakeywakey();
if (channel == dinkChannel1 || channel == dinkChannel2) {
beat[channel] = phaseTimer[channel];
phaseTimer[channel] = 0;
if (stepperPeriod[channel] == 0) {
stepperPeriod[channel] = beat[channel];
}
return;
}
else {
stepperNote[channel] = note;
stepperPeriod[channel] = microPeriods[note] / pitchShift[channel];
}
}
}
}
void dink1() {
phaseError[dinkChannel1] = phaseTimer[dinkChannel1];
if (phaseError[dinkChannel1] < beat[dinkChannel1] / 2) { //if phase is closer to the preceding beat, then speed up to catch the next one.
stepperPeriod[dinkChannel1] = (beat[dinkChannel1] - phaseError[dinkChannel1]) * (32.0 / 17.0) / 360 * 1.8; //microseconds per beat * 1 beat per 1 rev of gear 4 * gear ratio from gear 4 to gear 1 * 1 rev per 360 deg * 1.8 deg per 1 step = microseconds per step between beats
}
else //if phase is too close to the next beat, then speeding up may not be an option; just miss the upcoming beat and hit the next one.
stepperPeriod[dinkChannel1] = (2 * beat[dinkChannel1] - phaseError[dinkChannel1]) * (32.0 / 17.0) / 360 * 1.8; //microseconds per beat * 1 beat per 1 rev of gear 4 * gear ratio from gear 4 to gear 1 * 1 rev per 360 deg * 1.8 deg per 1 step = microseconds per step between beats
if (phaseTimer[dinkChannel1] > 2 * beat[dinkChannel1]) {
stepperPeriod[dinkChannel1] = 0; //If the beat hasn't been updated, then stop playing
}
}
void dink2() {
phaseError[dinkChannel2] = phaseTimer[dinkChannel2];
if (phaseError[dinkChannel2] < beat[dinkChannel2] / 2) { //if phase is closer to the preceding beat, then speed up to catch the next one.
stepperPeriod[dinkChannel2] = (beat[dinkChannel2] - phaseError[dinkChannel2]) * (32.0 / 17.0) / 360 * 1.8; //microseconds per beat * 1 beat per 1 rev of gear 4 * gear ratio from gear 4 to gear 1 * 1 rev per 360 deg * 1.8 deg per 1 step = microseconds per step between beats
}
else //if phase is too close to the next beat, then speeding up may not be an option; just miss the upcoming beat and hit the next one.
stepperPeriod[dinkChannel2] = (2 * beat[dinkChannel2] - phaseError[dinkChannel2]) * (32.0 / 17.0) / 360 * 1.8; //microseconds per beat * 1 beat per 1 rev of gear 4 * gear ratio from gear 4 to gear 1 * 1 rev per 360 deg * 1.8 deg per 1 step = microseconds per step between beats
if (phaseTimer[dinkChannel2] > 2 * beat[dinkChannel2]) {
stepperPeriod[dinkChannel2] = 0; //If the beat hasn't been updated, then stop playing
}
}
void myNoteOff(byte channel, byte note, byte velocity) {
// if (MAX_POSITION[channel] < 1) return; // don't turn off the note if it's percussion. percussion turns itself off after abs(MAX_POSITION[channel]) number of steps or after phaseError exceeds 3*beat
if (channel == 3 || channel == 8 || channel == 4 || channel == 9) return;
if (stepperNote[channel] == note) { //don't turn off notes that are not the current note playing
stepperPeriod[channel] = 0;
stepperNote[channel] = -1;
//Serial.print("ending note "); Serial.println(note);
}
}
void myControlChange(byte channel, byte control, byte value) {
Serial.print(channel); Serial.print(" , "); Serial.print(control); Serial.print(" , "); Serial.println(value);
if (control == 100) { //if control = 100, then that's because the DAW has paused the playback or resumed playback. We'll put them to sleep even if it's resuming since any note on will wake them up.
goToSleep();
return;
}
value = abs(value - 64);
vibratoSpeed = map(value, 10, 64, 30000, 6000);
//Serial.print(" | "); Serial.println(vibratoSpeed);
if (control == 10) {
if (value > 10) {
//If we indicated that we want to add vibrato, then assign channel to proper timer.
//First check to see that the channel was not already assigned to a channel
if (vchannel[0] == channel || vchannel[1] == channel || vchannel[2] == channel) {
for (int i = 0; i < 3; i++) {
if (vchannel[i] == channel) {
if (modTimerOn[i]) {
modTimer[i].update(vibratoSpeed);
}
else {
switch (i) {
case 0:
modTimer[i].begin(mod0, vibratoSpeed);
modTimerOn[i] = HIGH;
break;
case 1:
modTimer[i].begin(mod1, vibratoSpeed);
modTimerOn[i] = HIGH;
break;
case 2:
modTimer[i].begin(mod2, vibratoSpeed);
modTimerOn[i] = HIGH;
}
}
}
}
}
//If not already assigned to a channel, then assign via a round robin style timer picker. This will boot out the channel currently playing on it.
else {
vchannel[vchannelElmt] = channel; // assigns vibrato to channel specified in event
if (modTimerOn[vchannelElmt]) {
modTimer[vchannelElmt].update(vibratoSpeed);
}
else {
switch (vchannelElmt) {
case 0:
modTimer[vchannelElmt].begin(mod0, vibratoSpeed);
modTimerOn[vchannelElmt] = HIGH;
break;
case 1:
modTimer[vchannelElmt].begin(mod1, vibratoSpeed);
modTimerOn[vchannelElmt] = HIGH;
break;
case 2:
modTimer[vchannelElmt].begin(mod2, vibratoSpeed);
modTimerOn[vchannelElmt] = HIGH;
break;
}
}
// Change vchannel element for next time we run out of Interval Timers
vchannelElmt += 1;
if (vchannelElmt >= 3) { //sizeof gives you the number of bytes for the whole array. Divide by bytes of one element in the array.
vchannelElmt = 0;
}
}
}
//if value is less than or equal to 10, then vibrato is unwanted. Turn off the channel-corresponding timer.
else {
for (int i = 0; i < 3; i++) {
if (vchannel[i] == channel) {
Serial.println("modTimer should be off");
modTimer[i].end(); //Turn the Timer off
modTimerOn[i] = LOW; //Signal that this Timer is not running
vchannelElmt = i; //Next channel to use vibrato can use this one
stepperPeriod[vchannel[i]] = microPeriods[stepperNote[vchannel[i]]] / pitchShift[vchannel[i]];
}
}
}
}
}
void myPitchChange(byte channel, int pitch) {
pitch = pitch / 100;
pitchShift[channel] = pitchfactor[pitch + 81];
stepperPeriod[channel] = microPeriods[stepperNote[channel]] / pitchShift[channel];
//Serial.print(channel); Serial.print(" , "); Serial.print(pitch); Serial.print(" , "); Serial.println(stepperPeriod[channel]);
}
//Where flags turn into actions
void NoteProcessing() {
/*For stepper motors, execute the following
//if the current period is greater than zero (zero being not playing)
//stepperTick is incremented by the resolution to keep track of time between ticks
//if the current tick has passed the microperiod threshold for ticking for that channel
//reset the current Tick to zero
//If the position is at a bound, then reverse the direction, and set position to zero.
//then proceed with a tick by flipping the logic on the step pin
*/
for (int i = 2; i < 14; i++) { //for all the stepper channels
if (stepperPeriod[i] > 0) {
stepperTick[i] += RESOLUTION;
//Serial.print("The stepperTick on pin ");Serial.print(pin);Serial.print(" is ");Serial.println(stepperTick[i]);
if (stepperTick[i] >= stepperPeriod[i]) {
togglePin(i, channel2drive[i], channel2drive[i] + 1);
stepperTick[i] = 0;
}
}
}
}
void mod0() {
v0 += 1;
if (v0 > 12) {
v0 = 0;
}
stepperPeriod[vchannel[0]] = (microPeriods[stepperNote[vchannel[0]]] / (pitchShift[vchannel[0]] * vibrato[v0]));
//Serial.print(stepperPeriod[vchannel[0]]); Serial.println(" , This is mod0");
}
void mod1() {
v1 += 1;
if (v1 > 12) {
v1 = 0;
}
stepperPeriod[vchannel[1]] = (microPeriods[stepperNote[vchannel[1]]] / (pitchShift[vchannel[1]] * vibrato[v1]));
//Serial.print(stepperPeriod[vchannel[1]]); Serial.println(" , This is mod1");
}
void mod2() {
v2 += 1;
if (v2 > 12) {
v2 = 0;
}
stepperPeriod[vchannel[2]] = (microPeriods[stepperNote[vchannel[2]]] / (pitchShift[vchannel[2]] * vibrato[v2]));
//Serial.print(stepperPeriod[vchannel[2]]); Serial.println(" , This is mod2");
}
void togglePin(byte channel, int pin, int direction_pin) {
stepperState[pin] = !stepperState[pin];
digitalWrite(pin, stepperState[pin]);
Position[channel]++;
//Don't switch directions if MAX_POSITION<0
if (MAX_POSITION[channel] == 0) return;
// If stepper percussion, run until hit abs(MAX_POSITION), and then turn the note off and change direction
if (MAX_POSITION[channel] < 0) {
// Serial.println("Percussion channel found");
// Serial.println(abs(MAX_POSITION[channel]));
// Serial.println(Position[channel]);
if (Position[channel] >= abs(MAX_POSITION[channel])) {
stepperState[direction_pin] = !stepperState[direction_pin];
digitalWrite(direction_pin, stepperState[direction_pin]);
Position[channel] = 0;
stepperPeriod[channel] = 0;
stepperNote[channel] = -1;
}
return;
}
//Switch directions if end has been reached
if (Position[channel] >= MAX_POSITION[channel]) {
stepperState[direction_pin] = !stepperState[direction_pin];
digitalWrite(direction_pin, stepperState[direction_pin]);
Position[channel] = 0;
//Serial.println("DIRECTION CHANGE ");
}
}
void setup() {
//Stepper control
byte pin;
for (int i = 0; i < 16; i++) {
pin = channel2drive[i];
pinMode(pin, OUTPUT); //Step Pins
pinMode(pin + 1, OUTPUT); // Direction Pins
digitalWrite(pin, LOW); digitalWrite(pin + 1, LOW);
}
//DIRECTION SWITCHES
pinMode(sc1sw1, INPUT);
pinMode(sc1sw2, INPUT);
pinMode(sc2sw1, INPUT);
pinMode(sc2sw2, INPUT);
//Sleep Control
for (int i = 0; i<4; i++){
pinMode(sleepPins[i], OUTPUT);
}
goToSleep();
//dink
pinMode(30, OUTPUT);
digitalWrite(30, HIGH);
//attachInterrupt(digitalPinToInterrupt(channel2drive[dinkChannel1]), dink1, RISING);
// attachInterrupt(digitalPinToInterrupt(31), dink2, RISING);
pinMode(0, OUTPUT); //Logic Supply voltage. 0 is located conveniently next to ground.
digitalWrite(0, HIGH);
pinMode(12, OUTPUT); //Logic Supply voltage. 12 is located conveniently next to 3.3V.
digitalWrite(12, LOW);
mainTimer.priority(1);
mainTimer.begin(NoteProcessing, RESOLUTION);
usbMIDI.setHandleNoteOff(myNoteOff);
usbMIDI.setHandleNoteOn(myNoteOn);
usbMIDI.setHandleControlChange(myControlChange);
usbMIDI.setHandlePitchChange(myPitchChange);
Serial.begin(9600);
}
void loop() {
usbMIDI.read();
if(digitalRead(sc1sw1)){
digitalWrite(channel2drive[5]+1, LOW);
}
else if(digitalRead(sc1sw2)){
digitalWrite(channel2drive[5]+1, HIGH);
}
if(digitalRead(sc2sw1)){
digitalWrite(channel2drive[6]+1, HIGH);
}
else if(digitalRead(sc2sw2)){
digitalWrite(channel2drive[6]+1, LOW);
}
}
Xylophone
Other stepper motor actuated instruments
Board - Teensy 3.2
USB Type - MIDI
CPU Speed - 96 MHz (overclocked)
Optimize - "Faster"
Teensy 3.2
Code:
#include <SPI.h>
#define LATCH_PIN 9
#define RESOLUTION 200 //Microsecond resolution for notes processing
#define MIN_MIDI_NOTE 55
#define MAX_MIDI_NOTE 79
#define NOTE_RANGE (MAX_MIDI_NOTE - MIN_MIDI_NOTE + 1)
#define OUTPUTS_PER_REG 8
#define NUM_REGS ((NOTE_RANGE+(OUTPUTS_PER_REG-1))/OUTPUTS_PER_REG)
//Device properties
boolean notesPlayable[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //0
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //1
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //2
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //3
0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, //4
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, //5
1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, //6
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //7
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //8
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, //9
0, 0, 0, 0, 0, 0, 0, 0
}; //10
boolean currentState[131]; // use to keep track of the state of a note
int currentDuration[131];
boolean pinState[131]; // used to make square wave
byte currentRegister[NUM_REGS];
int maxOn = 8000 / RESOLUTION; // number of interrupt cycles to go until a solenoid turns back off --> time in microS/ RESOLUTION
int notesPlaying = 0; // number of notes playing; helpful to determine if teensy should go through full noteprocessing code or not
bool noUpdate = true;
//Our timer interrupt process that is responsible for playing notes
IntervalTimer noteProcessor;
void setup() {
pinMode(LATCH_PIN, OUTPUT);
digitalWrite(LATCH_PIN, LOW);
//Begin SPI
SPI.setBitOrder(MSBFIRST);
SPI.setDataMode(SPI_MODE0);
SPI.setClockDivider(SPI_CLOCK_DIV2);
SPI.begin();
//Begin Serial communication
Serial.begin(9600);
delay(1000);
Serial.println("The serial works");
//Setup our devices
noteProcessor.end();
usbMIDI.setHandleNoteOn(OnNoteOn);
usbMIDI.setHandleNoteOff(OnNoteOff);
noteProcessor.priority(255);
noteProcessor.begin(NoteProcessing, RESOLUTION);
writeAllBytes(currentRegister, NUM_REGS); // send the bytes to the shift registers
}
void loop() {
usbMIDI.read();
}
void OnNoteOn(byte channel, byte note, byte velocity) {
if(velocity == 0){
currentState[note] = 0;
}
if ((notesPlayable[note]) & (currentState[note] != true)) {
currentState[note] = true;
notesPlaying += 1;
noUpdate = false;
Serial.print("Playing note "); Serial.println(note);
}
}
void OnNoteOff(byte channel, byte note, byte velocity) {
if (notesPlayable[note]) {
currentState[note] = false;
currentDuration[note] = 0;
notesPlaying -= 1;
Serial.print("ending note "); Serial.println(note);
}
}
void NoteProcessing() {
for (int note = MIN_MIDI_NOTE; note <= MAX_MIDI_NOTE; note += 1) { // This block of code makes it so that the solenoid deploys at full 24V for maxOn duration
if (currentState[note]){
currentDuration[note] += 1;
if (currentDuration[note] > maxOn) {
pinState[note] = false;
currentState[note] = false;
currentDuration[note] = 0;
}
}
}
// if (notesPlaying < 1) noUpdate = true;
// if (noUpdate == true) {}
// else {
for (int i = 0; i < NUM_REGS; i++) { // make up the bytes to be sent to the shift registers
currentRegister[i] = calculateRegisterValue(i);
}
writeAllBytes(currentRegister, NUM_REGS); // send the bytes to the shift registers
// }
}
byte calculateRegisterValue(int registerNum) {
int noteStart = (registerNum * OUTPUTS_PER_REG) + MIN_MIDI_NOTE;
byte value = 0;
for (int i = 0; i < OUTPUTS_PER_REG; i++) {
int note = noteStart + i;
bitWrite(value, i, currentState[note]);
}
return value;
}
void latch() {
digitalWrite(LATCH_PIN, HIGH);
digitalWrite(LATCH_PIN, LOW);
}
void writeAllBytes(byte bytes[], int size) {
for (int i = size - 1; i >= 0; i--) SPI.transfer(bytes[i]);
latch();
}