bemusic/bemuse

View on GitHub
packages/bms/src/speedcore/index.ts

Summary

Maintainability
A
0 mins
Test Coverage
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<S extends SpeedSegment = SpeedSegment> {
  _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