SC Timeline Tutorial

  1. Scrolling and Zooming
  2. Sequencing and Patterns
  3. Visualizing Playback and Patterns
  4. Synth and Routine clips
  5. Colors and Dragging

1: Sequencing and Patterns

If you have not seen part 0, the beginning of this tutorial series can be found here, where we made a timeline data structure with visualization. In this installment, we will look at how to make the clips functional, i.e. play and stop playing.

Playing a clip

I will now modify our clip class from before.

I will add a variable isPlaying that keeps track of whether the clip is currently playing; a stop method that sets this to false; and a play method that takes a start offset (because, for example, the playhead might be in the middle of the clip when I press play) and an optional clock, sets isPlaying to true, waits the appropriate amount of time, and sets it to false again.

/* ESClip.sc */

ESClip {
  var <startTime, <duration;
  var <isPlaying = false;
  var playRout;

  *new { |startTime, duration|
    ^super.newCopyArgs(startTime, duration);
  }
  
  stop { 
    isPlaying = false; 
    // in case of premature stop:
    playRout.stop; 
  }

  play { |startOffset = 0.0, clock|
    // default to play on default TempoClock
    clock = clock ?? TempoClock.default;

    // stop if we're playing
    if (isPlaying) {
      this.stop;
    };

    // play the clip on a new Routine on this clock
    playRout = {
      isPlaying = true;

      // start the clip from specified start offset
      
      (duration - startOffset).wait;
      this.stop;
    }.fork(clock);
  }
}
    

Now, after recompiling, we can make sure this is doing the expected behavior. Make a new clip with a duration of 5 beats:

~clip = ESClip(0, 5);

~clip.isPlaying;     // -> false
    

Start it playing, and isPlaying should be true for the next 5 beats, after which it will become false again:

~clip.play;

~clip.isPlaying;    // evaluate this several times and notice when it stops being true
    

If we start it playing with e.g. a 4 beat offset, isPlaying should be true for only the next beat:

~clip.play(4);

~clip.isPlaying;
    

So, this basically works, but does nothing useful yet.

As Bob Ross said, here's where we have to make some big decisions.

What sort of things do I want to sequence? Functions? Routines? Synths? Envelopes? Patterns? etc?

I would eventually like to be able to sequence as many things as possible, but I think I will start with Patterns because I think they will be a bit complicated but also fun.

Things to sequence

So here's a little pattern that I made that has a lot of randomness to it, so that it can sound quite different each time you press play, and a basic reverb synth.

(
s.waitForBoot {
  SynthDef(\sin, { |out, freq = 100, gate = 1, amp = 0.1, preamp = 1.5, attack = 0.001, release = 0.01, pan, verbbus, verbamt, vibrato = 0.2|
    var env, sig;
    var lfo = XLine.ar(0.01, vibrato, ExpRand(0.5, 2.0)) * SinOsc.ar(5.4 + (LFDNoise3.kr(0.1) * 0.5));
    gate = gate + Impulse.kr(0);
    env = Env.adsr(attack, 0.1, 0.4, release).ar(2, gate);
    sig = SinOsc.ar(freq * lfo.midiratio) * env;
    sig = (sig * preamp).tanh;
    sig = Pan2.ar(sig, pan, amp);
    Out.ar(out, sig);
    Out.ar(verbbus, sig * verbamt);
  }).add;

  SynthDef(\verb, { |out, verbbus, gate = 1|
    var in = In.ar(verbbus, 2);
    var env = Env.adsr(0.01, 0, 1, 1.0).ar(2, gate);
    var verb = NHHall.ar(in) * env;
    Out.ar(out, verb);
  }).add;

  ~verbbus = Bus.audio(s, 2);

  ~pattern = Pbind(
    \instrument, \sin,
    \freq, (Pfunc({ 
      { (100, 150 .. 650).choose + rrand(0.0, 3.0) }.dup(rrand(1, 2))
    }) * Pdup(Pwhite(1, 4), Prand((0.5, 1 .. 3.5), inf))),
    \dur, Pbrown(0.0, 1.0).linexp(0.0, 1.0, 0.01, 1.0) * Pwhite(0.8, 1.0),
    \pan, Pbrown(-0.5, 0.5, 0.1),
    \dummy, Prand([1, Rest()], inf),
    \legato, Pwhite(0.0, 1.0).linexp(0.0, 1.0, 0.01, 1.0) + Pwrand([0, 2, 10], [0.9, 0.09, 0.01], inf),
    \attack, Pbrown().linexp(0.0, 1.0, 0.0001, 1.0),
    \release, Pbrown(0.0, 1.0, 0.25).linexp(0.0, 1.0, 0.05, 0.5),
    \vibrato, Pwhite(0.1, 0.3),
    \preamp, Pbrown().linexp(0.0, 1.0, 1.0, 1.5),
    \verbbus, ~verbbus,
    \verbamt, Pbrown().linexp(0.0, 1.0, 0.075, 1.5),
    \db, Pbrown(-32, -18, 1.0)
  );
};
)
    

To play it:

( // play pattern with reverb (different every time)
~player = ~pattern.play;
~synth = Synth(\verb, [verbbus: ~verbbus, addAction: \addToTail]);
)

( // stop it
~player.stop; 
~synth.release;
)
    
• For more on patterns, see James Harkins A Practical Guide to Patterns.

So I'm thinking if I were to put this sort of pattern inside of a clip on a timeline, I would like for the clip to be able to play the pattern back deterministically, i.e. with a fixed seed, as well as randomly.

I think I can achieve this using Pseed:

( // always the same
var randSeed = 1; // change this for different version
~player = Pseed(randSeed, ~pattern).play;
~synth = Synth(\verb, [verbbus: ~verbbus, addAction: \addToTail]);
)
    
• For more on randomness control, see Randomness control video from Reflectives

I also need to be able to "fast forward" the playback to the desired point in the pattern. I will use the slightly arcane fastForward method of a stream which returns a wait time until the first event in the truncated stream should play.

( // start 1 beat in
var startOffset = 1.0;
var randSeed = 1;

var stream = Pseed(randSeed, ~pattern).asStream;
var wait = stream.fastForward(startOffset);

// I am first assigning this wait Routine to ~player, so that if we call
// ~player.stop before the wait time is up, it will still stop.
~player = {
  wait.wait;
  ~player = EventStreamPlayer(stream).play;
}.fork;
~synth = Synth(\verb, [verbbus: ~verbbus, addAction: \addToTail]);
)
    

Now that we know what it takes to control the randomness and fast forward into the pattern, let's encapsulate this in a new kind of clip.

Subclassing ESClip

First we will modify ESClip a bit, to add methods prStart and prStop that our subclasses will override to make their specific start and stop behavior.

• SuperCollider doesn't have any way to make a method private/protected (uncallable from outside the class/subclasses). It is a convention to begin methods with "pr" to indicate that they are not meant to be called interactively.
/* ESClip.sc */

ESClip {
  var <startTime, <duration;
  var <isPlaying = false;
  var playRout;

  *new { |startTime, duration|
    ^super.newCopyArgs(startTime, duration);
  }

  stop {
    // stop the clip
    this.prStop;
    isPlaying = false;
    // in case of premature stop:
    playRout.stop;
  }

  play { |startOffset = 0.0, clock|
    // default to play on default TempoClock
    clock = clock ?? TempoClock.default;

    // stop if we're playing
    if (isPlaying) {
      this.stop;
    };

    // set isPlaying to true
    isPlaying = true;

    // play the clip on a new Routine on this clock
    playRout = {
      // start the clip from specified start offset
      this.prStart(startOffset, clock);

      // wait the appropriate time, then stop
      (duration - startOffset).wait;
      this.stop;
    }.fork(clock);
  }

  // override these in subclasses
  prStart { }
  prStop { }

  // helper methods
  endTime { ^startTime + duration }
}
    

And now we subclass it with a clip that can play a pattern either with a random seed or not, depending on isSeeded. Here we mostly reuse our code from above to play a pattern deterministically. I've added the helper method patternToPlay which decides whether to play the pattern randomly or with the given seed.

/* ESPatternClip.sc */

ESPatternClip : ESClip {
  var <pattern, <randSeed, <>isSeeded;
  var player;

  *new { |startTime, duration, pattern, randSeed, isSeeded = true|
    ^super.new(startTime, duration).init(pattern, randSeed, isSeeded);
  }

  init { |argPattern, argRandSeed, argIsSeeded|
    // copy pattern-specific args, with default (random) random seed
    pattern = argPattern;
    randSeed = argRandSeed ?? rand(2000000000);
    isSeeded = argIsSeeded;
  }

  // helper method to return the actual pattern that will be played
  patternToPlay {
    // if this clip is seeded, return the seeded pattern.
    if (isSeeded) {
      ^Pseed(randSeed, pattern);
    } {
      ^pattern;
    }
  }

  // pattern specific stop method
  prStop {
    player.stop;
  }

  // pattern specific start method
  prStart { |startOffset = 0.0, clock|
    // our player from before
    var stream = this.patternToPlay.asStream;
    var wait = stream.fastForward(startOffset);
    player = {
      wait.wait;
      player = EventStreamPlayer(stream).play(clock);
    }.fork(clock);
  }
}
    

So now, recompile and then evaluate our pattern definition from before, and now:

~clip = ESPatternClip(0, 5, ~pattern, 1);

~clip.play;

~clip.isPlaying;
    

each time you play the clip, it will start from the same point and last five beats. To make it random, set isSeeded:

~clip.isSeeded = false;
~clip.play; // different every time
    

So now, we can place several of these clips on a timeline, and visualize it, and play each one:

(
~timeline = ESTimeline([
  ESTrack([
    ESPatternClip(0, 5, ~pattern, 1),
    ESPatternClip(6, 2.2, ~pattern, 2),
    ESPatternClip(9, 10, ~pattern, 35),
  ])
])
)

(
Window.closeAll;
~window = Window("Timeline", Rect(0, Window.availableBounds.height - 500, Window.availableBounds.width, 500)).front;
~view = ESTimelineView(~window, ~window.bounds.copy.origin_(0@0), ~timeline);
)

~timeline.tracks[0].clips[0].play
~timeline.tracks[0].clips[1].play
~timeline.tracks[0].clips[2].play
    

But this is hardly satisfying, so let's get to sequencing a track.

Track sequencing

This is quite similar to our ESClip modifications, but in the play function I am using a variable t to represent the current logical time. More specifically in the comments.

/* ESTrack.sc */

ESTrack {
  var <clips;
  var <isPlaying = false;
  var playRout;

  *new { |clips|
    ^super.newCopyArgs(clips);
  }

  sortClips {
    // good to call this before assuming they are ordered
    clips.sort { |a, b|
      a.startTime > b.startTime
    };
  }

  currentClips {
    // returns all playing clips
    ^clips.select(_.isPlaying)
  }

  stop {
    playRout.stop;
    this.currentClips.do(_.stop);
  }

  play { |startTime = 0.0, clock|
    // default to play on default TempoClock
    clock = clock ?? TempoClock.default;
    // stop if we're playing
    if (this.isPlaying) { this.stop };
    // play the track on a new Routine on this clock
    playRout = fork {
      // the variable "t" tracks the current hypothetical playhead
      // as we are scheduling these events
      // it begins at our startTime
      var t = startTime;
      // make sure clips are in order
      this.sortClips;
      // iterate over all the clips
      clips.do { |clip|
        if (clip.endTime < t) {
          // skip all clips previous to t, i.e. startTime
        } {
          // how far into the clip do we need to start
          var offset = max(0, t - clip.startTime);
          // the playhead time we will start playing
          var startTime = clip.startTime + offset;
          // wait the appropriate amount of time
          (startTime - t).wait;
          // ...and play the clip
          clip.play(offset);
          // adjust t to the current time
          t = startTime;
        };
      };
    };
  }
}
      
    

And now, if you run the timeline example above, you can play the track, in time, using:

~timeline.tracks[0].play
    

It might not yet be beautiful music, but it's our music, and it's the same each time we play it. And we can start it from whenever we want:

~timeline.tracks[0].play(4.5)
~timeline.tracks[0].play(14.5)
    

Timeline sequencing

Lastly we need to make the timeline itself playable. This could be as simple as telling each of its tracks to play. But I'd also really like to be able to ask the timeline when "now" is at any given time while it's playing. So the way I think we'll go about this is to use the beats method of a clock:

TempoClock.default.beats    // evaluate multiple times and watch the beats increase
    

to record when the timeline begins playing, and "now" will be the current clock time minus that time plus the timeline's start time.... There will also need to be a playbar, i.e. the point from which the timeline will default to begin playing, and which will be "now" whenever the timeline isn't playing. Like this:

/* ESTimeline.sc */

ESTimeline {
  var <tracks;
  var <isPlaying = false;
  var <playbar = 0.0;
  var playBeats, playStartTime, playClock;

  *new { |tracks|
    ^super.newCopyArgs(tracks);
  }

  now {
    if (isPlaying) {
      ^(playClock.beats - playBeats) + playStartTime;
    } {
      ^playbar;
    };
  }

  now_ { |val|
    playbar = max(val, 0);
  }

  stop {
    tracks.do(_.stop);
    isPlaying = false;
  }

  play { |startTime, clock|
    // stop if playing
    if (isPlaying) { this.stop };
    isPlaying = true;

    if (startTime.notNil) {
      playbar = startTime;
    };
    // default to play on default TempoClock
    clock = clock ?? TempoClock.default;
    // save the starting conditions
    playClock = clock;
    playBeats = clock.beats;
    playStartTime = playbar;

    tracks.do(_.play(playbar));
  }
}
    

And here is how to use it:

~timeline.now   // keep evaluating while timeline is playing
~timeline.play
~timeline.stop
~timeline.isPlaying

~timeline.now = 5.0
~timeline.play
    

Conclusion

This is only a start; for example, I would eventually like to be able to split a pattern clip in the middle and have the newly created clip start from the given point in the pattern. But I will get to that later.

First, I want to update our visualization from before with a playhead that represents "now", and to give us more visual information about the patterns that are being played. For this, please proceed to the next installment.

You can download my final classes from this installment here.