screencast.mp4
The whole shape is treated as one bar.
- Click and drag nodes to any position.
- The "time signature" control changes the number of vertices, which acts as the number of beats per bar. This control allows between 3 and 8 vertices (inclusive).
- The "tempo" control changes the speed of the playhead in beats per minute (BPM).
- Press "R" to reset the shape and playhead position.
- Alt-click the tempo/time signature controls to reset them to their default values (120 BPM and 4/4, respectively).
In essence: the playhead position is incremented linearly and continuously, and the distance between points is used to track when the playhead "taps" a node.
Please note that, as per #1, the current system may skip nodes if they are "stepped over" in a frame.
The playhead "progress" is continuously updated each cycle. This value should be a value between 0.0
and 1.0
, and should wrap around if it ever exceeds 1.0
. Ideally, the playhead progress is incremented based on the tempo, which can be done following this formula:
Here,
Note: in this device, the time between calls (
$T$ ) is the time between each frame, but this may also be the time between samples, in which case you would use$T=$ 1.0 / sample_rate
.
Nodes — the two-dimensional points — may be placed wherever you choose. To distribute them uniformly to create regular shapes, see the below pseudocode:
// Our array of nodes, which are two-dimensional points:
let nodes = [ ... ]
// Some radius:
let radius = 250
// Divide 2π (tau) by the number of nodes:
let delta = 2π / number_of_nodes
// We iterate through all our nodes with a variable "i":
for i in 0 to number_of_nodes - 1:
// We count backwards so that we go clockwise:
let index = number_of_nodes - i
// Multiply the index and the delta together, and then add π / 2
// to rotate the shape 90º anti-clockwise so we start from the
// top, not the right:
let delta_angle = index * delta + (π / 2)
// cos() gets us our x (horizontal) position...
let x = cos(delta_angle) * radius
// and sin() gets us our y (vertical) position
let y = sin(delta_angle) * radius
// Then, set the i'th node to (x, y):
node[i] = (x, y)
end
See emplace_nodes()
for the actual implementation in this device.
Line segments are created between consecutive nodes, which can be used to track distance. Adding the length of all segments together will yield the total length (perimeter) of the current shape. This is needed to both maintain a consistent playhead speed and identify when a node has been passed, or "tapped".
Significantly, in this device each node holds "NoteEventData
", which may store any information you wish. In this example it simply encodes a MIDI note value, but could also, for example, encode filter cutoff frequency, distortion drive, or even a reference to an audio file for playback.
Whenever a node is "tapped", the sequencer requests its NoteEventData
, which is then processed further (the timing of the event is attached), and then sent to the audio thread to do its thing.
This device follows the following process for finding when a node has been "tapped":
- Calculate and store the length of all segments. This can be done with the hypotenuse:
$\sqrt{(b.x-a.x)^2+(b.y-a.y)^2}$ , where$a$ and$b$ are two 2-dimensional points. - Calculate and store the total length of all segments (i.e. the perimeter).
- Multiply the playhead progress (a value between
0.0
and1.0
) by the total length. - With this value, find the nearest two nodes to the playhead (what lengths is it between?).
- Check if the node "behind" the playhead has changed: if it has, then the new "behind" node has just been tapped. This node should be stored for the next call, so you can check if it changes again.
To find the position of the playhead between nodes (if you want to visualise it, for instance):
- Find the distance between nodes (let's call it
$\mathrm{dist}$ ) via inverse linear interpolation (inverse lerp):
where:
-
$l$ is the length from the beginning of the shape to the playhead position, -
$l_\mathrm{behind}$ is the length from the beginning of the shape to the node "behind" the playhead, -
$l_\mathrm{ahead}$ is the length from the beginning of the shape to the node "ahead of" the playhead.
- Interpolate between the two nodes via linear interpolation (lerp):
where:
-
$\mathrm{dist}$ is the interpolation factor from the previous equation, -
$p_\mathrm{behind}$ is the position of the node "behind" the playhead, -
$p_\mathrm{ahead}$ is the position of the node "ahead of" the playhead.