Code for Task #3 - Oh you gotta have Heart!

Post your code here, so we avoid spoilers in the main Task #3 thread

Ok… Here’s my heartbeat generator, which uses the envelope generator portion of my work-in-progress additive light synthesis engine, which is eventually going to let me do super cool things with midi.

This is a very general purpose toolkit that lets you send pulses of light down a strip, and shape their hue, saturation and brightness with an envelope generator, like you’d find on an audio synthesizer.

It allows me to set up and shape 3 pulses, corresponding to the P,R and T portions of the heartbeat waveform, and make them travel down the strip at about 40 beats per minute. It’s not as complicated as it looks at first glance – at its heart, it actually has a lot in common with the 3 moving pixels @jeff’s “An Intro to Pixelblaze Code” pattern.

Hope you enjoy reading this code. Please take, adapt and use whatever ideas or portions of it strike you as interesting or useful for your own work! I’ll do my best to answer any questions that may come up.

// WaveShaper Envelope Generator Testbed.   Allows generation of 
// multiple traveling pulses of light, shaped by an dASD envelope generator
// which can be variably applied to hue, saturation and brightness
//
// 2021 - ZRanger1 
// constant indices for wave properties
var _env = 0;         // envelope descriptor for wave
var _hue = 1;         // base hue of wave
var _hueEnvAmt = 2    // amount of envelope to apply to hue 
var _sat = 3          // base saturation of wave
var _satEnvAmt = 4    // amount of envelope to apply to saturation
var _bri = 5;         // base brightness of wave
var _briEnvAmt = 6    // amount of envelope to apply to brightness
var _speed = 7;       // speed at which wave moves across strip
var _moveWave = 8;    // function to calculate offset
var _applyEnv = 9;    // function to apply envelope to current pixel in render
var _offset = 10;     // original starting position of wave on strip
var _pos = 11;        // current position of wave on strip
var _envIndex = 12;   // index of current pixel into envelope array

var _wave_desc_size = 1+_envIndex;  // size of wave properties object
var nWaves = 3;                 // number of waves running
var waveList = array(nWaves);    // array to hold waves

// Global variables
var hue,sat,bri;    // used to build the final value that will be rendered for a pixel

// envelopes for characteristic parts of heartbeat
// parameters are: max amplitude, delay, Attack, Sustain, Decay 
// dASD parameters are in % of strip.
var envP = envelopeBuilder(0.1, 0, 0.02,0.01, 0.04);
var envR = envelopeBuilder(1.0, 0, 0.02,0.03, 0.02);
var envT = envelopeBuilder(0.3, 0, 0.04,0.001,0.04);

// Build table of waves to display
// in this case, our heartbeat -- a low amplitude wave follwed by a larger, higher amplitude wave  
function initWaves() {
    speed = 1.5;   // time in seconds to traverse strip, or about 40bpm

// wave setup parameters are:
// envelope,hue,hueEnvAmt,sat,satEnvAmt,bri,briEnvAmt,speed,offset
    waveList[0] = waveSetup(envP,0,0.04,1,0,1,1,speed,0.12);   // p
    waveList[1] = waveSetup(envR,0,0,1,0,1,1,speed,0.08);  // r    
    waveList[2] = waveSetup(envT,0,0.04,1,0,1,1,speed,0);      // t
}

// Move all existing waves according to their description
function moveWaves() {
  for (var i = 0; i < nWaves; i++) {
    var a = waveList[i];    
    a[_moveWave](a);
  }  
}

// draw all the waves that exist at a given pixel
function drawWaves(index) {
  var denom = 0;
  hue = 0; sat = 0; bri = 0;
  
  for (var i = 0; i < nWaves; i++) {
    var a = waveList[i];
    b = a[_applyEnv](a,index); 
    if (b) {
      hue += (b * a[_hueEnvAmt]) + a[_hue];
      sat += a[_sat] - (b * a[_satEnvAmt]);
      bri += b * a[_briEnvAmt] * a[_bri];
      denom++;
    }
  } 
  hue /= denom; 
  sat /= denom;
  bri /= denom;
}

// envelopeBuilder
// Creates an array describing an envelope for a waveform to be displayed on a strip.
// The components of the envelope are:
//   amplitude: amplitude limit (0-1)
//   delay: delay before onset of wave (% of strip)
//   attack: time to rise from 0 to full amplitude (% of strip)
//   sustain: time that wave will be output at full amplitude (% of strip)
//   decay: time to fall from full amplitude to 0 (% of strip)
function envelopeBuilder(amplitude,delay,attack,sustain,decay) {
  var i,j,val,incr,buf;

// convert parameters to absolute numbers of pixels  
  delay = floor(pixelCount * delay); // convert to absolute pixels
  attack = floor(pixelCount * attack);  
  sustain = floor(pixelCount * sustain);  
  decay = floor(pixelCount * decay);  
  
// calculate total envelope length, allocate array, and store length
// in first element
  val = 1 + delay + attack + sustain + decay;
  buf = array(val);
  buf[0] = val - 1;
  i = 1;
  
// delay
  while (i <= delay) {
    buf[i++] = 0;
  }
  
// attack
  incr = 1 / attack;
  for (j = 1;j <= attack; j++) {
    val = j * incr;
    buf[i++] = amplitude * pow(val,3);
  }
  
// sustain 
  for (j = 0;j < sustain; j++) {
    buf[i++] = amplitude;
  }
  
// decay   
  incr = 1 / decay;
  for (j = 1;j <= decay; j++) {
    val = 1 - (j * incr);
    buf[i++] = amplitude * pow(val,3);
  }
  return buf;
}

// create descriptor for waveform
function waveSetup(env,hue,hueAmt,sat,satAmt,bri,briAmt,speed,offset) {
  var w = array(_wave_desc_size)
  
  w[_env] = env;
  w[_hue] = hue;
  w[_hueEnvAmt] = hueAmt;
  w[_sat] = sat;
  w[_satEnvAmt] = satAmt;
  w[_bri] = bri;
  w[_briEnvAmt] = briAmt;
  
  // speed = time (seconds) to traverse strip  
  w[_speed] = abs(speed) / 65.536;  
  
  // set update functions according to direction of travel
  if (speed >= 0) {
    w[_moveWave] = calcOffsetPos;
    w[_applyEnv] = envGenPos;
  } else {
    w[_moveWave] = calcOffsetNeg;
    w[_applyEnv] = envGenNeg;    
  }
  
  // initialize position of wave on strip.  
  offset = floor(pixelCount * offset);
  w[_offset] = offset; 
  w[_pos] = offset;
  w[_envIndex] = -1;
  
  return w;
}

function envGenNeg(w,t) {
  if (t == w[_pos]) {
    w[_envIndex] = 0;
  }
  
  if (w[_envIndex] >= 0) {
    if (w[_envIndex] >= w[_env][0]) {
      w[_envIndex] = -1;
      return 0
    }
    
    return w[_env][1+w[_envIndex]++]  
  }
  return 0
}

function envGenPos(w,t) {
  if (t == w[_pos]) {
    w[_envIndex] = 0;
  }
  
  if (w[_envIndex] >= 0) {
    if (w[_envIndex] >= w[_env][0]) {
      w[_envIndex] = -1;
      return 0
    }
    
    return w[_env][w[_env][0] - w[_envIndex]++]  
  }
  return 0
}

// keep index within the bounds of the strip by wrapping at both ends
function wrap(index) {
  if (index < 0) {
    index = pixelCount - index;
  }
  return index % pixelCount;
}

function calcOffsetPos(w) {
  var val,e;
  val = floor(pixelCount * time(w[_speed])); 
  w[_pos] = wrap(val+ w[_offset]);
}

function calcOffsetNeg(w) {
  var val,e;
  val = pixelCount - floor(pixelCount * time(w[_speed])); 
  w[_pos] = wrap(val + w[_offset]);
}

initWaves()

export function beforeRender(delta) {
  moveWaves()
}

export function render(index) {
  drawWaves(index);
  hsv(hue,sat,bri);
}
2 Likes