@blibliki/transport 0.5.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -23
- package/dist/index.d.ts +155 -31
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
- package/src/Clock.ts +15 -0
- package/src/Transport.ts +127 -75
- package/src/index.ts +25 -1
- package/src/sources/BaseSource.ts +70 -0
- package/src/sources/SourceManager.ts +54 -0
- package/src/sources/StepSequencerSource.ts +310 -0
- package/src/sources/index.ts +0 -0
- package/src/utils.ts +66 -0
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/Clock.ts","../src/Position.ts","../src/utils.ts","../src/Timeline.ts","../src/Scheduler.ts","../src/Tempo.ts","../src/audio-context-timers.ts","../src/Timer.ts","../src/Transport.ts"],"sourcesContent":["import { Context } from \"@blibliki/utils\";\nimport { ClockTime, ContextTime, Seconds } from \"./types\";\n\nexport class Clock {\n private context: Readonly<Context>;\n private clockTimeAt: { start: ClockTime; stop: ClockTime };\n private contextTimeAt: { start: ContextTime; stop: ContextTime };\n private audioClockOffset: number;\n\n private _isRunning = false;\n\n constructor(context: Readonly<Context>, audioClockOffset: Seconds) {\n this.context = context;\n this.audioClockOffset = audioClockOffset;\n\n this.clockTimeAt = { start: 0, stop: 0 };\n this.contextTimeAt = { start: 0, stop: 0 };\n }\n\n get isRunning() {\n return this._isRunning;\n }\n\n time() {\n if (!this.isRunning) return this.clockTimeAt.stop;\n\n const now = this.context.currentTime;\n return this.clockTimeAt.start + (now - this.contextTimeAt.start);\n }\n\n start(actionAt: ContextTime) {\n this.contextTimeAt.start = actionAt;\n this.clockTimeAt.start = this.clockTimeAt.stop;\n this._isRunning = true;\n }\n\n stop(actionAt: ContextTime) {\n this._isRunning = false;\n this.clockTimeAt.stop =\n this.clockTimeAt.start + (actionAt - this.contextTimeAt.start);\n this.contextTimeAt.stop = actionAt;\n }\n\n jumpTo(time: ClockTime) {\n this.clockTimeAt.stop = time;\n }\n\n clockTimeToContextTime(clockTime: ClockTime): ContextTime {\n const type = this.isRunning ? \"start\" : \"stop\";\n\n return (\n this.audioClockOffset +\n this.contextTimeAt[type] +\n clockTime -\n this.clockTimeAt[type]\n );\n }\n}\n","import { isNumber } from \"@blibliki/utils\";\nimport { Ticks, TimeSignature, TPosition, TStringPosition } from \"./types\";\nimport { TPB } from \"./utils\";\n\nexport const sixteenthPerBeat = (timeSignature: Readonly<TimeSignature>) => {\n const denominator = timeSignature[1];\n return 16 / denominator;\n};\n\n/**\n * Represents a musical position that can be expressed in multiple formats:\n * - Ticks (raw MIDI time units)\n * - Bars:Beats:Sixteenths (string format like \"1:2:8\")\n * - Object format with bars, beats, and sixteenths properties\n */\nexport class Position {\n private readonly _ticks: Ticks;\n private timeSignature: Readonly<TimeSignature>;\n\n /**\n * Creates a new Position instance\n * @param value - Position value in ticks, string format (\"bars:beats:sixteenths\"), or object format\n * @param timeSignature - Time signature as [numerator, denominator] (e.g., [4, 4] for 4/4)\n */\n constructor(\n value: Ticks | TPosition | TStringPosition,\n timeSignature: TimeSignature,\n ) {\n this.timeSignature = timeSignature;\n\n if (isNumber(value)) {\n this._ticks = value;\n } else if (typeof value === \"string\") {\n this._ticks = this.parseStringPosition(value);\n } else {\n this._ticks = this.convertObjectToTicks(value);\n }\n }\n\n get ticks() {\n return this._ticks;\n }\n\n get bars() {\n return Math.floor(this.totalBeats / this.timeSignature[0]) + 1;\n }\n\n get beats() {\n return (this.totalBeats % this.timeSignature[0]) + 1;\n }\n\n get sixteenths() {\n return Math.floor(this.beatFraction * 4) + 1;\n }\n\n get totalBeats() {\n return Math.floor(this._ticks / TPB);\n }\n\n get beatFraction() {\n return this._ticks / TPB - this.totalBeats;\n }\n\n toString(): string {\n return `${this.bars}:${this.beats}:${this.sixteenths}`;\n }\n\n toObject(): TPosition {\n return {\n bars: this.bars,\n beats: this.beats,\n sixteenths: this.sixteenths,\n };\n }\n\n private parseStringPosition(positionString: TStringPosition): Ticks {\n const parts = positionString.split(\":\");\n\n const [barsStr, beatsStr, sixteenthsStr] = parts;\n const bars = Number(barsStr);\n const beats = Number(beatsStr);\n const sixteenths = Number(sixteenthsStr);\n\n const position: TPosition = { bars, beats, sixteenths };\n\n return this.convertObjectToTicks(position);\n }\n\n private convertObjectToTicks(position: TPosition): Ticks {\n const totalBeats =\n (position.bars - 1) * this.timeSignature[0] + (position.beats - 1);\n const beatFraction = position.sixteenths / 16;\n return (totalBeats + beatFraction) * TPB;\n }\n}\n","import { BPM, NormalRange, Ticks } from \"./types\";\n\n// Ticks per beat\nexport const TPB = 15360;\n\nexport const secondsPerTick = (tempo: BPM) => {\n return 60 / tempo / TPB;\n};\n\nexport const ticksPerSecond = (tempo: BPM) => {\n return (tempo / 60) * TPB;\n};\n\nexport const insertionIndexBy = <T>(\n arr: T[],\n n: T,\n comparatorFn: (a: T, b: T) => number,\n) => {\n const index = arr.findIndex((el) => comparatorFn(n, el) < 0);\n return index === -1 ? arr.length : index;\n};\n\nexport function lerp(t: number, a: number, b: number): number {\n return a + t * (b - a);\n}\n\nexport function unlerp(t: number, a: number, b: number): number {\n return (t - a) / (b - a);\n}\n\n/**\n * Compute swing.\n *\n * This function is an attempt at mirroring the behaviour in the Reason DAW,\n * where events before the 50% eighth mark are \"expanded\" and events past the\n * mark are \"compressed\". That is, this function transforms the underlying\n * transport time grid:\n *\n * No swing |<--50%-->|<--50%-->|<--50%-->| ...\n * 16ths 0 1 2 3 ...\n * 8ths 0 | 1 | ...\n * Notes X======X X===X | |\n * | | | |\n * | \\ | \\\n * 60% swing |<---60%--->|<-40%->|<---60%--->| ...\n * 16ths 0 1 2 3 ...\n * 8ths 0 | 1 | ...\n * Notes X=======X X==X | |\n *\n * If it is challenging to explain this mechanism to end users, you could\n * use the approach taken in the Logic DAW, which has 6 \"swing modes\":\n *\n * 16A: 50%\n * 16B: 54%\n * 16C: 58%\n * 16D: 62%\n * 16E: 66%\n * 16F: 71%\n *\n * For details, see\n * https://www.attackmagazine.com/technique/passing-notes/daw-drum-machine-swing/2/\n *\n * @param time Time of event, in transport ticks (1/4 = 15360 pulses)\n * @param amount Swing amount in range [0.5, 0.75].\n * @returns New time of the event, with swing applied.\n */\nexport function swing(time: Ticks, amount: NormalRange): Ticks {\n if (amount < 0.5 || amount > 0.75) {\n throw new Error(\"Invalid swing amount\");\n }\n\n /*\n Note lengths in ticks:\n\n 1/4 = 15360\n 1/8 = 7680\n 1/16 = 3840\n */\n\n const t8 = time / 7680; // Input time in 8ths\n const t8i = Math.floor(t8); // 8th in which the time sits\n const t = t8 - t8i; // Percentage position in the 8th in which the time sits\n\n /* \n ### We could make this a bit more efficient; we can simplify the \n <=0.5 case to\n\n (t / 0.5) * amount\n\n But the >0.5 case is more difficult:\n\n amount + ((t - 0.5) / 0.5) * (1 - amount)\n\n Using the lerp/unlerp functions here makes it clear what's going on,\n though, so we'll keep them for now.\n */\n let tn = 0;\n if (t <= 0.5) {\n tn = lerp(unlerp(t, 0.0, 0.5), 0.0, amount);\n } else {\n tn = lerp(unlerp(t, 0.5, 1.0), amount, 1.0);\n }\n\n // Map transformed time back into ticks\n return Math.floor((t8i + tn) * 7680);\n}\n","import { ClockTime } from \"./types\";\nimport { insertionIndexBy } from \"./utils\";\n\nexport type TimelineEvent = {\n time: ClockTime;\n};\n\nexport class Timeline<T extends TimelineEvent = TimelineEvent> {\n private _events: Readonly<T>[] = [];\n\n get events(): readonly Readonly<T>[] {\n return this._events;\n }\n\n /**\n * Add a new event to the timeline. It will be inserted according to its\n * `time` property (which must be present; otherwise an exception is thrown).\n *\n * If the time of the new event is exactly equal to the time of an existing\n * event in the timeline, the existing event is considered to occur before\n * the new event.\n */\n add(event: Readonly<T>) {\n const idx = insertionIndexBy(this._events, event, (a, b) => {\n return a.time - b.time;\n });\n this._events.splice(idx, 0, event);\n }\n\n find(start: ClockTime, end: ClockTime): readonly Readonly<T>[] {\n return this._events.filter((event) => {\n return event.time >= start && event.time < end;\n });\n }\n\n /** Return the last event that occurred at or before the specified time. */\n lastEventBefore(time: ClockTime) {\n for (let i = this._events.length - 1; i >= 0; i--) {\n const event = this._events[i];\n if (event && event.time <= time) {\n return event;\n }\n }\n return undefined;\n }\n\n remove(start: ClockTime, end: ClockTime) {\n this._events = this._events.filter((event) => {\n return !(event.time >= start && event.time < end);\n });\n }\n\n removeAllBefore(time: ClockTime) {\n this._events = this._events.filter((event) => event.time >= time);\n }\n\n clear() {\n this._events = [];\n }\n}\n","import { Timeline, TimelineEvent } from \"./Timeline\";\nimport { ClockTime, Seconds } from \"./types\";\n\n/**\n * Return all events that occur in the interval [start, end).\n */\nexport type EventGenerator<T extends TimelineEvent> = (\n start: ClockTime,\n end: ClockTime,\n) => readonly Readonly<T>[];\n\n/** Consume the speified event. */\nexport type EventConsumer<T extends TimelineEvent> = (\n events: readonly Readonly<T>[],\n) => void;\n\n/**\n * The scheduler is responsible for tracking the scheduling/playback window.\n *\n * Scheduling of events always precedes playback of events. The left hand\n * edge of the scheduling window is the point in time up to where events have\n * been consumed so far. The right hand edge of the window is the point in time\n * up to where events have been scheduled/prepared. The width of the window is\n * always the same (specified when the Scheduler instance is created).\n */\nexport class Scheduler<T extends TimelineEvent = TimelineEvent> {\n private timeline = new Timeline<T>();\n\n private scheduleAheadTime: Seconds;\n private consumedTime: ClockTime;\n\n private generator: EventGenerator<T>;\n private consumer: EventConsumer<T>;\n\n constructor(\n generator: EventGenerator<T>,\n consumer: EventConsumer<T>,\n scheduleAheadTime: Seconds,\n ) {\n this.generator = generator;\n this.consumer = consumer;\n this.scheduleAheadTime = scheduleAheadTime;\n this.consumedTime = -this.scheduleAheadTime;\n }\n\n private _schedule(start: Seconds, end: Seconds) {\n this.generator(start, end).forEach((e) => {\n this.timeline.add(e);\n });\n }\n\n /**\n * Consume events up to the specified time.\n *\n * This will also generate new events up until `time + scheduleAheadTime`.\n */\n runUntil(time: ClockTime) {\n if (time <= this.consumedTime) {\n throw new Error(\"Scheduling time is <= current time\");\n }\n\n /* \n Generate events that occur between the previous and new right-hand edges \n of the scheduling window. I.e., move scheduling time forwards by the \n specified amount.\n */\n this._schedule(\n this.consumedTime + this.scheduleAheadTime,\n time + this.scheduleAheadTime,\n );\n\n /*\n Consume events that occur between the previous and new left-hand edges\n of the scheduling window. I.e., move playback time forwards by the\n specified amount.\n */\n const events = this.timeline.find(this.consumedTime, time);\n this.consumer(events);\n this.consumedTime = time;\n\n /*\n Remove all events that were consumed from the timeline.\n */\n this.timeline.removeAllBefore(this.consumedTime);\n }\n\n /**\n * Jump to the specified destination time.\n */\n jumpTo(time: ClockTime) {\n this.timeline.clear();\n this.consumedTime = time - this.scheduleAheadTime;\n }\n}\n","import { BPM, ClockTime, Ticks } from \"./types\";\nimport { secondsPerTick, ticksPerSecond } from \"./utils\";\n\nexport class Tempo {\n private clockTimeAtLastTempoChange = 0;\n private ticksAtLastTempoChange = 0;\n private _bpm = 120;\n\n get bpm() {\n return this._bpm;\n }\n\n update(clockTime: ClockTime, tempo: BPM) {\n const ticks = this.getTicks(clockTime);\n this.clockTimeAtLastTempoChange = clockTime;\n this.ticksAtLastTempoChange = ticks;\n this._bpm = tempo;\n }\n\n getTicks(clockTime: ClockTime): Ticks {\n const clockDelta = clockTime - this.clockTimeAtLastTempoChange;\n const tickDelta = clockDelta * ticksPerSecond(this.bpm);\n return Math.floor(this.ticksAtLastTempoChange + tickDelta);\n }\n\n getClockTime(ticks: Ticks): ClockTime {\n const tickDelta = Math.floor(ticks - this.ticksAtLastTempoChange);\n const clockDelta = tickDelta * secondsPerTick(this.bpm);\n return this.clockTimeAtLastTempoChange + clockDelta;\n }\n\n reset(clockTime: ClockTime, ticks: Ticks) {\n this.clockTimeAtLastTempoChange = clockTime;\n this.ticksAtLastTempoChange = ticks;\n }\n}\n","import {\n AudioContext,\n AudioBuffer,\n AudioBufferSourceNode,\n} from \"@blibliki/utils/web-audio-api\";\n\nfunction generateUniqueNumber(map: Map<number, () => void>): number {\n let nextId = 1;\n\n // Keep incrementing until we find an unused ID\n while (map.has(nextId)) {\n nextId++;\n }\n\n return nextId;\n}\n\nclass WebAudioWrapper {\n private _audioBuffer: AudioBuffer | null = null;\n private _audioContext: AudioContext | null = null;\n private _sampleDuration: number | null = null;\n\n get audioContext(): AudioContext {\n this._audioContext ??= new AudioContext();\n return this._audioContext;\n }\n\n get audioBuffer(): AudioBuffer {\n this._audioBuffer ??= new AudioBuffer({\n length: 2,\n sampleRate: this.audioContext.sampleRate,\n });\n return this._audioBuffer;\n }\n\n get sampleDuration(): number {\n this._sampleDuration ??= 2 / this.audioContext.sampleRate;\n return this._sampleDuration;\n }\n}\n\nconst webAudioWrapper = new WebAudioWrapper();\n\nconst SCHEDULED_TIMEOUT_FUNCTIONS = new Map<number, () => void>();\nconst SCHEDULED_INTERVAL_FUNCTIONS = new Map<number, () => void>();\n\nenum TimerType {\n interval = \"interval\",\n timeout = \"timeout\",\n}\n\nconst callIntervalFunction = (id: number, type: TimerType) => {\n const functions =\n type === TimerType.interval\n ? SCHEDULED_INTERVAL_FUNCTIONS\n : SCHEDULED_TIMEOUT_FUNCTIONS;\n\n if (functions.has(id)) {\n const func = functions.get(id);\n\n if (func !== undefined) {\n func();\n\n if (type === TimerType.timeout) {\n SCHEDULED_TIMEOUT_FUNCTIONS.delete(id);\n }\n }\n }\n};\n\nconst scheduleFunction = (id: number, delay: number, type: TimerType) => {\n const now = performance.now();\n\n const audioBufferSourceNode = new AudioBufferSourceNode(\n webAudioWrapper.audioContext,\n { buffer: webAudioWrapper.audioBuffer },\n );\n\n audioBufferSourceNode.onended = () => {\n const elapsedTime = performance.now() - now;\n\n if (elapsedTime >= delay) {\n callIntervalFunction(id, type);\n } else {\n scheduleFunction(id, delay - elapsedTime, type);\n }\n\n audioBufferSourceNode.disconnect(webAudioWrapper.audioContext.destination);\n };\n audioBufferSourceNode.connect(webAudioWrapper.audioContext.destination);\n audioBufferSourceNode.start(\n Math.max(\n 0,\n webAudioWrapper.audioContext.currentTime +\n delay / 1000 -\n webAudioWrapper.sampleDuration,\n ),\n );\n};\n\nconst isNode = typeof window === \"undefined\";\n\n// exported methods\n\nconst acClearInterval = (id: number) => {\n SCHEDULED_INTERVAL_FUNCTIONS.delete(id);\n};\n\nconst acClearTimeout = (id: number) => {\n SCHEDULED_TIMEOUT_FUNCTIONS.delete(id);\n};\n\nconst acSetInterval = (func: () => void, delay: number) => {\n const id = generateUniqueNumber(SCHEDULED_INTERVAL_FUNCTIONS);\n\n SCHEDULED_INTERVAL_FUNCTIONS.set(id, () => {\n func();\n\n scheduleFunction(id, delay, TimerType.interval);\n });\n\n scheduleFunction(id, delay, TimerType.interval);\n\n return id;\n};\n\nconst acSetTimeout = (func: () => void, delay: number) => {\n const id = generateUniqueNumber(SCHEDULED_TIMEOUT_FUNCTIONS);\n\n SCHEDULED_TIMEOUT_FUNCTIONS.set(id, func);\n\n scheduleFunction(id, delay, TimerType.timeout);\n\n return id;\n};\n\nconst exportedClearInterval = isNode ? clearInterval : acClearInterval;\nconst exportedClearTimeout = isNode ? clearTimeout : acClearTimeout;\nconst exportedSetInterval = isNode ? setInterval : acSetInterval;\nconst exportedSetTimeout = isNode ? setTimeout : acSetTimeout;\n\nexport {\n exportedClearInterval as clearInterval,\n exportedClearTimeout as clearTimeout,\n exportedSetInterval as setInterval,\n exportedSetTimeout as setTimeout,\n};\n","import { Context } from \"@blibliki/utils\";\nimport { clearInterval, setInterval } from \"./audio-context-timers\";\nimport { ContextTime } from \"./types\";\n\nexport const scheduleAtContextTime = ({\n scheduleAt,\n context,\n callback,\n}: {\n scheduleAt: ContextTime;\n context: Context;\n callback: () => void;\n}) => {\n const now = context.currentTime;\n if (now > scheduleAt) throw Error(\"Could not schedule event at past\");\n\n const ms = (scheduleAt - now) * 1000;\n\n setTimeout(callback, ms);\n};\n\nexport class Timer {\n private timerId: number | undefined = undefined;\n\n private interval: number;\n private callback: () => void;\n\n constructor(callback: () => void, intervalMs: number) {\n this.callback = callback;\n this.interval = intervalMs;\n }\n\n start() {\n this.timerId = setInterval(() => {\n this.callback();\n }, this.interval);\n }\n\n stop() {\n if (this.timerId === undefined) return;\n\n clearInterval(this.timerId);\n this.timerId = undefined;\n }\n\n get isRunning() {\n return this.timerId !== undefined;\n }\n}\n","import { Context } from \"@blibliki/utils\";\nimport { Clock } from \"./Clock\";\nimport { Position } from \"./Position\";\nimport { Scheduler } from \"./Scheduler\";\nimport { Tempo } from \"./Tempo\";\nimport { TimelineEvent } from \"./Timeline\";\nimport { Timer } from \"./Timer\";\nimport {\n BPM,\n ClockTime,\n ContextTime,\n NormalRange,\n Ticks,\n TimeSignature,\n} from \"./types\";\nimport { swing } from \"./utils\";\n\nexport interface TransportEvent extends TimelineEvent {\n ticks: Ticks;\n contextTime: ContextTime;\n}\n\n/**\n * Given a (future) transport time window (in ticks), return all events that should\n * occur within the window. This function is responsible for setting the ticks\n * value for the events returned.\n *\n * IMPORTANT: Subsequent calls to this function may have overlapping time windows.\n * Be careful to not return the same event more than once.\n */\nexport type TransportEventGenerator<T extends TransportEvent> = (\n start: Ticks,\n end: Ticks,\n) => readonly Readonly<T>[];\n\n/**\n * Schedule the specified event with the audio system. The transport class is\n * responsible for setting the `contextTime` of the events.\n */\nexport type TransportEventConsumer<T extends TransportEvent> = (\n event: Readonly<T>,\n) => void;\n\nexport type TransportListener<T extends TransportEvent> = {\n generator: TransportEventGenerator<T>;\n consumer: TransportEventConsumer<T>;\n onJump: (ticks: Ticks) => void;\n onStart: (contextTime: ContextTime) => void;\n onStop: (contextTime: ContextTime) => void;\n silence: (contextTime: ContextTime) => void;\n};\n\n/**\n * Transport callback that gets invoked at (roughly) sixteenth note intervals.\n *\n * Returns the current clock time in seconds.\n *\n * IMPORTANT! Do not rely on this callback for audio precision mechanisms!\n * It is only accurate up to the precision of the event sheduler rate, and may\n * \"jump\" if the scheduling is struggling to keep up.\n */\nexport type TransportClockCallback = (time: ClockTime) => void;\n\nexport enum TransportState {\n playing = \"playing\",\n stopped = \"stopped\",\n paused = \"paused\",\n}\n\n/**\n * This class converts (music) transport time into audio clock time.\n */\nexport class Transport<T extends TransportEvent> {\n private _initialized = false;\n\n private context: Readonly<Context>;\n private clock: Readonly<Clock>;\n private timer: Readonly<Timer>;\n private scheduler: Readonly<Scheduler<T>>;\n\n private _timeSignature: TimeSignature = [4, 4];\n\n private tempo: Readonly<Tempo> = new Tempo();\n private clockTime = 0;\n private _swingAmount: NormalRange = 0.5;\n\n private listener: Readonly<TransportListener<T>>;\n\n private _clockCallbacks: TransportClockCallback[] = [];\n\n constructor(\n context: Readonly<Context>,\n listener: Readonly<TransportListener<T>>,\n ) {\n // ### Make these adapt to performance of app? Or let user set them?\n const SCHEDULE_INTERVAL_MS = 20;\n const SCHEDULE_WINDOW_SIZE_MS = 200;\n\n this.context = context;\n this.listener = listener;\n this.clock = new Clock(this.context, SCHEDULE_WINDOW_SIZE_MS / 1000);\n\n this.scheduler = new Scheduler<T>(\n this.generateEvents,\n this.consumeEvents,\n SCHEDULE_WINDOW_SIZE_MS / 1000,\n );\n\n this.timer = new Timer(() => {\n const time = this.clock.time();\n if (time <= this.clockTime) return;\n\n this.clockTime = time;\n this.scheduler.runUntil(time);\n this._clockCallbacks.forEach((callback) => {\n callback(this.clockTime);\n });\n }, SCHEDULE_INTERVAL_MS / 1000);\n\n this._initialized = true;\n }\n\n addClockCallback(callback: TransportClockCallback) {\n this._clockCallbacks.push(callback);\n }\n\n // ??? Make this more efficient (no need to compute full position, for example)\n addBarCallback(callback: (bar: number) => void) {\n let currentBar = Infinity;\n this.addClockCallback(() => {\n const pos = this.position;\n if (pos.bars !== currentBar) {\n callback(pos.bars);\n currentBar = pos.bars;\n }\n });\n }\n\n get time() {\n return this.clock.time();\n }\n\n /**\n * Return the (approximate) current Transport time, in ticks.\n */\n get position(): Readonly<Position> {\n const clockTime = this.clock.time();\n const ticks = this.tempo.getTicks(clockTime);\n return new Position(ticks, this.timeSignature);\n }\n\n /**\n * Set the current Transport time.\n */\n set position(position: Readonly<Position>) {\n this.jumpTo(position.ticks);\n }\n\n getPositionOfEvent(event: Readonly<TransportEvent>): Readonly<Position> {\n return new Position(event.ticks, this.timeSignature);\n }\n\n /**\n * Start the Transport.\n */\n start() {\n if (!this._initialized) throw new Error(\"Not initialized\");\n if (this.clock.isRunning) return;\n\n const actionAt = this.context.currentTime;\n\n this.listener.onStart(actionAt);\n this.clock.start(actionAt);\n this.timer.start();\n }\n\n /**\n * Stop the Transport.\n */\n stop() {\n if (!this._initialized) throw new Error(\"Not initialized\");\n\n const actionAt = this.context.currentTime;\n\n this.listener.silence(actionAt);\n this.listener.onStop(actionAt);\n this.clock.stop(actionAt);\n this.timer.stop();\n }\n\n /**\n * Reset the Transport to zero.\n */\n reset() {\n if (!this._initialized) throw new Error(\"Not initialized\");\n\n const actionAt = this.context.currentTime;\n this.listener.silence(actionAt);\n this.jumpTo(0);\n }\n\n get bpm(): BPM {\n return this.tempo.bpm;\n }\n\n set bpm(bpm: BPM) {\n this.tempo.update(this.clockTime, bpm);\n }\n\n get timeSignature() {\n return this._timeSignature;\n }\n\n set timeSignature(value: TimeSignature) {\n this._timeSignature = value;\n }\n\n get state() {\n if (this.clock.isRunning) return TransportState.playing;\n else if (this.time > 0) return TransportState.paused;\n else return TransportState.stopped;\n }\n\n get swingAmount() {\n return this._swingAmount;\n }\n\n set swingAmount(amount: NormalRange) {\n this._swingAmount = amount;\n }\n\n private jumpTo(ticks: Ticks) {\n const clockTime = this.tempo.getClockTime(ticks);\n this.tempo.reset(clockTime, ticks);\n this.clock.jumpTo(clockTime);\n this.scheduler.jumpTo(clockTime);\n this.clockTime = clockTime;\n this.listener.onJump(ticks);\n this._clockCallbacks.forEach((callback) => {\n callback(this.clockTime);\n });\n }\n\n private generateEvents = (\n start: ClockTime,\n end: ClockTime,\n ): readonly Readonly<T>[] => {\n // Get transport-time events and return them as clock-time events\n const transportStart = this.tempo.getTicks(start);\n const transportEnd = this.tempo.getTicks(end);\n return this.listener\n .generator(transportStart, transportEnd)\n .map((event) => {\n // Apply swing\n return {\n ...event,\n ticks: swing(event.ticks, this.swingAmount),\n };\n })\n .map((event) => {\n const time = this.tempo.getClockTime(event.ticks);\n const contextTime = this.clock.clockTimeToContextTime(time);\n return {\n ...event,\n time,\n contextTime,\n };\n });\n };\n\n private consumeEvents = (events: readonly Readonly<T>[]) => {\n events.forEach((event) => {\n this.listener.consumer(event);\n });\n };\n}\n"],"mappings":"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","names":["Clock","context","audioClockOffset","now","actionAt","time","clockTime","type","isNumber","TPB","secondsPerTick","tempo","ticksPerSecond","insertionIndexBy","arr","n","comparatorFn","index","el","lerp","t","a","b","unlerp","swing","time","amount","t8","t8i","tn","Position","value","timeSignature","isNumber","TPB","positionString","parts","barsStr","beatsStr","sixteenthsStr","bars","beats","sixteenths","position","totalBeats","beatFraction","Timeline","event","idx","insertionIndexBy","a","b","start","end","time","i","Scheduler","Timeline","generator","consumer","scheduleAheadTime","start","end","e","time","events","Tempo","clockTime","tempo","ticks","tickDelta","ticksPerSecond","clockDelta","secondsPerTick","AudioContext","AudioBuffer","AudioBufferSourceNode","generateUniqueNumber","map","nextId","WebAudioWrapper","webAudioWrapper","SCHEDULED_TIMEOUT_FUNCTIONS","SCHEDULED_INTERVAL_FUNCTIONS","callIntervalFunction","id","type","functions","SCHEDULED_INTERVAL_FUNCTIONS","SCHEDULED_TIMEOUT_FUNCTIONS","func","scheduleFunction","delay","now","audioBufferSourceNode","AudioBufferSourceNode","webAudioWrapper","elapsedTime","isNode","acClearInterval","acSetInterval","func","delay","id","generateUniqueNumber","SCHEDULED_INTERVAL_FUNCTIONS","scheduleFunction","exportedClearInterval","isNode","acClearInterval","exportedSetInterval","isNode","acSetInterval","Timer","callback","intervalMs","exportedSetInterval","exportedClearInterval","TransportState","Transport","Tempo","context","listener","Clock","Scheduler","Timer","time","callback","currentBar","pos","clockTime","ticks","Position","position","event","actionAt","bpm","value","amount","start","end","transportStart","transportEnd","swing","contextTime","events"]}
|
|
1
|
+
{"version":3,"sources":["../src/Clock.ts","../src/Position.ts","../src/utils.ts","../src/Timeline.ts","../src/Scheduler.ts","../src/Tempo.ts","../src/audio-context-timers.ts","../src/Timer.ts","../src/sources/SourceManager.ts","../src/Transport.ts","../src/sources/BaseSource.ts","../src/sources/StepSequencerSource.ts"],"sourcesContent":["import { Context } from \"@blibliki/utils\";\nimport { ClockTime, ContextTime, Seconds } from \"./types\";\n\nexport class Clock {\n private context: Readonly<Context>;\n private clockTimeAt: { start: ClockTime; stop: ClockTime };\n private contextTimeAt: { start: ContextTime; stop: ContextTime };\n private audioClockOffset: number;\n\n private _isRunning = false;\n\n constructor(context: Readonly<Context>, audioClockOffset: Seconds) {\n this.context = context;\n this.audioClockOffset = audioClockOffset;\n\n this.clockTimeAt = { start: 0, stop: 0 };\n this.contextTimeAt = { start: 0, stop: 0 };\n }\n\n get isRunning() {\n return this._isRunning;\n }\n\n time() {\n if (!this.isRunning) return this.clockTimeAt.stop;\n\n const now = this.context.currentTime;\n return this.clockTimeAt.start + (now - this.contextTimeAt.start);\n }\n\n start(actionAt: ContextTime) {\n this.contextTimeAt.start = actionAt;\n this.clockTimeAt.start = this.clockTimeAt.stop;\n this._isRunning = true;\n\n return this.clockTimeAt.start;\n }\n\n stop(actionAt: ContextTime) {\n this._isRunning = false;\n this.clockTimeAt.stop =\n this.clockTimeAt.start + (actionAt - this.contextTimeAt.start);\n this.contextTimeAt.stop = actionAt;\n\n return this.clockTimeAt.stop;\n }\n\n jumpTo(time: ClockTime) {\n this.clockTimeAt.stop = time;\n }\n\n clockTimeToContextTime(clockTime: ClockTime): ContextTime {\n const type = this.isRunning ? \"start\" : \"stop\";\n\n return (\n this.audioClockOffset +\n this.contextTimeAt[type] +\n clockTime -\n this.clockTimeAt[type]\n );\n }\n\n contextTimeToClockTime(contextTime: ContextTime): ClockTime {\n const type = this.isRunning ? \"start\" : \"stop\";\n\n return (\n contextTime -\n this.audioClockOffset -\n this.contextTimeAt[type] +\n this.clockTimeAt[type]\n );\n }\n}\n","import { isNumber } from \"@blibliki/utils\";\nimport { Ticks, TimeSignature, TPosition, TStringPosition } from \"./types\";\nimport { TPB } from \"./utils\";\n\nexport const sixteenthPerBeat = (timeSignature: Readonly<TimeSignature>) => {\n const denominator = timeSignature[1];\n return 16 / denominator;\n};\n\n/**\n * Represents a musical position that can be expressed in multiple formats:\n * - Ticks (raw MIDI time units)\n * - Bars:Beats:Sixteenths (string format like \"1:2:8\")\n * - Object format with bars, beats, and sixteenths properties\n */\nexport class Position {\n private readonly _ticks: Ticks;\n private timeSignature: Readonly<TimeSignature>;\n\n /**\n * Creates a new Position instance\n * @param value - Position value in ticks, string format (\"bars:beats:sixteenths\"), or object format\n * @param timeSignature - Time signature as [numerator, denominator] (e.g., [4, 4] for 4/4)\n */\n constructor(\n value: Ticks | TPosition | TStringPosition,\n timeSignature: TimeSignature,\n ) {\n this.timeSignature = timeSignature;\n\n if (isNumber(value)) {\n this._ticks = value;\n } else if (typeof value === \"string\") {\n this._ticks = this.parseStringPosition(value);\n } else {\n this._ticks = this.convertObjectToTicks(value);\n }\n }\n\n get ticks() {\n return this._ticks;\n }\n\n get bars() {\n return Math.floor(this.totalBeats / this.timeSignature[0]) + 1;\n }\n\n get beats() {\n return (this.totalBeats % this.timeSignature[0]) + 1;\n }\n\n get sixteenths() {\n return Math.floor(this.beatFraction * 4) + 1;\n }\n\n get totalBeats() {\n return Math.floor(this._ticks / TPB);\n }\n\n get beatFraction() {\n return this._ticks / TPB - this.totalBeats;\n }\n\n toString(): string {\n return `${this.bars}:${this.beats}:${this.sixteenths}`;\n }\n\n toObject(): TPosition {\n return {\n bars: this.bars,\n beats: this.beats,\n sixteenths: this.sixteenths,\n };\n }\n\n private parseStringPosition(positionString: TStringPosition): Ticks {\n const parts = positionString.split(\":\");\n\n const [barsStr, beatsStr, sixteenthsStr] = parts;\n const bars = Number(barsStr);\n const beats = Number(beatsStr);\n const sixteenths = Number(sixteenthsStr);\n\n const position: TPosition = { bars, beats, sixteenths };\n\n return this.convertObjectToTicks(position);\n }\n\n private convertObjectToTicks(position: TPosition): Ticks {\n const totalBeats =\n (position.bars - 1) * this.timeSignature[0] + (position.beats - 1);\n const beatFraction = position.sixteenths / 16;\n return (totalBeats + beatFraction) * TPB;\n }\n}\n","import { BPM, NormalRange, Ticks } from \"./types\";\n\n// Ticks per beat\nexport const TPB = 15360;\n\nexport const secondsPerTick = (tempo: BPM) => {\n return 60 / tempo / TPB;\n};\n\nexport const ticksPerSecond = (tempo: BPM) => {\n return (tempo / 60) * TPB;\n};\n\nexport const insertionIndexBy = <T>(\n arr: T[],\n n: T,\n comparatorFn: (a: T, b: T) => number,\n) => {\n const index = arr.findIndex((el) => comparatorFn(n, el) < 0);\n return index === -1 ? arr.length : index;\n};\n\nexport function lerp(t: number, a: number, b: number): number {\n return a + t * (b - a);\n}\n\nexport function unlerp(t: number, a: number, b: number): number {\n return (t - a) / (b - a);\n}\n\n/**\n * Compute swing.\n *\n * This function is an attempt at mirroring the behaviour in the Reason DAW,\n * where events before the 50% eighth mark are \"expanded\" and events past the\n * mark are \"compressed\". That is, this function transforms the underlying\n * transport time grid:\n *\n * No swing |<--50%-->|<--50%-->|<--50%-->| ...\n * 16ths 0 1 2 3 ...\n * 8ths 0 | 1 | ...\n * Notes X======X X===X | |\n * | | | |\n * | \\ | \\\n * 60% swing |<---60%--->|<-40%->|<---60%--->| ...\n * 16ths 0 1 2 3 ...\n * 8ths 0 | 1 | ...\n * Notes X=======X X==X | |\n *\n * If it is challenging to explain this mechanism to end users, you could\n * use the approach taken in the Logic DAW, which has 6 \"swing modes\":\n *\n * 16A: 50%\n * 16B: 54%\n * 16C: 58%\n * 16D: 62%\n * 16E: 66%\n * 16F: 71%\n *\n * For details, see\n * https://www.attackmagazine.com/technique/passing-notes/daw-drum-machine-swing/2/\n *\n * @param time Time of event, in transport ticks (1/4 = 15360 pulses)\n * @param amount Swing amount in range [0.5, 0.75].\n * @returns New time of the event, with swing applied.\n */\nexport function swing(time: Ticks, amount: NormalRange): Ticks {\n if (amount < 0.5 || amount > 0.75) {\n throw new Error(\"Invalid swing amount\");\n }\n\n /*\n Note lengths in ticks:\n\n 1/4 = 15360\n 1/8 = 7680\n 1/16 = 3840\n */\n\n const t8 = time / 7680; // Input time in 8ths\n const t8i = Math.floor(t8); // 8th in which the time sits\n const t = t8 - t8i; // Percentage position in the 8th in which the time sits\n\n /* \n ### We could make this a bit more efficient; we can simplify the \n <=0.5 case to\n\n (t / 0.5) * amount\n\n But the >0.5 case is more difficult:\n\n amount + ((t - 0.5) / 0.5) * (1 - amount)\n\n Using the lerp/unlerp functions here makes it clear what's going on,\n though, so we'll keep them for now.\n */\n let tn = 0;\n if (t <= 0.5) {\n tn = lerp(unlerp(t, 0.0, 0.5), 0.0, amount);\n } else {\n tn = lerp(unlerp(t, 0.5, 1.0), amount, 1.0);\n }\n\n // Map transformed time back into ticks\n return Math.floor((t8i + tn) * 7680);\n}\n\n// We will update the type to be more flexible when there is a need for it\nexport type Division =\n | \"1/64\"\n | \"1/48\"\n | \"1/32\"\n | \"1/24\"\n | \"1/16\"\n | \"1/12\"\n | \"1/8\"\n | \"1/6\"\n | \"3/16\"\n | \"1/4\"\n | \"5/16\"\n | \"1/3\"\n | \"3/8\"\n | \"1/2\"\n | \"3/4\"\n | \"1\"\n | \"1.5\"\n | \"2\"\n | \"3\"\n | \"4\"\n | \"6\"\n | \"8\"\n | \"16\"\n | \"32\"\n | \"infinity\";\n\nexport function divisionToTicks(division: Division): Ticks {\n // Defensive: handle invalid input (runtime safety for old data)\n if (typeof division !== \"string\") {\n console.error(`Invalid duration value, using default \"8n.\"`);\n division = \"1/8\";\n }\n\n let ticks: Ticks;\n\n if (division === \"infinity\") return Infinity;\n\n if (division.match(/\\d\\/\\d/)) {\n const [num, den] = division.split(\"/\").map(Number);\n if (!num || !den)\n throw Error(`Note duration parsing error for value: ${division}`);\n\n ticks = (TPB * num) / (den / 4);\n } else {\n ticks = TPB * Number(division) * 4;\n }\n\n return Math.round(ticks);\n}\n\nexport function divisionToFrequency(division: Division, bpm: BPM): number {\n const ticks = divisionToTicks(division);\n const beatsPerDivision = ticks / TPB;\n const secondsPerDivision = beatsPerDivision * (60 / bpm);\n return 1 / secondsPerDivision;\n}\n\nexport function divisionToMilliseconds(division: Division, bpm: BPM): number {\n const ticksPerDivision = divisionToTicks(division);\n const beatsPerDivision = ticksPerDivision / TPB;\n const secondsPerDivision = beatsPerDivision * (60 / bpm);\n return secondsPerDivision * 1000;\n}\n","import { ClockTime } from \"./types\";\nimport { insertionIndexBy } from \"./utils\";\n\nexport type TimelineEvent = {\n time: ClockTime;\n};\n\nexport class Timeline<T extends TimelineEvent = TimelineEvent> {\n private _events: Readonly<T>[] = [];\n\n get events(): readonly Readonly<T>[] {\n return this._events;\n }\n\n /**\n * Add a new event to the timeline. It will be inserted according to its\n * `time` property (which must be present; otherwise an exception is thrown).\n *\n * If the time of the new event is exactly equal to the time of an existing\n * event in the timeline, the existing event is considered to occur before\n * the new event.\n */\n add(event: Readonly<T>) {\n const idx = insertionIndexBy(this._events, event, (a, b) => {\n return a.time - b.time;\n });\n this._events.splice(idx, 0, event);\n }\n\n find(start: ClockTime, end: ClockTime): readonly Readonly<T>[] {\n return this._events.filter((event) => {\n return event.time >= start && event.time < end;\n });\n }\n\n /** Return the last event that occurred at or before the specified time. */\n lastEventBefore(time: ClockTime) {\n for (let i = this._events.length - 1; i >= 0; i--) {\n const event = this._events[i];\n if (event && event.time <= time) {\n return event;\n }\n }\n return undefined;\n }\n\n remove(start: ClockTime, end: ClockTime) {\n this._events = this._events.filter((event) => {\n return !(event.time >= start && event.time < end);\n });\n }\n\n removeAllBefore(time: ClockTime) {\n this._events = this._events.filter((event) => event.time >= time);\n }\n\n clear() {\n this._events = [];\n }\n}\n","import { Timeline, TimelineEvent } from \"./Timeline\";\nimport { ClockTime, Seconds } from \"./types\";\n\n/**\n * Return all events that occur in the interval [start, end).\n */\nexport type EventGenerator<T extends TimelineEvent> = (\n start: ClockTime,\n end: ClockTime,\n) => readonly Readonly<T>[];\n\n/** Consume the speified event. */\nexport type EventConsumer<T extends TimelineEvent> = (\n events: readonly Readonly<T>[],\n) => void;\n\n/**\n * The scheduler is responsible for tracking the scheduling/playback window.\n *\n * Scheduling of events always precedes playback of events. The left hand\n * edge of the scheduling window is the point in time up to where events have\n * been consumed so far. The right hand edge of the window is the point in time\n * up to where events have been scheduled/prepared. The width of the window is\n * always the same (specified when the Scheduler instance is created).\n */\nexport class Scheduler<T extends TimelineEvent = TimelineEvent> {\n private timeline = new Timeline<T>();\n\n private scheduleAheadTime: Seconds;\n private consumedTime: ClockTime;\n\n private generator: EventGenerator<T>;\n private consumer: EventConsumer<T>;\n\n constructor(\n generator: EventGenerator<T>,\n consumer: EventConsumer<T>,\n scheduleAheadTime: Seconds,\n ) {\n this.generator = generator;\n this.consumer = consumer;\n this.scheduleAheadTime = scheduleAheadTime;\n this.consumedTime = -this.scheduleAheadTime;\n }\n\n private _schedule(start: Seconds, end: Seconds) {\n this.generator(start, end).forEach((e) => {\n this.timeline.add(e);\n });\n }\n\n /**\n * Consume events up to the specified time.\n *\n * This will also generate new events up until `time + scheduleAheadTime`.\n */\n runUntil(time: ClockTime) {\n if (time <= this.consumedTime) {\n throw new Error(\"Scheduling time is <= current time\");\n }\n\n /* \n Generate events that occur between the previous and new right-hand edges \n of the scheduling window. I.e., move scheduling time forwards by the \n specified amount.\n */\n this._schedule(\n this.consumedTime + this.scheduleAheadTime,\n time + this.scheduleAheadTime,\n );\n\n /*\n Consume events that occur between the previous and new left-hand edges\n of the scheduling window. I.e., move playback time forwards by the\n specified amount.\n */\n const events = this.timeline.find(this.consumedTime, time);\n this.consumer(events);\n this.consumedTime = time;\n\n /*\n Remove all events that were consumed from the timeline.\n */\n this.timeline.removeAllBefore(this.consumedTime);\n }\n\n /**\n * Jump to the specified destination time.\n */\n jumpTo(time: ClockTime) {\n this.timeline.clear();\n this.consumedTime = time - this.scheduleAheadTime;\n }\n}\n","import { BPM, ClockTime, Ticks } from \"./types\";\nimport { secondsPerTick, ticksPerSecond } from \"./utils\";\n\nexport class Tempo {\n private clockTimeAtLastTempoChange = 0;\n private ticksAtLastTempoChange = 0;\n private _bpm = 120;\n\n get bpm() {\n return this._bpm;\n }\n\n update(clockTime: ClockTime, tempo: BPM) {\n const ticks = this.getTicks(clockTime);\n this.clockTimeAtLastTempoChange = clockTime;\n this.ticksAtLastTempoChange = ticks;\n this._bpm = tempo;\n }\n\n getTicks(clockTime: ClockTime): Ticks {\n const clockDelta = clockTime - this.clockTimeAtLastTempoChange;\n const tickDelta = clockDelta * ticksPerSecond(this.bpm);\n return Math.floor(this.ticksAtLastTempoChange + tickDelta);\n }\n\n getClockTime(ticks: Ticks): ClockTime {\n const tickDelta = Math.floor(ticks - this.ticksAtLastTempoChange);\n const clockDelta = tickDelta * secondsPerTick(this.bpm);\n return this.clockTimeAtLastTempoChange + clockDelta;\n }\n\n reset(clockTime: ClockTime, ticks: Ticks) {\n this.clockTimeAtLastTempoChange = clockTime;\n this.ticksAtLastTempoChange = ticks;\n }\n}\n","import {\n AudioContext,\n AudioBuffer,\n AudioBufferSourceNode,\n} from \"@blibliki/utils/web-audio-api\";\n\nfunction generateUniqueNumber(map: Map<number, () => void>): number {\n let nextId = 1;\n\n // Keep incrementing until we find an unused ID\n while (map.has(nextId)) {\n nextId++;\n }\n\n return nextId;\n}\n\nclass WebAudioWrapper {\n private _audioBuffer: AudioBuffer | null = null;\n private _audioContext: AudioContext | null = null;\n private _sampleDuration: number | null = null;\n\n get audioContext(): AudioContext {\n this._audioContext ??= new AudioContext();\n return this._audioContext;\n }\n\n get audioBuffer(): AudioBuffer {\n this._audioBuffer ??= new AudioBuffer({\n length: 2,\n sampleRate: this.audioContext.sampleRate,\n });\n return this._audioBuffer;\n }\n\n get sampleDuration(): number {\n this._sampleDuration ??= 2 / this.audioContext.sampleRate;\n return this._sampleDuration;\n }\n}\n\nconst webAudioWrapper = new WebAudioWrapper();\n\nconst SCHEDULED_TIMEOUT_FUNCTIONS = new Map<number, () => void>();\nconst SCHEDULED_INTERVAL_FUNCTIONS = new Map<number, () => void>();\n\nenum TimerType {\n interval = \"interval\",\n timeout = \"timeout\",\n}\n\nconst callIntervalFunction = (id: number, type: TimerType) => {\n const functions =\n type === TimerType.interval\n ? SCHEDULED_INTERVAL_FUNCTIONS\n : SCHEDULED_TIMEOUT_FUNCTIONS;\n\n if (functions.has(id)) {\n const func = functions.get(id);\n\n if (func !== undefined) {\n func();\n\n if (type === TimerType.timeout) {\n SCHEDULED_TIMEOUT_FUNCTIONS.delete(id);\n }\n }\n }\n};\n\nconst scheduleFunction = (id: number, delay: number, type: TimerType) => {\n const now = performance.now();\n\n const audioBufferSourceNode = new AudioBufferSourceNode(\n webAudioWrapper.audioContext,\n { buffer: webAudioWrapper.audioBuffer },\n );\n\n audioBufferSourceNode.onended = () => {\n const elapsedTime = performance.now() - now;\n\n if (elapsedTime >= delay) {\n callIntervalFunction(id, type);\n } else {\n scheduleFunction(id, delay - elapsedTime, type);\n }\n\n audioBufferSourceNode.disconnect(webAudioWrapper.audioContext.destination);\n };\n audioBufferSourceNode.connect(webAudioWrapper.audioContext.destination);\n audioBufferSourceNode.start(\n Math.max(\n 0,\n webAudioWrapper.audioContext.currentTime +\n delay / 1000 -\n webAudioWrapper.sampleDuration,\n ),\n );\n};\n\nconst isNode = typeof window === \"undefined\";\n\n// exported methods\n\nconst acClearInterval = (id: number) => {\n SCHEDULED_INTERVAL_FUNCTIONS.delete(id);\n};\n\nconst acClearTimeout = (id: number) => {\n SCHEDULED_TIMEOUT_FUNCTIONS.delete(id);\n};\n\nconst acSetInterval = (func: () => void, delay: number) => {\n const id = generateUniqueNumber(SCHEDULED_INTERVAL_FUNCTIONS);\n\n SCHEDULED_INTERVAL_FUNCTIONS.set(id, () => {\n func();\n\n scheduleFunction(id, delay, TimerType.interval);\n });\n\n scheduleFunction(id, delay, TimerType.interval);\n\n return id;\n};\n\nconst acSetTimeout = (func: () => void, delay: number) => {\n const id = generateUniqueNumber(SCHEDULED_TIMEOUT_FUNCTIONS);\n\n SCHEDULED_TIMEOUT_FUNCTIONS.set(id, func);\n\n scheduleFunction(id, delay, TimerType.timeout);\n\n return id;\n};\n\nconst exportedClearInterval = isNode ? clearInterval : acClearInterval;\nconst exportedClearTimeout = isNode ? clearTimeout : acClearTimeout;\nconst exportedSetInterval = isNode ? setInterval : acSetInterval;\nconst exportedSetTimeout = isNode ? setTimeout : acSetTimeout;\n\nexport {\n exportedClearInterval as clearInterval,\n exportedClearTimeout as clearTimeout,\n exportedSetInterval as setInterval,\n exportedSetTimeout as setTimeout,\n};\n","import { Context } from \"@blibliki/utils\";\nimport { clearInterval, setInterval } from \"./audio-context-timers\";\nimport { ContextTime } from \"./types\";\n\nexport const scheduleAtContextTime = ({\n scheduleAt,\n context,\n callback,\n}: {\n scheduleAt: ContextTime;\n context: Context;\n callback: () => void;\n}) => {\n const now = context.currentTime;\n if (now > scheduleAt) throw Error(\"Could not schedule event at past\");\n\n const ms = (scheduleAt - now) * 1000;\n\n setTimeout(callback, ms);\n};\n\nexport class Timer {\n private timerId: number | undefined = undefined;\n\n private interval: number;\n private callback: () => void;\n\n constructor(callback: () => void, intervalMs: number) {\n this.callback = callback;\n this.interval = intervalMs;\n }\n\n start() {\n this.timerId = setInterval(() => {\n this.callback();\n }, this.interval);\n }\n\n stop() {\n if (this.timerId === undefined) return;\n\n clearInterval(this.timerId);\n this.timerId = undefined;\n }\n\n get isRunning() {\n return this.timerId !== undefined;\n }\n}\n","import { Ticks } from \"@/types\";\nimport type { BaseSource, SourceEvent } from \"./BaseSource\";\n\nexport class SourceManager {\n private activeSources = new Map<string, BaseSource<SourceEvent>>();\n\n addSource<T extends SourceEvent>(source: BaseSource<T>) {\n this.activeSources.set(source.id, source);\n }\n\n removeSource(id: string) {\n this.activeSources.delete(id);\n }\n\n generator(start: Ticks, end: Ticks): readonly SourceEvent[] {\n const events: SourceEvent[] = [];\n\n this.activeSources.forEach((source) => {\n events.push(...(source.generator(start, end) as SourceEvent[]));\n });\n\n return events;\n }\n\n consumer(events: readonly SourceEvent[]) {\n events.forEach((event) => {\n this.activeSources.get(event.eventSourceId)?.consumer(event);\n });\n }\n\n onStart(ticks: Ticks) {\n this.activeSources.forEach((source) => {\n source.onStart(ticks);\n });\n }\n\n onStop(ticks: Ticks) {\n this.activeSources.forEach((source) => {\n source.onStop(ticks);\n });\n }\n\n onJump(ticks: Ticks) {\n this.activeSources.forEach((source) => {\n source.onJump(ticks);\n });\n }\n\n onSilence(ticks: Ticks) {\n this.activeSources.forEach((source) => {\n source.onSilence(ticks);\n });\n }\n}\n","import { Context } from \"@blibliki/utils\";\nimport { Clock } from \"./Clock\";\nimport { Position } from \"./Position\";\nimport { Scheduler } from \"./Scheduler\";\nimport { Tempo } from \"./Tempo\";\nimport { TimelineEvent } from \"./Timeline\";\nimport { Timer } from \"./Timer\";\nimport { SourceEvent, BaseSource } from \"./sources/BaseSource\";\nimport { SourceManager } from \"./sources/SourceManager\";\nimport {\n BPM,\n ClockTime,\n ContextTime,\n NormalRange,\n Ticks,\n TimeSignature,\n} from \"./types\";\nimport { swing } from \"./utils\";\n\nexport interface TransportEvent extends TimelineEvent {\n ticks: Ticks;\n contextTime: ContextTime;\n}\n\n/**\n * Transport callback that gets invoked at (roughly) sixteenth note intervals.\n *\n * Returns the current clock time in seconds.\n *\n * IMPORTANT! Do not rely on this callback for audio precision mechanisms!\n * It is only accurate up to the precision of the event sheduler rate, and may\n * \"jump\" if the scheduling is struggling to keep up.\n */\nexport type TransportClockCallback = (\n time: ClockTime,\n contextTime: ContextTime,\n ticks: Ticks,\n) => void;\n\n/**\n * Transport properties that can be observed for changes.\n */\nexport type TransportProperty = \"bpm\" | \"timeSignature\" | \"swingAmount\";\n\n/**\n * Transport callback that gets invoked when a property changes.\n */\nexport type TransportPropertyChangeCallback<T = unknown> = (\n value: T,\n contextTime: ContextTime,\n) => void;\n\nexport enum TransportState {\n playing = \"playing\",\n stopped = \"stopped\",\n paused = \"paused\",\n}\n\nexport type TransportParams = {\n onStart: (ticks: ContextTime) => void;\n onStop: (ticks: ContextTime) => void;\n};\n\n/**\n * This class converts (music) transport time into audio clock time.\n */\nexport class Transport {\n private _initialized = false;\n\n private context: Readonly<Context>;\n private clock: Readonly<Clock>;\n private timer: Readonly<Timer>;\n private scheduler: Readonly<Scheduler<SourceEvent>>;\n\n private _timeSignature: TimeSignature = [4, 4];\n\n private tempo: Readonly<Tempo> = new Tempo();\n private clockTime = 0;\n private _swingAmount: NormalRange = 0.5;\n\n private sourceManager: SourceManager;\n\n private _clockCallbacks: TransportClockCallback[] = [];\n private _propertyChangeCallbacks = new Map<\n TransportProperty,\n TransportPropertyChangeCallback[]\n >();\n\n private onStartCallback: TransportParams[\"onStart\"];\n private onStopCallback: TransportParams[\"onStop\"];\n\n constructor(context: Readonly<Context>, params: TransportParams) {\n const SCHEDULE_INTERVAL_MS = 20;\n const SCHEDULE_WINDOW_SIZE_MS = 100;\n\n this.context = context;\n this.sourceManager = new SourceManager();\n this.clock = new Clock(this.context, SCHEDULE_WINDOW_SIZE_MS / 1000);\n this.onStartCallback = params.onStart;\n this.onStopCallback = params.onStop;\n\n this.scheduler = new Scheduler<SourceEvent>(\n this.generateEvents,\n this.consumeEvents,\n SCHEDULE_WINDOW_SIZE_MS / 1000,\n );\n\n this.timer = new Timer(() => {\n const time = this.clock.time();\n const contextTime = this.clock.clockTimeToContextTime(time);\n const ticks = this.tempo.getTicks(time);\n if (time <= this.clockTime) return;\n\n this.clockTime = time;\n this.scheduler.runUntil(time);\n this._clockCallbacks.forEach((callback) => {\n callback(this.clockTime, contextTime, ticks);\n });\n }, SCHEDULE_INTERVAL_MS / 1000);\n\n this._initialized = true;\n }\n\n addClockCallback(callback: TransportClockCallback) {\n this._clockCallbacks.push(callback);\n }\n\n addPropertyChangeCallback(\n property: TransportProperty,\n callback: TransportPropertyChangeCallback,\n ) {\n if (!this._propertyChangeCallbacks.has(property)) {\n this._propertyChangeCallbacks.set(property, []);\n }\n this._propertyChangeCallbacks.get(property)!.push(callback);\n }\n\n private triggerPropertyChange(property: TransportProperty, value: unknown) {\n const callbacks = this._propertyChangeCallbacks.get(property);\n if (callbacks) {\n const contextTime = this.context.currentTime;\n callbacks.forEach((callback) => {\n callback(value, contextTime);\n });\n }\n }\n\n get time() {\n return this.clock.time();\n }\n\n getContextTimeAtTicks(ticks: Ticks): ContextTime {\n const clockTime = this.tempo.getClockTime(ticks);\n return this.clock.clockTimeToContextTime(clockTime);\n }\n\n getTicksAtContextTime(contextTime: ContextTime): Ticks {\n const clockTime = this.clock.contextTimeToClockTime(contextTime);\n return this.tempo.getTicks(clockTime);\n }\n\n /**\n * Return the (approximate) current Transport time, in ticks.\n */\n get position(): Readonly<Position> {\n const clockTime = this.clock.time();\n const ticks = this.tempo.getTicks(clockTime);\n return new Position(ticks, this.timeSignature);\n }\n\n /**\n * Set the current Transport time.\n */\n set position(position: Readonly<Position>) {\n this.jumpTo(position.ticks);\n }\n\n getPositionOfEvent(event: Readonly<TransportEvent>): Readonly<Position> {\n return new Position(event.ticks, this.timeSignature);\n }\n\n /**\n * Start the Transport.\n */\n start(actionAt: ContextTime) {\n if (!this._initialized) throw new Error(\"Not initialized\");\n if (this.clock.isRunning) return;\n\n const clockTime = this.clock.start(actionAt);\n this.timer.start();\n\n const ticks = this.tempo.getTicks(clockTime);\n this.sourceManager.onStart(ticks);\n this.onStartCallback(ticks);\n }\n\n /**\n * Stop the Transport.\n */\n stop(actionAt: ContextTime) {\n if (!this._initialized) throw new Error(\"Not initialized\");\n\n const clockTime = this.clock.stop(actionAt);\n this.timer.stop();\n\n const ticks = this.tempo.getTicks(clockTime);\n this.sourceManager.onSilence(ticks);\n this.sourceManager.onStop(ticks);\n this.onStopCallback(ticks);\n }\n\n /**\n * Reset the Transport to zero.\n */\n reset(actionAt: ContextTime) {\n if (!this._initialized) throw new Error(\"Not initialized\");\n\n this.sourceManager.onSilence(actionAt);\n this.jumpTo(0);\n }\n\n /**\n * Add a source to the transport\n */\n addSource<T extends SourceEvent>(source: BaseSource<T>) {\n this.sourceManager.addSource(source);\n }\n\n /**\n * Remove a source from the transport\n */\n removeSource(id: string) {\n this.sourceManager.removeSource(id);\n }\n\n get bpm(): BPM {\n return this.tempo.bpm;\n }\n\n set bpm(bpm: BPM) {\n const oldBpm = this.tempo.bpm;\n this.tempo.update(this.clockTime, bpm);\n\n // Trigger property change callbacks if BPM actually changed\n if (oldBpm !== bpm) {\n this.triggerPropertyChange(\"bpm\", bpm);\n }\n }\n\n get timeSignature() {\n return this._timeSignature;\n }\n\n set timeSignature(value: TimeSignature) {\n const oldValue = this._timeSignature;\n this._timeSignature = value;\n\n // Trigger property change callbacks if time signature actually changed\n if (oldValue[0] !== value[0] || oldValue[1] !== value[1]) {\n this.triggerPropertyChange(\"timeSignature\", value);\n }\n }\n\n get state() {\n if (this.clock.isRunning) return TransportState.playing;\n else if (this.time > 0) return TransportState.paused;\n else return TransportState.stopped;\n }\n\n get swingAmount() {\n return this._swingAmount;\n }\n\n set swingAmount(amount: NormalRange) {\n const oldValue = this._swingAmount;\n this._swingAmount = amount;\n\n // Trigger property change callbacks if swing amount actually changed\n if (oldValue !== amount) {\n this.triggerPropertyChange(\"swingAmount\", amount);\n }\n }\n\n private jumpTo(ticks: Ticks) {\n const clockTime = this.tempo.getClockTime(ticks);\n this.tempo.reset(clockTime, ticks);\n this.clock.jumpTo(clockTime);\n this.scheduler.jumpTo(clockTime);\n this.clockTime = clockTime;\n this.sourceManager.onJump(ticks);\n\n const contextTime = this.clock.clockTimeToContextTime(this.clockTime);\n this._clockCallbacks.forEach((callback) => {\n callback(this.clockTime, contextTime, ticks);\n });\n }\n\n private generateEvents = (\n start: ClockTime,\n end: ClockTime,\n ): readonly SourceEvent[] => {\n // Get transport-time events and return them as clock-time events\n const transportStart = this.tempo.getTicks(start);\n const transportEnd = this.tempo.getTicks(end);\n return this.sourceManager\n .generator(transportStart, transportEnd)\n .map((event) => {\n // Apply swing\n return {\n ...event,\n ticks: swing(event.ticks, this.swingAmount),\n };\n })\n .map((event) => {\n const time = this.tempo.getClockTime(event.ticks);\n const contextTime = this.clock.clockTimeToContextTime(time);\n return {\n ...event,\n time,\n contextTime,\n };\n });\n };\n\n private consumeEvents = (events: readonly SourceEvent[]) => {\n this.sourceManager.consumer(events);\n };\n}\n","import { uuidv4 } from \"@blibliki/utils\";\nimport { Transport, TransportEvent } from \"@/Transport\";\nimport { Ticks } from \"@/types\";\n\nexport interface SourceEvent extends TransportEvent {\n eventSourceId: string;\n}\n\nexport interface IBaseSource<T extends SourceEvent> {\n id: string;\n\n generator: (start: Ticks, end: Ticks) => readonly Readonly<T>[];\n consumer: (event: Readonly<T>) => void;\n\n // Optional lifecycle hooks\n onStart: (ticks: Ticks) => void;\n onStop: (ticks: Ticks) => void;\n onJump: (ticks: Ticks) => void;\n onSilence: (ticks: Ticks) => void;\n}\n\nexport abstract class BaseSource<\n T extends SourceEvent,\n> implements IBaseSource<T> {\n readonly id: string;\n\n protected transport: Transport;\n protected startedAt?: Ticks;\n protected stoppedAt?: Ticks;\n protected lastGeneratedTick?: Ticks;\n\n constructor(transport: Transport) {\n this.id = uuidv4();\n this.transport = transport;\n }\n\n abstract generator(start: Ticks, end: Ticks): readonly Readonly<T>[];\n abstract consumer(event: Readonly<T>): void;\n\n onStart(ticks: Ticks) {\n this.startedAt = ticks;\n this.stoppedAt = undefined;\n this.lastGeneratedTick = undefined;\n }\n\n onStop(ticks: Ticks) {\n this.stoppedAt = ticks;\n }\n\n onJump(ticks: Ticks) {\n this.lastGeneratedTick = ticks;\n }\n\n onSilence(_ticks: Ticks) {\n // Not implemented yet\n }\n\n protected isPlaying(start: Ticks, end: Ticks) {\n const isStarted = this.startedAt !== undefined && this.startedAt <= start;\n if (!isStarted) return false;\n\n return this.stoppedAt === undefined || this.stoppedAt >= end;\n }\n\n protected shouldGenerate(eventTick: Ticks): boolean {\n if (this.lastGeneratedTick === undefined) return true;\n\n return eventTick > this.lastGeneratedTick;\n }\n}\n","import { Transport } from \"@/Transport\";\nimport { Ticks } from \"@/types\";\nimport { Division, TPB } from \"@/utils\";\nimport { BaseSource } from \"./BaseSource\";\nimport type { SourceEvent } from \"./BaseSource\";\n\nexport interface StepSequencerSourceEvent extends SourceEvent {\n stepNo: number;\n pageNo: number;\n patternNo: number;\n step: IStep;\n}\n\nexport type IStepNote = {\n note: string; // \"C4\", \"E4\", \"G4\"\n velocity: number; // 0-127\n};\n\nexport type IStepCC = {\n cc: number;\n value: number;\n};\n\n// Individual step\nexport type IStep = {\n active: boolean; // Whether step is enabled/muted\n notes: IStepNote[]; // Multiple notes for chords\n ccMessages: IStepCC[]; // Multiple CC messages per step\n probability: number; // 0-100% chance to trigger\n microtimeOffset: number; // -50 to +50 ticks offset\n duration: Division;\n};\n\n// Page contains multiple steps\nexport type IPage = {\n name: string;\n steps: IStep[];\n};\n\n// Pattern contains multiple pages\nexport type IPattern = {\n name: string;\n pages: IPage[];\n};\n\nexport enum Resolution {\n thirtysecond = \"1/32\",\n sixteenth = \"1/16\",\n eighth = \"1/8\",\n quarter = \"1/4\",\n}\n\nexport enum PlaybackMode {\n loop = \"loop\",\n oneShot = \"oneShot\",\n}\n\ninterface StepSequencerSourceProps {\n onEvent: (event: StepSequencerSourceEvent) => void;\n patterns: IPattern[];\n stepsPerPage: number; // 1-16 steps per page\n resolution: Resolution;\n playbackMode: PlaybackMode;\n patternSequence: string; // Pattern sequence notation (e.g., \"2A4B2AC\")\n enableSequence: boolean; // Toggle to enable/disable sequence mode\n}\n\nconst RESOLUTION_TO_TICKS: Record<Resolution, number> = {\n [Resolution.thirtysecond]: TPB / 8, // 1920 ticks\n [Resolution.sixteenth]: TPB / 4, // 3840 ticks\n [Resolution.eighth]: TPB / 2, // 7680 ticks\n [Resolution.quarter]: TPB, // 15360 ticks\n};\n\nfunction expandPatternSequence(input: string): string[] {\n const result: string[] = [];\n let num = \"\";\n\n for (const ch of input) {\n if (ch >= \"0\" && ch <= \"9\") {\n num += ch;\n } else {\n const count = Number(num);\n for (let i = 0; i < count; i++) {\n result.push(ch);\n }\n num = \"\";\n }\n }\n\n return result.map((v) => v.toUpperCase());\n}\n\nexport class StepSequencerSource extends BaseSource<StepSequencerSourceEvent> {\n props: StepSequencerSourceProps;\n private expandedSequence: string[] = [];\n private pageMapping: { patternNo: number; pageNo: number }[] = [];\n\n constructor(transport: Transport, props: StepSequencerSourceProps) {\n super(transport);\n\n this.props = props;\n this.expandedSequence = expandPatternSequence(props.patternSequence);\n this.pageMapping = this.buildPageMapping();\n }\n\n /**\n * Build a mapping of absolute pages to (patternNo, pageNo) for one full sequence cycle\n * Example: sequence \"2A1B\" with A having 2 pages, B having 1 page produces:\n * [A0, A1, A0, A1, B0]\n */\n private buildPageMapping(): { patternNo: number; pageNo: number }[] {\n const mapping: { patternNo: number; pageNo: number }[] = [];\n\n if (this.props.enableSequence && this.expandedSequence.length > 0) {\n // For each pattern letter in the expanded sequence\n for (const patternLetter of this.expandedSequence) {\n // Find the pattern by name\n const patternNo = this.props.patterns.findIndex(\n (p) => p.name.toUpperCase() === patternLetter,\n );\n\n if (patternNo === -1) continue;\n\n const pattern = this.props.patterns[patternNo];\n if (!pattern) continue;\n\n // Add all pages of this pattern to the mapping\n for (let pageNo = 0; pageNo < pattern.pages.length; pageNo++) {\n mapping.push({ patternNo, pageNo });\n }\n }\n } else {\n // No sequence mode - build mapping for all patterns sequentially\n for (\n let patternNo = 0;\n patternNo < this.props.patterns.length;\n patternNo++\n ) {\n const pattern = this.props.patterns[patternNo];\n if (!pattern) continue;\n\n for (let pageNo = 0; pageNo < pattern.pages.length; pageNo++) {\n mapping.push({ patternNo, pageNo });\n }\n }\n }\n\n return mapping;\n }\n\n get stepResolution(): Ticks {\n return RESOLUTION_TO_TICKS[this.props.resolution];\n }\n\n onStart(ticks: Ticks) {\n // Quantize to the start of the next bar\n const timeSignature = this.transport.timeSignature;\n const ticksPerBar = TPB * timeSignature[0];\n\n if (ticks % ticksPerBar === 0) {\n super.onStart(ticks);\n return;\n }\n\n // Calculate which bar we're in and round up to the next bar\n const currentBar = Math.floor(ticks / ticksPerBar);\n const nextBarTicks = (currentBar + 1) * ticksPerBar;\n\n super.onStart(nextBarTicks);\n }\n\n extractStepsTicks(start: Ticks, end: Ticks): Ticks[] {\n const result: number[] = [];\n const stepResolution = this.stepResolution;\n\n // Calculate which step we should be at, then convert back to absolute ticks\n const stepsSinceStart = Math.floor(\n (start - this.startedAt!) / stepResolution,\n );\n const actualStart = this.startedAt! + stepsSinceStart * stepResolution;\n\n for (let value = actualStart; value <= end; value += stepResolution) {\n if (!this.shouldGenerate(value)) continue;\n\n result.push(value);\n this.lastGeneratedTick = value;\n }\n\n return result;\n }\n\n private getStep(patternNo: number, pageNo: number, stepNo: number): IStep {\n const pattern = this.props.patterns[patternNo];\n if (!pattern) throw Error(\"Pattern not found\");\n\n const page = pattern.pages[pageNo];\n if (!page) throw Error(\"Page not found\");\n\n const step = page.steps[stepNo];\n if (!step) throw Error(\"Step not found\");\n\n return step;\n }\n\n /**\n * Calculate ticks per page based on step resolution and steps per page\n */\n get ticksPerPage(): Ticks {\n return this.stepResolution * this.props.stepsPerPage;\n }\n\n /**\n * Calculate absolute page number from tick position\n */\n private getAbsolutePageFromTicks(ticks: Ticks): number {\n if (this.startedAt === undefined) return 0;\n\n const ticksSinceStart = ticks - this.startedAt;\n return Math.floor(ticksSinceStart / this.ticksPerPage);\n }\n\n /**\n * Calculate step number within the current page from tick position\n */\n private getStepNoInPage(ticks: Ticks): number {\n if (this.startedAt === undefined) return 0;\n\n const ticksSinceStart = ticks - this.startedAt;\n const ticksIntoPage = ticksSinceStart % this.ticksPerPage;\n return Math.floor(ticksIntoPage / this.stepResolution);\n }\n\n /**\n * Map absolute page number to actual pattern and page indices\n * Takes into account sequence mode and playback mode (loop vs oneShot)\n */\n private getPatternAndPageFromAbsolutePage(absolutePage: number): {\n patternNo: number;\n pageNo: number;\n } {\n if (this.pageMapping.length === 0) {\n return { patternNo: 0, pageNo: 0 };\n }\n\n let index: number;\n if (this.props.playbackMode === PlaybackMode.loop) {\n index = absolutePage % this.pageMapping.length;\n } else {\n // oneShot mode - stop at the last page\n index = Math.min(absolutePage, this.pageMapping.length - 1);\n }\n\n return this.pageMapping[index] ?? { patternNo: 0, pageNo: 0 };\n }\n\n generator(\n start: Ticks,\n end: Ticks,\n ): readonly Readonly<StepSequencerSourceEvent>[] {\n if (!this.isPlaying(start, end) || this.startedAt === undefined) return [];\n\n const stepTicks = this.extractStepsTicks(start, end);\n\n // Check if we should stop in oneShot mode\n if (\n this.props.playbackMode === PlaybackMode.oneShot &&\n stepTicks.length > 0\n ) {\n const lastTick = stepTicks[stepTicks.length - 1];\n if (lastTick !== undefined) {\n const absolutePage = this.getAbsolutePageFromTicks(lastTick);\n const stepNo = this.getStepNoInPage(lastTick);\n\n // If we've reached or passed the last page and last step, stop the source\n if (\n absolutePage >= this.pageMapping.length - 1 &&\n stepNo >= this.props.stepsPerPage - 1\n ) {\n // Stop after this final step\n this.onStop(lastTick + this.stepResolution);\n }\n }\n }\n\n return stepTicks.map((ticks) => {\n const absolutePage = this.getAbsolutePageFromTicks(ticks);\n const { patternNo, pageNo } =\n this.getPatternAndPageFromAbsolutePage(absolutePage);\n const stepNo = this.getStepNoInPage(ticks);\n\n const step = this.getStep(patternNo, pageNo, stepNo);\n\n return {\n ticks,\n time: 0,\n contextTime: 0,\n eventSourceId: this.id,\n stepNo,\n pageNo,\n patternNo,\n step,\n };\n });\n }\n\n consumer(event: Readonly<StepSequencerSourceEvent>) {\n this.props.onEvent(event);\n }\n}\n"],"mappings":"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","names":["Clock","context","audioClockOffset","now","actionAt","time","clockTime","type","contextTime","isNumber","TPB","secondsPerTick","tempo","ticksPerSecond","insertionIndexBy","arr","n","comparatorFn","index","el","lerp","t","a","b","unlerp","swing","time","amount","t8","t8i","tn","divisionToTicks","division","ticks","num","den","divisionToFrequency","bpm","divisionToMilliseconds","Position","value","timeSignature","isNumber","TPB","positionString","parts","barsStr","beatsStr","sixteenthsStr","bars","beats","sixteenths","position","totalBeats","beatFraction","Timeline","event","idx","insertionIndexBy","a","b","start","end","time","i","Scheduler","Timeline","generator","consumer","scheduleAheadTime","start","end","e","time","events","Tempo","clockTime","tempo","ticks","tickDelta","ticksPerSecond","clockDelta","secondsPerTick","AudioContext","AudioBuffer","AudioBufferSourceNode","generateUniqueNumber","map","nextId","WebAudioWrapper","webAudioWrapper","SCHEDULED_TIMEOUT_FUNCTIONS","SCHEDULED_INTERVAL_FUNCTIONS","callIntervalFunction","id","type","functions","SCHEDULED_INTERVAL_FUNCTIONS","SCHEDULED_TIMEOUT_FUNCTIONS","func","scheduleFunction","delay","now","audioBufferSourceNode","AudioBufferSourceNode","webAudioWrapper","elapsedTime","isNode","acClearInterval","acSetInterval","func","delay","id","generateUniqueNumber","SCHEDULED_INTERVAL_FUNCTIONS","scheduleFunction","exportedClearInterval","isNode","acClearInterval","exportedSetInterval","isNode","acSetInterval","Timer","callback","intervalMs","exportedSetInterval","exportedClearInterval","SourceManager","source","id","start","end","events","event","ticks","TransportState","Transport","Tempo","context","params","SourceManager","Clock","Scheduler","Timer","time","contextTime","ticks","callback","property","value","callbacks","clockTime","Position","position","event","actionAt","source","id","bpm","oldBpm","oldValue","amount","start","end","transportStart","transportEnd","swing","events","uuidv4","BaseSource","transport","ticks","_ticks","start","end","eventTick","Resolution","PlaybackMode","RESOLUTION_TO_TICKS","TPB","expandPatternSequence","input","result","num","ch","count","i","v","StepSequencerSource","BaseSource","transport","props","mapping","patternLetter","patternNo","p","pattern","pageNo","ticks","timeSignature","ticksPerBar","nextBarTicks","start","end","stepResolution","stepsSinceStart","actualStart","value","stepNo","page","step","ticksSinceStart","ticksIntoPage","absolutePage","index","stepTicks","lastTick","event"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blibliki/transport",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -15,10 +15,13 @@
|
|
|
15
15
|
"dist"
|
|
16
16
|
],
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@types/audioworklet": "0.0.
|
|
18
|
+
"@types/audioworklet": "0.0.93",
|
|
19
|
+
"vite-tsconfig-paths": "6.0.5",
|
|
20
|
+
"vitest": "4.0.18"
|
|
19
21
|
},
|
|
20
22
|
"dependencies": {
|
|
21
|
-
"
|
|
23
|
+
"es-toolkit": "1.44.0",
|
|
24
|
+
"@blibliki/utils": "^0.9.0"
|
|
22
25
|
},
|
|
23
26
|
"scripts": {
|
|
24
27
|
"build": "tsup",
|
|
@@ -27,6 +30,7 @@
|
|
|
27
30
|
"tsc": "tsc --noEmit",
|
|
28
31
|
"format": "prettier . --write",
|
|
29
32
|
"format:check": "prettier . --check",
|
|
33
|
+
"test": "vitest",
|
|
30
34
|
"bump": "npm version patch",
|
|
31
35
|
"release": "pnpm run build && pnpm publish --access public"
|
|
32
36
|
}
|
package/src/Clock.ts
CHANGED
|
@@ -32,6 +32,8 @@ export class Clock {
|
|
|
32
32
|
this.contextTimeAt.start = actionAt;
|
|
33
33
|
this.clockTimeAt.start = this.clockTimeAt.stop;
|
|
34
34
|
this._isRunning = true;
|
|
35
|
+
|
|
36
|
+
return this.clockTimeAt.start;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
stop(actionAt: ContextTime) {
|
|
@@ -39,6 +41,8 @@ export class Clock {
|
|
|
39
41
|
this.clockTimeAt.stop =
|
|
40
42
|
this.clockTimeAt.start + (actionAt - this.contextTimeAt.start);
|
|
41
43
|
this.contextTimeAt.stop = actionAt;
|
|
44
|
+
|
|
45
|
+
return this.clockTimeAt.stop;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
jumpTo(time: ClockTime) {
|
|
@@ -55,4 +59,15 @@ export class Clock {
|
|
|
55
59
|
this.clockTimeAt[type]
|
|
56
60
|
);
|
|
57
61
|
}
|
|
62
|
+
|
|
63
|
+
contextTimeToClockTime(contextTime: ContextTime): ClockTime {
|
|
64
|
+
const type = this.isRunning ? "start" : "stop";
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
contextTime -
|
|
68
|
+
this.audioClockOffset -
|
|
69
|
+
this.contextTimeAt[type] +
|
|
70
|
+
this.clockTimeAt[type]
|
|
71
|
+
);
|
|
72
|
+
}
|
|
58
73
|
}
|
package/src/Transport.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { Scheduler } from "./Scheduler";
|
|
|
5
5
|
import { Tempo } from "./Tempo";
|
|
6
6
|
import { TimelineEvent } from "./Timeline";
|
|
7
7
|
import { Timer } from "./Timer";
|
|
8
|
+
import { SourceEvent, BaseSource } from "./sources/BaseSource";
|
|
9
|
+
import { SourceManager } from "./sources/SourceManager";
|
|
8
10
|
import {
|
|
9
11
|
BPM,
|
|
10
12
|
ClockTime,
|
|
@@ -20,36 +22,6 @@ export interface TransportEvent extends TimelineEvent {
|
|
|
20
22
|
contextTime: ContextTime;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
/**
|
|
24
|
-
* Given a (future) transport time window (in ticks), return all events that should
|
|
25
|
-
* occur within the window. This function is responsible for setting the ticks
|
|
26
|
-
* value for the events returned.
|
|
27
|
-
*
|
|
28
|
-
* IMPORTANT: Subsequent calls to this function may have overlapping time windows.
|
|
29
|
-
* Be careful to not return the same event more than once.
|
|
30
|
-
*/
|
|
31
|
-
export type TransportEventGenerator<T extends TransportEvent> = (
|
|
32
|
-
start: Ticks,
|
|
33
|
-
end: Ticks,
|
|
34
|
-
) => readonly Readonly<T>[];
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Schedule the specified event with the audio system. The transport class is
|
|
38
|
-
* responsible for setting the `contextTime` of the events.
|
|
39
|
-
*/
|
|
40
|
-
export type TransportEventConsumer<T extends TransportEvent> = (
|
|
41
|
-
event: Readonly<T>,
|
|
42
|
-
) => void;
|
|
43
|
-
|
|
44
|
-
export type TransportListener<T extends TransportEvent> = {
|
|
45
|
-
generator: TransportEventGenerator<T>;
|
|
46
|
-
consumer: TransportEventConsumer<T>;
|
|
47
|
-
onJump: (ticks: Ticks) => void;
|
|
48
|
-
onStart: (contextTime: ContextTime) => void;
|
|
49
|
-
onStop: (contextTime: ContextTime) => void;
|
|
50
|
-
silence: (contextTime: ContextTime) => void;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
25
|
/**
|
|
54
26
|
* Transport callback that gets invoked at (roughly) sixteenth note intervals.
|
|
55
27
|
*
|
|
@@ -59,7 +31,24 @@ export type TransportListener<T extends TransportEvent> = {
|
|
|
59
31
|
* It is only accurate up to the precision of the event sheduler rate, and may
|
|
60
32
|
* "jump" if the scheduling is struggling to keep up.
|
|
61
33
|
*/
|
|
62
|
-
export type TransportClockCallback = (
|
|
34
|
+
export type TransportClockCallback = (
|
|
35
|
+
time: ClockTime,
|
|
36
|
+
contextTime: ContextTime,
|
|
37
|
+
ticks: Ticks,
|
|
38
|
+
) => void;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Transport properties that can be observed for changes.
|
|
42
|
+
*/
|
|
43
|
+
export type TransportProperty = "bpm" | "timeSignature" | "swingAmount";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Transport callback that gets invoked when a property changes.
|
|
47
|
+
*/
|
|
48
|
+
export type TransportPropertyChangeCallback<T = unknown> = (
|
|
49
|
+
value: T,
|
|
50
|
+
contextTime: ContextTime,
|
|
51
|
+
) => void;
|
|
63
52
|
|
|
64
53
|
export enum TransportState {
|
|
65
54
|
playing = "playing",
|
|
@@ -67,16 +56,21 @@ export enum TransportState {
|
|
|
67
56
|
paused = "paused",
|
|
68
57
|
}
|
|
69
58
|
|
|
59
|
+
export type TransportParams = {
|
|
60
|
+
onStart: (ticks: ContextTime) => void;
|
|
61
|
+
onStop: (ticks: ContextTime) => void;
|
|
62
|
+
};
|
|
63
|
+
|
|
70
64
|
/**
|
|
71
65
|
* This class converts (music) transport time into audio clock time.
|
|
72
66
|
*/
|
|
73
|
-
export class Transport
|
|
67
|
+
export class Transport {
|
|
74
68
|
private _initialized = false;
|
|
75
69
|
|
|
76
70
|
private context: Readonly<Context>;
|
|
77
71
|
private clock: Readonly<Clock>;
|
|
78
72
|
private timer: Readonly<Timer>;
|
|
79
|
-
private scheduler: Readonly<Scheduler<
|
|
73
|
+
private scheduler: Readonly<Scheduler<SourceEvent>>;
|
|
80
74
|
|
|
81
75
|
private _timeSignature: TimeSignature = [4, 4];
|
|
82
76
|
|
|
@@ -84,23 +78,28 @@ export class Transport<T extends TransportEvent> {
|
|
|
84
78
|
private clockTime = 0;
|
|
85
79
|
private _swingAmount: NormalRange = 0.5;
|
|
86
80
|
|
|
87
|
-
private
|
|
81
|
+
private sourceManager: SourceManager;
|
|
88
82
|
|
|
89
83
|
private _clockCallbacks: TransportClockCallback[] = [];
|
|
84
|
+
private _propertyChangeCallbacks = new Map<
|
|
85
|
+
TransportProperty,
|
|
86
|
+
TransportPropertyChangeCallback[]
|
|
87
|
+
>();
|
|
90
88
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
) {
|
|
95
|
-
// ### Make these adapt to performance of app? Or let user set them?
|
|
89
|
+
private onStartCallback: TransportParams["onStart"];
|
|
90
|
+
private onStopCallback: TransportParams["onStop"];
|
|
91
|
+
|
|
92
|
+
constructor(context: Readonly<Context>, params: TransportParams) {
|
|
96
93
|
const SCHEDULE_INTERVAL_MS = 20;
|
|
97
|
-
const SCHEDULE_WINDOW_SIZE_MS =
|
|
94
|
+
const SCHEDULE_WINDOW_SIZE_MS = 100;
|
|
98
95
|
|
|
99
96
|
this.context = context;
|
|
100
|
-
this.
|
|
97
|
+
this.sourceManager = new SourceManager();
|
|
101
98
|
this.clock = new Clock(this.context, SCHEDULE_WINDOW_SIZE_MS / 1000);
|
|
99
|
+
this.onStartCallback = params.onStart;
|
|
100
|
+
this.onStopCallback = params.onStop;
|
|
102
101
|
|
|
103
|
-
this.scheduler = new Scheduler<
|
|
102
|
+
this.scheduler = new Scheduler<SourceEvent>(
|
|
104
103
|
this.generateEvents,
|
|
105
104
|
this.consumeEvents,
|
|
106
105
|
SCHEDULE_WINDOW_SIZE_MS / 1000,
|
|
@@ -108,12 +107,14 @@ export class Transport<T extends TransportEvent> {
|
|
|
108
107
|
|
|
109
108
|
this.timer = new Timer(() => {
|
|
110
109
|
const time = this.clock.time();
|
|
110
|
+
const contextTime = this.clock.clockTimeToContextTime(time);
|
|
111
|
+
const ticks = this.tempo.getTicks(time);
|
|
111
112
|
if (time <= this.clockTime) return;
|
|
112
113
|
|
|
113
114
|
this.clockTime = time;
|
|
114
115
|
this.scheduler.runUntil(time);
|
|
115
116
|
this._clockCallbacks.forEach((callback) => {
|
|
116
|
-
callback(this.clockTime);
|
|
117
|
+
callback(this.clockTime, contextTime, ticks);
|
|
117
118
|
});
|
|
118
119
|
}, SCHEDULE_INTERVAL_MS / 1000);
|
|
119
120
|
|
|
@@ -124,22 +125,40 @@ export class Transport<T extends TransportEvent> {
|
|
|
124
125
|
this._clockCallbacks.push(callback);
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
128
|
+
addPropertyChangeCallback(
|
|
129
|
+
property: TransportProperty,
|
|
130
|
+
callback: TransportPropertyChangeCallback,
|
|
131
|
+
) {
|
|
132
|
+
if (!this._propertyChangeCallbacks.has(property)) {
|
|
133
|
+
this._propertyChangeCallbacks.set(property, []);
|
|
134
|
+
}
|
|
135
|
+
this._propertyChangeCallbacks.get(property)!.push(callback);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private triggerPropertyChange(property: TransportProperty, value: unknown) {
|
|
139
|
+
const callbacks = this._propertyChangeCallbacks.get(property);
|
|
140
|
+
if (callbacks) {
|
|
141
|
+
const contextTime = this.context.currentTime;
|
|
142
|
+
callbacks.forEach((callback) => {
|
|
143
|
+
callback(value, contextTime);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
137
146
|
}
|
|
138
147
|
|
|
139
148
|
get time() {
|
|
140
149
|
return this.clock.time();
|
|
141
150
|
}
|
|
142
151
|
|
|
152
|
+
getContextTimeAtTicks(ticks: Ticks): ContextTime {
|
|
153
|
+
const clockTime = this.tempo.getClockTime(ticks);
|
|
154
|
+
return this.clock.clockTimeToContextTime(clockTime);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
getTicksAtContextTime(contextTime: ContextTime): Ticks {
|
|
158
|
+
const clockTime = this.clock.contextTimeToClockTime(contextTime);
|
|
159
|
+
return this.tempo.getTicks(clockTime);
|
|
160
|
+
}
|
|
161
|
+
|
|
143
162
|
/**
|
|
144
163
|
* Return the (approximate) current Transport time, in ticks.
|
|
145
164
|
*/
|
|
@@ -163,48 +182,69 @@ export class Transport<T extends TransportEvent> {
|
|
|
163
182
|
/**
|
|
164
183
|
* Start the Transport.
|
|
165
184
|
*/
|
|
166
|
-
start() {
|
|
185
|
+
start(actionAt: ContextTime) {
|
|
167
186
|
if (!this._initialized) throw new Error("Not initialized");
|
|
168
187
|
if (this.clock.isRunning) return;
|
|
169
188
|
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
this.listener.onStart(actionAt);
|
|
173
|
-
this.clock.start(actionAt);
|
|
189
|
+
const clockTime = this.clock.start(actionAt);
|
|
174
190
|
this.timer.start();
|
|
191
|
+
|
|
192
|
+
const ticks = this.tempo.getTicks(clockTime);
|
|
193
|
+
this.sourceManager.onStart(ticks);
|
|
194
|
+
this.onStartCallback(ticks);
|
|
175
195
|
}
|
|
176
196
|
|
|
177
197
|
/**
|
|
178
198
|
* Stop the Transport.
|
|
179
199
|
*/
|
|
180
|
-
stop() {
|
|
200
|
+
stop(actionAt: ContextTime) {
|
|
181
201
|
if (!this._initialized) throw new Error("Not initialized");
|
|
182
202
|
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
this.listener.silence(actionAt);
|
|
186
|
-
this.listener.onStop(actionAt);
|
|
187
|
-
this.clock.stop(actionAt);
|
|
203
|
+
const clockTime = this.clock.stop(actionAt);
|
|
188
204
|
this.timer.stop();
|
|
205
|
+
|
|
206
|
+
const ticks = this.tempo.getTicks(clockTime);
|
|
207
|
+
this.sourceManager.onSilence(ticks);
|
|
208
|
+
this.sourceManager.onStop(ticks);
|
|
209
|
+
this.onStopCallback(ticks);
|
|
189
210
|
}
|
|
190
211
|
|
|
191
212
|
/**
|
|
192
213
|
* Reset the Transport to zero.
|
|
193
214
|
*/
|
|
194
|
-
reset() {
|
|
215
|
+
reset(actionAt: ContextTime) {
|
|
195
216
|
if (!this._initialized) throw new Error("Not initialized");
|
|
196
217
|
|
|
197
|
-
|
|
198
|
-
this.listener.silence(actionAt);
|
|
218
|
+
this.sourceManager.onSilence(actionAt);
|
|
199
219
|
this.jumpTo(0);
|
|
200
220
|
}
|
|
201
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Add a source to the transport
|
|
224
|
+
*/
|
|
225
|
+
addSource<T extends SourceEvent>(source: BaseSource<T>) {
|
|
226
|
+
this.sourceManager.addSource(source);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Remove a source from the transport
|
|
231
|
+
*/
|
|
232
|
+
removeSource(id: string) {
|
|
233
|
+
this.sourceManager.removeSource(id);
|
|
234
|
+
}
|
|
235
|
+
|
|
202
236
|
get bpm(): BPM {
|
|
203
237
|
return this.tempo.bpm;
|
|
204
238
|
}
|
|
205
239
|
|
|
206
240
|
set bpm(bpm: BPM) {
|
|
241
|
+
const oldBpm = this.tempo.bpm;
|
|
207
242
|
this.tempo.update(this.clockTime, bpm);
|
|
243
|
+
|
|
244
|
+
// Trigger property change callbacks if BPM actually changed
|
|
245
|
+
if (oldBpm !== bpm) {
|
|
246
|
+
this.triggerPropertyChange("bpm", bpm);
|
|
247
|
+
}
|
|
208
248
|
}
|
|
209
249
|
|
|
210
250
|
get timeSignature() {
|
|
@@ -212,7 +252,13 @@ export class Transport<T extends TransportEvent> {
|
|
|
212
252
|
}
|
|
213
253
|
|
|
214
254
|
set timeSignature(value: TimeSignature) {
|
|
255
|
+
const oldValue = this._timeSignature;
|
|
215
256
|
this._timeSignature = value;
|
|
257
|
+
|
|
258
|
+
// Trigger property change callbacks if time signature actually changed
|
|
259
|
+
if (oldValue[0] !== value[0] || oldValue[1] !== value[1]) {
|
|
260
|
+
this.triggerPropertyChange("timeSignature", value);
|
|
261
|
+
}
|
|
216
262
|
}
|
|
217
263
|
|
|
218
264
|
get state() {
|
|
@@ -226,7 +272,13 @@ export class Transport<T extends TransportEvent> {
|
|
|
226
272
|
}
|
|
227
273
|
|
|
228
274
|
set swingAmount(amount: NormalRange) {
|
|
275
|
+
const oldValue = this._swingAmount;
|
|
229
276
|
this._swingAmount = amount;
|
|
277
|
+
|
|
278
|
+
// Trigger property change callbacks if swing amount actually changed
|
|
279
|
+
if (oldValue !== amount) {
|
|
280
|
+
this.triggerPropertyChange("swingAmount", amount);
|
|
281
|
+
}
|
|
230
282
|
}
|
|
231
283
|
|
|
232
284
|
private jumpTo(ticks: Ticks) {
|
|
@@ -235,20 +287,22 @@ export class Transport<T extends TransportEvent> {
|
|
|
235
287
|
this.clock.jumpTo(clockTime);
|
|
236
288
|
this.scheduler.jumpTo(clockTime);
|
|
237
289
|
this.clockTime = clockTime;
|
|
238
|
-
this.
|
|
290
|
+
this.sourceManager.onJump(ticks);
|
|
291
|
+
|
|
292
|
+
const contextTime = this.clock.clockTimeToContextTime(this.clockTime);
|
|
239
293
|
this._clockCallbacks.forEach((callback) => {
|
|
240
|
-
callback(this.clockTime);
|
|
294
|
+
callback(this.clockTime, contextTime, ticks);
|
|
241
295
|
});
|
|
242
296
|
}
|
|
243
297
|
|
|
244
298
|
private generateEvents = (
|
|
245
299
|
start: ClockTime,
|
|
246
300
|
end: ClockTime,
|
|
247
|
-
): readonly
|
|
301
|
+
): readonly SourceEvent[] => {
|
|
248
302
|
// Get transport-time events and return them as clock-time events
|
|
249
303
|
const transportStart = this.tempo.getTicks(start);
|
|
250
304
|
const transportEnd = this.tempo.getTicks(end);
|
|
251
|
-
return this.
|
|
305
|
+
return this.sourceManager
|
|
252
306
|
.generator(transportStart, transportEnd)
|
|
253
307
|
.map((event) => {
|
|
254
308
|
// Apply swing
|
|
@@ -268,9 +322,7 @@ export class Transport<T extends TransportEvent> {
|
|
|
268
322
|
});
|
|
269
323
|
};
|
|
270
324
|
|
|
271
|
-
private consumeEvents = (events: readonly
|
|
272
|
-
|
|
273
|
-
this.listener.consumer(event);
|
|
274
|
-
});
|
|
325
|
+
private consumeEvents = (events: readonly SourceEvent[]) => {
|
|
326
|
+
this.sourceManager.consumer(events);
|
|
275
327
|
};
|
|
276
328
|
}
|