midi-player-demo / src /common /player /EventScheduler.ts
Yann
test
f23825d
import { DistributiveOmit } from "../types"
export type SchedulableEvent = {
tick: number
}
export interface EventSchedulerLoop {
begin: number
end: number
}
type WithTimestamp<E> = {
event: E
timestamp: number
}
/**
* Player でイベントを随時読み取るためのクラス
* 精確にスケジューリングするために先読みを行う
* https://www.html5rocks.com/ja/tutorials/audio/scheduling/
*/
/**
* Player Classes for reading events at any time
* Perform prefetching for accurate scheduling
* https://www.html5rocks.com/ja/tutorials/audio/scheduling/
*/
export default class EventScheduler<E extends SchedulableEvent> {
// 先読み時間 (ms)
// Leading time (MS)
lookAheadTime = 100
// 1/4 拍子ごとの tick 数
// 1/4 TICK number for each beat
timebase = 480
loop: EventSchedulerLoop | null = null
private _currentTick = 0
private _scheduledTick = 0
private _prevTime: number | undefined = undefined
private _getEvents: (startTick: number, endTick: number) => E[]
private _createLoopEndEvents: () => Omit<E, "tick">[]
constructor(
getEvents: (startTick: number, endTick: number) => E[],
createLoopEndEvents: () => DistributiveOmit<E, "tick">[],
tick = 0,
timebase = 480,
lookAheadTime = 100,
) {
this._getEvents = getEvents
this._createLoopEndEvents = createLoopEndEvents
this._currentTick = tick
this._scheduledTick = tick
this.timebase = timebase
this.lookAheadTime = lookAheadTime
}
get scheduledTick() {
return this._scheduledTick
}
millisecToTick(ms: number, bpm: number) {
return (((ms / 1000) * bpm) / 60) * this.timebase
}
tickToMillisec(tick: number, bpm: number) {
return (tick / (this.timebase / 60) / bpm) * 1000
}
seek(tick: number) {
this._currentTick = this._scheduledTick = Math.max(0, tick)
}
readNextEvents(bpm: number, timestamp: number): WithTimestamp<E>[] {
const withTimestamp =
(currentTick: number) =>
(e: E): WithTimestamp<E> => {
const waitTick = e.tick - currentTick
const delayedTime =
timestamp + Math.max(0, this.tickToMillisec(waitTick, bpm))
return { event: e, timestamp: delayedTime }
}
const getEventsInRange = (
startTick: number,
endTick: number,
currentTick: number,
) => this._getEvents(startTick, endTick).map(withTimestamp(currentTick))
if (this._prevTime === undefined) {
this._prevTime = timestamp
}
const delta = timestamp - this._prevTime
const deltaTick = Math.max(0, this.millisecToTick(delta, bpm))
const nowTick = Math.floor(this._currentTick + deltaTick)
// 先読み時間
// Leading time
const lookAheadTick = Math.floor(
this.millisecToTick(this.lookAheadTime, bpm),
)
// 前回スケジュール済みの時点から、
// From the previous scheduled point,
// 先読み時間までを処理の対象とする
// Target of processing up to read time
const startTick = this._scheduledTick
const endTick = nowTick + lookAheadTick
this._prevTime = timestamp
if (
this.loop !== null &&
startTick < this.loop.end &&
endTick >= this.loop.end
) {
const loop = this.loop
const offset = endTick - loop.end
const endTick2 = loop.begin + offset
const currentTick = loop.begin - (loop.end - nowTick)
this._currentTick = currentTick
this._scheduledTick = endTick2
return [
...getEventsInRange(startTick, loop.end, nowTick),
...this._createLoopEndEvents().map((e) =>
withTimestamp(currentTick)({ ...e, tick: loop.begin } as E),
),
...getEventsInRange(loop.begin, endTick2, currentTick),
]
} else {
this._currentTick = nowTick
this._scheduledTick = endTick
return getEventsInRange(startTick, endTick, nowTick)
}
}
}