// 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) ;
}