
But first, the hardware. This can be bread-boarded or put into a fine walnut and bronze case – your choice is unlimited. I, being the Junkbox Junkie, decided to put my interactive control panel into what I call “The Seedy CD Case”. I know, I know. Sounds like a detective novel...
At any rate, I used an old cylindrical (empty) CD case and added:
- 6 potentiometers (variable resistors)
- 1 SPDT (run / stop) slide switch
- 1 bi-color LED run / stop indicator (one could use two LEDs, if need be)
- a handful of wire, solder, etc.
It uses the Arduino's analog inputs A0 – A5 and three digital GPIO lines; one input for the run/stop switch and 2 outputs for the LED indicator.
~~~Safety!~~~
As I've said before, if you know your way around a soldering iron and electronics, great. If not, please consult with someone who does. This isn't fun if you get damaged in the process!
~~~Safety!~~~
The schematic is on the Arty Schematics page. It is pretty straightforward. I managed to wire the potentiometers 'backwards' so that increasing values are obtained by moving the knob counter clockwise. I caught the goof, but then decided to keep it that way just to be different. You may do whatever you choose.
Some changes to the code need to happen to accommodate the use of six knobs and a switch. The interactive code is facilitated by using constants instead of #defines. I don't want to spawn any age-old C programming disagreements. You can find arguments on either side. Either way gets the job done, especially in the Arduino. These define our GPIO and analog port numbers for the 6 knobs and the red and green sides of the LED. It also provides constants for the three ways we can coerce the 0 to 1023 value range of the analog inputs down to our usable range of 0 to 127. More on that in just a bit.
The 'mapping' of the physical knobs can be a sticky point. In my case, I organized the six knobs in clock-wise order around the Seedy Case. Keeping with the analog port numbers, the lower left knob is knob0 and is connected to A0. Next, going clock-wise, knob1 is attached to A1. Finally, the sixth knob is knob5, attached to A5. This helps keep it all straight in my configuration. This is also flexible, because you can re-map which analog port goes to which knob, if you need to. As it stands, now, iKnobs[X] is filled by reading the analog port using the knobX constant, which in my system is connected to physical analog port AX (where X is 0 through 5).
One of the possibilities is using the knobs to set the range of our random functions. I've modified the getARand function by adding an optional 'switch' to the arguments list. interAct will be false by default, so using x=getARand(rType); will operate as it has in the past. A slight code change will be needed for cases where we used something like x=getARand(rType, value) or x=getARand(rType,loValue, hiValue); in that the interAct parameter must now be supplied as 'false'. So, they will have to be changed to x=getARand(rType, false, value) or x=getARand(rType, false, loValue, hiValue); respectively.
Interaction can be 'hard-coded', as I show in the RND_GAUSS, RND_GAUSS2, RND_SIN and RND_VOSSPREV cases in getARand(). Use this method if you are locked in to always using that knob for that function when interAct is true.
Another way to do is, with interAct being false, is to pass the knob values into the getARand() function as the loVal and hiVal arguments: x=getARand(RND_VOSSF,false,iKnobs[2],iKnobs[3]); for instance. I was shooting for maximum flexibility here, since there are so many ways to approach this whole aleatoric business.
In support of the run/stop switch and the LEDs used as running status indicators, a new function called setILED() is available. It has a default argument which is overridden at startup ('reboot') of the Arduino. Since I most deviously selected two PWM lines (digital GPIO 5 & 6) for the LED signals, this startup code will turn my bi-color LED orange if the run/stop switch is in run mode at startup. It does this using the analogWrite() function built-in to the Arduino language. Otherwise, if the Seedy Case is in run mode, the LED is green (and can be made to flicker, if you'd like). When I put it in stop mode, the LED is red.
The updateKnobs() function is what we use to grab the analog values of the knobs, and scale them to a value we can use. The first step, of course, is to read the knob values and store them in the iKnobs[] array. Then we can do one of three scaling operations.
updateKnobs() takes an argument which defaults to the upSHIFT mode of scaling. upSHIFT moves the bits of the binary number representing the knob value to the right three places, dropping off the three least significant bits of the number. A right shift operation is essentially like doing an integer divide by 2. We do it three times so it is like integer dividing 1023 by 8. It has an advantage that it cuts down on the 'jitter' that can happen as the analog signal is converted into a digital number. For example, the decimal number 955 is binary 1110111011. Pushing the three rightmost bits off into the right bit bucket makes the binary number = 1110111, or 119 decimal. 955 / 8 = 119.375, but as I said, this is an integer division, so the fractional value is simply chopped off, giving 119.
upTRUNC simply chops off the top, or three most significant bits of the number. The odd effect of this is that the number will go from 0 to 127 eight times during the turning of the knob from minimum to maximum, since we will only keep the lower 7 bits of the 10 bit number.
upSQUISH uses another built-in function called map(). This is almost, but not quite exactly like the upSHIFT function. Whereas the upSHIFT method simply drops bits, the map function does some math you can find at the reference page. The map() function, on the plus side, can let you spread the values however you want, as opposed to the upSHIFT method being pretty fixed. The downside is this post on the Arduino forum, the gist being it doesn't work 100% correctly, hence I have wrapped it with yet another built-in function, constrain().
Another new function is the checkRunStop() function, which reads the SPDT switch to determine if we are running or stopping. Actually, it just checks if we asked it to stop, but you could add an else if you have some other code you might want to run each time the switch is found to be in run mode. To stop, we call setILED() to change the color of the status LED and then turn off any notes that may be lingering. I also run the audioBootTest() function for audio confirmation of the stop. This was useful to me while recording samples, because I could hear that I had hit stop, rather than having just recorded a really long pause. It's not a necessary call in most cases.
setup() gets a few changes, too, as we define the pinMode for the two LED lines and the run/stop switch sense GPIOs. We call setILED(), and since this is at the start of everything, we set the argument of startup to true. This will make sure the LED is orange if we left the run/stop switch it in run mode before booting. Again, this was just a feature I added because I was recording samples and didn't want it to fire off in run mode before I was ready for it.
Last, but not least, we get to the main loop() function. For the basic example we run the three new functions at the head of the loop, for housekeeping and pulling in the knob values. Next, we pick a random note value, using the secondary method of passing the knob values into the getARand() function. When we send the MIDI information, the velocity value is determined from the iKnobs[2] value, making that knob function as a volume knob. Next, we manually turn off the green run LED to make it flicker as sort of a visual tempo indicator. Lastly, we use iKnobs[3] as a “tempo” control to change the speed of the delay. After turning off the MIDI note just sent, the loop repeats.
+-------------------------------------------------+
| Arty The Aleatoric Arduino: random interactive |
| based on random gaze final |
| Part I of the Arty series |
| Perambulations in the Field |
| of Artifical Music Generation |
| Brought to you by "Junkbox Junkie, the Musical" |
+-------------------------------------------------+
Copyright © 2015 by Bill L. Behrendt
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdarg.h>
#include <stdio.h>
#include <SoftwareSerial.h>
//software serial port pins for MIDI
#define MIDI_OUT 11
#define MIDI_IN 12 //unused
//MIDI cmd
#define CMD_NOTE_ON 0x90 //channel 1
#define CMD_NOTE_OFF 0x80 //channel 1
//random types
#define RND_GAUSS 0
#define RND_GAUSS2 1
#define RND_LIN 2
#define RND_SIN 3
#define RND_TAN 4
#define RND_VOSSF 5
#define RND_VOSSPREV 6
#define RND_VOSSGAUSS 7
//interactive constants and variables
const byte runStop = 4; //1=run; 0=stop
const byte gLED = 5;
const byte rLED = 6;
const byte knob0 = 0;
const byte knob1 = 1;
const byte knob2 = 2;
const byte knob3 = 3;
const byte knob4 = 4;
const byte knob5 = 5;
const byte upSHIFT = 0;
const byte upTRUNC = 1;
const byte upSQUISH = 2;
//array for knob values
unsigned int iKnobs[6];
//interactive
//global variables
SoftwareSerial MIDI(MIDI_IN, MIDI_OUT); // RX, TX
float pi_eye = -3.14159;
byte prevVal[] = {2, 3, 4, 3, 1, 1, 2, 7, 2}; // for use in getARand() function
byte noteVal;
//-----random functions
byte Voss_f(int last) {
float probit, u;
int Nu, J, K, L;
Nu = 0;
K = random(23);
L = last;
probit = 0.029384;
while (K > 0) {
J = L / K;
if (J == 1) L -= K;
u = random(-10, 21);
if (u < probit) J = 1 - J;
Nu += (J * K);
K /= 2;
probit *= random(1.25, 2.5);
}
return Nu;
}
byte Gauss_R(int range) {
int count;
float summ, std, mean;
summ = 0.0;
std = 0.2;
mean = 0.5;
for (count = 1; count < 25; count++) {
summ += random(2);
}
return (byte((std * 0.707106781 * (summ - 12) + mean) * range + 1));
}
byte getARand(byte rType, boolean interAct = false, byte loVal = 0, byte hiVal = 127) {
//get random stuffs
// added interactive 08/08/2015
byte retVal = 0;
byte tmpVal;
pi_eye += 0.05;
switch (rType) {
case RND_GAUSS:
if (interAct) {
retVal = Gauss_R(iKnobs[0]);
} else {
retVal = Gauss_R(random(loVal, hiVal + 1)); //must be hiVal+1 to include 127 (random is loVal to hiVal-1)
}
break;
case RND_GAUSS2:
if (interAct) {
retVal = Gauss_R(iKnobs[1]) + Gauss_R(iKnobs[2]);
} else {
retVal = Gauss_R(loVal) + Gauss_R(hiVal);
}
break;
case RND_LIN:
retVal = random(loVal, hiVal + 1) - prevVal[rType];
break;
case RND_SIN:
if (interAct) {
retVal = sin(pi_eye / (iKnobs[5] + 1)) * iKnobs[4];
} else {
retVal = sin(pi_eye / (loVal + 1)) * hiVal;
}
break;
case RND_TAN:
retVal = tan(pi_eye / (loVal + 1)) * hiVal;
break;
case RND_VOSSF:
retVal = Voss_f(hiVal * prevVal[rType]) + loVal;
break;
case RND_VOSSPREV:
if (interAct) {
retVal = Voss_f(prevVal[rType]) * iKnobs[4];
} else {
retVal = Voss_f(prevVal[rType]) * hiVal;
}
break;
case RND_VOSSGAUSS:
retVal = Voss_f(int(Gauss_R((hiVal - loVal) / 2)));
break;
}
prevVal[rType] = (retVal & 127);
return prevVal[rType];
}
//----- end random functions
void flashWorkLED(char start) {
if (start == 1) {
digitalWrite(LED_BUILTIN, HIGH);
delay(250);
digitalWrite(LED_BUILTIN, LOW);
delay(250);
}
for (char tm = 0; tm < 5; tm++) {
digitalWrite(LED_BUILTIN, HIGH);
delay(125);
digitalWrite(LED_BUILTIN, LOW);
delay(75);
}
}
void sendMIDI(unsigned char cmd, unsigned char data1, unsigned char data2)
{
MIDI.write(cmd); //note on or note off
MIDI.write(data1); //MIDI note 0-127
MIDI.write(data2); //velocity (loudness) of note
}
void audioBootTest(void) {
sendMIDI(CMD_NOTE_ON, 36, 64);
sendMIDI(CMD_NOTE_ON, 60, 64);
sendMIDI(CMD_NOTE_ON, 64, 64);
sendMIDI(CMD_NOTE_ON, 67, 64);
sendMIDI(CMD_NOTE_ON, 72, 64);
flashWorkLED(1);
delay(1000);
flashWorkLED(0);
sendMIDI(CMD_NOTE_OFF, 36, 64);
sendMIDI(CMD_NOTE_OFF, 60, 64);
sendMIDI(CMD_NOTE_OFF, 64, 64);
sendMIDI(CMD_NOTE_OFF, 67, 64);
sendMIDI(CMD_NOTE_OFF, 72, 64);
}
void chanNotesOff(int channel = 0) {
sendMIDI(0xB0 + channel, 0x7B, 0x00);
}
void allNotesOff(void) {
for (int anoff = 0; anoff < 16; anoff++) {
chanNotesOff(anoff);
}
}
void setILED(boolean startup = false) {
digitalWrite(rLED, LOW);
digitalWrite(gLED, LOW);
if (startup) {
if (digitalRead(runStop) == 1) {
analogWrite(rLED, 127);
analogWrite(gLED, 200);
} else {
analogWrite(rLED, 0);
analogWrite(gLED, 0);
}
while (digitalRead(runStop) == 1);
} else {
if (digitalRead(runStop) == 1) {
digitalWrite(rLED, LOW);
digitalWrite(gLED, HIGH);
} else {
digitalWrite(rLED, HIGH);
digitalWrite(gLED, LOW);
}
}
}
void updateKnobs(byte style = upSHIFT) {
iKnobs[0] = analogRead(knob0);
iKnobs[1] = analogRead(knob1);
iKnobs[2] = analogRead(knob2);
iKnobs[3] = analogRead(knob3);
iKnobs[4] = analogRead(knob4);
iKnobs[5] = analogRead(knob5);
switch (style) {
case upSHIFT:
//drop off the 3 LSB's by shifting right - cuts down on 'jitter' and keeps the value from 0-127
iKnobs[0] = iKnobs[0] >> 3;
iKnobs[1] = iKnobs[1] >> 3;
iKnobs[2] = iKnobs[2] >> 3;
iKnobs[3] = iKnobs[3] >> 3;
iKnobs[4] = iKnobs[4] >> 3;
iKnobs[5] = iKnobs[5] >> 3;
break;
case upTRUNC:
// mask the value - will cause 'roll over' to start at 0
//as the value goes over 128, 256 and 512
iKnobs[0] &= 127;
iKnobs[1] &= 127;
iKnobs[2] &= 127;
iKnobs[3] &= 127;
iKnobs[4] &= 127;
iKnobs[5] &= 127;
break;
case upSQUISH:
//use the map function (may be slow) to condense 0-1023 to 0-127
iKnobs[0] = constrain(map(iKnobs[0], 0, 1024, 0, 128),0,127);
iKnobs[1] = constrain(map(iKnobs[1], 0, 1024, 0, 128),0,127);
iKnobs[2] = constrain(map(iKnobs[2], 0, 1024, 0, 128),0,127);
iKnobs[3] = constrain(map(iKnobs[3], 0, 1024, 0, 128),0,127);
iKnobs[4] = constrain(map(iKnobs[4], 0, 1024, 0, 128),0,127);
iKnobs[5] = constrain(map(iKnobs[5], 0, 1024, 0, 128),0,127);
break;
}
}
void checkRunStop(void) {
if (digitalRead(runStop) == LOW)
{
setILED();
chanNotesOff();
audioBootTest();
while (digitalRead(runStop) == LOW);
setILED();
}
}
void setup() {
Serial.begin(115200); // for looking at debug things
MIDI.begin(31250); // our software serial MIDI connection
//interactive setup
pinMode(runStop, INPUT);
pinMode(rLED, OUTPUT);
pinMode(gLED, OUTPUT);
setILED(true);
//end interactive setup
//turn all notes off, all channels
allNotesOff();
//get a random seed from an unattached analog pin
randomSeed(analogRead(0));
}
void loop() {
//random types are:
// RND_GAUSS, RND_GAUSS2, RND_LIN, RND_SIN, RND_TAN, RND_VOSSF, RND_VOSSPREV and RND_VOSSGAUSS
setILED();
updateKnobs(upSQUISH);
checkRunStop();
noteVal = getARand(RND_GAUSS,false,iKnobs[0],iKnobs[1]);
sendMIDI(CMD_NOTE_ON, noteVal, iKnobs[2]);
digitalWrite(gLED, LOW);
delay(100+(2*iKnobs[3])); //a tempo of sorts
sendMIDI(CMD_NOTE_OFF, noteVal, 64);
}
Some things to explore are using upTRUNC and upSHIFT as the parameter to the updateKnobs() function; changing the random function selecting noteVal, including using the interAct = true feature. To randomize the loudness in the notes, try something like:
sendMIDI(CMD_NOTE_ON, noteVal, constrain((getARand(RND_SIN)/4)+(iKnobs[2]),0,127))
which will allow random variation in the notes hit, and still allow the knob to be used as a 'volume' control.
Have fun with it!