import { Speedcore } from '../speedcore' import { uniq, map } from '../util/lodash' import { BMSChart } from '../bms/chart' import { SpeedSegment } from '../speedcore/segment' const precedence = { bpm: 1, stop: 2 } /** * A Timing represents the timing information of a musical score. * A Timing object provides facilities to synchronize between * metric time (seconds) and musical time (beats). * * A Timing are created from a series of actions: * * - BPM changes. * - STOP action. */ export class Timing { _speedcore: Speedcore _eventBeats: number[] /** * Constructs a Timing with an initial BPM and specified actions. * * Generally, you would use `Timing.fromBMSChart` to create an instance * from a BMSChart, but the constructor may also be used in other situations * unrelated to the BMS file format. (e.g. bmson package) */ constructor(initialBPM: number, actions: TimingAction[]) { const state = { bpm: initialBPM, beat: 0, seconds: 0 } const segments: TimingSegment[] = [] segments.push({ t: 0, x: 0, dx: state.bpm / 60, bpm: state.bpm, inclusive: true, }) actions = actions.slice() actions.sort(function (a, b) { return a.beat - b.beat || precedence[a.type] - precedence[b.type] }) actions.forEach(function (action) { const beat = action.beat let seconds = state.seconds + ((beat - state.beat) * 60) / state.bpm switch (action.type) { case 'bpm': state.bpm = action.bpm segments.push({ t: seconds, x: beat, dx: state.bpm / 60, bpm: state.bpm, inclusive: true, }) break case 'stop': segments.push({ t: seconds, x: beat, dx: 0, bpm: state.bpm, inclusive: true, }) seconds += ((action.stopBeats || 0) * 60) / state.bpm segments.push({ t: seconds, x: beat, dx: state.bpm / 60, bpm: state.bpm, inclusive: false, }) break default: throw new Error('Unrecognized segment object!') } state.beat = beat state.seconds = seconds }) this._speedcore = new Speedcore(segments) this._eventBeats = uniq(map(actions, (action) => action.beat)) } /** * Convert the given beat into seconds. * @param {number} beat */ beatToSeconds(beat: number) { return this._speedcore.t(beat) } /** * Convert the given second into beats. * @param {number} seconds */ secondsToBeat(seconds: number) { return this._speedcore.x(seconds) } /** * Returns the BPM at the specified beat. * @param {number} beat */ bpmAtBeat(beat: number) { return this._speedcore.segmentAtX(beat).bpm } /** * Returns an array representing the beats where there are events. */ getEventBeats() { return this._eventBeats } /** * Creates a Timing instance from a BMSChart. * @param {BMSChart} chart */ static fromBMSChart(chart: BMSChart) { void BMSChart const actions: TimingAction[] = [] chart.objects.all().forEach(function (object) { let bpm const beat = chart.measureToBeat(object.measure, object.fraction) if (object.channel === '03') { bpm = parseInt(object.value, 16) actions.push({ type: 'bpm', beat: beat, bpm: bpm }) } else if (object.channel === '08') { bpm = +chart.headers.get('bpm' + object.value)! if (!isNaN(bpm)) actions.push({ type: 'bpm', beat: beat, bpm: bpm }) } else if (object.channel === '09') { const stopBeats = +chart.headers.get('stop' + object.value)! / 48 actions.push({ type: 'stop', beat: beat, stopBeats: stopBeats }) } }) return new Timing(+chart.headers.get('bpm')! || 60, actions) } } export type TimingAction = BPMTimingAction | StopTimingAction export interface BaseTimingAction { /** where this action occurs */ beat: number } export interface BPMTimingAction extends BaseTimingAction { type: 'bpm' /** BPM to change to */ bpm: number } export interface StopTimingAction extends BaseTimingAction { type: 'stop' /** number of beats to stop */ stopBeats: number } interface TimingSegment extends SpeedSegment { bpm: number }