An overview of the JSON script for the not-things TimeSeq module.
This page describes the concepts used in the TimeSeq script. It introduces the different object types that are used in the script and how they interact with each other. It does not go into every detail on all the properties. For a full detailed description of all the objects, see the Script JSON Reference page. When looking for an hierarchical overview of the script structure, the Table of Contents on that page is also structured to represent that hierarchy. When a new JSON object type is introduced on this page, it will also link to its section within the full details on the JSON Reference page.
To facilitate easier editing of a TimeSeq JSON script, a JSON Schema definition is available for it. When used in combination with a JSON Schema aware editor (such as Visual Studio Code), it enables inline validation and auto completion suggestions while creating the script. Although not all aspects of the TimeSeq JSON script will be validated through the schema (e.g. referencing non-existing component-pool items), it does greatly speed up script editing.
The schema can be associated with a script file by adding following property at the root element of the JSON data:
{
"$schema": "https://not-things.com/schemas/timeseq-script-1.1.0.schema.json"
...
}
As new features are added to the TimeSeq JSON script, its version number will also increment. This will result in updated schemas to match those changes. The script versions page provides an overview of the script versions along with their corresponding JSON schemas.
For running the script, TimeSeq is tied to the active sample rate of VCV Rack. Each sample in VCV Rack will result in a processing cycle in TimeSeq (e.g. when set to 44.1Khz sample rate, there will be 44100 processing cycles per second in TimeSeq). During each such processing cycle, TimeSeq will check if any action should be performed for the running script. Since the TimeSeq processing is tied to the sample rate, the internal processor timing is also based on samples, with all other timing specification that can be used in the script being translated into the corresponding sample count.
Because the execution of a script is tied to the active sample rate, TimeSeq will have to reload a script if the VCV Rack sample rate is changed. When TimeSeq detects a sample rate change, it will automatically reload the current script and reset and pause it.
When it comes to the order of processing, a single processing cycle in TimeSeq will execute all logic in the order that they appear in the script. The only exception is the execution of actions, where the timing
of an action can have an influence on the processing order within the segment (see action in the Script JSON Reference for more details).
From a high-level sequencing view, a TimeSeq script contains:
A script can also contain following items at the root level:
The action level of the TimeSeq script contains the functional part of the sequencer. It’s where the actual interaction- and processing logic occurs. Depending on the timing
property, three types of actions can be distinguished:
start
or the end
of a segment)glide
from one value to another for the whole duration of a segment)gate
for the duration of a segment)One-time actions are executed either at the start or at the end of a segment. In both cases, they follow the same execution logic:
if
condition evaluates to true
(or if there is no if
condition set on the action).set-variable
, a set-value
, a set-polyphony
, set-label
or a trigger
:
name
) to a specific voltage. This variable can then be used by other actions and conditions. The voltage to use for the variable is determined by a value, which optionally has calculations applied to it (add, subtract, multiply or divide with another value). The voltage of a value is retrieved from either:
set-variable
operation is determined.2
and 16
channels) or monophonic (when setting it to 1
channel).trigger
operation will fire an internal trigger (see triggers).Just like one-time actions, a glide action has an optional condition. If this condition does not evaluate to true
, the glide action will not be executed.
Also like one-time actions, glide actions can set the voltage of either a variable or an output port. Unlike one-time actions however, glide actions don’t just set one voltage value. Instead, a start
value and an end
value are defined, and the glide action will gradually move from the start value to the end value for the duration of the segment. By default, the glide action will move linearly between the two values, but an optional easing factor allows the action to change faster in the beginning and ease out towards the end, or start moving slowly at the start and speed up towards the end.
A gate action allows a gate signal to be generated on an output port. It will change the output port voltage to 10v at the start of a segment, and change it to 0v as the segment progresses. By default, the change to 0v will occur halfway through the duration of the segment, but it is possible to change this position using the gate-high-ratio
, moving it more towards the start or the end of the segment.
If needed, values can be quantized to notes. The simplest way to quantize is by setting the quantize
property of a value to true
. This will cause the voltage of the value to be quantized to the nearest semitone, using the 1V/Oct standard.
It is however also possible to perform more fine-grained quantization using tunings. A tuning defines a list of notes to which a value should be quantized. The tuning can then be applied to a value by using a calc with the quantize
operation. This allows values to be quantized to scales or, since the tuning notes can be specified using floats, to special tunings that don’t follow the usual semitone note values.
The TimeSeq core processor contains support for internal triggers. Internal triggers are always referenced by their id
(as chosen by the user).
Internal triggers can either be fired by a trigger action that is executed when a segment starts or ends, or because an external trigger or gate signal is detected on an input port that is monitored by an input-trigger.
All internal triggers that get fired as part of a TimeSeq processing cycle will be collected in a list. At the start of the next processing cycle, this list of collected triggers can then influence the running status of lanes: a lane can specify the IDs of triggers that cause a status change of the lane if that trigger is fired:
start-trigger
identifies the trigger ID that will cause the lane to start if it is not running yetrestart-trigger
identifies the trigger ID that will cause the lane to start if it is not running yet, or restart if it is already running.stop-trigger
identifies the trigger ID that will cause the lane to stop if it is running.Once the states of the lanes have been updated, the triggers will be cleared and the processing of actions can fire new triggers, which can then influence the lane running states at the start of the next processing cycle.
This trigger mechanism can be used for multiple purposes:
The component-pool allows JSON objects to be defined which are not directly placed into the sequences of a script, but can instead be referenced from other parts of the scripts. This allows a single definition of an object to be re-used in multiple places in the script (avoiding the need to duplicate the same object multiple times in different places) and can help in bringing structure in more complex scripts.
The component-pool allows re-usable instances to be defined for segments, segment-blocks, actions, ifs, values, calcs, inputs and outputs. Next to the regular supported properties for each of these types, an additional id
property is required to be set on each object that is added to the component-pool. This id
must be unique per object type, but two objects of different types are allowed to use the same id
. E.g. there can be only one segment with the id
“me-myself-and-id”, but it is allowed for a segment and a value to both have the id
“me-myself-and-id”.
Any component that has been defined in the component-pool can then be used in other places in the TimeSeq script by using the ref
property name and the id
as value in the place where you would normally write the full object definition inline.
In the following JSON action, the value of channel 9 on output port 6 is set to the current voltage of channel 2 on input port 4:
{
"value": {
"input": {
"index": 4,
"channel": 2
}
},
"output": {
"index": 6,
"channel": 9
}
}
Since values, inputs and outputs can be defined in the component-pool, it’s possible to define re-usable components for these:
{
"component-pool": {
"values": [
{
"id": "value-4.2",
"input": { "ref": "input-4.2" }
}
],
"inputs": [
{
"id": "input-4.2",
"index": 4,
"channel": 2
}
],
"outputs": [
{
"id": "output-6.9",
"index": 6,
"channel": 2
}
]
}
}
This definition already shows the first usage of a reference: the value doesn’t define the full input object anymore inline, but instead references the newly defined input-4.2
input object by using a ref
towards it.
Using the same ref
mechanism, the original action JSON can now be written as:
{
"value": { "ref": "value-4.2" },
"output": { "ref": "output-6.9" }
}
Due to the hierarchical structure of the TimeSeq script, it is possible to create a setup where a circular reference occurs. For example in following script snipplet:
{
"component-pool": {
"values": [
{
"id": "my-first-value",
"value": {
"input": 2,
"calc": [
{
"add": { "ref": "my-first-value" }
}
]
}
}
]
}
}
the value with id my-first-value
takes the current voltage of input port 2 and then adds another value to it. In this add
calc operation, it tries to use the my-first-value
again by reference, resulting in a circular reference. This specific circular reference is only one level deep, but circular references can occur several levels deep with multiple objects in between. They will however always result in a reference loop that can not be resolved. TimeSeq will detect these kinds of circular references when a script is loaded and treat this as an error.
If a segment is used by id
reference in multiple timelines, the duration of that segment can be different in each timeline dependent on the time-scale of the timeline. E.g. if one timeline specifies a bpm
of 120 in its time-scale, and the other a bpm
of 90, then a segment that has a duration of 4 beats
will have a different duration (in corresponding milliseconds) when placed in those two timelines.
When loading a script, TimeSeq will validate the script before actually loading it. This validation is done in three phases:
ref
can not be resolved, a segment uses beats
while the time-scale does not specify a bpm
, …).If an error is encountered during any of these phases, loading of the new script will be aborted. If a script was already loaded, TimeSeq will keep that old script loaded. Phases (2) and (3) of the script validation can detect multiple errors in a script. If that is the case, the error message popup will display the information of the first encountered error. The dialog will also give the option to copy the full list of errors to the clipboard so they can be pasted elsewhere (e.g. a text editor) for further investigation.
The error messages try to provide as much relevant information as possible. Errors encountered during validation phase (1) will usually include a location indication with the line and column (=character position in the line) where the error was detected. Errors encountered during validation phases (2) and (3) will have following format:
<error location> : <error message> [<error code>]
<error location>
: Since these errors are encountered after initial JSON parsing has completed, the error location can be identified using the hierarchical location within the JSON structure. The different levels within the JSON hierarchy will be separated by a /
and items within a list will be identified by their 0-based index (i.e. the first item has index 0
, the second has index 1
, etc.).
E.g. error location /timelines/1/lanes/3/segments/2
would indicate that in the second timeline of the script, the fourth lane has an error in its third segment.<error message>
: A description detailing the exact error that was encountered<error code>
: An internal error code that details the type of validation check that resulted in the error.The JSON schema validation is performed using strict validation: not only known properties with unknown or invalid values are treated as errors, unknown properties will also result in an error duration validation. This avoids a scenario where a typo results in a script not behaving in the expected way. E.g. if the auto-start
of a lane is accidentally mistyped as auto-stort
, this could go by unnoticed if TimeSeq ignored unknown properties. The script would still be considered valid, but the relevant lane would not automatically start. Due to the strict validation, the loading of that script will indicate that there is an unknown property on the lane, allowing the typo to be corrected.
One exception to the strict validation mentioned in Script Errors is that TimeSeq will ignore any properties that start with x-
(e.g. x-auto-start
). This exception has been introduced to:
x-
to temporarily make TimeSeq ignore them when loading a scriptx-description
or x-comment
to add descriptive information to sections of the script can help in understanding the flow of a script if it is revisited at a later time.