This guide complements the JSON Schema in describing the DSPX format version 1.0.0. While the schema defines the document structure, field types, and validation constraints, this guide explains the semantics and processing rules needed to interpret and manipulate DSPX files.
DSPX uses a fixed resolution of 480 ticks per quarter note (TPQN). A tick represents musical time; its duration in seconds depends on the tempo. The resolution remains 480 TPQN regardless of the time signature.
A time signature with numerator N and denominator D has N beats per measure, each with the note value 1/D of a whole note. Its measure length is:
ticksPerBeat = 480 * 4 / D
ticksPerMeasure = N * ticksPerBeat
For example, a measure in 4/4 contains 1920 ticks, while a measure in either 3/4 or 6/8 contains 1440 ticks. In 6/8, an eighth note is 240 ticks and a dotted-quarter pulse is 720 ticks; a quarter note is still 480 ticks.
Time signatures are stored in content.timeline.timeSignatures. Each entry’s index is a zero-based measure number, rather than a tick position: index = 0 identifies the first measure. To locate a later measure on the project timeline, add the lengths of the preceding measures using the time signatures in effect for those measures.
Tempo changes are stored in content.timeline.tempos. Each entry’s pos is a tick position on the project timeline, and its value is the number of quarter notes per minute (BPM). This tempo unit remains quarter notes per minute even when the time signature has a denominator other than 4.
Within a segment at a constant tempo:
seconds = ticks * 60 / (480 * BPM)
ticks = seconds * 480 * BPM / 60
For example, at 120 BPM, 480 ticks last 0.5 seconds, and one tick lasts approximately 1.041667 milliseconds. At 60 BPM, the same 480 ticks last 1 second.
When an interval crosses tempo changes, split it at each change, convert each segment using the tempo in effect there, and sum the segment durations. For example, if the tempo is 120 BPM from project tick 0 and changes to 60 BPM at tick 960, the elapsed time from tick 0 to tick 1440 is:
960 * 60 / (480 * 120) + 480 * 60 / (480 * 60) = 2 seconds
For a clip-relative interval, first map its endpoints to project tick positions, as described in the clip trimming section, so that tempo changes are applied at the correct positions.
Project tick 0 is the start of the project. Timeline label positions, tempo change positions, and a clip’s time.pos use this project timeline. A note’s pos is relative to the start of its clip’s underlying content, before trimming; it is not an absolute project position or an offset from the visible clip start.
Not every time-related field uses ticks. In particular, a phoneme’s start is measured in milliseconds relative to the note onset and can be negative. Vibrato start and end are ratios of the note length, while vibrato freq is measured in hertz. Interpret each field using its documented unit and time origin.
A clip has an underlying content range and a visible range selected from that content. Trimming changes the visible range without deleting the underlying content. In the diagram, the hatched regions are trimmed out, while the solid region is the visible portion of the clip.
The clip’s time object contains four fields, all measured in ticks:
| Field | Meaning |
|---|---|
pos |
The position of the visible start of the clip on the project timeline. |
clipStart |
The offset from the start of the underlying content to the visible start of the clip. |
clipLen |
The length of the visible range, measured from clipStart. |
length |
The length of the underlying content before trimming. A value of 0 means unlimited, rather than empty content. |
The start of the underlying content on the project timeline is:
contentStart = pos - clipStart
The visible range in content coordinates is [clipStart, clipStart + clipLen). On the project timeline, the same range is [pos, pos + clipLen). These intervals include their start and exclude their end.
A position t measured from the start of the underlying content maps to the project timeline as follows:
projectPosition = pos - clipStart + t
When length is nonzero, the underlying content ends at content position length, or project position pos - clipStart + length. When length is 0, it does not impose a finite content end; clipLen still specifies the visible length.
For example, consider this clip time:
{
"pos": 1920,
"clipStart": 480,
"clipLen": 960,
"length": 2400
}
The underlying content starts at project tick 1440 and ends at tick 3840. Its visible portion is content ticks [480, 1440), which appear at project ticks [1920, 2880). The first 480 ticks and the last 960 ticks of the content are trimmed out.
To move the clip without changing which content is visible, change pos and leave the other three fields unchanged. To trim another 240 ticks from the left while keeping the remaining content at the same project positions, increase both pos and clipStart by 240, and decrease clipLen by 240. In this example, the result is pos = 2160, clipStart = 720, and clipLen = 720, with length unchanged. To trim from the right, decrease clipLen while keeping pos, clipStart, and length unchanged.
The DSPX specification does not prescribe a set of parameters or the meaning of their values. These are determined by the editor and synthesis architecture. Most editors use the same normalization logic as DiffScope, described below.
Parameters are stored by name in a singing clip’s params object. Each parameter has three curve lists: original, edited, and transform.
DSPX files generated by DiffScope store parameter values as normalized values multiplied by 8064000 and rounded to integers:
storedValue = round(normalizedValue * 8064000)
normalizedValue = storedValue / 8064000
This scaling applies to parameter values in both anchor nodes (y) and free-curve samples (values). Decode the stored integers into normalized values before applying the evaluation rules below. Do not apply the normalization factor to time coordinates.
Most editors and synthesis architectures support a pitch parameter. Where it supplies a value, pitch overrides the note’s pitch. It is an absolute pitch, rather than an offset to add to the note’s pitch. For DiffScope, multiplying the normalized pitch by 12800 gives its value in cents:
pitchInCents = normalizedPitch * 12800
Each of the three curve lists can contain anchor curves (type = "anchor") and free curves (type = "free"). When both types supply a value at the same time, the anchor-curve value overrides the free-curve value. Resolve this precedence within each list before combining original, edited, and transform.
At each time $t$, evaluate the interpolated, normalized curves as follows:
edited(t) when it is valid; otherwise, use original(t). An edited value replaces the original value; the two values are not added together.transform(t) is valid, multiply the base value by 2 * transform(t). Otherwise, use a multiplier of 1.[0, 1].Equivalently:
\[B(t) = \begin{cases} edited(t), & \text{if } edited(t) \text{ is valid} \\ original(t), & \text{otherwise} \end{cases}\] \[V(t) = \operatorname{clamp}\!\left(B(t) \times \begin{cases} 2\,transform(t), & \text{if } transform(t) \text{ is valid} \\ 1, & \text{otherwise} \end{cases},\,0,\,1\right)\]Here, a value is valid when the curve supplies a value at time $t$. Validity is separate from the numeric value: 0 is a valid value and does not mean missing. These equations assume a valid base value is available; they do not prescribe a default when both edited(t) and original(t) are missing.
Thus, a normalized transform value of 0.5 preserves the base value, 0 sets it to zero, and 1 doubles it before clamping. For example, a base value of 0.75 with transform(t) = 1 produces 1 after clamping, while a valid edited(t) = 0 overrides a nonzero original value.
In DSPX, the following interpolation algorithm is used to construct a smooth curve of the hermite interpolation type between two known points $(x_1, y_1)$ and $(x_2, y_2)$. This algorithm calculates derivatives using the Fritsch-Butland form via divided differences and performs piecewise cubic Hermite polynomial interpolation.
A C++ reference implementation can be found in opendspx::interpolator within opendspx.
$P_{ref1}$ is the adjacent anchor node to the left of $P_1$, and $P_{ref2}$ is the adjacent parameter anchor node to the right of $P_2$. If a curve exists between an endpoint and its reference point (regardless of that curve’s interpolation type), the reference point should be provided as input to the algorithm; if no curve exists between them, the reference point is not input.
Define the slope function:
\[\Delta(x_1, y_1, x_2, y_2) = \frac{y_1 - y_2}{x_1 - x_2}\]Define the derivative estimation function $f_b(x_l, y_l, x, y, x_r, y_r)$:
Let:
\[h_l = x - x_l, \quad h_r = x_r - x\] \[\delta_l = \Delta(x, y, x_l, y_l), \quad \delta_r = \Delta(x, y, x_r, y_r)\]Defined as:
\[f_b = \begin{cases} 0 & \text{if } \delta_l \cdot \delta_r \le 0 \\ \frac{3(h_l + h_r)}{\frac{2h_r + h_l}{\delta_l} + \frac{h_r + 2h_l}{\delta_r}} & \text{if } \delta_l \cdot \delta_r > 0 \end{cases}\]Calculate $d_1$:
\[\begin{equation} d_1 = \begin{cases} fb(x_{ref1}, y_{ref1}, x_1, y_1, x_2, y_2) & \text{if } P_{ref1} \text{ exists} \\ \Delta(x_1, y_1, x_2, y_2) & \text{otherwise} \end{cases} \end{equation}\]Calculate $d_2$:
\[\begin{equation} d_2 = \begin{cases} fb(x_1, y_1, x_2, y_2, x_{ref2}, y_{ref2}) & \text{if } P_{ref2} \text{ exists} \\ \Delta(x_1, y_1, x_2, y_2) & \text{otherwise} \end{cases} \end{equation}\]Next, based on the endpoint coordinates and derivatives $(x_1, y_1, d_1)$ and $(x_2, y_2, d_2)$, calculate the intermediate parameters:
\[\begin{align} \delta &= \Delta(x_1, y_1, x_2, y_2)\\ c_0 &= y_1\\ c_1 &= d_1\\ c_2 = \delta_1^{(2)} &= \Delta(x_1, d_1, x_2, \delta)\\ \delta_2^{(2)} &= \Delta(x_1, \delta, x_2, d_2)\\ c_3 &= \Delta(x_1, \delta_1^{(2)}, x_2, \delta_2^{(2)}) \end{align}\]Define the interpolation expression:
\[H(x) = a_3x^3 + a_2x^2 + a_1x + a_0\]Where:
\[\begin{align} a_3 &= c_3 \\ a_2 &= c_2 - c_3(2x_1 + x_2) \\ a_1 &= c_1 - 2x_1c_2 + c_3(x_1^2 + 2x_1x_2) \\ a_0 &= c_0 - x_1c_1 + x_1^2c_2 - x_1^2x_2c_3 \end{align}\]