import { Segment, SpeedSegment } from './segment' /** * Speedcore is a small internally-used library. * A Speedcore represents a single dimensional keyframed linear motion * (as in equation x = f(t)), and is useful when working * with BPM changes ({Timing}), note spacing factor ({Spacing}), or scrolling * segments ({Positioning}). * A Speedcore is constructed from an array of Segments. * * A {Segment} is defined as `{ t, x, dx }`, such that: * * * speedcore.x(segment.t) = segment.x * * speedcore.t(segment.x) = segment.t * * speedcore.x(segment.t + dt) = segment.x + (segment.dx / dt) * * * ## Explanation * * One way to think of these segments is to think about tempo changes, where: * * * `t` is the elapsed time (in seconds) since song start. * * `x` is the elapsed beat since song start. * * `dx` is the amount of `x` increase per `t`. In this case, it has the * unit of beats per second. * * For example, consider a song that starts at 140 BPM. * 32 beats later, the tempo changes to 160 BPM. * 128 beats later (at beat 160), the tempo reverts to 140 BPM. * * We can derive three segments: * * 1. At time 0, we are at beat 0, and moving at 2.333 beats per second. * 2. At 13.714s, we are at beat 32, moving at 2.667 beats per second. * 3. At 61.714s, we are at beat 160, moving at 2.333 beats per second. * * This maps out to this data structure: * * ```js * [ [0]: { t: 0.000, x: 0, dx: 2.333, inclusive: true }, * [1]: { t: 13.714, x: 32, dx: 2.667, inclusive: true }, * [2]: { t: 61.714, x: 160, dx: 2.333, inclusive: true } ] * ``` * * With this data, it is possible to find out the value of `x` at any given `t`. * * For example, to answer the question, “what is the beat number at 30s?” * First, we find the segment with maximum value of `t < 30`, and we get * the segment `[1]`. * * We calculate `segment.x + (t - segment.t) * segment.dx`. * The result beat number is (32 + (30 - 13.714) * 2.667) = 75.435. * * We can also perform the reverse calculation in a similar way, by reversing * the equation. * * Interestingly, we can use these segments to represent the effect of * both BPM changes and STOP segments in the same array. * For example, a 150-BPM song with a 2-beat stop in the 32nd beat * can be represented like this: * * ```js * [ [0]: { t: 0.0, x: 0, dx: 2.5, inclusive: true }, * [1]: { t: 12.8, x: 32, dx: 0, inclusive: true }, * [2]: { t: 13.6, x: 32, dx: 2.5, inclusive: false } ] * ``` */ export class Speedcore { _segments: S[] /** * Constructs a new `Speedcore` from given segments. */ constructor(segments: S[]) { segments.forEach(Segment) this._segments = segments } _reached(index: number, targetFn: (segment: S) => number, position: number) { if (index >= this._segments.length) return false const segment = this._segments[index] const target = targetFn(segment) return segment.inclusive ? position >= target : position > target } _segmentAt(targetFn: (segment: S) => number, position: number): S { for (let i = 0; i < this._segments.length; i++) { if (!this._reached(i + 1, targetFn, position)) return this._segments[i] } throw new Error( 'Unable to find a segment matching a criteria (this should never happen)!' ) } segmentAtX(x: number) { return this._segmentAt(X, x) } segmentAtT(t: number) { return this._segmentAt(T, t) } /** * Calculates the _t_, given _x_. */ t(x: number) { const segment = this.segmentAtX(x) return segment.t + (x - segment.x) / (segment.dx || 1) } /** * Calculates the _x_, given _t_. * @param {number} t */ x(t: number) { const segment = this.segmentAtT(t) return segment.x + (t - segment.t) * segment.dx } /** * Finds the _dx_, given _t_. * @param {number} t */ dx(t: number) { const segment = this.segmentAtT(t) return segment.dx } } const T = (segment: SpeedSegment) => segment.t const X = (segment: SpeedSegment) => segment.x