Spaces:
Sleeping
Sleeping
File size: 3,929 Bytes
f23825d |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 |
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)
}
}
}
|