@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 CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  > Musical transport and scheduler on top of the WebAudio API.
4
4
 
5
+ A precision timing engine for music applications. Converts musical time (bars, beats, ticks) into sample-accurate Web Audio scheduling using a sliding-window scheduler.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Features](#features)
10
+ - [Installation](#installation)
11
+ - [Quick Start](#quick-start)
12
+ - [Key Concepts](#key-concepts)
13
+ - [API Overview](#api-overview)
14
+ - [Documentation](#documentation)
15
+ - [Development](#development)
16
+
17
+ ## Features
18
+
19
+ - **Precision Scheduling** - Sample-accurate event timing via 200ms lookahead scheduler
20
+ - **Musical Time** - 15,360 ticks per beat with support for any time signature
21
+ - **Tempo Control** - Change BPM mid-playback without losing position
22
+ - **Swing** - Built-in swing/shuffle timing (0.5 to 0.75)
23
+ - **Position Management** - Jump to any bar:beat:sixteenth position
24
+ - **UI Callbacks** - Clock and bar callbacks for visual synchronization
25
+ - **TypeScript** - Fully typed with generic event support
26
+ - **Minimal** - Zero dependencies except `@blibliki/utils`
27
+
5
28
  ## Installation
6
29
 
7
30
  ```sh
@@ -85,41 +108,95 @@ async function main() {
85
108
  main();
86
109
  ```
87
110
 
88
- Stop the transport with `transport.stop()`, pause with `transport.stop()` (without resetting) and reset the playhead to the beginning with `transport.reset()`.
111
+ ## Key Concepts
112
+
113
+ ### Listener Callbacks
114
+
115
+ The listener object bridges musical intent and actual audio:
116
+
117
+ - **`generator(startTicks, endTicks)`** - Returns events in the time window (in ticks)
118
+ - **`consumer(event)`** - Schedules audio nodes using `event.contextTime`
119
+ - **`onStart/onStop/onJump/silence`** - Lifecycle hooks
89
120
 
90
- ## Listener Responsibilities
121
+ **Important:** Keep your generator idempotent—the transport may call it with overlapping windows.
91
122
 
92
- The listener that you pass into the constructor bridges musical intent and actual audio nodes:
123
+ ### Musical Time
93
124
 
94
- - `generator(startTicks, endTicks)` **must** return every event that occurs in the half-open window `[startTicks, endTicks)`. The transport may call this with overlapping windows when rescheduling, so keep your generator idempotent and avoid emitting duplicate events.
95
- - `consumer(event)` receives the events produced by the generator with `time` (transport clock) and `contextTime` (AudioContext time) populated. This is where you schedule audio nodes, MIDI messages, etc.
96
- - `onStart(contextTime)` / `onStop(contextTime)` happen just before the transport starts or stops advancing.
97
- - `onJump(ticks)` is emitted whenever `transport.position` changes abruptly (for example through manual assignment or `reset()`).
98
- - `silence(contextTime)` is called whenever playback should be made quiet immediately—useful for clearing envelopes on stop/reset.
125
+ - Transport runs at **15,360 ticks per quarter note**
126
+ - Use `Position` helper for conversions: `new Position("2:1:1", [4, 4])`
127
+ - Control via properties: `bpm`, `timeSignature`, `swingAmount`, `position`
99
128
 
100
- ## Working with Musical Time
129
+ ### UI Callbacks
101
130
 
102
- - The transport runs at `15360` ticks per quarter note. Use the `Position` helper to convert between ticks, strings (`"bars:beats:sixteenths"`), and object notation, as in `new Position("2:1:1", [4, 4]).ticks`.
103
- - Control tempo via the `bpm` getter/setter. Updating the tempo while playing keeps the current transport position intact.
104
- - Change the time signature with the `timeSignature` setter. The default is `4/4`.
105
- - Apply swing by setting `transport.swingAmount` to a value between `0.5` (straight) and `0.75`.
106
- - Reach the current musical position with the `position` getter. Assigning to `transport.position` jumps the playhead and invokes `onJump`.
131
+ Register callbacks for UI updates:
107
132
 
108
- ## UI-Friendly Clocking
133
+ - `transport.addClockCallback()` - Fires every ~20ms
109
134
 
110
- To keep visual components in sync you can register clock callbacks:
135
+ ## API Overview
136
+
137
+ ### Transport Control
111
138
 
112
139
  ```ts
113
- transport.addClockCallback((clockTime) => {
114
- console.log("transport clock is at", clockTime, "seconds of audio time");
115
- });
140
+ transport.start(); // Start playback
141
+ transport.stop(); // Stop (pause) playback
142
+ transport.reset(); // Reset to position 0
143
+ transport.state; // "playing" | "stopped" | "paused"
144
+ ```
116
145
 
117
- transport.addBarCallback((bar) => {
118
- // e.g. update the UI playhead when a new bar begins
119
- });
146
+ ### Properties
147
+
148
+ ```ts
149
+ transport.bpm = 120; // Tempo (beats per minute)
150
+ transport.timeSignature = [4, 4]; // Time signature
151
+ transport.swingAmount = 0.6; // Swing (0.5 = none, 0.75 = max)
152
+ transport.position; // Current position (get/set)
120
153
  ```
121
154
 
122
- Clock callbacks fire at roughly 16th-note resolution, intended for UI feedback rather than sample-accurate DSP.
155
+ ### Position
156
+
157
+ ```ts
158
+ import { Position } from "@blibliki/transport";
159
+
160
+ const pos = new Position("2:1:3", [4, 4]); // Bar 2, beat 1, sixteenth 3
161
+ console.log(pos.ticks); // Get ticks
162
+ console.log(pos.toString()); // "2:1:3"
163
+ ```
164
+
165
+ ### Utilities
166
+
167
+ ```ts
168
+ import { TPB, divisionToTicks } from "@blibliki/transport";
169
+
170
+ const sixteenthTicks = TPB / 4; // 3840 ticks
171
+ const eighthTicks = divisionToTicks("1/8"); // 7680 ticks
172
+ ```
173
+
174
+ ## Documentation
175
+
176
+ For comprehensive guides and examples:
177
+
178
+ - **[DOCUMENTATION.md](./DOCUMENTATION.md)** - Complete architecture guide, API reference, and detailed examples
179
+ - **[SCHEDULER_EXAMPLES.md](./SCHEDULER_EXAMPLES.md)** - Advanced scheduler patterns and use cases
180
+
181
+ ### What's Covered
182
+
183
+ **DOCUMENTATION.md includes:**
184
+
185
+ - Three time systems (ticks, clock time, context time)
186
+ - Scheduler deep dive with timing diagrams
187
+ - Complete architecture breakdown
188
+ - Position and tempo management
189
+ - Full API reference
190
+ - Troubleshooting guide
191
+
192
+ **SCHEDULER_EXAMPLES.md includes:**
193
+
194
+ - Minimal scheduler implementation
195
+ - Visual scheduler simulation
196
+ - Step sequencer patterns
197
+ - Dynamic event generation
198
+ - Multi-track scheduling
199
+ - Advanced patterns (tempo automation, event modification, MIDI recording)
123
200
 
124
201
  ## Development
125
202
 
package/dist/index.d.ts CHANGED
@@ -49,32 +49,39 @@ type TimelineEvent = {
49
49
  time: ClockTime;
50
50
  };
51
51
 
52
+ interface SourceEvent extends TransportEvent {
53
+ eventSourceId: string;
54
+ }
55
+ interface IBaseSource<T extends SourceEvent> {
56
+ id: string;
57
+ generator: (start: Ticks, end: Ticks) => readonly Readonly<T>[];
58
+ consumer: (event: Readonly<T>) => void;
59
+ onStart: (ticks: Ticks) => void;
60
+ onStop: (ticks: Ticks) => void;
61
+ onJump: (ticks: Ticks) => void;
62
+ onSilence: (ticks: Ticks) => void;
63
+ }
64
+ declare abstract class BaseSource<T extends SourceEvent> implements IBaseSource<T> {
65
+ readonly id: string;
66
+ protected transport: Transport;
67
+ protected startedAt?: Ticks;
68
+ protected stoppedAt?: Ticks;
69
+ protected lastGeneratedTick?: Ticks;
70
+ constructor(transport: Transport);
71
+ abstract generator(start: Ticks, end: Ticks): readonly Readonly<T>[];
72
+ abstract consumer(event: Readonly<T>): void;
73
+ onStart(ticks: Ticks): void;
74
+ onStop(ticks: Ticks): void;
75
+ onJump(ticks: Ticks): void;
76
+ onSilence(_ticks: Ticks): void;
77
+ protected isPlaying(start: Ticks, end: Ticks): boolean;
78
+ protected shouldGenerate(eventTick: Ticks): boolean;
79
+ }
80
+
52
81
  interface TransportEvent extends TimelineEvent {
53
82
  ticks: Ticks;
54
83
  contextTime: ContextTime;
55
84
  }
56
- /**
57
- * Given a (future) transport time window (in ticks), return all events that should
58
- * occur within the window. This function is responsible for setting the ticks
59
- * value for the events returned.
60
- *
61
- * IMPORTANT: Subsequent calls to this function may have overlapping time windows.
62
- * Be careful to not return the same event more than once.
63
- */
64
- type TransportEventGenerator<T extends TransportEvent> = (start: Ticks, end: Ticks) => readonly Readonly<T>[];
65
- /**
66
- * Schedule the specified event with the audio system. The transport class is
67
- * responsible for setting the `contextTime` of the events.
68
- */
69
- type TransportEventConsumer<T extends TransportEvent> = (event: Readonly<T>) => void;
70
- type TransportListener<T extends TransportEvent> = {
71
- generator: TransportEventGenerator<T>;
72
- consumer: TransportEventConsumer<T>;
73
- onJump: (ticks: Ticks) => void;
74
- onStart: (contextTime: ContextTime) => void;
75
- onStop: (contextTime: ContextTime) => void;
76
- silence: (contextTime: ContextTime) => void;
77
- };
78
85
  /**
79
86
  * Transport callback that gets invoked at (roughly) sixteenth note intervals.
80
87
  *
@@ -84,16 +91,28 @@ type TransportListener<T extends TransportEvent> = {
84
91
  * It is only accurate up to the precision of the event sheduler rate, and may
85
92
  * "jump" if the scheduling is struggling to keep up.
86
93
  */
87
- type TransportClockCallback = (time: ClockTime) => void;
94
+ type TransportClockCallback = (time: ClockTime, contextTime: ContextTime, ticks: Ticks) => void;
95
+ /**
96
+ * Transport properties that can be observed for changes.
97
+ */
98
+ type TransportProperty = "bpm" | "timeSignature" | "swingAmount";
99
+ /**
100
+ * Transport callback that gets invoked when a property changes.
101
+ */
102
+ type TransportPropertyChangeCallback<T = unknown> = (value: T, contextTime: ContextTime) => void;
88
103
  declare enum TransportState {
89
104
  playing = "playing",
90
105
  stopped = "stopped",
91
106
  paused = "paused"
92
107
  }
108
+ type TransportParams = {
109
+ onStart: (ticks: ContextTime) => void;
110
+ onStop: (ticks: ContextTime) => void;
111
+ };
93
112
  /**
94
113
  * This class converts (music) transport time into audio clock time.
95
114
  */
96
- declare class Transport<T extends TransportEvent> {
115
+ declare class Transport {
97
116
  private _initialized;
98
117
  private context;
99
118
  private clock;
@@ -103,12 +122,18 @@ declare class Transport<T extends TransportEvent> {
103
122
  private tempo;
104
123
  private clockTime;
105
124
  private _swingAmount;
106
- private listener;
125
+ private sourceManager;
107
126
  private _clockCallbacks;
108
- constructor(context: Readonly<Context>, listener: Readonly<TransportListener<T>>);
127
+ private _propertyChangeCallbacks;
128
+ private onStartCallback;
129
+ private onStopCallback;
130
+ constructor(context: Readonly<Context>, params: TransportParams);
109
131
  addClockCallback(callback: TransportClockCallback): void;
110
- addBarCallback(callback: (bar: number) => void): void;
132
+ addPropertyChangeCallback(property: TransportProperty, callback: TransportPropertyChangeCallback): void;
133
+ private triggerPropertyChange;
111
134
  get time(): number;
135
+ getContextTimeAtTicks(ticks: Ticks): ContextTime;
136
+ getTicksAtContextTime(contextTime: ContextTime): Ticks;
112
137
  /**
113
138
  * Return the (approximate) current Transport time, in ticks.
114
139
  */
@@ -121,15 +146,23 @@ declare class Transport<T extends TransportEvent> {
121
146
  /**
122
147
  * Start the Transport.
123
148
  */
124
- start(): void;
149
+ start(actionAt: ContextTime): void;
125
150
  /**
126
151
  * Stop the Transport.
127
152
  */
128
- stop(): void;
153
+ stop(actionAt: ContextTime): void;
129
154
  /**
130
155
  * Reset the Transport to zero.
131
156
  */
132
- reset(): void;
157
+ reset(actionAt: ContextTime): void;
158
+ /**
159
+ * Add a source to the transport
160
+ */
161
+ addSource<T extends SourceEvent>(source: BaseSource<T>): void;
162
+ /**
163
+ * Remove a source from the transport
164
+ */
165
+ removeSource(id: string): void;
133
166
  get bpm(): BPM;
134
167
  set bpm(bpm: BPM);
135
168
  get timeSignature(): TimeSignature;
@@ -142,4 +175,95 @@ declare class Transport<T extends TransportEvent> {
142
175
  private consumeEvents;
143
176
  }
144
177
 
145
- export { type BPM, type ClockTime, type ContextTime, Position, type Seconds, type Ticks, type TimeSignature, Transport, type TransportEvent, TransportState };
178
+ declare const TPB = 15360;
179
+ type Division = "1/64" | "1/48" | "1/32" | "1/24" | "1/16" | "1/12" | "1/8" | "1/6" | "3/16" | "1/4" | "5/16" | "1/3" | "3/8" | "1/2" | "3/4" | "1" | "1.5" | "2" | "3" | "4" | "6" | "8" | "16" | "32" | "infinity";
180
+ declare function divisionToTicks(division: Division): Ticks;
181
+ declare function divisionToFrequency(division: Division, bpm: BPM): number;
182
+ declare function divisionToMilliseconds(division: Division, bpm: BPM): number;
183
+
184
+ interface StepSequencerSourceEvent extends SourceEvent {
185
+ stepNo: number;
186
+ pageNo: number;
187
+ patternNo: number;
188
+ step: IStep;
189
+ }
190
+ type IStepNote = {
191
+ note: string;
192
+ velocity: number;
193
+ };
194
+ type IStepCC = {
195
+ cc: number;
196
+ value: number;
197
+ };
198
+ type IStep = {
199
+ active: boolean;
200
+ notes: IStepNote[];
201
+ ccMessages: IStepCC[];
202
+ probability: number;
203
+ microtimeOffset: number;
204
+ duration: Division;
205
+ };
206
+ type IPage = {
207
+ name: string;
208
+ steps: IStep[];
209
+ };
210
+ type IPattern = {
211
+ name: string;
212
+ pages: IPage[];
213
+ };
214
+ declare enum Resolution {
215
+ thirtysecond = "1/32",
216
+ sixteenth = "1/16",
217
+ eighth = "1/8",
218
+ quarter = "1/4"
219
+ }
220
+ declare enum PlaybackMode {
221
+ loop = "loop",
222
+ oneShot = "oneShot"
223
+ }
224
+ interface StepSequencerSourceProps {
225
+ onEvent: (event: StepSequencerSourceEvent) => void;
226
+ patterns: IPattern[];
227
+ stepsPerPage: number;
228
+ resolution: Resolution;
229
+ playbackMode: PlaybackMode;
230
+ patternSequence: string;
231
+ enableSequence: boolean;
232
+ }
233
+ declare class StepSequencerSource extends BaseSource<StepSequencerSourceEvent> {
234
+ props: StepSequencerSourceProps;
235
+ private expandedSequence;
236
+ private pageMapping;
237
+ constructor(transport: Transport, props: StepSequencerSourceProps);
238
+ /**
239
+ * Build a mapping of absolute pages to (patternNo, pageNo) for one full sequence cycle
240
+ * Example: sequence "2A1B" with A having 2 pages, B having 1 page produces:
241
+ * [A0, A1, A0, A1, B0]
242
+ */
243
+ private buildPageMapping;
244
+ get stepResolution(): Ticks;
245
+ onStart(ticks: Ticks): void;
246
+ extractStepsTicks(start: Ticks, end: Ticks): Ticks[];
247
+ private getStep;
248
+ /**
249
+ * Calculate ticks per page based on step resolution and steps per page
250
+ */
251
+ get ticksPerPage(): Ticks;
252
+ /**
253
+ * Calculate absolute page number from tick position
254
+ */
255
+ private getAbsolutePageFromTicks;
256
+ /**
257
+ * Calculate step number within the current page from tick position
258
+ */
259
+ private getStepNoInPage;
260
+ /**
261
+ * Map absolute page number to actual pattern and page indices
262
+ * Takes into account sequence mode and playback mode (loop vs oneShot)
263
+ */
264
+ private getPatternAndPageFromAbsolutePage;
265
+ generator(start: Ticks, end: Ticks): readonly Readonly<StepSequencerSourceEvent>[];
266
+ consumer(event: Readonly<StepSequencerSourceEvent>): void;
267
+ }
268
+
269
+ export { type BPM, type ClockTime, type ContextTime, type Division, type IPage, type IPattern, type IStep, type IStepCC, type IStepNote, PlaybackMode, Position, Resolution, type Seconds, StepSequencerSource, type StepSequencerSourceEvent, TPB, type Ticks, type TimeSignature, Transport, type TransportEvent, type TransportProperty, type TransportPropertyChangeCallback, TransportState, divisionToFrequency, divisionToMilliseconds, divisionToTicks };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- var l=class{context;clockTimeAt;contextTimeAt;audioClockOffset;_isRunning=!1;constructor(t,e){this.context=t,this.audioClockOffset=e,this.clockTimeAt={start:0,stop:0},this.contextTimeAt={start:0,stop:0}}get isRunning(){return this._isRunning}time(){if(!this.isRunning)return this.clockTimeAt.stop;let t=this.context.currentTime;return this.clockTimeAt.start+(t-this.contextTimeAt.start)}start(t){this.contextTimeAt.start=t,this.clockTimeAt.start=this.clockTimeAt.stop,this._isRunning=!0}stop(t){this._isRunning=!1,this.clockTimeAt.stop=this.clockTimeAt.start+(t-this.contextTimeAt.start),this.contextTimeAt.stop=t}jumpTo(t){this.clockTimeAt.stop=t}clockTimeToContextTime(t){let e=this.isRunning?"start":"stop";return this.audioClockOffset+this.contextTimeAt[e]+t-this.clockTimeAt[e]}};import{isNumber as M}from"@blibliki/utils";var c=15360,b=n=>60/n/c,S=n=>n/60*c,_=(n,t,e)=>{let i=n.findIndex(s=>e(t,s)<0);return i===-1?n.length:i};function C(n,t,e){return t+n*(e-t)}function g(n,t,e){return(n-t)/(e-t)}function E(n,t){if(t<.5||t>.75)throw new Error("Invalid swing amount");let e=n/7680,i=Math.floor(e),s=e-i,o=0;return s<=.5?o=C(g(s,0,.5),0,t):o=C(g(s,.5,1),t,1),Math.floor((i+o)*7680)}var a=class{_ticks;timeSignature;constructor(t,e){this.timeSignature=e,M(t)?this._ticks=t:typeof t=="string"?this._ticks=this.parseStringPosition(t):this._ticks=this.convertObjectToTicks(t)}get ticks(){return this._ticks}get bars(){return Math.floor(this.totalBeats/this.timeSignature[0])+1}get beats(){return this.totalBeats%this.timeSignature[0]+1}get sixteenths(){return Math.floor(this.beatFraction*4)+1}get totalBeats(){return Math.floor(this._ticks/c)}get beatFraction(){return this._ticks/c-this.totalBeats}toString(){return`${this.bars}:${this.beats}:${this.sixteenths}`}toObject(){return{bars:this.bars,beats:this.beats,sixteenths:this.sixteenths}}parseStringPosition(t){let e=t.split(":"),[i,s,o]=e,r=Number(i),k=Number(s),B=Number(o),P={bars:r,beats:k,sixteenths:B};return this.convertObjectToTicks(P)}convertObjectToTicks(t){let e=(t.bars-1)*this.timeSignature[0]+(t.beats-1),i=t.sixteenths/16;return(e+i)*c}};var u=class{_events=[];get events(){return this._events}add(t){let e=_(this._events,t,(i,s)=>i.time-s.time);this._events.splice(e,0,t)}find(t,e){return this._events.filter(i=>i.time>=t&&i.time<e)}lastEventBefore(t){for(let e=this._events.length-1;e>=0;e--){let i=this._events[e];if(i&&i.time<=t)return i}}remove(t,e){this._events=this._events.filter(i=>!(i.time>=t&&i.time<e))}removeAllBefore(t){this._events=this._events.filter(e=>e.time>=t)}clear(){this._events=[]}};var T=class{timeline=new u;scheduleAheadTime;consumedTime;generator;consumer;constructor(t,e,i){this.generator=t,this.consumer=e,this.scheduleAheadTime=i,this.consumedTime=-this.scheduleAheadTime}_schedule(t,e){this.generator(t,e).forEach(i=>{this.timeline.add(i)})}runUntil(t){if(t<=this.consumedTime)throw new Error("Scheduling time is <= current time");this._schedule(this.consumedTime+this.scheduleAheadTime,t+this.scheduleAheadTime);let e=this.timeline.find(this.consumedTime,t);this.consumer(e),this.consumedTime=t,this.timeline.removeAllBefore(this.consumedTime)}jumpTo(t){this.timeline.clear(),this.consumedTime=t-this.scheduleAheadTime}};var h=class{clockTimeAtLastTempoChange=0;ticksAtLastTempoChange=0;_bpm=120;get bpm(){return this._bpm}update(t,e){let i=this.getTicks(t);this.clockTimeAtLastTempoChange=t,this.ticksAtLastTempoChange=i,this._bpm=e}getTicks(t){let i=(t-this.clockTimeAtLastTempoChange)*S(this.bpm);return Math.floor(this.ticksAtLastTempoChange+i)}getClockTime(t){let i=Math.floor(t-this.ticksAtLastTempoChange)*b(this.bpm);return this.clockTimeAtLastTempoChange+i}reset(t,e){this.clockTimeAtLastTempoChange=t,this.ticksAtLastTempoChange=e}};import{AudioContext as N,AudioBuffer as D,AudioBufferSourceNode as L}from"@blibliki/utils/web-audio-api";function O(n){let t=1;for(;n.has(t);)t++;return t}var f=class{_audioBuffer=null;_audioContext=null;_sampleDuration=null;get audioContext(){return this._audioContext??=new N,this._audioContext}get audioBuffer(){return this._audioBuffer??=new D({length:2,sampleRate:this.audioContext.sampleRate}),this._audioBuffer}get sampleDuration(){return this._sampleDuration??=2/this.audioContext.sampleRate,this._sampleDuration}},m=new f,A=new Map,p=new Map;var U=(n,t)=>{let e=t==="interval"?p:A;if(e.has(n)){let i=e.get(n);i!==void 0&&(i(),t==="timeout"&&A.delete(n))}},v=(n,t,e)=>{let i=performance.now(),s=new L(m.audioContext,{buffer:m.audioBuffer});s.onended=()=>{let o=performance.now()-i;o>=t?U(n,e):v(n,t-o,e),s.disconnect(m.audioContext.destination)},s.connect(m.audioContext.destination),s.start(Math.max(0,m.audioContext.currentTime+t/1e3-m.sampleDuration))},y=typeof window>"u",j=n=>{p.delete(n)};var z=(n,t)=>{let e=O(p);return p.set(e,()=>{n(),v(e,t,"interval")}),v(e,t,"interval"),e};var R=y?clearInterval:j;var w=y?setInterval:z;var d=class{timerId=void 0;interval;callback;constructor(t,e){this.callback=t,this.interval=e}start(){this.timerId=w(()=>{this.callback()},this.interval)}stop(){this.timerId!==void 0&&(R(this.timerId),this.timerId=void 0)}get isRunning(){return this.timerId!==void 0}};var I=(i=>(i.playing="playing",i.stopped="stopped",i.paused="paused",i))(I||{}),x=class{_initialized=!1;context;clock;timer;scheduler;_timeSignature=[4,4];tempo=new h;clockTime=0;_swingAmount=.5;listener;_clockCallbacks=[];constructor(t,e){this.context=t,this.listener=e,this.clock=new l(this.context,200/1e3),this.scheduler=new T(this.generateEvents,this.consumeEvents,200/1e3),this.timer=new d(()=>{let o=this.clock.time();o<=this.clockTime||(this.clockTime=o,this.scheduler.runUntil(o),this._clockCallbacks.forEach(r=>{r(this.clockTime)}))},20/1e3),this._initialized=!0}addClockCallback(t){this._clockCallbacks.push(t)}addBarCallback(t){let e=1/0;this.addClockCallback(()=>{let i=this.position;i.bars!==e&&(t(i.bars),e=i.bars)})}get time(){return this.clock.time()}get position(){let t=this.clock.time(),e=this.tempo.getTicks(t);return new a(e,this.timeSignature)}set position(t){this.jumpTo(t.ticks)}getPositionOfEvent(t){return new a(t.ticks,this.timeSignature)}start(){if(!this._initialized)throw new Error("Not initialized");if(this.clock.isRunning)return;let t=this.context.currentTime;this.listener.onStart(t),this.clock.start(t),this.timer.start()}stop(){if(!this._initialized)throw new Error("Not initialized");let t=this.context.currentTime;this.listener.silence(t),this.listener.onStop(t),this.clock.stop(t),this.timer.stop()}reset(){if(!this._initialized)throw new Error("Not initialized");let t=this.context.currentTime;this.listener.silence(t),this.jumpTo(0)}get bpm(){return this.tempo.bpm}set bpm(t){this.tempo.update(this.clockTime,t)}get timeSignature(){return this._timeSignature}set timeSignature(t){this._timeSignature=t}get state(){return this.clock.isRunning?"playing":this.time>0?"paused":"stopped"}get swingAmount(){return this._swingAmount}set swingAmount(t){this._swingAmount=t}jumpTo(t){let e=this.tempo.getClockTime(t);this.tempo.reset(e,t),this.clock.jumpTo(e),this.scheduler.jumpTo(e),this.clockTime=e,this.listener.onJump(t),this._clockCallbacks.forEach(i=>{i(this.clockTime)})}generateEvents=(t,e)=>{let i=this.tempo.getTicks(t),s=this.tempo.getTicks(e);return this.listener.generator(i,s).map(o=>({...o,ticks:E(o.ticks,this.swingAmount)})).map(o=>{let r=this.tempo.getClockTime(o.ticks),k=this.clock.clockTimeToContextTime(r);return{...o,time:r,contextTime:k}})};consumeEvents=t=>{t.forEach(e=>{this.listener.consumer(e)})}};export{a as Position,x as Transport,I as TransportState};
1
+ var l=class{context;clockTimeAt;contextTimeAt;audioClockOffset;_isRunning=!1;constructor(t,e){this.context=t,this.audioClockOffset=e,this.clockTimeAt={start:0,stop:0},this.contextTimeAt={start:0,stop:0}}get isRunning(){return this._isRunning}time(){if(!this.isRunning)return this.clockTimeAt.stop;let t=this.context.currentTime;return this.clockTimeAt.start+(t-this.contextTimeAt.start)}start(t){return this.contextTimeAt.start=t,this.clockTimeAt.start=this.clockTimeAt.stop,this._isRunning=!0,this.clockTimeAt.start}stop(t){return this._isRunning=!1,this.clockTimeAt.stop=this.clockTimeAt.start+(t-this.contextTimeAt.start),this.contextTimeAt.stop=t,this.clockTimeAt.stop}jumpTo(t){this.clockTimeAt.stop=t}clockTimeToContextTime(t){let e=this.isRunning?"start":"stop";return this.audioClockOffset+this.contextTimeAt[e]+t-this.clockTimeAt[e]}contextTimeToClockTime(t){let e=this.isRunning?"start":"stop";return t-this.audioClockOffset-this.contextTimeAt[e]+this.clockTimeAt[e]}};import{isNumber as j}from"@blibliki/utils";var s=15360,_=n=>60/n/s,I=n=>n/60*s,M=(n,t,e)=>{let i=n.findIndex(o=>e(t,o)<0);return i===-1?n.length:i};function E(n,t,e){return t+n*(e-t)}function A(n,t,e){return(n-t)/(e-t)}function N(n,t){if(t<.5||t>.75)throw new Error("Invalid swing amount");let e=n/7680,i=Math.floor(e),o=e-i,r=0;return o<=.5?r=E(A(o,0,.5),0,t):r=E(A(o,.5,1),t,1),Math.floor((i+r)*7680)}function x(n){typeof n!="string"&&(console.error('Invalid duration value, using default "8n."'),n="1/8");let t;if(n==="infinity")return 1/0;if(n.match(/\d\/\d/)){let[e,i]=n.split("/").map(Number);if(!e||!i)throw Error(`Note duration parsing error for value: ${n}`);t=s*e/(i/4)}else t=s*Number(n)*4;return Math.round(t)}function U(n,t){return 1/(x(n)/s*(60/t))}function F(n,t){return x(n)/s*(60/t)*1e3}var u=class{_ticks;timeSignature;constructor(t,e){this.timeSignature=e,j(t)?this._ticks=t:typeof t=="string"?this._ticks=this.parseStringPosition(t):this._ticks=this.convertObjectToTicks(t)}get ticks(){return this._ticks}get bars(){return Math.floor(this.totalBeats/this.timeSignature[0])+1}get beats(){return this.totalBeats%this.timeSignature[0]+1}get sixteenths(){return Math.floor(this.beatFraction*4)+1}get totalBeats(){return Math.floor(this._ticks/s)}get beatFraction(){return this._ticks/s-this.totalBeats}toString(){return`${this.bars}:${this.beats}:${this.sixteenths}`}toObject(){return{bars:this.bars,beats:this.beats,sixteenths:this.sixteenths}}parseStringPosition(t){let e=t.split(":"),[i,o,r]=e,c=Number(i),a=Number(o),p=Number(r),v={bars:c,beats:a,sixteenths:p};return this.convertObjectToTicks(v)}convertObjectToTicks(t){let e=(t.bars-1)*this.timeSignature[0]+(t.beats-1),i=t.sixteenths/16;return(e+i)*s}};var h=class{_events=[];get events(){return this._events}add(t){let e=M(this._events,t,(i,o)=>i.time-o.time);this._events.splice(e,0,t)}find(t,e){return this._events.filter(i=>i.time>=t&&i.time<e)}lastEventBefore(t){for(let e=this._events.length-1;e>=0;e--){let i=this._events[e];if(i&&i.time<=t)return i}}remove(t,e){this._events=this._events.filter(i=>!(i.time>=t&&i.time<e))}removeAllBefore(t){this._events=this._events.filter(e=>e.time>=t)}clear(){this._events=[]}};var T=class{timeline=new h;scheduleAheadTime;consumedTime;generator;consumer;constructor(t,e,i){this.generator=t,this.consumer=e,this.scheduleAheadTime=i,this.consumedTime=-this.scheduleAheadTime}_schedule(t,e){this.generator(t,e).forEach(i=>{this.timeline.add(i)})}runUntil(t){if(t<=this.consumedTime)throw new Error("Scheduling time is <= current time");this._schedule(this.consumedTime+this.scheduleAheadTime,t+this.scheduleAheadTime);let e=this.timeline.find(this.consumedTime,t);this.consumer(e),this.consumedTime=t,this.timeline.removeAllBefore(this.consumedTime)}jumpTo(t){this.timeline.clear(),this.consumedTime=t-this.scheduleAheadTime}};var d=class{clockTimeAtLastTempoChange=0;ticksAtLastTempoChange=0;_bpm=120;get bpm(){return this._bpm}update(t,e){let i=this.getTicks(t);this.clockTimeAtLastTempoChange=t,this.ticksAtLastTempoChange=i,this._bpm=e}getTicks(t){let i=(t-this.clockTimeAtLastTempoChange)*I(this.bpm);return Math.floor(this.ticksAtLastTempoChange+i)}getClockTime(t){let i=Math.floor(t-this.ticksAtLastTempoChange)*_(this.bpm);return this.clockTimeAtLastTempoChange+i}reset(t,e){this.clockTimeAtLastTempoChange=t,this.ticksAtLastTempoChange=e}};import{AudioContext as G,AudioBuffer as z,AudioBufferSourceNode as H}from"@blibliki/utils/web-audio-api";function J(n){let t=1;for(;n.has(t);)t++;return t}var C=class{_audioBuffer=null;_audioContext=null;_sampleDuration=null;get audioContext(){return this._audioContext??=new G,this._audioContext}get audioBuffer(){return this._audioBuffer??=new z({length:2,sampleRate:this.audioContext.sampleRate}),this._audioBuffer}get sampleDuration(){return this._sampleDuration??=2/this.audioContext.sampleRate,this._sampleDuration}},m=new C,R=new Map,k=new Map;var V=(n,t)=>{let e=t==="interval"?k:R;if(e.has(n)){let i=e.get(n);i!==void 0&&(i(),t==="timeout"&&R.delete(n))}},b=(n,t,e)=>{let i=performance.now(),o=new H(m.audioContext,{buffer:m.audioBuffer});o.onended=()=>{let r=performance.now()-i;r>=t?V(n,e):b(n,t-r,e),o.disconnect(m.audioContext.destination)},o.connect(m.audioContext.destination),o.start(Math.max(0,m.audioContext.currentTime+t/1e3-m.sampleDuration))},w=typeof window>"u",W=n=>{k.delete(n)};var $=(n,t)=>{let e=J(k);return k.set(e,()=>{n(),b(e,t,"interval")}),b(e,t,"interval"),e};var B=w?clearInterval:W;var D=w?setInterval:$;var g=class{timerId=void 0;interval;callback;constructor(t,e){this.callback=t,this.interval=e}start(){this.timerId=D(()=>{this.callback()},this.interval)}stop(){this.timerId!==void 0&&(B(this.timerId),this.timerId=void 0)}get isRunning(){return this.timerId!==void 0}};var S=class{activeSources=new Map;addSource(t){this.activeSources.set(t.id,t)}removeSource(t){this.activeSources.delete(t)}generator(t,e){let i=[];return this.activeSources.forEach(o=>{i.push(...o.generator(t,e))}),i}consumer(t){t.forEach(e=>{this.activeSources.get(e.eventSourceId)?.consumer(e)})}onStart(t){this.activeSources.forEach(e=>{e.onStart(t)})}onStop(t){this.activeSources.forEach(e=>{e.onStop(t)})}onJump(t){this.activeSources.forEach(e=>{e.onJump(t)})}onSilence(t){this.activeSources.forEach(e=>{e.onSilence(t)})}};var q=(i=>(i.playing="playing",i.stopped="stopped",i.paused="paused",i))(q||{}),P=class{_initialized=!1;context;clock;timer;scheduler;_timeSignature=[4,4];tempo=new d;clockTime=0;_swingAmount=.5;sourceManager;_clockCallbacks=[];_propertyChangeCallbacks=new Map;onStartCallback;onStopCallback;constructor(t,e){this.context=t,this.sourceManager=new S,this.clock=new l(this.context,100/1e3),this.onStartCallback=e.onStart,this.onStopCallback=e.onStop,this.scheduler=new T(this.generateEvents,this.consumeEvents,100/1e3),this.timer=new g(()=>{let r=this.clock.time(),c=this.clock.clockTimeToContextTime(r),a=this.tempo.getTicks(r);r<=this.clockTime||(this.clockTime=r,this.scheduler.runUntil(r),this._clockCallbacks.forEach(p=>{p(this.clockTime,c,a)}))},20/1e3),this._initialized=!0}addClockCallback(t){this._clockCallbacks.push(t)}addPropertyChangeCallback(t,e){this._propertyChangeCallbacks.has(t)||this._propertyChangeCallbacks.set(t,[]),this._propertyChangeCallbacks.get(t).push(e)}triggerPropertyChange(t,e){let i=this._propertyChangeCallbacks.get(t);if(i){let o=this.context.currentTime;i.forEach(r=>{r(e,o)})}}get time(){return this.clock.time()}getContextTimeAtTicks(t){let e=this.tempo.getClockTime(t);return this.clock.clockTimeToContextTime(e)}getTicksAtContextTime(t){let e=this.clock.contextTimeToClockTime(t);return this.tempo.getTicks(e)}get position(){let t=this.clock.time(),e=this.tempo.getTicks(t);return new u(e,this.timeSignature)}set position(t){this.jumpTo(t.ticks)}getPositionOfEvent(t){return new u(t.ticks,this.timeSignature)}start(t){if(!this._initialized)throw new Error("Not initialized");if(this.clock.isRunning)return;let e=this.clock.start(t);this.timer.start();let i=this.tempo.getTicks(e);this.sourceManager.onStart(i),this.onStartCallback(i)}stop(t){if(!this._initialized)throw new Error("Not initialized");let e=this.clock.stop(t);this.timer.stop();let i=this.tempo.getTicks(e);this.sourceManager.onSilence(i),this.sourceManager.onStop(i),this.onStopCallback(i)}reset(t){if(!this._initialized)throw new Error("Not initialized");this.sourceManager.onSilence(t),this.jumpTo(0)}addSource(t){this.sourceManager.addSource(t)}removeSource(t){this.sourceManager.removeSource(t)}get bpm(){return this.tempo.bpm}set bpm(t){let e=this.tempo.bpm;this.tempo.update(this.clockTime,t),e!==t&&this.triggerPropertyChange("bpm",t)}get timeSignature(){return this._timeSignature}set timeSignature(t){let e=this._timeSignature;this._timeSignature=t,(e[0]!==t[0]||e[1]!==t[1])&&this.triggerPropertyChange("timeSignature",t)}get state(){return this.clock.isRunning?"playing":this.time>0?"paused":"stopped"}get swingAmount(){return this._swingAmount}set swingAmount(t){let e=this._swingAmount;this._swingAmount=t,e!==t&&this.triggerPropertyChange("swingAmount",t)}jumpTo(t){let e=this.tempo.getClockTime(t);this.tempo.reset(e,t),this.clock.jumpTo(e),this.scheduler.jumpTo(e),this.clockTime=e,this.sourceManager.onJump(t);let i=this.clock.clockTimeToContextTime(this.clockTime);this._clockCallbacks.forEach(o=>{o(this.clockTime,i,t)})}generateEvents=(t,e)=>{let i=this.tempo.getTicks(t),o=this.tempo.getTicks(e);return this.sourceManager.generator(i,o).map(r=>({...r,ticks:N(r.ticks,this.swingAmount)})).map(r=>{let c=this.tempo.getClockTime(r.ticks),a=this.clock.clockTimeToContextTime(c);return{...r,time:c,contextTime:a}})};consumeEvents=t=>{this.sourceManager.consumer(t)}};import{uuidv4 as Z}from"@blibliki/utils";var f=class{id;transport;startedAt;stoppedAt;lastGeneratedTick;constructor(t){this.id=Z(),this.transport=t}onStart(t){this.startedAt=t,this.stoppedAt=void 0,this.lastGeneratedTick=void 0}onStop(t){this.stoppedAt=t}onJump(t){this.lastGeneratedTick=t}onSilence(t){}isPlaying(t,e){return this.startedAt!==void 0&&this.startedAt<=t?this.stoppedAt===void 0||this.stoppedAt>=e:!1}shouldGenerate(t){return this.lastGeneratedTick===void 0?!0:t>this.lastGeneratedTick}};var L=(o=>(o.thirtysecond="1/32",o.sixteenth="1/16",o.eighth="1/8",o.quarter="1/4",o))(L||{}),O=(e=>(e.loop="loop",e.oneShot="oneShot",e))(O||{}),K={"1/32":s/8,"1/16":s/4,"1/8":s/2,"1/4":s};function Q(n){let t=[],e="";for(let i of n)if(i>="0"&&i<="9")e+=i;else{let o=Number(e);for(let r=0;r<o;r++)t.push(i);e=""}return t.map(i=>i.toUpperCase())}var y=class extends f{props;expandedSequence=[];pageMapping=[];constructor(t,e){super(t),this.props=e,this.expandedSequence=Q(e.patternSequence),this.pageMapping=this.buildPageMapping()}buildPageMapping(){let t=[];if(this.props.enableSequence&&this.expandedSequence.length>0)for(let e of this.expandedSequence){let i=this.props.patterns.findIndex(r=>r.name.toUpperCase()===e);if(i===-1)continue;let o=this.props.patterns[i];if(o)for(let r=0;r<o.pages.length;r++)t.push({patternNo:i,pageNo:r})}else for(let e=0;e<this.props.patterns.length;e++){let i=this.props.patterns[e];if(i)for(let o=0;o<i.pages.length;o++)t.push({patternNo:e,pageNo:o})}return t}get stepResolution(){return K[this.props.resolution]}onStart(t){let e=this.transport.timeSignature,i=s*e[0];if(t%i===0){super.onStart(t);return}let r=(Math.floor(t/i)+1)*i;super.onStart(r)}extractStepsTicks(t,e){let i=[],o=this.stepResolution,r=Math.floor((t-this.startedAt)/o),c=this.startedAt+r*o;for(let a=c;a<=e;a+=o)this.shouldGenerate(a)&&(i.push(a),this.lastGeneratedTick=a);return i}getStep(t,e,i){let o=this.props.patterns[t];if(!o)throw Error("Pattern not found");let r=o.pages[e];if(!r)throw Error("Page not found");let c=r.steps[i];if(!c)throw Error("Step not found");return c}get ticksPerPage(){return this.stepResolution*this.props.stepsPerPage}getAbsolutePageFromTicks(t){if(this.startedAt===void 0)return 0;let e=t-this.startedAt;return Math.floor(e/this.ticksPerPage)}getStepNoInPage(t){if(this.startedAt===void 0)return 0;let i=(t-this.startedAt)%this.ticksPerPage;return Math.floor(i/this.stepResolution)}getPatternAndPageFromAbsolutePage(t){if(this.pageMapping.length===0)return{patternNo:0,pageNo:0};let e;return this.props.playbackMode==="loop"?e=t%this.pageMapping.length:e=Math.min(t,this.pageMapping.length-1),this.pageMapping[e]??{patternNo:0,pageNo:0}}generator(t,e){if(!this.isPlaying(t,e)||this.startedAt===void 0)return[];let i=this.extractStepsTicks(t,e);if(this.props.playbackMode==="oneShot"&&i.length>0){let o=i[i.length-1];if(o!==void 0){let r=this.getAbsolutePageFromTicks(o),c=this.getStepNoInPage(o);r>=this.pageMapping.length-1&&c>=this.props.stepsPerPage-1&&this.onStop(o+this.stepResolution)}}return i.map(o=>{let r=this.getAbsolutePageFromTicks(o),{patternNo:c,pageNo:a}=this.getPatternAndPageFromAbsolutePage(r),p=this.getStepNoInPage(o),v=this.getStep(c,a,p);return{ticks:o,time:0,contextTime:0,eventSourceId:this.id,stepNo:p,pageNo:a,patternNo:c,step:v}})}consumer(t){this.props.onEvent(t)}};export{O as PlaybackMode,u as Position,L as Resolution,y as StepSequencerSource,s as TPB,P as Transport,q as TransportState,U as divisionToFrequency,F as divisionToMilliseconds,x as divisionToTicks};
2
2
  //# sourceMappingURL=index.js.map