Crossfade multiple movies

Hi,

Thank you for asking this question because it was a nice coding challenge! :slight_smile:

Here is my solution :

  • Let’s suppose you have a list of movies, it’s a Sequence

  • Each Movie has it’s own duration

  • If you were to draw a timeline of the sequence with every movie, you would have this :

  • Now we want to have linear crossfade which mean we can represent it like this (centered around the cut) :

  • When we want to display the sequence at a certain time, we need to first loop through the list of movies and find the one we are currently in, we can do this with this code :

// We start at 0
float previousCut = 0;

// Loop through movies
for (Movie movie : movies) {
  // The next cut
  float nextCut = previousCut + movie.duration;

  if (currentTime < nextCut) {
    // We display the movie
    // Then break the loop
    break;
  }

  // It's not this movie, increase the cut
  previousCut += movie.duration
}
  • Then we need to handle two cases, when the timeline cursor is at the end of the clip and we fade out, we need to crossfade is this condition is true :

  • We do the same for when the cursor is at the beginning of a clip and we fade in :

  • We then simply linearly interpolate (with map()) the alpha value for both movies (the current one and the previous/next one for fade in/out). Be careful to switch the order by which you display both movies because it’s going to glitch as soon as the cursor pass after a cut.

These two methods compute the alpha value by passing the distance from the beginning of the fade :

// Compute the alpha value when fading out
float computeFadeOut(float distFromCut) {
  return map(distFromCut, 0, fadeDuration, 255, 0);
}

// Same for fade in but reverse
float computeFadeIn(float distFromCut) {
  return map(distFromCut, 0, fadeDuration, 0, 255);
}

Here you have a complete sketch with the above principle :

Sequence sequence;

void setup() {
  size(1000, 600);

  // Create the sequence
  sequence = new Sequence(5000);

  // Initialize random clips in milliseconds
  sequence.initializeRandom(5, 5000, 10000);
}


void draw() {
  // Reset the background
  background(0);

  // Display the sequence output
  sequence.displayPlayback();

  // Display the timeline
  sequence.displayTimeline(width / 2, height - 50, width - 50, 50);

  // Stop when finished
  if (millis() > sequence.getTotalDuration()) noLoop();
}

/*
* The Movie class with a duration and a color
 */
class Movie {
  float duration;
  color col;

  Movie(float duration, color col) {
    this.duration = duration;
    this.col = col;
  }

  // Initialize the movie with a black background
  Movie(float duration) {
    this(duration, color(0));
  }

  void display(float x, float y, float sizeX, float sizeY, float alpha) {
    fill(col, alpha);
    rect(x, y, sizeX, sizeY);
  }

  int getDurationSeconds() {
    return floor(duration / 1000);
  }

  void randomizeColor() {
    col = color(random(255), random(255), random(255));
  }

  // Display the movie on the whole window
  void fullScreen(float alpha) {
    display(0, 0, width, height, alpha);
  }
}

/*
* The Sequence class is holding a list of movies and
 * a fade duration for crossfading between movies
 */
class Sequence {
  ArrayList<Movie> movies;
  float fadeDuration;

  Sequence(float fadeDuration) {
    this.movies = new ArrayList<Movie>();
    this.fadeDuration = fadeDuration;
  }

  // Randomly create movies with different durations
  void initializeRandom(int n, float minDuration, float maxDuration) {
    for (int i = 0; i < n; i++) {
      Movie movie = new Movie(random(minDuration, maxDuration));
      movie.randomizeColor();
      movies.add(movie);
    }
  }

  // Return the total duration of the sequence
  float getTotalDuration() {
    float total = 0;
    for (Movie movie : movies) total += movie.duration;
    return total;
  }

  // Compute the alpha value when fading out
  float computeFadeOut(float distFromCut) {
    return map(distFromCut, 0, fadeDuration, 255, 0);
  }

  // Same for fade in but reverse
  float computeFadeIn(float distFromCut) {
    return map(distFromCut, 0, fadeDuration, 0, 255);
  }

  // The display method where we do the crossfade
  void displayPlayback() {
    noStroke();

    float previousCut = 0;
    float currentTime = millis();

    for (int i = 0; i < movies.size(); i++) {
      Movie movie = movies.get(i);

      // The timestamp of the next movie
      float nextCut = previousCut + movie.duration;

      // If true this is the current movie
      if (currentTime < nextCut) {
        // Test if we need to crossfade
        if (nextCut - currentTime < fadeDuration / 2) {
          float distFromCut = currentTime - (nextCut - fadeDuration / 2);

          movie.fullScreen(computeFadeOut(distFromCut));

          if (i < movies.size() - 1) {
            movies.get(i + 1).fullScreen(computeFadeIn(distFromCut));
          }
        } else if (currentTime - previousCut < fadeDuration / 2) { // After the previous clip
          float distFromCut = currentTime - (previousCut - fadeDuration / 2);

          if (i > 0) {
            movies.get(i - 1).fullScreen(computeFadeOut(distFromCut));
          }

          movie.fullScreen(computeFadeIn(distFromCut));
        } else {
          // We are not crossfading
          movie.fullScreen(255);
        }

        // Display time stamp
        fill(0);
        textAlign(CENTER, CENTER);
        textSize(45);
        text((previousCut + movie.duration - millis()) / 1000, width / 2, height / 2);

        // Be break to stop the loop
        break;
      }

      // Each time we add the movie length
      previousCut += floor(movie.duration);
    }
  }

  void displayTimeline(float centerX, float centerY, float hSize, float vSize) {
    float totalDuration = getTotalDuration();
    float leftX = centerX - (hSize / 2);
    float incrX = leftX;
    float topY = centerY - vSize / 2;
    float downY = centerY + vSize / 2;
    float fadeSize = map(fadeDuration, 0, totalDuration, 0, hSize);

    for (int i = 0; i < movies.size(); i++) {
      float clipWidth = map(movies.get(i).duration, 0, totalDuration, 0, hSize);

      // Display clip
      stroke(0);
      strokeWeight(3);
      movies.get(i).display(incrX, topY, clipWidth, vSize, 200);

      // Display cross fade
      stroke(255, 0, 0);
      strokeWeight(2);
      noFill();

      if (i > 0) {
        // Fade in
        line(incrX - fadeSize / 2, downY, incrX + fadeSize / 2, topY);

        // Fade out
        line(incrX - fadeSize / 2, topY, incrX + fadeSize / 2, downY);
      } else {
        // Secial case for the first fade in
        line(incrX, centerY, incrX + fadeSize / 2, topY);
      }

      // Also for the last one
      if (i == movies.size() - 1) {
        line(incrX + clipWidth - fadeSize / 2, topY, incrX + clipWidth, centerY);
      }

      // Display duration
      fill(0);
      noStroke();
      textSize(20);
      text(movies.get(i).getDurationSeconds(), incrX + clipWidth / 2, topY - 20);

      // Add the size of the drawn clip
      incrX += clipWidth;
    }

    // Display the time cursor
    float cursorX = map(millis(), 0, totalDuration, leftX, leftX + hSize);
    stroke(255, 0, 0);
    strokeWeight(4);
    line(cursorX, topY, cursorX, downY);

    // Draw triangle on top
    fill(255, 0, 0);
    triangle(cursorX, topY + 5, cursorX - 5, topY - 5, cursorX + 5, topY - 5);
  }
}

Note : I put comments to explain it and don’t feel lost, it’s just that I made the whole drawing part for the timeline and it’s not the real core of the problem, you should look at the void displayPlayback() method first.
I also didn’t use the Processing movie library but rather simulate a movie with a solid color background. It should be easy to adapt :wink:

Hope it was clear!