Spaces:
Sleeping
Sleeping
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) | |
} | |
} | |
} | |