@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/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
|
-
|
|
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
|
-
|
|
121
|
+
**Important:** Keep your generator idempotent—the transport may call it with overlapping windows.
|
|
91
122
|
|
|
92
|
-
|
|
123
|
+
### Musical Time
|
|
93
124
|
|
|
94
|
-
-
|
|
95
|
-
-
|
|
96
|
-
- `
|
|
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
|
-
|
|
129
|
+
### UI Callbacks
|
|
101
130
|
|
|
102
|
-
|
|
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
|
-
|
|
133
|
+
- `transport.addClockCallback()` - Fires every ~20ms
|
|
109
134
|
|
|
110
|
-
|
|
135
|
+
## API Overview
|
|
136
|
+
|
|
137
|
+
### Transport Control
|
|
111
138
|
|
|
112
139
|
```ts
|
|
113
|
-
transport.
|
|
114
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
125
|
+
private sourceManager;
|
|
107
126
|
private _clockCallbacks;
|
|
108
|
-
|
|
127
|
+
private _propertyChangeCallbacks;
|
|
128
|
+
private onStartCallback;
|
|
129
|
+
private onStopCallback;
|
|
130
|
+
constructor(context: Readonly<Context>, params: TransportParams);
|
|
109
131
|
addClockCallback(callback: TransportClockCallback): void;
|
|
110
|
-
|
|
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
|
-
|
|
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
|