// Software to control aeolian harp (DIY ebows). // // Use https://github.com/scandum/rotate #include "rotate.h" const unsigned long loopCount = 4; const int leftRotate = loopCount - 1; // This array contains all waveforms we can pick from const int loopChoiceArray[][4] = { // silence { 0, 0, 0, 0 }, // basic { 120, 254, 120, 0 }, // basic quiet { 0, 20, 250, 20 }, // basic loud { 0, 220, 250, 220 }, // plucking A { 255, 254, 253, 255 }, // plucking B { 255, 255, 255, 254 }, }; // Set which pins use electromagnets const int pinArray[3] = { 3, 5, 6 }; // This array sets the frequency of each magnet. // periodTime is calculated: ((1 / frequency * 1000000) / loopCount) // Default is 120Hz -> 8333 periodTime unsigned long defaultPeriodTime = 8333 / loopCount; unsigned long periodTimeArray[3] = { defaultPeriodTime, defaultPeriodTime, defaultPeriodTime }; // The last time when the magnet was triggered unsigned long lastTimeArray[3] = { 0, 0, 0 }; // isPlaying actually specifies whether we make a fadein or a fadeout. bool isPlayingArray[3] = { false, false, false }; double amplitudeFactorArray[3] = { 0, 0, 0 }; unsigned long randomMaxArray[3] = { 25, 25, 25 }; // Array of waveforms int loopArray[3][4] = { { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, { 0, 0, 0, 0 }, }; // How many magnets exist? const int magnetCount = sizeof(pinArray) / sizeof(*pinArray); // Protocoll string char msg[128]; int msgIndex; const int msgCount = 128; bool msgStarted = false; const char msgDelimiterStart = '#'; const char msgDelimiterEnd = '\n'; unsigned long currentTime; // 4 seconds const unsigned long fadeInDuration = 4000000; // 5 seconds const unsigned long fadeOutDuration = 5000000; double fallFactorArray[3] = { 0.9935, 0.9935, 0.9935 }; double riseFactorArray[3] = { 1.0065, 1.0065, 1.0065 }; const double minAmplitudeFactor = 0.01; const double maxAmplitudeFactor = 1; const double fallAmplitudeFactorRatio = minAmplitudeFactor / maxAmplitudeFactor; const double riseAmplitudeFactorRatio = maxAmplitudeFactor / minAmplitudeFactor; // Min difference between two micros calls! // https://www.arduino.cc/reference/en/language/functions/time/micros/ // arduino says 4 to 8 miliseconds precision. // But the loop frequency is much slower. // This is // (1) because the code takes so long // (2) because the baudrate is too low (must be higher) // // If we have 5 control points we can represent frequencies up // to 340Hz with this value. This should be ok. const unsigned long minPeriodTime = 580; void setup() { // Set up log infos in serial monitor Serial.begin(230400); // Initialize pins as output int i; for(i = 0; i < magnetCount; i++) { pinMode(pinArray[i], OUTPUT); } } void loop() { currentTime = micros(); // String execution int i; for (i = 0; i < magnetCount; i++) { executeString(i, currentTime); } // Protocoll transmission // Basic technique copied from // https://makersportal.com/blog/2019/12/15/controlling-arduino-pins-from-the-serial-monitor // First collect all characters until we found a line break char in_char = ' '; while (Serial.available()) { in_char = Serial.read(); if (int(in_char) != -1) { if (msgStarted) { msg[msgIndex] = in_char; msgIndex += 1; } else if (in_char==msgDelimiterStart) { msgStarted = true; if (msg) { cleanupMsg(); } } } if (msgStarted && in_char==msgDelimiterEnd) { msgStarted = false; Serial.print("Received msg: "); Serial.print(msg); decodeMsg(msg); cleanupMsg(); } } } void cleanupMsg() { int i; // Cleanup: make msg empty, set msgIndex to 0. for (i = 0; i < msgCount ; i++) { msg[i] = '\0'; } msgIndex = 0; } void executeString(int i, unsigned long currentTime) { double amplitudeFactor = amplitudeFactorArray[i]; if (amplitudeFactor > 0) { unsigned long lastTime = lastTimeArray[i]; unsigned long periodTime = periodTimeArray[i]; // "micros" resets itself after 70 minutes // (see https://www.arduino.cc/reference/en/language/functions/time/micros/) // So it could happen that the lastTime looks bigger than the currentTime: // in this case set lastTime to 0 unsigned long difference; if (lastTime > currentTime) { difference = currentTime; } else { difference = currentTime - lastTime; } if (difference > periodTime) { // isPlaying actually specifies whether we make a fadein or a fadeout. bool isPlaying = isPlayingArray[i]; long randNumber = random(randomMaxArray[i]); int value = (loopArray[i][0] * amplitudeFactor) - randNumber; if (value < 0) { value = 0; } // debug // Serial.println(difference); // Serial.println(value); analogWrite(pinArray[i], value); lastTimeArray[i] = currentTime; auxiliary_rotation(loopArray[i], 1, leftRotate); // Fadein / Fadeout double amplitudeFactorFactor; if (isPlayingArray[i]) { amplitudeFactorFactor = riseFactorArray[i]; } else { amplitudeFactorFactor = fallFactorArray[i]; } double newAmplitudeFactor = amplitudeFactor * amplitudeFactorFactor; if (newAmplitudeFactor > maxAmplitudeFactor) { newAmplitudeFactor = maxAmplitudeFactor; } else if (newAmplitudeFactor < minAmplitudeFactor) { newAmplitudeFactor = 0; // The next time the magnet is ignored, because // its amplitudeFactor is already 0. So 0 won't // be written to the magnet, so it's still a bit // on. But we want to safely turn it off. analogWrite(pinArray[i], newAmplitudeFactor); } amplitudeFactorArray[i] = newAmplitudeFactor; } } } void decodeMsg(char str[]) { char* token; char* rest = str; unsigned long tokenArray[3]; int tokenIndex = 0; while ((token = strtok_r(rest, " ", &rest))) { tokenArray[tokenIndex] = strtoul(token, NULL, 10); tokenIndex += 1; } if (tokenIndex != 3) { Serial.println("INVALID MSG!"); return; } else { int pinIndex = tokenArray[0]; int mode = tokenArray[1]; // 0 = setFrequency; 1 = setLoop unsigned long periodTimeOrLoopIndex = tokenArray[2]; // Sanity check if (pinIndex >= magnetCount || pinIndex < 0) { Serial.print("INVALID PIN INDEX "); Serial.println(pinIndex); return; } if (periodTimeOrLoopIndex < 0) { Serial.print("INVALID periodTimeOrLoopIndex "); Serial.println(periodTimeOrLoopIndex); return; } int pin = pinArray[pinIndex]; // LOG Serial.print("Pin ("); Serial.print(pin); // Execute command if (mode == 0) { setFrequency(pinIndex, periodTimeOrLoopIndex); } else { setEnvelope(pinIndex, periodTimeOrLoopIndex); } } } void setFrequency(int pinIndex, unsigned long periodTime) { Serial.print("): set frequency to "); Serial.println(periodTime); // We need to divide the frequency value by our loopCount, // because one loop equals one repeating period e.g. the period // size of the frequeny. periodTime /= loopCount; if (periodTime < minPeriodTime) { periodTime = minPeriodTime; Serial.println("WARNING: Received too fast frequency. Autoset to higher freq."); } periodTimeArray[pinIndex] = periodTime; riseFactorArray[pinIndex] = calculateRiseFactor(periodTime); fallFactorArray[pinIndex] = calculateFallFactor(periodTime); } void setEnvelope (int pinIndex, unsigned long loopIndex) { Serial.print("): set envelope to "); Serial.println(loopIndex); if (loopIndex == 0) { // When setting to 'false', amplitudeFactor // is becoming smaller and smaller. When it's // small enough it is set to 0 and the magnet // stops playing. isPlayingArray[pinIndex] = false; } else { isPlayingArray[pinIndex] = true; // We only override curve if it's not 0. // Otherwise we don't have any fadein/fadeout. int i; for (i = 0; i < loopCount; i++) { loopArray[pinIndex][i] = loopChoiceArray[loopIndex][i]; } // Fadein if not already playing if (amplitudeFactorArray[pinIndex] == 0) { // When setting to 'true', amplitudeFactor is // becoming bigger and bigger until it reaches 1. // But our loop still ignores our magnet if we // don't specify a value which is already bigger // than 0. This is why set the starting seed 0.01. amplitudeFactorArray[pinIndex] = minAmplitudeFactor; } } } double calculateFallFactor(unsigned long periodTime) { double attackCount = fadeOutDuration / periodTime; double fallFactor = pow(fallAmplitudeFactorRatio, 1.0 / attackCount); return fallFactor; } double calculateRiseFactor(unsigned long periodTime) { double attackCount = fadeInDuration / periodTime; double riseFactor = pow(riseAmplitudeFactorRatio, 1.0 / attackCount); return riseFactor; } // Useful for debug // See: https://forum.arduino.cc/t/printing-a-double-variable/44327/2 void printDouble( double val, unsigned int precision){ // prints val with number of decimal places determine by precision // NOTE: precision is 1 followed by the number of zeros for the desired number of decimial places // example: printDouble( 3.1415, 100); // prints 3.14 (two decimal places) Serial.print (int(val)); //prints the int part Serial.print("."); // print the decimal point unsigned int frac; if(val >= 0) frac = (val - int(val)) * precision; else frac = (int(val)- val ) * precision; int frac1 = frac; while( frac1 /= 10 ) precision /= 10; precision /= 10; while( precision /= 10) Serial.print("0"); Serial.println(frac, DEC) ; }