Skip to content

Sequencing

Elizabeth Hudnott edited this page Oct 6, 2019 · 25 revisions

Concepts

At present composing a tune requires writing an unwieldy amount of code. Eventually I'll write both a graphical composition tool and routines for saving tunes as binary files and reloading them, in which case the code on this page will be mostly redundant because the objects will be created automatically by the forthcoming tools. Nonetheless this page should remain informative with respect to explaining the underlying concepts irrespective of the final interface.

I'll start by configuring two channels in a way that'll be useful for our short demo song.

system.set(Synth.Param.LINE_TIME, 8, 0, Synth.ChangeType.SET, -1);
system.set(Synth.Param.WAVEFORM, Synth.Wave.SAWTOOTH + 0.5); // 50-50 mixture of custom and sawtooth waveforms
system.set(Synth.Param.WAVEFORM, Synth.Wave.TRIANGLE, 0, Synth.ChangeType.SET, 1); // Triangle wave on the 2nd channel

So far nothing is new. I could have embedded these initial parameter settings into the song itself but for simplicity I've left them separate.

So far we have used:

  • set() to set one parameter at one instant in time. Although automated parameter fades are possible we still only provide one value as the endpoint for the fade. set() can adjust either a single channel or all channels uniformly.
  • setParameters() to set several parameters simultaneously at one instant in time on one channel.

We'll now look at:

  • Phrases, which allow us to describe parameter changes across a range of different points in time, either on a single channel or (in the case of a master track) applied to all channels uniformly.
  • Tracks, which are just relatively long phrases, although they do support an additional feature which enables shorter phrases to be embedded inside a track.
  • Patterns, which allow us to set parameters across a range of different points in time, with different parameter settings applied to different channels. A pattern basically describes which tracks will get played on which channels.
  • Songs, which allow us to organize patterns into larger compositions and to reuse patterns more than once.

Phrases

Let's create our first phrase.

phraseA = new Sequencer.Phrase('A', 12);
parameterMap = new Map();
parameterMap.set(Synth.Param.NOTES, new Synth.Change(Synth.ChangeType.SET, [69]));
parameterMap.set(Synth.Param.GATE, new Synth.Change(Synth.ChangeType.SET, Synth.Gate.TRIGGER));
phraseA.rows[0] = parameterMap;
phraseA.rows[2] = parameterMap;
phraseA.rows[6] = parameterMap;

Here I'm creating a new phrase, giving it the label 'A', and making it twelve rows (or lines) long. Then I'm creating a Map of parameter changes, exactly like the ones we've previously passed to setParameters(). In this case I'm describing playing the note A4. I'm then placing those parameter changes on rows 0, 2 and 6. Currently nothing is scheduled to happen in rows 1, 3, 4, 5, 7, 8, 9, 10 and 11. Here I'm assigning the same map to several rows. This means that if I were to edit the map then I would be editing several lines of our melody simultaneously, which may or might not be what you want to happen. If I had chosen to use a different map each time then it wouldn't be necessary to include the pitch information every time because the default behaviour is to replay whatever note the parameter was last set to.

Let's add some more notes.

parameterMap = new Map();
parameterMap.set(Synth.Param.NOTES, new Synth.Change(Synth.ChangeType.SET, [71]));
parameterMap.set(Synth.Param.GATE, new Synth.Change(Synth.ChangeType.SET, Synth.Gate.TRIGGER));
phraseA.rows[4] = parameterMap;
phraseA.rows[5] = parameterMap;

We now have a phrase that describes the sequence A, A, B-B, A, rest. Let's play it!

phraseA.play(system);

Shorthand Syntax

A shorthand syntax can be used to notate short tunes in JavaScript a little more compactly. Phrase A could have been written as:

phraseA = Sequencer.Phrase.fromObject({
  0: [Synth.Param.NOTES, [69], Synth.Param.GATE, Synth.Gate.TRIGGER],
  2: [Synth.Param.NOTES, [69], Synth.Param.GATE, Synth.Gate.TRIGGER],
  4: [Synth.Param.NOTES, [71], Synth.Param.GATE, Synth.Gate.TRIGGER],
  5: [Synth.Param.NOTES, [71], Synth.Param.GATE, Synth.Gate.TRIGGER],
  6: [Synth.Param.NOTES, [69], Synth.Param.GATE, Synth.Gate.TRIGGER],
  length: 12
});

All changes expressed using Phrase.fromObject() use Synth.ChangeType.SET. The name of the new phrase is called 'Untitled'. This can be changed by assigning to the name property of the returned phrase.

Patterns

So far we've only made use of one sound channel. If we want more than one instrument in our ensemble then we'll want to use a second channel. Actually, using another channel is not always necessary because we still have empty rows in Phrase A that we could add information to, switching back and forth between instruments if needed. But in this case we will use two channels.

To compose for multiple channels (and to generally coordinate multiple phrases) we need to create some patterns.

pattern1 = new Sequencer.Pattern(2, 12);
pattern1.columns[1] = phraseA;
song = new Sequencer.Song();
pattern1.play(system, song);

Here I've created a pattern that has two main columns numbered 1 and 2 and 12 rows. The default number of rows is 64. Column 0 is reserved for the master track, which I'll cover in the next section. I've placed our Phrase A in Column 1. In order to play our pattern the sequencer needs a Song object to provide some contextual information. I'll cover songs in a later section but for now simply initializing an empty song will be sufficient. Column 1 is always played on the system's first channel, Column 2 on the second channel and so on.

At the moment our pattern sounds exactly the same as the phrase we had before, so let's put a phrase into the second column. For brevity I'm going to reuse the exact same phrase, which is atypical but hopefully suffices in order to depict the general structure. To spice things up a bit I've configured the second channel to start playing the phrase from row "-1". Naturally there is no row numbered -1 so the effect is to delay playing the phrase by one row. So on the second channel Row 0 of Phrase A plays on Row 1 of the pattern while simultaneously Row 1 of Phrase A also plays on Row 1 of the pattern but on the first channel.

pattern1.columns[2] = phraseA;
pattern1.offsets[2] = -1;
pattern1.play(system, song);

Master Tracks

A master track is simply a phrase whose parameter settings are applied to all channels that participate in the pattern rather than only being applied to one particular channel. The master track is always placed in Column 0. Additionally a few parameters can only be set inside a master track, such as Synth.Param.LOOP_START and Synth.Param.LOOPS, which combine to create a loop. These parameters only make sense on the master track because altering which row gets played next necessarily affects the behaviour of every column.

A few tracker programs (such as LSDJ) do permit each column to execute commands independently at different speeds and with differing loop configurations and so forth. However, I've chosen to adopt the more traditional approach where each column executes in lockstep with a clear concept of a current line number consistent across every column.

Let's add a master track to our pattern.

masterPhrase = new Sequencer.Phrase('M', 12);
parameterMap = new Map();
parameterMap.set(Synth.Param.GLISSANDO, new Synth.Change(Synth.ChangeType.SET, -1));
masterPhrase.rows[0] = parameterMap;
parameterMap = new Map();
parameterMap.set(Synth.Param.GLISSANDO, new Synth.Change(Synth.ChangeType.SET, 0));
masterPhrase.rows[4] = parameterMap;
pattern1.columns[0] = masterPhrase;
pattern1.play(system, song);

Shorthand Pattern Syntax

We could have created our pattern and inserted the phrases into it simultaneously.

pattern1 = Sequencer.Pattern.fromArgs(masterPhrase, phraseA, phraseA, -1);

Here we specify the phrases to place in each column and optionally follow each one with a starting offset.

Tracks Containing Phrases (Also Known as Nested Phrases)

Let's extend our song by writing another phrase. However, our new phrase will mostly have the same content as Phrase A but with a few modifications. We have an easy way of expressing exactly that.

phraseB = new Sequencer.Phrase('B', 12);
parameterMap = new Map();
parameterMap.set(Synth.Param.PHRASE, new Synth.Change(Synth.ChangeType.SET, 'A'));
parameterMap.set(Synth.Param.NOTES, new Synth.Change(Synth.ChangeType.SET, [75]));
phraseB.rows[0] = parameterMap;
song.addOrReplacePhrase(phraseA);
phraseB.play(system, song);

Here I'm using the Synth.Param.PHRASE parameter to insert the content of Phrase A into Phrase B. Here Phrase A starts playing on Row 0 of Phrase B but equally I could have started Phrase A from another row of Phrase B (e.g. by writing phraseB.rows[2] = parameterMap;). In this case I didn't choose to, but I could have have skipped over the beginning of Phrase A and begun playing it from part way through using the Synth.Param.PHRASE_OFFSET parameter to set the row number within Phrase A to begin playing from (which defaults to Row 0). I did set Synth.Param.NOTES to the value representing D#5 though, thus overriding how this parameter has been configured in the original phrase. Where the original phrase had A, A, B-B, A the new phrase has D#, A, B-B, A. I also need to add Phrase A to the pool of phrases (usually shorter phrases) that are available for other phrases to refer to. Usually they are referred to by longer phrases that are informally called "tracks" and which are assigned to one or more column locations. Only one level of phrase nesting is supported. An alternative to inserting an overriding change (in this case to override A with D#) is to instead insert Synth.Change.NONE, which cancels the change described by the nested phrase and stops the parameter in question from changing at all in that row-column position (unless the master track also changes that parameter). So in our case, applying Synth.Change.NONE to Synth.Param.NOTES would mean that no note was played.

In my example I've subverted the paradigm slightly by using Phrase A both as a track (twice in pattern1) and as a nested phrase inside Phrase B. Normally in a tracker program each particular sequence of changes would either be used as a track or as a (nested) phrase but not both, but my JavaScript engine doesn't care about this convention.

Let's put our new phrase inside a new pattern. For brevity I'll only bother to compose sound for a single column and therefore for a single channel.

pattern2 = new Sequencer.Pattern(1, 12);
pattern2.columns[1] = phraseB;

Songs

We'd quite like to link pattern1 and pattern2 together so they can be played one after the other. We specify this relationship using the patterns property of our instance of the Song class. We can then play the entire song using the Song class' play() method.

song.patterns[0] = pattern1;
song.patterns[1] = pattern2;
song.play(system);