A Sample script for the not-things TimeSeq module.
One of the basic functionalities of TimeSeq is to create repeating chord and note sequences. On this page, we’ll start with a simple note sequence and gradually transform this into a sequence that can drive a bass voice, and an arpeggiated notes voice.
To create a basic four note sequence, we’ll need a timeline with a single lane. The lane is set to auto-start
and loop
so that the sequence starts when the script is loaded and keeps repeating.
There will be four segments in the lane, one for each note. Each segment will last 2000 milliseconds (or 2 seconds) and will set the voltage of output port 1 to the 1V/Oct value of the current note. For the first segment, this will be a C3
, and that full segment will look as follows:
{
"duration": { "millis": 2000 },
"actions": [
{
"timing": "start",
"set-value": {
"output": { "index": 1 },
"value": { "note": "C3" }
}
}
]
}
There is an opportunity for shortening this a bit however:
timing
property, it will default to start
. So it is not needed to specify the timing
property for any actions that should trigger at the start of a segment. In the script, we’ll only specify the timing
property if it is different from the default start
timing.The updated segment notation will thus become:
{
"duration": { "millis": 2000 },
"actions": [
{
"set-value": {
"output": 1,
"value": "C3"
}
}
]
}
The other three segments will look identical, except that their value
s will be notes F3
, G3
and D3
.
The full script can be found in note-seq-root-notes.json, and the note-seq-root-notes.vcv patch will show it in action by playing the note sequence through a VCO after showing the current note on the Hot Tuna tuner module.
In the second iteration of this script, we’ll first move away from millis timing, and instead use the musical bpm
(beats per minute) and bpb
(beats per bar) to express timing by adding a time-scale to the timeline:
{
"time-scale": {
"bpm": 120,
"bpb": 4
}
}
The note sequence segments can now express its duration in beats
and bars
. We’ll change them to two bars, which results in the a total of 4 seconds for each (120bpm = 0.5 secs per beat; 0.5s * 4 beats per bar * 2 bars = 4s):
{
"duration": { "bars": 2, "beats": 0 },
"actions": [
{
"set-value": {
"output": 1,
"value": "C3"
}
}
]
}
For the arpeggiated notes, we’ll use segment-blocks to keep the script manageable. Each segment-block will contain the four notes that make up the arpeggiated chord which will be sent to the second output port. Each note chord play for half a beat, and each segment-block is repeated four times so we end up with the same two bars duration as in the original note sequence (0.5 beats * 4 notes * 4 repeats = 8 beats = 2 bars). The first segment-block for the arpeggiated C chord will look as follows inside the component-pool:
"component-pool": {
"segment-blocks" : [
{
"id": "c-arp",
"repeat": 4,
"segments": [
{
"duration": { "beats": 0.5 },
"actions": [
{
"set-value": { "output": 2, "value": "C4" }
}
]
},
{
"duration": { "beats": 0.5 },
"actions": [
{
"set-value": { "output": 2, "value": "E4" }
}
]
},
{
"duration": { "beats": 0.5 },
"actions": [
{
"set-value": { "output": 2, "value": "G4" }
}
]
},
{
"duration": { "beats": 0.5 },
"actions": [
{
"set-value": { "output": 2, "value": "C5" }
}
]
}
]
}
]
}
Similar segment-blocks will be defined for the F, G an D chords, and these segment blocks can then be added to a new lane in the original timeline so that they play alongside the original note sequence:
{
"auto-start": true,
"loop": true,
"segments": [
{ "segment-block": "c-arp" },
{ "segment-block": "f-arp" },
{ "segment-block": "d-arp" },
{ "segment-block": "g-arp" }
]
}
The full script can be found in note-seq-arp.json, and the note-seq-arp.vcv patch contains a second VCO to play the arpeggiated chord notes.
The next step in this patch is to separate the arpeggiated chord notes using an envelope. For this, we’ll need a gate signal (or a trigger when using an AD envelope instead of an ADSR envelope). This can be achieved by adding a gate action to each of the arpeggiated note segments. Since this action will be used in multiple places, it will be added once to the component-pool:
{
"component-pool": {
"actions": [
{
"id": "arp-gate-action",
"timing": "gate",
"output": 3
}
]
}
}
This will cause a high (10V) signal to be present on output port 3 for the first half of the segment, and a low (0V) signal for the rest. While it is possible to move the change from high to low to a different position in the segment using the gate-high-ratio
property, we’ll leave it on the halfway point of the segment for this script.
This gate can now be used by reference on each of the arpeggiated chord notes:
{
"duration": { "beats": 0.5 },
"actions": [
{
"set-value": { "output": 2, "value": "C4" }
},
{
"ref": "arp-gate-action"
}
]
},
A script version with gate actions added to all the arpeggiated chord notes can be found int note-seq-arp-gate.json, and the note-seq-arp-gate.vcv patch uses this gate for applying a volume envelope on those notes.
When there are multiple looping lanes in a timeline that are expected to stay in sync, unless the loop-lock
is activated on the timeline, it’s important to make sure that all the lanes have the same duration. This can be demonstrated by making the segment-block play half-note triplets instead of eight notes. A triplet segment can be written as:
{
"duration": { "beats": 0.666 },
"actions": [
{
"set-value": { "output": 2, "value": "D4" }
},
{
"ref": "arp-gate-action"
}
]
}
But while three of these triplets are expected to have the same duration as four eighteenth notes, using three 0.666
beats ends up lasting 1.998
beats instead of exactly 2
beats. When repeating this in a loop, this will cause a timing drift. In order to fix this, we can just add the missing duration to the length of the last triplet, resulting in following segment block:
{
"id": "g-arp",
"repeat": 4,
"segments": [
{
"duration": { "beats": 0.666 },
"actions": [
{
"set-value": { "output": 2, "value": "D4" }
},
{
"ref": "arp-gate-action"
}
]
},
{
"duration": { "beats": 0.666 },
"actions": [
{
"set-value": { "output": 2, "value": "G4" }
},
{
"ref": "arp-gate-action"
}
]
},
{
"duration": { "beats": 0.668 },
"actions": [
{
"set-value": { "output": 2, "value": "B4" }
},
{
"ref": "arp-gate-action"
}
]
}
]
}
The last 0.668
duration makes the whole segment run in sync with the others again. Note how the gate actions of these triplet notes also automatically adjusts to the new segment length.
The updated script can be found in note-seq-arp-triplets.json, with the note-seq-arp-triplets.vcv patch playing it.
Before adding more new logic to the script it’s helpful to simplify the existing structure. Up until now, the arpeggiated note sequences were hardcoded: each chord had its own segment-block with fixed notes and gates. This duplication makes the script harder to maintain and extend. What we’ll do instead is restructure the script to:
This change makes the script shorter, more readable, and allows features to be applied uniformly.
The new generic arp
segment-block would look as follows:
{
"id": "arp",
"repeat": 4,
"segments": [
{
"duration": { "beats": 0.5 },
"actions": [
{
"set-value": { "output": 1, "value": { "variable": "root-note" } }
},
{
"set-value": { "output": 2, "value": { "variable": "chord-note-1" } }
},
{
"ref": "arp-gate-action"
}
]
},
{
"duration": { "beats": 0.5 },
"actions": [
{
"set-value": { "output": 2, "value": { "variable": "chord-note-2" } }
},
{
"ref": "arp-gate-action"
}
]
},
{
"duration": { "beats": 0.5 },
"actions": [
{
"set-value": { "output": 2, "value": { "variable": "chord-note-3" } }
},
{
"ref": "arp-gate-action"
}
]
},
{
"duration": { "beats": 0.5 },
"actions": [
{
"set-value": {
"output": 2,
"value": {
"variable": "chord-note-1",
"calc": [
{ "add": 1 }
]
}
}
},
{
"ref": "arp-gate-action"
}
]
}
]
}
Instead of using fixed note values, this segment-block now uses
root-note
variable to set the root note on output 1 in the first segment,chord-note-1
variable to set the first arpeggiated note on output 2 in the first segment,chord-note-2
variable to set the second arpeggiated note on output 2 in the second segment,chord-note-3
variable to set the third arpeggiated note on output 2 in the third segment,chord-note-1
variable, but adds 1V to it to make it one octave higher in the fourth segmentInstead of using the the different segment-blocks c-arp
, f-arp
, … for each of the chords, we can now assign values to the different variables and call on the arp
segment-block instead.
To set the variables, we’ll leverage the fact that you can add actions to a segment that points to a segment-block. These actions will then be executed before (if they have a start
timing
) or after (if they have an end
timing
). We’ll use the default start
timing here to perform set-variable actions:
{
"actions": [
{
"set-variable": {
"name": "root-note",
"value": "C3"
}
},
{
"set-variable": {
"name": "chord-note-1",
"value": "C4"
}
},
{
"set-variable": {
"name": "chord-note-2",
"value": "E4"
}
},
{
"set-variable": {
"name": "chord-note-3",
"value": "G4"
}
}
],
"segment-block": "arp"
}
This first segment contains the C4 chord: it sets the root-note
variable to a C3
, the chord-note-1
to a C4
, the chord-note-2
to an E4
, the chord-note-3
to a G4
and points to the previously created arp
segment-block that will use these variables.
By using less newlines in the JSON, this can also be written a bit more compact, as this second segment for the F chord shows:
{
"actions": [
{ "set-variable": { "name": "root-note", "value": "F3" } },
{ "set-variable": { "name": "chord-note-1", "value": "C4" } },
{ "set-variable": { "name": "chord-note-2", "value": "F4" } },
{ "set-variable": { "name": "chord-note-3", "value": "A4" } }
],
"segment-block": "arp"
}
The full updated script can be found here, and the note-seq-arp-rework.vcv patch has it loaded into TimeSeq.
To bring some variation in the script output, we’ll add some chance to the arpeggiated chord notes: on each action that causes a note to play, add a 1-in-4 chance that the note will not be played. This is why the script got reworked in the previous step: there are less places where this chance will have to be added.
Adding chance to the execution of an action can be done using two steps: first generate a random value, and then make the action conditional based on that random value. To generate the random value that determines if an arpeggiated note action will be performed, following action is added to the actions
list of the component-pool:
{
"id": "determine-chance",
"set-variable": {
"name": "play-note",
"value": {
"rand": {
"lower": 0,
"upper": 10
}
}
}
}
This action will generate a random voltage between 0
and 10
and assign it to the play-note
variable. In order to use this variable to make the note actions have a 1-in-4 chance of not playing, we’ll need an if. Since this if condition will be used multiple times, we’ll add it to the ifs
section of the component-pool:
{
"component-pool": {
"ifs": [
{
"id": "should-play-note",
"gt": [
{ "variable": "play-note" },
2.5
]
}
]
}
}
This should-play-note
conditional will evaluate to true
if the play-note
variable (which contains the random value between 0
and 10
) is above 2.5
. When used as if condition on an action, this will give it a 3-in-4 chance of executing. We can now add this new action and the if condition to the segments that play the notes:
{
"duration": { "beats": 0.5 },
"actions": [
{ "ref": "determine-chance" },
{
"if": { "ref": "should-play-note" },
"set-value": { "output": 2, "value": { "variable": "chord-note-2" } }
},
{
"ref": "arp-gate-action"
}
]
}
The actions are executed in the order they appear in the script, so first the referenced determine-chance
action will determine the chance that the note will play, then the second action will only play the note if the referenced should-play-note
condition evaluates to true
, and then the arp-gate-action
will generate the gate that drives the note volume envelope. We’ll also have to make this gate action conditional based on the same chance, so that action (which already existed in the component-pool) now becomes:
{
"id": "arp-gate-action",
"timing": "gate",
"if": { "ref": "should-play-note" },
"output": 3
},
Note that the first action of the arp
segment-block is the one that sets the root note on output port 1. This action should not be made conditional, since the root note should always update.
The full updated script for this step can be found in note-seq-arp-chance.json, and the note-seq-arp-chance.vcv is the same patch as before, but now with the new version of the script loaded.
To bring some more variation in the output, we’ll allow the chance that was introduced in the previous step to be controlled by an external voltage. Instead of always having a 75% chance that a note will play, an external voltage should be able to move that chance around, allowing external modulation sources to influence how often the notes are triggered. We’ll set it up so that a 0V to 10V signal moves the probability between 25% and 75%. All that is needed for this is to update the should-play-note
condition to:
"ifs": [
{
"id": "should-play-note",
"gt": [
{ "variable": "play-note" },
{
"input": 1,
"calc": [
{ "div": 2 },
{ "add": 2.5 }
]
}
]
}
],
Instead of checking that the previously generated play-note
random value is above 2.5V (=75% chance), this updated condition will (using an input and calculations):
2
(-> a value between 0V and 5V)2.5
to this (-> a value between 2.5V and 7.5V)When checking that value against the randomly generated play-note
variable (which will be between 0V and 10V), we get a 25% to 75% chance that a note will trigger, depending on the input voltage.
The resulting script for this step can be found in note-seq-arp-chance-mod.json, with the note-seq-arp-chance-mod.vcv patch using an LFO to generate the input voltage. A scope will also show the LFO output and the resulting gates coming from TimeSeq: as the LFO value is low, there is a higher chance of a note playing and when the LFO is high, there is a lower chance.
A final addition to this script will be to generate a random melody. In order to let this melody fit in with the rest of the patch, it can not just play any note. It will have to be quantized to a scale that fits with the chords. We’ll use a C major pentatonic scale for this (which contains the c, d, e, g and a notes). In the component-pool, we create a tuning for this:
"tunings": [
{
"id": "c-major-pentatonic",
"notes": [ "c", "d", "e", "g", "a" ]
}
],
A tuning can be used in the calc section of a value to quantize
the voltage of that value to the notes
in the tuning:
{
"auto-start": true,
"loop": true,
"segments": [
{
"duration": { "beats": 1 },
"actions": [
{
"set-value": {
"output": 4,
"value": {
"rand": {
"lower": 1,
"upper": 2
},
"calc": [
{ "quantize": { "ref": "c-major-pentatonic" } }
]
}
}
},
{
"timing": "gate",
"output": 5
}
]
}
]
}
This lane repeats one segment that updates the voltage on the 4th output of TimeSeq to a value. This value is randomly generated between 1V and 2V, and then gets quantized by the calc to the c-major-pentatonic
tuning. A second action in that segment also generates a gate signal on output 5 that can be used to generate a volume envelope for the note.
The resulting script for this step can be found in note-seq-quantize-random.json, with the note-seq-quantize-random.vcv sending the newly generated note sequence through an additional VCO, with the new gate signal driving another ADSR envelope generator.