// Teensy Audio Library
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>

//BioData Library
#include <Heart.h>
#include <Respiration.h>
#include <SkinConductance.h>

// frequency to diatonic note conversion file
#include "notes.h"

// using Massenger for Serial Coms to Max/MSP
#include <AsciiMassenger.h>
AsciiMassenger msg; // By default, AsciiMassenger uses Serial.
bool manualMode = false; // used for messaging with Max/MSP

//variable for attenuating data flow to serial port
const long printInterval = 4; // millis

// declare biosensor pins for the BioData library
Heart heart(A0);
Respiration resp(A1);
SkinConductance sc(A2);

//declare LEDs for echoing biodata back to users in visual form
int heartLED = 2;
int respLED = 3;
int scLED = 4;

//at one point I was interesting in reading values from a floating pin as though it were a
//heart signal. Somewhat interesting as a strategy for getting randomness for free, but
//needs to be explored more thoroughly.
Heart randomHeart(A6);

//values for generating lowpass filters for BPM
float BPMalpha_1 = 0.5;
float BPMalpha_2 = 0.005;

float gsrFinal; //for processing sc: note that GSR and SC are the same thing...

///////////Section for declaring the Triwave envelope variables

bool doOnce1 = false; // variable for turning on/off the envelope
unsigned long sustainMillis1 = 0; // will store how long sustain was held
const long sustainInterval1 = 100; // interval to hold sustain (milliseconds)

bool doOnce2 = false; // variable for turning on/off the envelope
unsigned long sustainMillis2 = 0; // will store how long sustain was held
const long sustainInterval2 = 100; // interval to hold sustain (milliseconds)

bool doOnce3 = false; // variable for turning on/off the envelope
unsigned long sustainMillis3 = 0; // will store how long sustain was held
const long sustainInterval3 = 62; // interval to hold sustain (milliseconds)

unsigned long delayMillis;
const long delayInterval2 = 16;
const long delayInterval3 = 43;

unsigned long respDelayMillis;
unsigned long respDelayInterval1 = 60000;
unsigned long respDelayInterval2 = 100; // dlay before second guitar pluck

//variables for my experimental "random Heart "
bool doOnce4 = false;
unsigned long sustainMillis4 = 0; // will store how long sustain was held
const long sustainInterval4 = 100; // interval to hold sustain (milliseconds)

bool doOnceGSR = false;
/////Section for connecting elements in the Teensy Audio Library////////////

// GUItool: begin automatically generated code
AudioSynthWaveform waveform1; //xy=83,91
AudioSynthWaveformSineModulated Triwave3; //xy=85,222
AudioSynthWaveformSineModulated Triwave2; //xy=107,183
AudioEffectEnvelope TriwavEnv3; //xy=227,221
AudioEffectEnvelope TriwavEnv2; //xy=254,183
AudioSynthWaveformSineModulated Triwave1; //xy=262,147
AudioSynthWaveformSineModulated BPMsine; //xy=383,43
AudioMixer4 TriwaveBlender; //xy=436,187
AudioSynthKarplusStrong string1; //xy=439,302
AudioMixer4 FinalMixer; //xy=667,187
AudioOutputAnalog dac1; //xy=927,249
AudioConnection patchCord1(waveform1, BPMsine);
AudioConnection patchCord2(Triwave3, TriwavEnv3);
AudioConnection patchCord3(Triwave2, TriwavEnv2);
AudioConnection patchCord4(TriwavEnv3, 0, TriwaveBlender, 2);
AudioConnection patchCord5(TriwavEnv2, 0, TriwaveBlender, 1);
AudioConnection patchCord6(Triwave1, 0, TriwaveBlender, 0);
AudioConnection patchCord7(BPMsine, 0, FinalMixer, 0);
AudioConnection patchCord8(TriwaveBlender, 0, FinalMixer, 1);
AudioConnection patchCord9(string1, 0, FinalMixer, 2);
AudioConnection patchCord11(FinalMixer, dac1);
// GUItool: end automatically generated code

void setup() {
Serial.begin(4800); // make sure that this matches the baud rate of Max/MSP patch
AudioMemory(20);

//declare LEDpins as outputs
pinMode(heartLED, OUTPUT);
pinMode(respLED, OUTPUT);
pinMode(scLED, OUTPUT);

// setup biodata pins
heart.reset();
randomHeart.reset();
sc.reset();
resp.reset();

/////////////declare setups for audio waveforms, envelopes and instruments

waveform1.begin(WAVEFORM_SAWTOOTH);

//initiate Triwaves attached to heart sensor
Triwave1.amplitude(1.0);
Triwave1.frequency(400);
Triwave2.amplitude(1.0);
Triwave2.frequency(800);
Triwave3.amplitude(0.8);
Triwave3.frequency(1200);

//set gain on each channel of an internal mixer
TriwaveBlender.gain(0, 0.3);
TriwaveBlender.gain(1, 0.3);
TriwaveBlender.gain(2, 0.3);
TriwaveBlender.gain(3, 0);

//set envelope parameters

// TriwavEnv2.delay(16) // TODO - implement in loop
TriwavEnv2.attack(40);
TriwavEnv2.hold(2);
TriwavEnv2.decay(45);
TriwavEnv2.sustain(0.4); //hold 80ms
TriwavEnv2.release(45);

// TriwavEnv3.delay(93) TODO - implement in loop
TriwavEnv3.attack(1);
TriwavEnv3.hold(1);
TriwavEnv3.decay(53);
TriwavEnv3.sustain(0.3); //hold 42ms
TriwavEnv3.release(60);

//an experiment with floating analog pin to generate random behaviors

// RandomWave1.amplitude(0.1);
// RandomWave1.frequency(400);

// RespSaw.begin(0.3, 15, WAVEFORM_TRIANGLE_VARIABLE);

/* randomEnvelope.attack(1);
randomEnvelope.hold(1);
randomEnvelope.decay(53);
randomEnvelope.sustain(0.3); //hold 42ms
randomEnvelope.release(60);*/

sustainMillis1 = sustainInterval1; // make sure the note doesn't play automatically
sustainMillis2 = sustainInterval2; // make sure the note doesn't play automatically
sustainMillis3 = sustainInterval3; // make sure the note doesn't play automatically


//Low sine pitch controlled by BPM
BPMsine.amplitude(0.075);
BPMsine.frequency(76);

}

void loop() {

unsigned long currentMillis = millis(); // update time

//update the biosensors constantly
randomHeart.update();
heart.update();
sc.update();
resp.update();

float conductanceLevel = sc.getSCL(); // read skin conductance
float BVPA = heart.amplitudeChange()*360; // read amplitude of heart sig and puts into range

// convert SCL to fundamental note
float fundamental = pow(conductanceLevel*100, 1.15); //uses exponents to convert arousal/skin conductance into a frequency value
int fundamentalNote = (int)fundamental; //convert decimal into integer
float fundamentalFreq = noteToFrequency(fundamentalNote); //translates integer value into a note in diatonic system
fundamentalFreq = fundamentalFreq*40; // puts frequency into a higher range

waveform1.frequency(heart.getBPM());
waveform1.amplitude(BPMlop1);

Triwave1.frequency(fundamentalFreq);
Triwave1.phase(BVPA);
Triwave1.amplitude(heart.getNormalized()*0.1);

Triwave2.frequency(fundamentalFreq*(heart.getBPM()*0.7));
Triwave2.phase(BVPA);

Triwave3.frequency(fundamentalFreq*(BPMlop2+5));
Triwave3.phase(BVPA);

//generate two different low passes of BPM
float BPMlop1 = (BPMalpha_1*heart.getBPM() + (1 - BPMalpha_1)*BPMlop1)*0.1;
float BPMlop2 = (BPMalpha_2*heart.getBPM() + (1 - BPMalpha_2)*BPMlop2)*0.1;

BPMsine.amplitude(1.0-resp.getNormalized()); // gain of BPMsine controlled by exhalation of breath

//elements are fed into Mixers in setup: each mixer has 4 independent channels
FinalMixer.gain(0, 0.2);
FinalMixer.gain(1, sc.getSCR()+0.08); // heartbeat is only heard when arousal is high
FinalMixer.gain(2, (1-sc.getSCR())*0.1); // shifts in heart amplitude only heard when arousal is low

if(resp.breathDetected()){
respDelayMillis = currentMillis;
string1.noteOn(sc.getSCL()*5000, resp.amplitudeChange());
respDelayInterval2 = heart.getBPM()*3;
}

/*
if (currentMillis - respDelayMillis == respDelayInterval2) string2.noteOn(sc.getSCL()*2000, resp.amplitudeChange());
if (currentMillis - respDelayMillis == respDelayInterval1){
string2.noteOff(resp.amplitudeChange());
string1.noteOff(resp.amplitudeChange());
}*/
/*
if (randomHeart.beatDetected()){
randomEnvelope.noteOn();
}*/

/* if (randomEnvelope.isSustain()){
if (doOnce4 == true){
sustainMillis4 = currentMillis;
doOnce4 = false;
}
}
else {
doOnce4 = true; // reset
}*/


/*
gsrFinal = sc.getSCR()+0.1;
if (gsrFinal > 0.9) gsrFinal = 0.9;

GSRamp.gain(gsrFinal);
*/

//RandomWave1.frequency(fundamentalFreq*10);
//RespAmp1.gain(resp.getNormalized());
//amp2.gain(1.0-resp.getNormalized());

//RespSaw.frequency(resp.getBPM()*0.5);


if (heart.beatDetected()){
// TriwavEnv1.noteOn();
delayMillis = currentMillis;
}

if (currentMillis - delayMillis == delayInterval2) TriwavEnv2.noteOn();
if (currentMillis - delayMillis == delayInterval3) TriwavEnv3.noteOn();

/* if (TriwavEnv1.isSustain()){
if (doOnce1 == true){
sustainMillis1 = currentMillis;
doOnce1 = false;
}
}
else {
doOnce1 = true; // reset
}*/

if (TriwavEnv2.isSustain()){
if (doOnce2 == true){
sustainMillis2 = currentMillis;
doOnce2 = false;
}
}
else {
doOnce2 = true; // reset
}

if (TriwavEnv3.isSustain()){
if (doOnce3 == true){
sustainMillis3 = currentMillis;
doOnce3 = false;
}
}
else {
doOnce3 = true; // reset
}

// check to see if it's time to turn off the note

/* if (currentMillis - sustainMillis1 >= sustainInterval1) { /// if sustain interval has been surpassed turn it off
TriwavEnv1.noteOff();
}
*/
if (currentMillis - sustainMillis2 >= sustainInterval2) { // if sustain interval has been surpassed turn it off
TriwavEnv2.noteOff();
}

if (currentMillis - sustainMillis3 >= sustainInterval3) { // if sustain interval has been surpassed turn it off
TriwavEnv3.noteOff();
}

/* if (currentMillis - sustainMillis4 >= sustainInterval4) { // if sustain interval has been surpassed turn it off
randomEnvelope.noteOff();
}*/

////////////////// LEDs and Serial comms are only processed at timed intervals
if (currentMillis%printInterval == 0) {

ledUpdate(); // lights help you test the sensors to make sure they are connected properly

//////////////////////////////////////TESTING///////////////////////////////////
//Serial.println(BPMlop1);
//Serial.println(BPMlop2);

/* msg.beginPacket("heart"); // create a packet of serial data labeled as heart sensor data
msg.addFloat(heart.getNormalized()); // hearsensor values normalized to values between 0.0 and 1.0
msg.addFloat(heart.getBPM());
msg.addFloat(heart.bpmChange()); // detects shifts in average BPM between 0.0 and 1.0 -
// 0.5 is avg, < 0.5 is below avg, > 0.5 above avg
msg.addFloat(heart.amplitudeChange());// detects shifts in average pulse amplitude between 0.0 and 1.0
// - 0.5 is avg, < 0.5 is below avg, > 0.5 above avg

//msg.endPacket();

msg.beginPacket("gsr"); // begin a packet of data labeled as skin conductance data
msg.addFloat(sc.getSCL());
msg.addFloat(sc.getSCR());
msg.endPacket();

msg.beginPacket("resp");
msg.addFloat(resp.getNormalized());
msg.addFloat(resp.getBPM());
msg.addFloat(resp.bpmChange());
msg.addFloat(resp.amplitudeChange());
msg.endPacket();*/
}

}

void ledUpdate() {
int heartMap = heart.getNormalized()*100;
int heartSigLed = map(heartMap, 0, 100, 0, 255);
analogWrite(heartLED, heartSigLed);

int respMap = resp.getNormalized()*100;
int respSigLed = map(respMap, 0, 100, 0, 255);
analogWrite(respLED, respSigLed);

int scMap = sc.getSCR()*100;
int scSigLed = map(scMap, 0, 100, 0, 255);
analogWrite(scLED, scSigLed);
}


{kind: code, author: EG, function: documentation, keywords: [Teensy, DSP, sonification]}

This page collects additional and complementary materials developed and worked on durin Erin's ALMAT residency. These comprise photos, sketches, drawings, schematics, code and patches.

{function: contextual}

Materials

EG: There is a lot of "commented" code that isn't documentated in this Arduino/Teensy file from my {type: contextual}last iteration during the Thresholds exhibition. This commented code that I'm referring to resulted from experiments that didn't work in the moment during the final weeks of the residency in Bergen, but I still might explore them in the future. I was playing around with different ways of overlapping the interactions in biodata each day and didn't want to lose the previous experiments. In this way the file contains sketches and traces of previous iterations, a record of some things I tried while in Norway. I don't often put my "artistic" work into a version control (maybe I should) but I keep the BioData library itself well organized like that.

 

{function: contextual}

EG: The use of serial communications presented a stumbling block at one point as I realized that there was a significant lag between Max/MSP and the hardware due to the inefficiency of this protocol for communications. At a certain point I just gave up on resolving the issue because the use of Max/MSP was more as an exploratory tool, and I was glad to realize that one advantage of working in hardware as final output was the lack of latency issues when shuttling information from sensors to computers. In most cases I don't know if these latency issues matter but because the connection from body to sound was important I couldn't afford any latency, which takes away from the "liveness" of the connection...

 

{author: EG, group: msp, function: comment, keywords: [patch, serial, Max/MSP]}

EG: Even though the online Teensy GUI is visually similar to programs like Max/MSP and Pd, the objects available are still limited, and the graphical interface is used more to generate fast setup code than anything. Still I often documented my connections in my sketchbook because for some odd reason, memory bugs could occur and made the code stop compiling, and you would have to build the original connections from scratch.  Sometimes you would redesign something quickly as a test and forget what the original relationships were as well. You can use your sketchbook to try out new ideas when you are away from your computer, this also helps me think sometimes to get a critical distance, or even to understand more what I'm doing. 

 

{group: sketch, keywords: [sketchbook, wires, synthesis, patch, DSP]}

EG: This section is where the "sonification" processes take place. You can see how intuitive it is to create envelopes in Max/MSP with an interactive graphic object? Compare this to how in Teensy I manually input the various stages of the envelopes (attack, hold, delay, sustain, release) and had to code in a trigger for triggering and releasing the entire thing!

 

I coded in Max/MSP because I could do it much more intuitively than using the Teensy audio library, and limited myself to very simple processes that could be replicated using their library. I'm still not sure how the filters in Max/MSP would translate to Teensy. I found that the sounds that my Max/MSP process generated were pretty lovely, however I couldn't replicate these sounds perfectly in Teensy.  In addition to this, during the translation process I started discovering other things I was interested in, so the translation process wasn't really the end of the exploration.

 

I think in the future I might continue using the Max/MSP route as a means of sharing ideas and communicating with other artists who don't use the Teensy Audio GUI, but the fact that the translation really didn't result in similar sounds at all makes me think that this is a very rough sketchbook or starting place rather than a real working environment for the BioSynth.

 

{author: EG, group: msp, function: comment, keywords: [patch, DSP, Max/MSP, GUI, workflow]}

 

EG: In this section I visualize and monitor incoming biodata: pulse sensor, skin conductivity, respiration sensors. The BioData library conditions this data on the hardware side before it arrives in Max/MSP, but you can see most the data is counting peaks, measuring amplitudes, and detecting changes in both.

 

{author: EG, group: msp, function: comment, keywords: [patch, biodata, body]}

EG: These are two experiments that I recorded in May. I was looking for ways to make the shifts in biodata as transparent as possible but also not tiresome in contexts of repetitive listening...at the time I was frustrated with how simple they sounded but now I listen to these older experiments and find their minimalism pretty nice! At the time I was still waiting for the new hardware boards with the respiration sensors to come in the mail, so I was only using heart beat and skin conductance data. An aspect of the translation process that I found frustrating is that I don't think that the way that the signals are added/multiplied in Teensy is the same as they are in my Max/MSP patch, which would obviously effect the way the final signals sound. So even though I found these nice tones, I reused the processes and found they didn't work as well in the hardware.  (I'm pretty sure that the processes for calculating the sine tones are also different, for example). I also I found an interesting glitch/click due to signal saturation in Max/MSP that I couldn't replicate in the Teensy (when the signal saturates in Teensy it distorts in really uncomfortable ways, in this Max/MSP example you just hear a nice cold "click"). 

It was relatively easy to shift back and forth between signal shapes in Max/MSP, I would like to work with that more in Teensy in the future but I couldn't think of a logical way to impose a "rule" on the biodata data to make the waveform change. I was trying to avoid illogical rules or categorizations during the residency, however suppose I could always break my own rules for aesthetic reasons in the future.

 

{author: EG, group: Max/MSP, function: comment, keywords: [sonification, biodata, Teensy, experiment, Max/MSP, workflow]}

---
meta: true
function: documentation
author: EG
artwork: Pinch And Soothe
keywords: [electronic, patch, sonification, Teensy, biosignals, schematics, code]
---