Skip to content

Summary of Process

Here you will find the entire process of making the final piece of the Hearty Party. You will also find the fabrication files and code for everything here. For the more detailed process of how we got to this point then please refer to the previous sections which I have organised into 3 different explorations.

  • Form - Here you will find all the development of the shape and material research of the project.
  • Sound - Here is all the development relating to the sound design of the project.
  • Lights - All of the development in the use of BrightDot neopixels.

Materials BOM

<a href="https:&#x2F;&#x2F;www.canva.com&#x2F;design&#x2F;DAFiebKjzZ4&#x2F;view?utm_content=DAFiebKjzZ4&amp;utm_campaign=designshare&amp;utm_medium=embeds&amp;utm_source=link" target="_blank" rel="noopener"></a>

Connections

Here are the main schematics for the piece.

Sewing

This is a short introduction to the sewing process I used to create the soft circuits.

The Process of Embedding the Electronics

Moulding with Fish Leather

Here is a short process video showing my technique for moulding fish leather very simply.

The Code

This is the main code that I used to program the Teensy. Please do note that its prefered to use Teensyduino for this although with small modifications it can be used in Arduino too for many other development boards. However, this code does use some of the Teensy Audio Shield Libraries so that would have to be taken into consideration when adapting it.

// Libraries
#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>
#include <Adafruit_NeoPixel.h>

// Audio Engine Setup. Generated by the GUI Tool at https://www.pjrc.com/teensy/gui
AudioPlaySdWav           elton;     //xy=70,307
AudioPlaySdWav           beat3;       //xy=127,205
AudioPlaySdWav           beat2;       //xy=128,151
AudioPlaySdWav           beat1;       //xy=137,92
AudioPlaySdWav           beat0;       //xy=147,41
AudioMixer4              beatMix;           //xy=461,199
AudioEffectGranular      granR;      //xy=193,266
AudioEffectGranular      granL;      //xy=201,353
AudioEffectFreeverb      verbR;      //xy=322,270
AudioEffectFreeverb      verbL;      //xy=327,359
AudioMixer4              verbMixR;         //xy=366,498
AudioMixer4              verbMixL;         //xy=367,566
AudioFilterStateVariable filterR;        //xy=457,271
AudioFilterStateVariable filterL;        //xy=469,355
AudioMixer4              outMixR;         //xy=636,258
AudioMixer4              outMixL;         //xy=640,329
AudioOutputI2S           audioOutput;    //xy=684,72

// Sound sources
AudioConnection          patchCord1(elton, 0, granR, 0);
AudioConnection          patchCord2(elton, 1, granL, 0);
AudioConnection          patchCord3(beat3, 0, beatMix, 3);
AudioConnection          patchCord4(beat2, 0, beatMix, 2);
AudioConnection          patchCord5(beat1, 0, beatMix, 1);
AudioConnection          patchCord6(beat0, 0, beatMix, 0);

// ELTON mode effects chain
AudioConnection          patchCord7(granR, verbR);
AudioConnection          patchCord8(granL, verbL);
//AudioConnection          patchCord9(verbR, 0, filterR, 0);
//AudioConnection          patchCord10(verbL, 0, filterL, 0);
AudioConnection          patchCord11(filterR, 2, outMixR, 3);
AudioConnection          patchCord12(filterL, 2, outMixL, 3);

// Beat mixer
AudioConnection          patchCord13(beatMix, 0, outMixR, 0);
AudioConnection          patchCord14(beatMix, 0, outMixL, 0);

// Output mixer
AudioConnection          patchCord15(outMixR, 0, audioOutput, 0);
AudioConnection          patchCord16(outMixL, 0, audioOutput, 1);

// Audio shield control chip.
AudioControlSGTL5000     sgtl5000_1;     //xy=629,601

// Reverb DRY/WET mixer
AudioConnection          patchCord17(granR, 0, verbMixR, 3);
AudioConnection          patchCord18(granL, 0, verbMixL, 3);
AudioConnection          patchCord19(verbR, 0, verbMixR, 0);
AudioConnection          patchCord20(verbL, 0, verbMixL, 0);
AudioConnection          patchCord21(verbMixR, 0, filterR, 0);
AudioConnection          patchCord22(verbMixL, 0, filterL, 0);

// Peak Analyzer
AudioAnalyzePeak         peaks[4];
AudioConnection          patchCord23(beat0, 0, peaks[0], 0);
AudioConnection          patchCord24(beat1, 0, peaks[1], 0);
AudioConnection          patchCord25(beat2, 0, peaks[2], 0);
AudioConnection          patchCord26(beat3, 0, peaks[3], 0);

// SD card pin definitions. Directly from the examples.
#define SDCARD_CS_PIN    10
#define SDCARD_MOSI_PIN  7
#define SDCARD_SCK_PIN   14

// LDR pin stuff
const byte LDR_PINS[] = {A2, A3, A0, A1};
int LDR_0[] = {0, 0, 0, 0}; // LDR ambient valules for 0.0
//int LDR_1[] = {1023, 1023, 1023, 1023}; // LDR ambient valules for 1.0
int LDR_M[] = {1023, 1023, 1023, 1023};
float ldr_ratios[] = {0.0, 0.0, 0.0, 0.0}; // LDR ratios. Will be read and used every loop. 

const int REVERB_LDR = 3;
const int HP_LDR = 2;
const int LENGTH_LDR = 0;
const int FREEZE_LDR = 1;

// Sample declaration
const char ELTON_SAMPLE[] = "SOUNDS/ELTON-03.WAV";
const char BEAT0_SAMPLE[] = "SOUNDS/BEAT-01.WAV";
const char BEAT1_SAMPLE[] = "SOUNDS/BEAT-02.WAV";
const char BEAT2_SAMPLE[] = "SOUNDS/BEAT-03.WAV";
const char BEAT3_SAMPLE[] = "SOUNDS/BEAT-04.WAV";

// Peak detection ?????????
elapsedMillis msecs; // Special variable to keep track of time elapsed.  Needed for blinking LEDs.

// Parameters that will need to be tested and tuned.
const int AUDIO_MEMORY = 16;
const int AMBIENT_PADDING = 15; // 
const float TRIGGER_ELTON_THRESHOLD = 0.2; // The LDR ratios must all reach this value to trigger ELTON.
const float REMAIN_ELTON_THRESHOLD = 0.2; // Any one LDR must be above this value to remain in ELTON mode.
const float OUTPUT_VOLUME = 0.6; // Volume of the output device.
const float BEAT_MAX_VOL = 0.7; // Maximum volume of each heart beat sample, to avoid clipping.
const float BEAT_VOL_MIN_THRESHOLD = 0.1; // LDR value to reach in order to start  bringing up the volume.
const float BEAT_VOL_MAX_THRESHOLD = 0.5; // LDR value for max volume.
const int PEAK_TIME = 20; // The time between peak measurements for the heart beat LEDs.
const float WHITE_WASH_RATIO = 0.5; // To maintain even brightness during the high-pass LED effect.
const float GRANULAR_FREEZE_THRESHOLD = 0.2; // The LDR ratio to trigger the granular freeze effect.

// ELTON effects control parameters
const float FILTER_RESONANCE = 3.0; // Resonance of the high-pass filter.
const unsigned int ELTON_GRACE_PERIOD_TIME = 1000; // Time after ELTON mode is anabled where the LDRs are not used.
const unsigned int FILTER_MAX = 5000; // The maximum frequency of the filter. Everything above this will be audible.

// ELTON LED control parameters
const unsigned int RAINBOW_INTERVAL = 4;

// TESTING granular stuff
#define GRANULAR_MEMORY_SIZE 12800  // enough for 290 ms at 44.1 kHz
int16_t granMemR[GRANULAR_MEMORY_SIZE];
int16_t granMemL[GRANULAR_MEMORY_SIZE];

// LED stuff
#define NUMPIXELS        16   // <------ CHANGE ME
#define LED_PIN          2
#define brightnessValue  150
Adafruit_NeoPixel pixels = Adafruit_NeoPixel(NUMPIXELS, LED_PIN, NEO_GRB + NEO_KHZ800);
//const int pixelGroups[5] = {0, 0, 1, 2, 3}; // Assign each LED to a group.
//const int pixelGroups[8] = {0, 0, 0, 1, 1, 2, 2, 3}; // Assign each LED to a group.
//const int pixelGroups[11] = {0, 0, 0, 1, 1, 1, 2, 2, 3, 3, 3}; // Assign each LED to a group.
//const int pixelGroups[16] = {0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3}; // Assign each LED to a group. 3-5-3-5
const int pixelGroups[16] = {0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3}; // Assign each LED to a group. 3-5-5-3

// The pixel colors in ELTON mode are stored here. This is needed to mix colors from different effect modes.
unsigned int COLORS[NUMPIXELS][3] = {};

// DEBUGGING
const bool DEBUG = true;
const bool DEBUG_ELTON = false;

///////////
// Setup //
///////////
void setup() {
  Serial.begin(9600);

  // Lights DEV
    pixels.begin();                         // This initializes the NeoPixel library.
    pixels.setBrightness(brightnessValue);  // full brightness=255 (not recommended)
    pixels.show();                          // Initialize all pixels to 'off'

    int LED_STARTUP_TIME = 50;

    Serial.println("Turn all LEDs to WHITE");
    colorWipe(pixels.Color(255, 255, 255), LED_STARTUP_TIME * 5);     // White
    delay(3*LED_STARTUP_TIME);
    Serial.println("Turn all LEDs off");
    colorWipe(pixels.Color(0, 0, 0), LED_STARTUP_TIME);            // Off
    delay(10*LED_STARTUP_TIME);

    int LDR_1[] = {1023, 1023, 1023, 1023};
    rawLDRValues(LDR_1);

    // Pixel introduction. This is NOT needed.
    Serial.println("Turn all LEDs to RED");
    colorWipe(pixels.Color(255, 0, 0), LED_STARTUP_TIME);         // Red
    delay(3*LED_STARTUP_TIME);
    Serial.println("Turn all LEDs to GREEN");
    colorWipe(pixels.Color(0, 255, 0), LED_STARTUP_TIME);         // Green
    delay(3*LED_STARTUP_TIME);
    Serial.println("Turn all LEDs to BLUE");
    colorWipe(pixels.Color(0, 0, 255), LED_STARTUP_TIME);         // Blue
    delay(3*LED_STARTUP_TIME);
    Serial.println("Turn all LEDs to WHITE");
    colorWipe(pixels.Color(255, 255, 255), LED_STARTUP_TIME);     // White
    delay(3*LED_STARTUP_TIME);
    Serial.println("Turn all LEDs off");
    colorWipe(pixels.Color(0, 0, 0), LED_STARTUP_TIME);            // Off

      // Teensy does not reccomend using this, since the increased reolution will mostly be noise. 
      // https://forum.pjrc.com/threads/25111-Decreasing-noise-on-Teensy-ADC?p=42891&viewfull=1#post42891
      //analogReadRes(16);          // Teensy 3.0: set ADC resolution to this many bits

      // NYI: This might be useful to keep the input signals a bit more "steady".
      //analogReadAveraging(16);    // average this many readings

  // Audio connections require memory to work.  For more
  // detailed information, see the MemoryAndCpuUsage example
  AudioMemory(AUDIO_MEMORY);

  // Enable and set the volume of the Audio shield output.
  sgtl5000_1.enable();
  sgtl5000_1.volume(OUTPUT_VOLUME);

  // The Granular effect requires memory to operate. Most be pre-allocated.
  granR.begin(granMemR, GRANULAR_MEMORY_SIZE);
  granL.begin(granMemL, GRANULAR_MEMORY_SIZE);

  // Set the High Pass filters.
  filterR.resonance(FILTER_RESONANCE);
  filterL.resonance(FILTER_RESONANCE);
  filterR.frequency(0);
  filterL.frequency(0);

  // Set initial volumes for the dynamic volume mixer, beatMix.
  muteBeatMix(); // Set the initial beatMix levels to 0.
  setVerbDW(0.0); // Set Reverb fully DRY

  // Set static mix volumes. These should not need to be changed.
  setOutMixChannel(0, 1.0); // Forward the beatMix audio at unity volume.
  setOutMixChannel(1, 0.0); // Unused channel set to 0.
  setOutMixChannel(2, 0.0); // Unused channel set to 0.
  setOutMixChannel(3, 1.0); // Forward the ELTON mode audio at unity volume.
  verbMixR.gain(1, 0.0); // verbMix channel not used.
  verbMixR.gain(2, 0.0); // verbMix channel not used.
  verbMixL.gain(1, 0.0); // verbMix channel not used.
  verbMixL.gain(2, 0.0); // verbMix channel not used.

  // Get the ambient LDR readings.
  rawLDRValues(LDR_0);
  // Add a small value to the initial LDR readings in order to emphasize the zero value at ambeint conditions.
  for(int k = 0; k < 4; k++){
    LDR_0[k] += AMBIENT_PADDING;
    LDR_M[k] = LDR_1[k] - LDR_0[k]; // Populate the LDR_M (LDR_Max) array for calibration. 

    // If the differece is small, the calibration has failed. Setting to a more wide value.
    if(LDR_M[k] < AMBIENT_PADDING){
      LDR_M[k] = 1023 - LDR_0[k];
      // NYI: Do we wanna stop execution in stead?
    }
  }

  // Some SD card testing. Directly taken from the example.
  SPI.setMOSI(SDCARD_MOSI_PIN);
  SPI.setSCK(SDCARD_SCK_PIN);
  if (!(SD.begin(SDCARD_CS_PIN))) {
    // stop here, but print a message repetitively
    while (true) {
      Serial.println("Unable to access the SD card");
      delay(500);
    }
  }

  // Stop if the samples on the SD card can't be read.
  if(!SD.exists(BEAT0_SAMPLE) or !SD.exists(BEAT1_SAMPLE) or !SD.exists(BEAT2_SAMPLE) or !SD.exists(BEAT3_SAMPLE) or !SD.exists(ELTON_SAMPLE)) {
    while (true) {
      Serial.println("Unable to load file(s) from SD card.");
      delay(500);
    }
  }
}


///////////////
// Main Loop //
///////////////
void loop() {

  // Start the loop by getting the current LDR ratios.
  storeLDRRatios();

  //delay(100);

  // Set the mix, depending on the LDR values.
  setBeatMix();

  // Loop all the sounds. This is rough way of doing it. 
  // NYI: Use a different library to be able to adjust the playback speed.
  if (!beat0.isPlaying()) {
    beat0.play(BEAT0_SAMPLE);
  }
  if (!beat1.isPlaying()) {
    beat1.play(BEAT1_SAMPLE);
  }
  if (!beat2.isPlaying()) {
    beat2.play(BEAT2_SAMPLE);
  }
  if (!beat3.isPlaying()) {
    beat3.play(BEAT3_SAMPLE);
  }

  // Every PEAK_TIME milli seconds, update the status of the peak detecting LEDs.
  // BUG: I do not like this. It is very easy to miss out on ticks here under load.
  // Can be done more easily with millis()
  if (msecs % PEAK_TIME == 0) {
    blinkHeartBeatLEDs();  
  }

  if (triggerElton()){
    Serial.println("Trigger Elton Mode!");

    // Stop and mute all the beats playing. We're in ELTON MODE now!
    beat0.stop();
    beat1.stop();
    beat2.stop();
    beat3.stop();
    muteBeatMix();

    // Initialise LED variables.
    unsigned int wheel_pos = 0; // Needed for the rainbow effect. "Counts" the position on the color wheel.
    unsigned int last_rainbow_tick = millis() - RAINBOW_INTERVAL; 
    unsigned int blink_time = millis(); // Needed to blink stuff. Should probably be unsigned long
    unsigned int blink_interval = 150;
    bool blink_state = true;
    uint8_t slow_wheel_pos = 0;
    unsigned int slow_wheel_tick = millis();

    // Effects variables
    bool freeze_enabled = false;

    // Grace period
    unsigned int grace_period = millis() + ELTON_GRACE_PERIOD_TIME;

    // The isPlaying function of the sample player sometimes takes a few milliseconds to represent the correct value.
    // Usually this is solved with a delay in example codes. But delays don't work well here. 
    unsigned int is_playing_compensation = millis() + 100;

    // Loop while the ELTON sample plays.
    while(remainElton() or elton.isPlaying() or millis() < is_playing_compensation){

      // Loop the ELTON sample
      if (!elton.isPlaying()) {
        elton.play(ELTON_SAMPLE);
        is_playing_compensation = millis() + 100; // See above comment
      }

      // Need to re-read the LDR values in here when in ELTON mode.
      storeLDRRatios();

      //////////////////
      // ELTON Effect //
      //////////////////

      // Effects control section. Only active after the GRACE_PERIOD_TIME.
      if (millis() > grace_period) {
        reverbControl(REVERB_LDR); // Reverb control
        filterControl(HP_LDR); // High-pass filter control.
        freeze_enabled = granularControl(freeze_enabled, FREEZE_LDR, LENGTH_LDR); // Granular freeze control and grain length control.
      }

      ////////////////
      // ELTON LEDs //
      ////////////////

      // The basic "Disco" effect of ELTON mode. Sets the basic light pattern.
      eltonDiscoLEDs(wheel_pos);

      if (millis() > grace_period) {
        freezeLEDsControl(FREEZE_LDR, LENGTH_LDR, blink_state, freeze_enabled);
        // Wash color effect for reverb and high-pass.
        washColorEffect(Wheel((slow_wheel_pos) & 255), HP_LDR, REVERB_LDR); // Reverb & high-pass effect
      }

      // Timing control of the basic ELTON disco mode.
      if (millis() - last_rainbow_tick >= RAINBOW_INTERVAL){
        wheel_pos++;
        last_rainbow_tick = millis();
      }

      // Write all the colors we've been caluculating to the pixel but wait to display them. 
      commitEltonColors();

      // Show all the pixels we've been setting.
      pixels.show();

      ////////////////////////////
      // ELTON LED Timing stuff //
      ////////////////////////////

      // Timing control of the wash colors.
      if (millis() - slow_wheel_tick >= RAINBOW_INTERVAL * 8){
        slow_wheel_pos++;
        slow_wheel_tick = millis();
        if (slow_wheel_pos > 255){
          slow_wheel_pos = 0;
        }
      }

      // Time control for the freeze LED blink effect.
      if (!freeze_enabled){
        blink_interval = getFreezeLength(LENGTH_LDR);
      }
        if(millis() - blink_time >= blink_interval){
          blink_state = !blink_state;
          blink_time = millis();
        }
    }
  }
  else {
  }
  // NYI: Remember to reset all the effects. Sometimes they linger on startup which is strange during the grace_period.
}

void freezeLEDsControl(int freeze_ldr_n, int length_ldr_n, bool blink_state, bool freeze_enabled){
  if (!freeze_enabled){
    // Return if freeze is not active. Decided in the audio part.
    return ;
  }
  long blink_color;
  if (blink_state){
    blink_color = mixColors(pixels.Color(255, 0, 0), pixels.Color(128, 0, 128), ldr_ratios[FREEZE_LDR]);
  }
  else {
    blink_color = mixColors(pixels.Color(0, 0, 255), pixels.Color(128, 0, 128), ldr_ratios[FREEZE_LDR]);
  }
  for (int k = 0; k < pixels.numPixels(); k++){
    setEltonPixelColor(k, blink_color);
  }
}

//////////////////////////
// Functions below here //
//////////////////////////

// Reverb Control
void reverbControl(int ldr_n){
  verbR.roomsize(ldr_ratios[ldr_n]);
  verbL.roomsize(ldr_ratios[ldr_n]);
  //verbL.damping(ldr_ratios[ldr_n]);
  //verbR.damping(ldr_ratios[ldr_n]);
  setVerbDW(volume_scaling(ldr_ratios[ldr_n]));
}

// Reverb DRY/WET balance
void setVerbDW(float dw){
  // NB: Maybe response is not linear!
  verbMixR.gain(0, dw);       // WET Channel R
  verbMixR.gain(3, 1.0 - dw); // DRY Channel R
  verbMixL.gain(0, dw);       // WET Channel L
  verbMixL.gain(3, 1.0 - dw); // DRY Channel L
}

// High-pass filter control
void filterControl(int ldr_n){
  filterR.frequency(int(ldr_ratios[ldr_n] * FILTER_MAX));
  filterL.frequency(int(ldr_ratios[ldr_n] * FILTER_MAX));
}

// Get the length of the freeze effect.
int getFreezeLength(int length_ldr_n) {
  // NYI: Needs more testing
  return 240 - int(200*ldr_ratios[length_ldr_n]);
}

bool granularControl(bool freeze_enabled, int freeze_ldr_n, int length_ldr_n){
  bool freeze_event = ldr_ratios[freeze_ldr_n] > GRANULAR_FREEZE_THRESHOLD; // NB: Need to test this value.
  if(freeze_event and !freeze_enabled){
    granR.beginFreeze(getFreezeLength(length_ldr_n));
    granL.beginFreeze(getFreezeLength(length_ldr_n));
    granR.setSpeed(1.0);
    granL.setSpeed(1.0);
    freeze_enabled = true;
  }
  else if (!freeze_event and freeze_enabled){
    granR.setSpeed(1.0);
    granL.setSpeed(1.0);
    granR.stop();
    granL.stop();
    freeze_enabled = false;
  }
  else if(freeze_event and freeze_enabled){
    // Use ldr_ratios to set the playback speed of the grain.
    if (ldr_ratios[freeze_ldr_n] < 0.7) { // NYI Need to test the ratios here further.
      granR.setSpeed(1.0 - (1.0 - ldr_ratios[freeze_ldr_n])*0.3); // NYI Need to test the ratios here further.
      granL.setSpeed(1.0 - (1.0 - ldr_ratios[freeze_ldr_n])*0.3); // NYI Need to test the ratios here further.
    }
    else {
      granR.setSpeed(1.0);
      granL.setSpeed(1.0);
    }
  }
  return freeze_enabled;
}

// Set the volume of each channel, R/L, of the output mixer. Should only be used in setup.
void setOutMixChannel(int channel, float volume_ratio){
  outMixR.gain(channel, volume_ratio);
  outMixL.gain(channel, volume_ratio);
}

// Automatically set the volume of all the heart beat samples. Reads the ldr_ratios directly as they stand.
void setBeatMix() {
  for ( int k = 0; k < 4; k++){
    beatMix.gain(k, volume_scaling(ldr_ratios[k]));
  }
}

// Mute all the heart beats. Used in ELTON mode.
void muteBeatMix() {
  for ( int k = 0; k < 4; k++){
    beatMix.gain(k, 0.0);
  }

}

// Read the LDR pins and return them, called from storeLDRRatios(). They are then converted to ratios over there.
void rawLDRValues(int *values){
  for(int k = 0; k < 4; k++){
    values[k] = analogRead(LDR_PINS[k]);
  }
  if (DEBUG){
    for(int k = 0; k < 4; k++){
      Serial.print(values[k]);
      Serial.print("\t(");
      Serial.print(ldr_ratios[k]);
      Serial.print(")\t");
    }
    Serial.println();
  }
}

// Convert the LDR values (read using the rawLDRValues() function) to ratios and store them in memory.
void storeLDRRatios(){
  int raw_values[] = {0, 0, 0, 0};
  rawLDRValues(raw_values);
  for(int k = 0; k < 4; k++){
    // min(max()); in order to constrain values in case of failed ambient readings.
    //ldr_ratios[k] = min(max(float(raw_values[k] - LDR_0[k]) / float(LDR_M[k]), 0.0), 1.0);
    float new_value =  min(max(float(raw_values[k] - LDR_0[k]) / float(LDR_M[k]), 0.0), 1.0);
    // Some sort of "glide" impllementation. Did not work well.
//    if (ldr_ratios[k] - new_value > 0.02){
//      new_value = ldr_ratios[k] - 0.01;
//    }
//    else if (ldr_ratios[k] - new_value < 0.02){
//      new_value = ldr_ratios[k] + 0.01;
//    }
      ldr_ratios[k] = min(max(new_value, 0.0), 1.0);
  }

}

// Checks if ELTON mode should be triggered. The for loop uses fancy programming math to check if all the ldr_ratios are above the threshold.
bool triggerElton(){
  // Returns true if all LDR values are above the TRIGGER_ELTON_THRESHOLD.
  bool condition = true;
  for (auto element : ldr_ratios){
    condition &= element > TRIGGER_ELTON_THRESHOLD;
  }
  return condition or DEBUG_ELTON;
}

// Checks if we should remain in ELTON mode.. The for loop uses fancy programming math to check if any the ldr_ratios are above the threshold.
bool remainElton(){
  // Returns true if any LDR value is above the REMAIN_ELTON_THRESHOLD.
  bool condition = false;
  for (auto element : ldr_ratios){
    condition |= element > REMAIN_ELTON_THRESHOLD;
  }
  return condition or DEBUG_ELTON;
}

// Scale the ldr_ratio values for controling volume. This is done to quickly (but smoothly) bring the volume to max, since subtle mixing isn't interesting here.
float volume_scaling(float value_in) {
    if (value_in < BEAT_VOL_MIN_THRESHOLD) {
      return 0.0;  
    }
    else if (value_in > BEAT_VOL_MAX_THRESHOLD) {
      return 1.0;  
    }
    else {
        float vol_val = (value_in - BEAT_VOL_MIN_THRESHOLD) / (BEAT_VOL_MAX_THRESHOLD - BEAT_VOL_MIN_THRESHOLD);
        return vol_val;
    }
}

///////////////////
// LED functions //
///////////////////

// The basic LED pattern of the unaffected ELTON mode. LEDs cycling through nice colors at a brisk pace. 
void eltonDiscoLEDs(int wheel_pos){
  for (int k=0; k<pixels.numPixels(); k++){
    long color = Wheel(((k * 256 /pixels.numPixels()) + wheel_pos) & 255); 
    setEltonPixelColor(k, color);
  }
}

// The reverb and high-pass filter LED effects combo.
void washColorEffect(long slow_color, int hp_ldr_n, int rev_ldr_n){
  float mix = max(ldr_ratios[hp_ldr_n], ldr_ratios[rev_ldr_n]); // Use the higher of the LDR values to control the magnitude of the effect.
  float white_mix = ldr_ratios[hp_ldr_n]; // How desaturated is the final color?
  uint8_t wash_white = (1.0 - white_mix * WHITE_WASH_RATIO) * 255; // Maintain even brightness since white is just turning all the colors on.
  long wash_color = mixColors(pixels.Color(wash_white, wash_white, wash_white), slow_color, white_mix); // create the washed base color
  for (uint8_t k = 0; k < pixels.numPixels(); k++){
    long color = mixColors(wash_color, pixels.Color(COLORS[k][0], COLORS[k][1], COLORS[k][2]), mix); // Mix the washed color with the basic "disco" mode color.
    setEltonPixelColor(k, color);
  }
}

// Set pixel colors in an array, COLORS, before commiting them to the pixels themselves. This is for having a dynamic color effect that can be affected. 
void setEltonPixelColor(int n, long color){
  COLORS[n][0] = getRed(color);
  COLORS[n][1] = getGreen(color);
  COLORS[n][2] = getBlue(color);
}

// Commit the ELTON mode colors to the pixels but don't display them. Use pixels.show() for that.
void commitEltonColors(){
  for (int k = 0; k < pixels.numPixels(); k++){
    pixels.setPixelColor(k, pixels.Color(COLORS[k][0], COLORS[k][1], COLORS[k][2]));
  }
}

// Blink all the LEDs as needed in time with the heart beats. This is run from the main program.
void blinkHeartBeatLEDs(){
  for (int k = 0; k < 4; k++){
    heartBeatLED(k);  
  }
}

// Control each LED group by detecting the heart beats. 
void heartBeatLED(int n){
    if (peaks[n].available()){
      float p = peaks[n].read();
      uint32_t c = pixels.Color(255 * p * ldr_ratios[n], 0, 0, 0);
      setPixelGroupColor(n, c);
      pixels.show();
   }
}

// Set the color of each pixel in a group at the same time. The groups are defined in the global pixelGroups variable.
void setPixelGroupColor(int n, int c){
  for (int k = 0; k < pixels.numPixels(); k++)
  {
    if(pixelGroups[k] == n){
      pixels.setPixelColor(k, c);
    }
  }
}

// Color Wipe. Only used at startup. Used to indicate the calibration status. 
void colorWipe(uint32_t c, uint16_t wait)
{
  for(uint16_t i=0; i<pixels.numPixels(); i++)
  {
      pixels.setPixelColor(i, c);
      pixels.show();   
      delay(wait);
  }
}

// Input a value 0 to 255 to get a color value.
// The colours are a transition r - g - b - back to r.
// Borrowed directly from the BrightDot example program.
uint32_t Wheel(byte WheelPos)
{
  if(WheelPos < 85)
  {
   return pixels.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
  } else if(WheelPos < 170)
  {
   WheelPos -= 85;
   return pixels.Color(255 - WheelPos * 3, 0, WheelPos * 3);
  }
  else
  {
   WheelPos -= 170;
   return pixels.Color(0, WheelPos * 3, 255 - WheelPos * 3);
  }
}

// Mix 2 colors to yield a linear combination. 
long mixColors(long c1, long c2, float mix){
  uint8_t r = getRed(c1) * mix + getRed(c2) * (1.0 - mix);
  uint8_t g = getGreen(c1) * mix + getGreen(c2) * (1.0 - mix); 
  uint8_t b = getBlue(c1) * mix + getBlue(c2) * (1.0 - mix);
  return pixels.Color(r, g, b);
}

// Extract individual colors from a full color. This is used to combine multiple LED effects in ELTON mode
uint8_t getRed(long color){
  return (uint8_t)((color >> 16) & 0xff);
}

// Extract individual colors from a full color. This is used to combine multiple LED effects in ELTON mode
uint8_t getGreen(long color){
  return (uint8_t)((color >> 8) & 0xff);
}

// Extract individual colors from a full color. This is used to combine multiple LED effects in ELTON mode
uint8_t getBlue(long color){
  return (uint8_t)(color & 0xff);
}

Fabrication Files

Here you will find all of the files for the 3D printed moulds I used to shape the hearty party alongside the companion ring for that piece. They are named left and right and divided in A for atrium and V for ventricle.

STL Files on Google Drive for all Moulds

StoryBoarding

My initial ideas for the video are here.

1


Last update: 2023-06-28