Can the output expander drive strips at different brightness?

Hello, I’m new to the Pixelblaze world and trying to understand what is possible.

I’m hoping to drive 3 separate strips with a single Pixelblaze, sending the same pattern simultaneously to each strip (same type and number of LEDs in each).

However, I would like to be able to control the brightness of each of the 3 strips independently. Would this be possible with the Pixelblaze output expander?

Hi @phirex! Welcome to Pixelblaze!

Yes, for independent brightness the Output Expander is necessary. If you can say how many LEDs are in each strip, I can write you a little snippet you can paste in each pattern that will duplicate the output but give you independent brightness sliders for each strip.

2 Likes

Oh fantastic!! This is cool, ok so the output expander basically lets you send separate output code to each strip, but buffers them so they play simultaneously? Nice!

I’d love a snippet. My strips are 150, 150 and 142 ws2812.

Separate but related question, given that code will send separate strip assignments sequentially, then they are buffered on the output expander and then sent out ‘simultaneously’, is there a point where ‘overloading’ the output expander will result in a drop in framerate?

Thanks so much for the reply!

I 100% didn’t realize this was possible – commenting partly to follow the answer for a future project.

Hey, sorry this took a bit!

First,

While they are buffered in the output expander, you won’t overload it. You’ll see a proportional drop in frame rate the same way you would for any pattern that’s not using the output expander: When the pattern math gets more intense.

Replicating Patterns Across Strips

Since you have a total of 442 pixels, first set that up in Settings:

Monosnap Arch 2023-09-14 15-39-32

Optional: Pixel Map

I’m going to add a pixel map. This is optional for you - it might help make 2D patterns look nice, but really I just want preview output I can capture easily for this forum post. This map will shows the three strips as horizontal lines. I don’t totally understand this map code, I had ChatGPT write the core of it.

function (pixelCount) {
  // We'll reuse this constant in our pattern in a sec...
  var STRIP_LENGTHS = [150, 150, 142]
  
  const map = generateLines(STRIP_LENGTHS)
  
  // Add aesthetic ghost pixel spacers to compress lines vertically
  return [ ...map, [0,-10], [0, 12]]
}

// Map generation helper for horizontal lines of various lengths
const generateLines = (arr) => {
  return arr.flatMap((value, index) => 
    Array.from({ length: value }, (_, i) => [i + 1, index])
  );
};

 

Adapting Pattern Code

Let’s start with the default new pattern, because it uses all the things we’ll want to adapt:

export function beforeRender(delta) {
  t1 = time(.1)
}

export function render(index) {
  h = t1 + index/pixelCount
  s = 1
  v = 1
  hsv(h, s, v)
}

Here’s what it looks like through our map. Notice it’s not identical through each strip.

First I want to modify any part of beforeRender() that uses pixelCount, the overall number of pixels. pixelCount will be set for you to 442 from the Settings page, but for now I’m making the decision that most patterns will look best if the longest strip is considered a full strip, and any shorter strips are just truncated. That means I want to set it to 150, our longest strip length.

export function beforeRender(delta) {
  pixelCount = 150  // NEW
  t1 = time(.1)
}

export function render(index) {
  h = t1 + index/pixelCount
  s = 1
  v = 1
  hsv(h, s, v)
}

Let’s make the whole thing more configurable for other arbitrary strip lengths.

var STRIP_LENGTHS = [150, 150, 142]
var MAX_STRIP_LEN = STRIP_LENGTHS.reduce((acc, v) => max(acc, v), 0)

export function beforeRender(delta) {
  pixelCount = MAX_STRIP_LEN
  t1 = time(.1)
}

export function render(index) {
  h = t1 + index/pixelCount
  s = 1
  v = 1
  hsv(h, s, v)
}

Wow, looks pretty great - are we done? Well, maybe for this simple pattern, but we really haven’t fixed things for most others. If we made the strips somewhat different…

var STRIP_LENGTHS = [100, 150, 142]

It’s not duplicated as I wanted. The problem is that we’re not re-assigning index correctly within render() back to 0 at the start of each strip.

Here’s a very verbose way that only works for this three strip array. I want to write it out first so you can understand the intent of a more robust solution.

var STRIP_LENGTHS = [100, 150, 142]
var MAX_STRIP_LEN = STRIP_LENGTHS.reduce((acc, v) => max(acc, v), 0)

export function beforeRender(delta) {
  pixelCount = MAX_STRIP_LEN
  t1 = time(.1)
}

export function render(index) {
  // The long way
  if (index >= STRIP_LENGTHS[0] + STRIP_LENGTHS[1]) {
    // We're in the third strip. In this example, strips are: [100, 150, 142],
    // this means that index 250 is actually the first pixel of the third
    // strip. The first pixel of any strip should have index be 0. Index 391
    // is actially the 142nd pixel of the third strip, so it should have index = 141
    
    index = index - (STRIP_LENGTHS[0] + STRIP_LENGTHS[1])
    
  } else if (index >= STRIP_LENGTHS[0]) {
    // We're not in the third strip, so if index >= 100, we must be in the second strip
    
    index = index - STRIP_LENGTHS[0] 
  }
  // if (index < STRIP_LENGTHS[0]) { 
    // No remapping of `index` needed in this case - it's already correct
  
  
  h = t1 + index/pixelCount
  s = 1
  v = 1
  hsv(h, s, v)
}

And the output is much more what I was hoping for:

 

Generalized for Any Number of Strips

It would be nice if we could take the overall global index and retrieve both the strip number and the pixel index within that strip, for any number of strips.

We could write two functions that take the overall global pixel index: one that returns the strip number, and one that returns the local index in that strip. I’ll write one function since the approach I’d use for both is similar. When one function needs to return two answers in JS, since we don’t have objects in Pixelblaze (yet), we could return a two element array, [stripNumber, localIndex]. Instead, I’ll choose to return the local index from this one function and stash the strip number for the current pixel index in a global variable. With the strip number known, now we can also quickly retrieve the number of pixels in the current strip, and optionally decide to use that as the overall pixelCount in the patterns we’re adapting. If we choose that route, strips with a different number of pixels all scale the overall pattern so that the start and end of different length strips all display similar to each other.

var STRIP_LENGTHS = [100, 150, 142]
var MAX_STRIP_LEN = STRIP_LENGTHS.reduce((acc, v) => max(acc, v), 0)


var strip // 0-indexed strip number, from 0..(STRIP_LENGTHS.length - 1)

function findLocalIdx(globalIndex) {
  var runningTotal = 0
  for (i = 0; i < STRIP_LENGTHS.length; i++) {
    runningTotal += STRIP_LENGTHS[i]
    if (globalIndex < runningTotal) {
      strip = i
      return globalIndex - (runningTotal - STRIP_LENGTHS[i])
    }
  }
  // If not found, this means the index was out of bounds.
  strip = -1 
  return -1 
}


export function beforeRender(delta) {
  // pixelCount = MAX_STRIP_LEN
  t1 = time(.1)
}

export function render(index) {
  // The flexible way
  index = findLocalIdx(index)
  pixelCount = STRIP_LENGTHS[strip]
  
  h = t1 + index/pixelCount
  s = 1
  v = 1
  hsv(h, s, v)
}

Notice how the overall distribution of colors is the same regardless of strip length.

These code snippets are the basic building blocks we can use with most patterns from the library. Let’s look at another one. This is “marching rainbow”:

export function beforeRender(delta) {
  t1 = time(.1)
  t2 = time(.05)
}

export function render(index) {
  w1 = wave(t1 + index/pixelCount)
  w2 = wave(t2-index/pixelCount*10+.2)
  v = w1 - w2
  h = wave(wave(wave(t1 + index/pixelCount)) - index/pixelCount)
  hsv(h,1,v)
}

And now adapted with the same new stuff from above:


var STRIP_LENGTHS = [100, 150, 142]
var MAX_STRIP_LEN = STRIP_LENGTHS.reduce((acc, v) => max(acc, v), 0)

var strip // 0-indexed strip number, from 0..(STRIP_LENGTHS.length - 1)

function findLocalIdx(globalIndex) {
  var runningTotal = 0
  for (i = 0; i < STRIP_LENGTHS.length; i++) {
    runningTotal += STRIP_LENGTHS[i]
    if (globalIndex < runningTotal) {
      strip = i
      return globalIndex - (runningTotal - STRIP_LENGTHS[i])
    }
  }
  // If not found, this means the index was out of bounds.
  strip = -1 
  return -1 
}


export function beforeRender(delta) {
  // pixelCount = MAX_STRIP_LEN
  
  t1 = time(.1)
  t2 = time(.05)
}

export function render(index) {
  // Added these two lines here...
  index = findLocalIdx(index)
  pixelCount = STRIP_LENGTHS[strip]

  w1 = wave(t1 + index/pixelCount)
  w2 = wave(t2-index/pixelCount*10+.2)
  v = w1 - w2
  h = wave(wave(wave(t1 + index/pixelCount)) - index/pixelCount)
  hsv(h,1,v)
}

Or, back to the approach where shorter strips are just subsegments of the longest one:

//...
export function beforeRender(delta) {
  pixelCount = MAX_STRIP_LEN // Uncommented this, and
//...
export function render(index) {
  index = findLocalIdx(index)
  // pixelCount = STRIP_LENGTHS[strip] // Disabled this line
//...

If for some reason you really need to boost frame rate and reduce the repeat calculations being done for each pixel in each frame, you could cache the computed localIndex and strip number in arrays, which would result in O(1) lookups for each pixel. I’ll skip that for now since I don’t think it’ll be an issue for setups with <1000 pixels.

 

Use with Patterns that use a Pixel Buffer

Patterns like “sparks” allocate arrays to hold brightness or hue values for each pixel. As long as we’re thoughtful, these patterns can be adapted as well. Here’s the original:

And here’s how I used the same snippet to adapt it:

var STRIP_LENGTHS = [100, 150, 142]
var MAX_STRIP_LEN = STRIP_LENGTHS.reduce((acc, v) => max(acc, v), 0)
pixelCount = MAX_STRIP_LEN // This is new, for the array size allocations

var strip // 0-indexed strip number, from 0..(STRIP_LENGTHS.length - 1)

function findLocalIdx(globalIndex) {
  var runningTotal = 0
  for (i = 0; i < STRIP_LENGTHS.length; i++) {
    runningTotal += STRIP_LENGTHS[i]
    if (globalIndex < runningTotal) {
      strip = i
      return globalIndex - (runningTotal - STRIP_LENGTHS[i])
    }
  }
  // If not found, this means the index was out of bounds.
  strip = -1 
  return -1 
}


// Below is the original sparks pattern with just two lines added
numSparks = 20;
friction = 1 / pixelCount ;
sparks = array(numSparks);
sparkX = array(numSparks);
pixels = array(pixelCount);
 
export function beforeRender(delta) {
  pixelCount = MAX_STRIP_LEN // This was added to the original pattern
  
  delta *= .1;
  for (i = 0; i < pixelCount; i++)
    pixels[i] = pixels[i] *.2
  for (i = 0; i < numSparks; i++) {
    if (sparks[i] <= 0) {
      sparks[i] = 1 + random(.4);
      sparkX[i] = random(5);
    }
    sparks[i] -= friction * delta;
    sparkX[i] += sparks[i] * sparks[i] * delta;
    if (sparkX[i] > pixelCount) {
      sparkX[i] = 0;
      sparks[i] = 0;
    }
    pixels[sparkX[i]] += sparks[i];
  }
}

export function render(index) {
  index = findLocalIdx(index) // This was added to the original pattern
  
  v = pixels[index];
  hsv(.02, 1.1 - v*v, v * v)
}

Last but not least, you asked about having separate brightness sliders for each strip. Going back to the original simple rainbow pattern, here’s how you’d do that with the tools we now have.

var STRIP_LENGTHS = [100, 150, 142]
var MAX_STRIP_LEN = STRIP_LENGTHS.reduce((acc, v) => max(acc, v), 0)

var stripBrightnesses = array(STRIP_LENGTHS.length)
export function sliderStrip0Brightness(_v) {
  stripBrightnesses[0] = _v
}
export function sliderStrip1Brightness(_v) {
  stripBrightnesses[1] = _v
}
export function sliderStrip2Brightness(_v) {
  stripBrightnesses[2] = _v
}

var strip // 0-indexed strip number, from 0..(STRIP_LENGTHS.length - 1)

function findLocalIdx(globalIndex) {
  var runningTotal = 0
  for (i = 0; i < STRIP_LENGTHS.length; i++) {
    runningTotal += STRIP_LENGTHS[i]
    if (globalIndex < runningTotal) {
      strip = i
      return globalIndex - (runningTotal - STRIP_LENGTHS[i])
    }
  }
  // If not found, this means the index was out of bounds.
  strip = -1 
  return -1 
}


export function beforeRender(delta) {
  // pixelCount = MAX_STRIP_LEN
  t1 = time(.1)
}

export function render(index) {
  // The flexible way
  index = findLocalIdx(index)
  pixelCount = STRIP_LENGTHS[strip]
  
  h = t1 + index/pixelCount
  s = 1
  v = 1
  hsv(h, s, v * stripBrightnesses[strip])
}

Oh, you made it to the end? Hungry for more? Sounds like you really need to fall into the rabbit hole by taking a look at @zranger1’s “Multisegment” pattern :rabbit2: :hole: :wave:

Hope this helps! Sorry again for taking several days to respond.

5 Likes