Keyframe loop rollover - or: "how I learned to stop worrying and love the modulo"


#1

Hey there,

I’m having trouble figuring out how to seemlessly rollover my keyframes.

void loop() {

currentTime = millis() % runTimeMillis; 

for(int j = nextKey; j < NUM_KEYS; j++) {

  if (currentTime >= nextKeyMillis) {    
     currentKey = j; 
     nextKey = j + 1; 

         if (nextKey >= NUM_KEYS) {
             nextKey = 0;
         }     
     break;    
  }  
}

currentKeyMillis = map(TIMESTAMPS_ARRAY[currentKey], 0, 1440, 0, runTimeMillis);
nextKeyMillis = map(TIMESTAMPS_ARRAY[nextKey], 0, 1440, 0, runTimeMillis);

// down here I control the LEDs, which works fine
}

The Modulus controlled currentTime rolls over from (for example) 14999 to 0, never reaching the last keyframe at 15000. If I fix it manually with currentTime + 100 >= nextKeyMillis, the rollover works, but it obviously throws the timing of the other keyframes off. If I have the last keyframe less than runTimeMillis it’ll rollover too early.

Any input greatly appreciated, I’ve been trying this the whole day…
Thanks,
Tobias


#2

Does currentTime = millis() % (runTimeMillis + 1); or currentTime = (millis() % runTimeMillis) + 1; work? Perhaps I don’t understand – what are you setting runTimeMillis to currently?


#3

runTimeMillis is the running time in milliseconds, at the moment 15000, in the installation it’ll be about 900000 (15mins).

unsigned long runTimeMillis = 0.25 * 60000;

so +1 doesn’t help much. here’s the “rollover moment”. The digits are as follows:
currentTime, currentKey, nextKey

14963,11,12
14974,11,12
14985,11,12
14996,11,12
7,11,12
18,11,12
29,11,12
40,11,12

here is what happens if I adjust the if-statement to if(currentTime + 10 >= nextKeyMillis):

14954,11,12
14966,11,12
14977,11,12
14988,11,12
14999,12,0
10,0,1
21,0,1
32,0,1
43,0,1
54,0,1

I guess I need to adjust the if-statement according to how often I update the millis()? At the moment I have a delay(10) and now it works with a +10 in the if-statement. However there are some glitches in the LEDs. And much more so if I choose to update only with a delay(100) (as all the keyframes in-between will be timed wrongly by 100ms).

Could there be a solution that affects only the rollover-moment, without tempering with the if-statement and affecting the timing of all the other keyframes?


#4

Also tried currentTime = (millis() % (runTimeMillis)) + 10; which has the same effect. The problem is it’s not bullet proof. Here for example the millis()were updated at 14999, which then failed to set back the keyframes to 0

14966,11,12
14977,11,12
14988,11,12
14999,11,12
10,11,12
21,11,12
32,11,12
43,11,12

#5

I think the problem is that millis() is not guaranteed to be called with any regularity, so you’ll probably have to take a different approach to mapping the system time. I’d recommend the delta T mapping approach. Something like …

long lastTime = 0; 
   ///< this is the actual time you checked during the last loop
long myCurrentKeyFrameTime = 0; 
   ///< this is the time you use to check your current keyframe - not sure of units

/// ...

long now = millis();

long deltaTime = now - lastTime; // this is the actual amount of time that has passed.

long deltaTimeInKeyFrameTime = map(deltaTime, 0, 1, 0, 1000);
    ///< For every 1 millisecond that passes, 1000 of the keyframe time units passes.

myCurrentKeyFrameTime += deltaTimeInKeyFrameTime; // add it to our current time.

/// Now do tests on myCurrentKeyFrameTime to see if you need to move to the next keyframe, etc.


#6

hmm, I’ve implemented the delta timeline. However, as the delta time is based on millis(), all it does at the moment is to give the timecode three more zeros. The problem persists. What did I miss? Before I can say that I love the modulo (as you hinted in the subject line) I have to figure out how to actually use it. :smile:

I’m still using it like this: unsigned long currentTime = millis() % (runTimeMillis);. Which means the time rolls over before it hits the last keyframe, unable to roll over the lastKey and nextKey.

Below the whole code for a bigger picture.


#include "FastLED.h"      // LED library
#include <avr/pgmspace.h> // include library to save to PROGMEM
#define DATA_PIN 6        // Data output to LED strip

/////////// PASTE DATA FROM OPEN FRAMEWORKS ///////////

#define NUM_LEDS 6
#define NUM_KEYS 4

PROGMEM prog_uchar RGB_ARRAY [4] [18] = { 
	{ 254, 0, 0, 254, 0, 0, 254, 0, 0, 254, 0, 0, 254, 0, 0, 254, 0, 0 }, 
	{ 20, 0, 233, 69, 0, 184, 135, 0, 118, 199, 0, 54, 244, 0, 9, 0, 0, 0 }, 
	{ 0, 0, 254, 2, 0, 252, 35, 0, 218, 100, 0, 152, 174, 0, 79, 229, 0, 23 }, 
	{ 0, 0, 254, 0, 0, 254, 0, 0, 254, 0, 0, 254, 0, 0, 254, 0, 0, 254 }
};

int TIMESTAMPS_ARRAY [4] = { 0, 480, 960, 1439 };

/////////// END OF OPEN FRAMEWORKS ///////////

CRGB leds[NUM_LEDS];      // array of LED pixels

unsigned long lastTime = 0;
unsigned long currentKeyTime;
unsigned long lastKeyTime;
unsigned long nextKeyTime;
byte lastKey;
byte nextKey;

byte red;
byte green;
byte blue;

int deltaUnit = 1000;
byte brightness = 255;
unsigned long runTimeMillis = 0.25 * 60000;

void setup() {
      Serial.begin(9600);
      delay(2000);
      LEDS.setBrightness(brightness);
      FastLED.addLeds<WS2811, DATA_PIN, GRB>(leds, NUM_LEDS);    
     
     lastKey = 0;
     nextKey = 1;
     lastKeyTime = map(TIMESTAMPS_ARRAY[lastKey], 0, 1440, 0, runTimeMillis * deltaUnit);
     nextKeyTime = map(TIMESTAMPS_ARRAY[nextKey], 0, 1440, 0, runTimeMillis * deltaUnit);
}


void loop() {

unsigned long currentTime = millis() % (runTimeMillis);
unsigned long deltaTimeMillis = currentTime - lastTime;
unsigned long deltaTime = map(deltaTimeMillis, 0, 1, 0, deltaUnit);

currentKeyTime += deltaTime;

for(int j = nextKey; j < NUM_KEYS; j++) {  
  
  if (currentKeyTime >= nextKeyTime) { 
    
     lastKey = j; 
     nextKey = j + 1; 
         if (nextKey >= NUM_KEYS) {
             nextKey = 0;
         }     
    break;    
  }  
}

lastKeyTime = map(TIMESTAMPS_ARRAY[lastKey], 0, 1440, 0, runTimeMillis * deltaUnit);
nextKeyTime = map(TIMESTAMPS_ARRAY[nextKey], 0, 1440, 0, runTimeMillis * deltaUnit);

 for(int i = 0; i<NUM_LEDS; i++) { 
   
     red = map(currentKeyTime, lastKeyTime, nextKeyTime, pgm_read_byte_near(*RGB_ARRAY + lastKey * NUM_LEDS * 3 + i * 3), pgm_read_byte_near(*RGB_ARRAY + nextKey * NUM_LEDS * 3 + i * 3));
     green = map(currentKeyTime, lastKeyTime, nextKeyTime, pgm_read_byte_near(*RGB_ARRAY + lastKey * NUM_LEDS * 3 + i * 3 + 1), pgm_read_byte_near(*RGB_ARRAY + nextKey * NUM_LEDS * 3 + i * 3 + 1));
     blue = map(currentKeyTime, lastKeyTime, nextKeyTime, pgm_read_byte_near(*RGB_ARRAY + lastKey * NUM_LEDS * 3 + i * 3 + 2), pgm_read_byte_near(*RGB_ARRAY + nextKey * NUM_LEDS * 3 + i * 3 + 2));
   
     leds[i] = CRGB(red, green, blue); 
 }

    LEDS.show();    
    lastTime = currentTime;
    delay(10);
}````

#7

The mod operation is going to cause the rollover here. In this line of code, your currentTime can never end up being equal to runTimeMillis (which is the value of your last key frame time, right?). A number divided by itself has no remainder, so if millis() is exactly equal to runTimeMillis, your currentTime will be 0 and you’ll never reach the time of your last key frame. (You can play around with the mod operator in [google calculator][1] if that helps.)

Assuming these variables exist:

desiredRunTime = 15000; // milliseconds that you want arduino to take to run through one loop of your frames
elapsedRunTime = 0; // milliseconds that we have progressed within one loop of your frames (i.e. will rollover at desiredRunTime)

Then your loop might look like this:

// Find the elapsed real world time
currentTime = millis(); // arduino milliseconds since started running
elapsedTime = millis() - lastTime; // arduino milliseconds since last loop() execution
lastTime = currentTime;

// Convert from the real world time to key frame time
deltaRunTime = map(elapsedTime, 0, 1, 0, deltaUnit);  

// Handle the rollover
if (elapsedRunTime + deltaRunTime > desiredRunTimeMillis) {
    rollover = true;
    lastKey = NUM_KEYS-1;
    nextKey = 0;
    // Update your lastKeyTime and nextKeyTime variables as well
}

// Now you can do mod without worrying
elapsedRunTime += deltaRunTime;
elapsedRunTime = elapsedRunTime % desiredRunTimeMillis;
// Alternatively, you could replace the last line with the following if statement.  (Assuming your desiredRunTimeMillis is much greater than the elapsed time between loop() calls in arduino) 
//    if (elapsedRunTime >= desiredRunTimeMillis) elapsedRunTime -= desiredRunTimeMillis;

// Rest of loop code

And hopefully I didn’t forget to account for something and this ends up being helpful.
[1]: https://www.google.com/webhp?tab=ww&authuser=0&ei=DZoBU6uQJpTrqAGToICgAQ&ved=0CBgQ1S4#authuser=0&q=6000+mod+6000


#8

Going off of these two lines of code, I think you might end up having a problem even after the rollover is fixed. There is no time budgeted for your last key frame of your animation. Your max time for your loop is stated as 1440 in your map() call. So key frame 1 lasts from 0 to 480, key frame 2 from 480 to 960, key frame 3 from 960 to 1439 and key frame 4 only lasts from 1439 to 1440.

I would modify your code to add an extra 460 to your max of both of your map function calls:

nextKeyTime = map(TIMESTAMPS_ARRAY[nextKey], 0, 1900, 0, runTimeMillis * deltaUnit);

Where I see the problem occurring:

When you lastKey is (NUM_KEYS-1) and your nextKey is 0, then your nextKeyTime will be 0. As soon as this executes:

Your nextKey will become 1 and your lastKey will become 0 (because currentKeyTime>=0 always) . You won’t end up interpolating your colors between your last frame and your first frame.