@dawcore/transport 0.0.3 → 0.0.4
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 +33 -1
- package/dist/index.d.mts +47 -4
- package/dist/index.d.ts +47 -4
- package/dist/index.js +185 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +185 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,6 +92,38 @@ transport.setMetronomeEnabled(true);
|
|
|
92
92
|
transport.play();
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
### Tempo Automation
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
const transport = new Transport(audioContext, { tempo: 100 });
|
|
99
|
+
|
|
100
|
+
// Linear ramp from 100 to 160 BPM over 8 bars
|
|
101
|
+
transport.setTempo(160, transport.barToTick(9), { interpolation: 'linear' });
|
|
102
|
+
|
|
103
|
+
// Query interpolated BPM at any position
|
|
104
|
+
transport.getTempo(transport.barToTick(5)); // 130 BPM (midway through ramp)
|
|
105
|
+
|
|
106
|
+
// Curved ramp: ease-in (slow start, fast end)
|
|
107
|
+
transport.clearTempos();
|
|
108
|
+
transport.setTempo(80);
|
|
109
|
+
transport.setTempo(160, transport.barToTick(9), {
|
|
110
|
+
interpolation: { type: 'curve', slope: 0.2 }, // concave
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Curved ramp: ease-out (fast start, slow end)
|
|
114
|
+
transport.clearTempos();
|
|
115
|
+
transport.setTempo(80);
|
|
116
|
+
transport.setTempo(160, transport.barToTick(9), {
|
|
117
|
+
interpolation: { type: 'curve', slope: 0.8 }, // convex
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Mix step and linear: jump to 80 BPM at bar 4, ramp to 140 at bar 8
|
|
121
|
+
transport.clearTempos();
|
|
122
|
+
transport.setTempo(120);
|
|
123
|
+
transport.setTempo(80, transport.barToTick(5)); // step (instant jump)
|
|
124
|
+
transport.setTempo(140, transport.barToTick(9), { interpolation: 'linear' }); // ramp
|
|
125
|
+
```
|
|
126
|
+
|
|
95
127
|
### Effects
|
|
96
128
|
|
|
97
129
|
```typescript
|
|
@@ -151,7 +183,7 @@ new Transport(audioContext: AudioContext, options?: TransportOptions)
|
|
|
151
183
|
- `setLoopSamples(enabled, startSample: Sample, endSample: Sample)` — Set loop region in samples (convenience)
|
|
152
184
|
|
|
153
185
|
**Tempo & Meter:**
|
|
154
|
-
- `setTempo(bpm, atTick?)` / `getTempo(atTick
|
|
186
|
+
- `setTempo(bpm, atTick?, options?)` / `getTempo(atTick?: Tick)` — options: `{ interpolation: 'step' | 'linear' | { type: 'curve', slope } }`
|
|
155
187
|
- `clearTempos()` — remove all tempo entries
|
|
156
188
|
- `setMeter(numerator, denominator, atTick?: Tick)` / `getMeter(atTick?: Tick)`
|
|
157
189
|
- `removeMeter(atTick: Tick)` / `clearMeters()`
|
package/dist/index.d.mts
CHANGED
|
@@ -57,11 +57,20 @@ interface MeterEntry {
|
|
|
57
57
|
/** Cached cumulative bar count from tick 0 to this entry. Derived — do not set manually. */
|
|
58
58
|
readonly barAtTick: number;
|
|
59
59
|
}
|
|
60
|
+
/** How to interpolate tempo from the previous entry to this one.
|
|
61
|
+
* 'step' = instant jump (default). 'linear' = linear ramp.
|
|
62
|
+
* { type: 'curve', slope } = Möbius-Ease curve (slope 0-1 exclusive). */
|
|
63
|
+
type TempoInterpolation = 'step' | 'linear' | {
|
|
64
|
+
type: 'curve';
|
|
65
|
+
slope: number;
|
|
66
|
+
};
|
|
60
67
|
interface TempoEntry {
|
|
61
68
|
/** Tick position where this tempo starts */
|
|
62
69
|
tick: Tick;
|
|
63
70
|
/** Beats per minute */
|
|
64
71
|
bpm: number;
|
|
72
|
+
/** How to arrive at this BPM from the previous entry */
|
|
73
|
+
readonly interpolation: TempoInterpolation;
|
|
65
74
|
/** Cached cumulative seconds up to this tick (for O(log n) lookup). Derived — do not set manually. */
|
|
66
75
|
readonly secondsAtTick: number;
|
|
67
76
|
}
|
|
@@ -95,19 +104,52 @@ declare class Clock {
|
|
|
95
104
|
isRunning(): boolean;
|
|
96
105
|
}
|
|
97
106
|
|
|
107
|
+
interface SetTempoOptions {
|
|
108
|
+
interpolation?: TempoInterpolation;
|
|
109
|
+
}
|
|
98
110
|
declare class TempoMap {
|
|
99
111
|
private _ppqn;
|
|
100
112
|
private _entries;
|
|
101
113
|
constructor(ppqn?: number, initialBpm?: number);
|
|
102
114
|
getTempo(atTick?: Tick): number;
|
|
103
|
-
setTempo(bpm: number, atTick?: Tick): void;
|
|
115
|
+
setTempo(bpm: number, atTick?: Tick, options?: SetTempoOptions): void;
|
|
104
116
|
ticksToSeconds(ticks: Tick): number;
|
|
105
117
|
secondsToTicks(seconds: number): Tick;
|
|
106
118
|
beatsToSeconds(beats: number): number;
|
|
107
119
|
secondsToBeats(seconds: number): number;
|
|
108
120
|
clearTempos(): void;
|
|
121
|
+
/** Get the interpolated BPM at a tick position */
|
|
122
|
+
private _getTempoAt;
|
|
109
123
|
private _ticksToSecondsInternal;
|
|
110
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Exact integration for a linear BPM ramp using the logarithmic formula.
|
|
126
|
+
* For bpm(t) = bpm0 + r*t where r = (bpm1-bpm0)/T:
|
|
127
|
+
* seconds = (T * 60) / (ppqn * (bpm1-bpm0)) * ln(bpmAtTick / bpm0)
|
|
128
|
+
*/
|
|
129
|
+
private _ticksToSecondsLinear;
|
|
130
|
+
/**
|
|
131
|
+
* Inverse of _ticksToSecondsLinear: given seconds, return ticks.
|
|
132
|
+
* Closed-form via exponential: bpmAtTick = bpm0 * exp(seconds * deltaBpm * ppqn / (60 * T))
|
|
133
|
+
* then ticks = (bpmAtTick - bpm0) * T / deltaBpm
|
|
134
|
+
*
|
|
135
|
+
* Note: exp(log(x)) has ~1 ULP floating-point error, so round-trips depend on
|
|
136
|
+
* Math.round() in the caller (secondsToTicks). This is sufficient for all tested
|
|
137
|
+
* BPM ranges (10–300 BPM) but is not algebraically exact like the previous
|
|
138
|
+
* trapezoidal/quadratic approach was.
|
|
139
|
+
*/
|
|
140
|
+
private _secondsToTicksLinear;
|
|
141
|
+
/**
|
|
142
|
+
* Subdivided trapezoidal integration for a Möbius-Ease tempo curve.
|
|
143
|
+
* The BPM at progress p is: bpm0 + curveNormalizedAt(p, slope) * (bpm1 - bpm0).
|
|
144
|
+
* We subdivide into CURVE_SUBDIVISIONS intervals and apply trapezoidal rule.
|
|
145
|
+
*/
|
|
146
|
+
private _ticksToSecondsCurve;
|
|
147
|
+
/**
|
|
148
|
+
* Inverse of _ticksToSecondsCurve: given seconds into a curved segment,
|
|
149
|
+
* return ticks. Uses binary search since there's no closed-form inverse.
|
|
150
|
+
*/
|
|
151
|
+
private _secondsToTicksCurve;
|
|
152
|
+
private _entryIndexAt;
|
|
111
153
|
private _recomputeCache;
|
|
112
154
|
}
|
|
113
155
|
|
|
@@ -314,6 +356,7 @@ declare class Transport {
|
|
|
314
356
|
private _playing;
|
|
315
357
|
private _endTime;
|
|
316
358
|
private _loopEnabled;
|
|
359
|
+
private _loopStartTick;
|
|
317
360
|
private _loopStartSeconds;
|
|
318
361
|
private _listeners;
|
|
319
362
|
constructor(audioContext: AudioContext, options?: TransportOptions);
|
|
@@ -339,7 +382,7 @@ declare class Transport {
|
|
|
339
382
|
setLoopSeconds(enabled: boolean, startSec: number, endSec: number): void;
|
|
340
383
|
/** Convenience — sets loop in samples */
|
|
341
384
|
setLoopSamples(enabled: boolean, startSample: Sample, endSample: Sample): void;
|
|
342
|
-
setTempo(bpm: number, atTick?: Tick): void;
|
|
385
|
+
setTempo(bpm: number, atTick?: Tick, options?: SetTempoOptions): void;
|
|
343
386
|
getTempo(atTick?: Tick): number;
|
|
344
387
|
setMeter(numerator: number, denominator: number, atTick?: Tick): void;
|
|
345
388
|
getMeter(atTick?: Tick): MeterSignature;
|
|
@@ -391,4 +434,4 @@ declare class NativePlayoutAdapter implements PlayoutAdapter {
|
|
|
391
434
|
dispose(): void;
|
|
392
435
|
}
|
|
393
436
|
|
|
394
|
-
export { type ClipEvent, ClipPlayer, Clock, MasterNode, type MeterEntry, MeterMap, type MeterSignature, type MetronomeEvent, MetronomePlayer, NativePlayoutAdapter, type Sample, SampleTimeline, Scheduler, type SchedulerEvent, type SchedulerListener, type SchedulerOptions, type TempoEntry, TempoMap, type Tick, Timer, TrackNode, Transport, type TransportEvents, type TransportOptions, type TransportPosition };
|
|
437
|
+
export { type ClipEvent, ClipPlayer, Clock, MasterNode, type MeterEntry, MeterMap, type MeterSignature, type MetronomeEvent, MetronomePlayer, NativePlayoutAdapter, type Sample, SampleTimeline, Scheduler, type SchedulerEvent, type SchedulerListener, type SchedulerOptions, type SetTempoOptions, type TempoEntry, type TempoInterpolation, TempoMap, type Tick, Timer, TrackNode, Transport, type TransportEvents, type TransportOptions, type TransportPosition };
|
package/dist/index.d.ts
CHANGED
|
@@ -57,11 +57,20 @@ interface MeterEntry {
|
|
|
57
57
|
/** Cached cumulative bar count from tick 0 to this entry. Derived — do not set manually. */
|
|
58
58
|
readonly barAtTick: number;
|
|
59
59
|
}
|
|
60
|
+
/** How to interpolate tempo from the previous entry to this one.
|
|
61
|
+
* 'step' = instant jump (default). 'linear' = linear ramp.
|
|
62
|
+
* { type: 'curve', slope } = Möbius-Ease curve (slope 0-1 exclusive). */
|
|
63
|
+
type TempoInterpolation = 'step' | 'linear' | {
|
|
64
|
+
type: 'curve';
|
|
65
|
+
slope: number;
|
|
66
|
+
};
|
|
60
67
|
interface TempoEntry {
|
|
61
68
|
/** Tick position where this tempo starts */
|
|
62
69
|
tick: Tick;
|
|
63
70
|
/** Beats per minute */
|
|
64
71
|
bpm: number;
|
|
72
|
+
/** How to arrive at this BPM from the previous entry */
|
|
73
|
+
readonly interpolation: TempoInterpolation;
|
|
65
74
|
/** Cached cumulative seconds up to this tick (for O(log n) lookup). Derived — do not set manually. */
|
|
66
75
|
readonly secondsAtTick: number;
|
|
67
76
|
}
|
|
@@ -95,19 +104,52 @@ declare class Clock {
|
|
|
95
104
|
isRunning(): boolean;
|
|
96
105
|
}
|
|
97
106
|
|
|
107
|
+
interface SetTempoOptions {
|
|
108
|
+
interpolation?: TempoInterpolation;
|
|
109
|
+
}
|
|
98
110
|
declare class TempoMap {
|
|
99
111
|
private _ppqn;
|
|
100
112
|
private _entries;
|
|
101
113
|
constructor(ppqn?: number, initialBpm?: number);
|
|
102
114
|
getTempo(atTick?: Tick): number;
|
|
103
|
-
setTempo(bpm: number, atTick?: Tick): void;
|
|
115
|
+
setTempo(bpm: number, atTick?: Tick, options?: SetTempoOptions): void;
|
|
104
116
|
ticksToSeconds(ticks: Tick): number;
|
|
105
117
|
secondsToTicks(seconds: number): Tick;
|
|
106
118
|
beatsToSeconds(beats: number): number;
|
|
107
119
|
secondsToBeats(seconds: number): number;
|
|
108
120
|
clearTempos(): void;
|
|
121
|
+
/** Get the interpolated BPM at a tick position */
|
|
122
|
+
private _getTempoAt;
|
|
109
123
|
private _ticksToSecondsInternal;
|
|
110
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Exact integration for a linear BPM ramp using the logarithmic formula.
|
|
126
|
+
* For bpm(t) = bpm0 + r*t where r = (bpm1-bpm0)/T:
|
|
127
|
+
* seconds = (T * 60) / (ppqn * (bpm1-bpm0)) * ln(bpmAtTick / bpm0)
|
|
128
|
+
*/
|
|
129
|
+
private _ticksToSecondsLinear;
|
|
130
|
+
/**
|
|
131
|
+
* Inverse of _ticksToSecondsLinear: given seconds, return ticks.
|
|
132
|
+
* Closed-form via exponential: bpmAtTick = bpm0 * exp(seconds * deltaBpm * ppqn / (60 * T))
|
|
133
|
+
* then ticks = (bpmAtTick - bpm0) * T / deltaBpm
|
|
134
|
+
*
|
|
135
|
+
* Note: exp(log(x)) has ~1 ULP floating-point error, so round-trips depend on
|
|
136
|
+
* Math.round() in the caller (secondsToTicks). This is sufficient for all tested
|
|
137
|
+
* BPM ranges (10–300 BPM) but is not algebraically exact like the previous
|
|
138
|
+
* trapezoidal/quadratic approach was.
|
|
139
|
+
*/
|
|
140
|
+
private _secondsToTicksLinear;
|
|
141
|
+
/**
|
|
142
|
+
* Subdivided trapezoidal integration for a Möbius-Ease tempo curve.
|
|
143
|
+
* The BPM at progress p is: bpm0 + curveNormalizedAt(p, slope) * (bpm1 - bpm0).
|
|
144
|
+
* We subdivide into CURVE_SUBDIVISIONS intervals and apply trapezoidal rule.
|
|
145
|
+
*/
|
|
146
|
+
private _ticksToSecondsCurve;
|
|
147
|
+
/**
|
|
148
|
+
* Inverse of _ticksToSecondsCurve: given seconds into a curved segment,
|
|
149
|
+
* return ticks. Uses binary search since there's no closed-form inverse.
|
|
150
|
+
*/
|
|
151
|
+
private _secondsToTicksCurve;
|
|
152
|
+
private _entryIndexAt;
|
|
111
153
|
private _recomputeCache;
|
|
112
154
|
}
|
|
113
155
|
|
|
@@ -314,6 +356,7 @@ declare class Transport {
|
|
|
314
356
|
private _playing;
|
|
315
357
|
private _endTime;
|
|
316
358
|
private _loopEnabled;
|
|
359
|
+
private _loopStartTick;
|
|
317
360
|
private _loopStartSeconds;
|
|
318
361
|
private _listeners;
|
|
319
362
|
constructor(audioContext: AudioContext, options?: TransportOptions);
|
|
@@ -339,7 +382,7 @@ declare class Transport {
|
|
|
339
382
|
setLoopSeconds(enabled: boolean, startSec: number, endSec: number): void;
|
|
340
383
|
/** Convenience — sets loop in samples */
|
|
341
384
|
setLoopSamples(enabled: boolean, startSample: Sample, endSample: Sample): void;
|
|
342
|
-
setTempo(bpm: number, atTick?: Tick): void;
|
|
385
|
+
setTempo(bpm: number, atTick?: Tick, options?: SetTempoOptions): void;
|
|
343
386
|
getTempo(atTick?: Tick): number;
|
|
344
387
|
setMeter(numerator: number, denominator: number, atTick?: Tick): void;
|
|
345
388
|
getMeter(atTick?: Tick): MeterSignature;
|
|
@@ -391,4 +434,4 @@ declare class NativePlayoutAdapter implements PlayoutAdapter {
|
|
|
391
434
|
dispose(): void;
|
|
392
435
|
}
|
|
393
436
|
|
|
394
|
-
export { type ClipEvent, ClipPlayer, Clock, MasterNode, type MeterEntry, MeterMap, type MeterSignature, type MetronomeEvent, MetronomePlayer, NativePlayoutAdapter, type Sample, SampleTimeline, Scheduler, type SchedulerEvent, type SchedulerListener, type SchedulerOptions, type TempoEntry, TempoMap, type Tick, Timer, TrackNode, Transport, type TransportEvents, type TransportOptions, type TransportPosition };
|
|
437
|
+
export { type ClipEvent, ClipPlayer, Clock, MasterNode, type MeterEntry, MeterMap, type MeterSignature, type MetronomeEvent, MetronomePlayer, NativePlayoutAdapter, type Sample, SampleTimeline, Scheduler, type SchedulerEvent, type SchedulerListener, type SchedulerOptions, type SetTempoOptions, type TempoEntry, type TempoInterpolation, TempoMap, type Tick, Timer, TrackNode, Transport, type TransportEvents, type TransportOptions, type TransportPosition };
|
package/dist/index.js
CHANGED
|
@@ -252,28 +252,43 @@ var SampleTimeline = class {
|
|
|
252
252
|
};
|
|
253
253
|
|
|
254
254
|
// src/timeline/tempo-map.ts
|
|
255
|
+
var CURVE_EPSILON = 1e-15;
|
|
256
|
+
var CURVE_SUBDIVISIONS = 64;
|
|
257
|
+
function curveNormalizedAt(x, slope) {
|
|
258
|
+
if (slope > 0.499999 && slope < 0.500001) return x;
|
|
259
|
+
const p = Math.max(CURVE_EPSILON, Math.min(1 - CURVE_EPSILON, slope));
|
|
260
|
+
return p * p / (1 - p * 2) * (Math.pow((1 - p) / p, 2 * x) - 1);
|
|
261
|
+
}
|
|
255
262
|
var TempoMap = class {
|
|
256
263
|
constructor(ppqn = 960, initialBpm = 120) {
|
|
257
264
|
this._ppqn = ppqn;
|
|
258
|
-
this._entries = [{ tick: 0, bpm: initialBpm, secondsAtTick: 0 }];
|
|
265
|
+
this._entries = [{ tick: 0, bpm: initialBpm, interpolation: "step", secondsAtTick: 0 }];
|
|
259
266
|
}
|
|
260
267
|
getTempo(atTick = 0) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
268
|
+
return this._getTempoAt(atTick);
|
|
269
|
+
}
|
|
270
|
+
setTempo(bpm, atTick = 0, options) {
|
|
271
|
+
const interpolation = options?.interpolation ?? "step";
|
|
272
|
+
if (typeof interpolation === "object" && interpolation.type === "curve") {
|
|
273
|
+
const s = interpolation.slope;
|
|
274
|
+
if (!Number.isFinite(s) || s <= 0 || s >= 1) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
"[waveform-playlist] TempoMap: curve slope must be between 0 and 1 (exclusive), got " + s
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
265
280
|
if (atTick === 0) {
|
|
266
|
-
this._entries[0] = { ...this._entries[0], bpm };
|
|
281
|
+
this._entries[0] = { ...this._entries[0], bpm, interpolation: "step" };
|
|
267
282
|
this._recomputeCache(0);
|
|
268
283
|
return;
|
|
269
284
|
}
|
|
270
285
|
let i = this._entries.length - 1;
|
|
271
286
|
while (i > 0 && this._entries[i].tick > atTick) i--;
|
|
272
287
|
if (this._entries[i].tick === atTick) {
|
|
273
|
-
this._entries[i] = { ...this._entries[i], bpm };
|
|
288
|
+
this._entries[i] = { ...this._entries[i], bpm, interpolation };
|
|
274
289
|
} else {
|
|
275
290
|
const secondsAtTick = this._ticksToSecondsInternal(atTick);
|
|
276
|
-
this._entries.splice(i + 1, 0, { tick: atTick, bpm, secondsAtTick });
|
|
291
|
+
this._entries.splice(i + 1, 0, { tick: atTick, bpm, interpolation, secondsAtTick });
|
|
277
292
|
i = i + 1;
|
|
278
293
|
}
|
|
279
294
|
this._recomputeCache(i);
|
|
@@ -294,6 +309,28 @@ var TempoMap = class {
|
|
|
294
309
|
}
|
|
295
310
|
const entry = this._entries[lo];
|
|
296
311
|
const secondsIntoSegment = seconds - entry.secondsAtTick;
|
|
312
|
+
const nextEntry = lo < this._entries.length - 1 ? this._entries[lo + 1] : null;
|
|
313
|
+
if (nextEntry && nextEntry.interpolation === "linear") {
|
|
314
|
+
return Math.round(
|
|
315
|
+
entry.tick + this._secondsToTicksLinear(
|
|
316
|
+
secondsIntoSegment,
|
|
317
|
+
entry.bpm,
|
|
318
|
+
nextEntry.bpm,
|
|
319
|
+
nextEntry.tick - entry.tick
|
|
320
|
+
)
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
if (nextEntry && typeof nextEntry.interpolation === "object") {
|
|
324
|
+
return Math.round(
|
|
325
|
+
entry.tick + this._secondsToTicksCurve(
|
|
326
|
+
secondsIntoSegment,
|
|
327
|
+
entry.bpm,
|
|
328
|
+
nextEntry.bpm,
|
|
329
|
+
nextEntry.tick - entry.tick,
|
|
330
|
+
nextEntry.interpolation.slope
|
|
331
|
+
)
|
|
332
|
+
);
|
|
333
|
+
}
|
|
297
334
|
const ticksPerSecond = entry.bpm / 60 * this._ppqn;
|
|
298
335
|
return Math.round(entry.tick + secondsIntoSegment * ticksPerSecond);
|
|
299
336
|
}
|
|
@@ -305,15 +342,122 @@ var TempoMap = class {
|
|
|
305
342
|
}
|
|
306
343
|
clearTempos() {
|
|
307
344
|
const first = this._entries[0];
|
|
308
|
-
this._entries = [{ tick: 0, bpm: first.bpm, secondsAtTick: 0 }];
|
|
345
|
+
this._entries = [{ tick: 0, bpm: first.bpm, interpolation: "step", secondsAtTick: 0 }];
|
|
346
|
+
}
|
|
347
|
+
/** Get the interpolated BPM at a tick position */
|
|
348
|
+
_getTempoAt(atTick) {
|
|
349
|
+
const entryIndex = this._entryIndexAt(atTick);
|
|
350
|
+
const entry = this._entries[entryIndex];
|
|
351
|
+
const nextEntry = entryIndex < this._entries.length - 1 ? this._entries[entryIndex + 1] : null;
|
|
352
|
+
if (nextEntry && nextEntry.interpolation !== "step") {
|
|
353
|
+
const segmentTicks = nextEntry.tick - entry.tick;
|
|
354
|
+
const ticksInto = atTick - entry.tick;
|
|
355
|
+
if (segmentTicks > 0) {
|
|
356
|
+
const progress = ticksInto / segmentTicks;
|
|
357
|
+
if (nextEntry.interpolation === "linear") {
|
|
358
|
+
return entry.bpm + (nextEntry.bpm - entry.bpm) * progress;
|
|
359
|
+
}
|
|
360
|
+
const t = curveNormalizedAt(progress, nextEntry.interpolation.slope);
|
|
361
|
+
return entry.bpm + (nextEntry.bpm - entry.bpm) * t;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return entry.bpm;
|
|
309
365
|
}
|
|
310
366
|
_ticksToSecondsInternal(ticks) {
|
|
311
|
-
const
|
|
367
|
+
const entryIndex = this._entryIndexAt(ticks);
|
|
368
|
+
const entry = this._entries[entryIndex];
|
|
312
369
|
const ticksIntoSegment = ticks - entry.tick;
|
|
370
|
+
const nextEntry = entryIndex < this._entries.length - 1 ? this._entries[entryIndex + 1] : null;
|
|
371
|
+
if (nextEntry && nextEntry.interpolation === "linear") {
|
|
372
|
+
const segmentTicks = nextEntry.tick - entry.tick;
|
|
373
|
+
return entry.secondsAtTick + this._ticksToSecondsLinear(ticksIntoSegment, entry.bpm, nextEntry.bpm, segmentTicks);
|
|
374
|
+
}
|
|
375
|
+
if (nextEntry && typeof nextEntry.interpolation === "object") {
|
|
376
|
+
const segmentTicks = nextEntry.tick - entry.tick;
|
|
377
|
+
return entry.secondsAtTick + this._ticksToSecondsCurve(
|
|
378
|
+
ticksIntoSegment,
|
|
379
|
+
entry.bpm,
|
|
380
|
+
nextEntry.bpm,
|
|
381
|
+
segmentTicks,
|
|
382
|
+
nextEntry.interpolation.slope
|
|
383
|
+
);
|
|
384
|
+
}
|
|
313
385
|
const secondsPerTick = 60 / (entry.bpm * this._ppqn);
|
|
314
386
|
return entry.secondsAtTick + ticksIntoSegment * secondsPerTick;
|
|
315
387
|
}
|
|
316
|
-
|
|
388
|
+
/**
|
|
389
|
+
* Exact integration for a linear BPM ramp using the logarithmic formula.
|
|
390
|
+
* For bpm(t) = bpm0 + r*t where r = (bpm1-bpm0)/T:
|
|
391
|
+
* seconds = (T * 60) / (ppqn * (bpm1-bpm0)) * ln(bpmAtTick / bpm0)
|
|
392
|
+
*/
|
|
393
|
+
_ticksToSecondsLinear(ticks, bpm0, bpm1, totalSegmentTicks) {
|
|
394
|
+
if (totalSegmentTicks === 0) return 0;
|
|
395
|
+
const bpmAtTick = bpm0 + (bpm1 - bpm0) * (ticks / totalSegmentTicks);
|
|
396
|
+
if (Math.abs(bpm1 - bpm0) < 1e-10) {
|
|
397
|
+
return ticks * 60 / (bpm0 * this._ppqn);
|
|
398
|
+
}
|
|
399
|
+
const deltaBpm = bpm1 - bpm0;
|
|
400
|
+
return totalSegmentTicks * 60 / (this._ppqn * deltaBpm) * Math.log(bpmAtTick / bpm0);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Inverse of _ticksToSecondsLinear: given seconds, return ticks.
|
|
404
|
+
* Closed-form via exponential: bpmAtTick = bpm0 * exp(seconds * deltaBpm * ppqn / (60 * T))
|
|
405
|
+
* then ticks = (bpmAtTick - bpm0) * T / deltaBpm
|
|
406
|
+
*
|
|
407
|
+
* Note: exp(log(x)) has ~1 ULP floating-point error, so round-trips depend on
|
|
408
|
+
* Math.round() in the caller (secondsToTicks). This is sufficient for all tested
|
|
409
|
+
* BPM ranges (10–300 BPM) but is not algebraically exact like the previous
|
|
410
|
+
* trapezoidal/quadratic approach was.
|
|
411
|
+
*/
|
|
412
|
+
_secondsToTicksLinear(seconds, bpm0, bpm1, totalSegmentTicks) {
|
|
413
|
+
if (totalSegmentTicks === 0 || seconds === 0) return 0;
|
|
414
|
+
if (Math.abs(bpm1 - bpm0) < 1e-10) {
|
|
415
|
+
return seconds * bpm0 * this._ppqn / 60;
|
|
416
|
+
}
|
|
417
|
+
const deltaBpm = bpm1 - bpm0;
|
|
418
|
+
const bpmAtTick = bpm0 * Math.exp(seconds * deltaBpm * this._ppqn / (60 * totalSegmentTicks));
|
|
419
|
+
return (bpmAtTick - bpm0) / deltaBpm * totalSegmentTicks;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Subdivided trapezoidal integration for a Möbius-Ease tempo curve.
|
|
423
|
+
* The BPM at progress p is: bpm0 + curveNormalizedAt(p, slope) * (bpm1 - bpm0).
|
|
424
|
+
* We subdivide into CURVE_SUBDIVISIONS intervals and apply trapezoidal rule.
|
|
425
|
+
*/
|
|
426
|
+
_ticksToSecondsCurve(ticks, bpm0, bpm1, totalSegmentTicks, slope) {
|
|
427
|
+
if (totalSegmentTicks === 0 || ticks === 0) return 0;
|
|
428
|
+
const n = CURVE_SUBDIVISIONS;
|
|
429
|
+
const dt = ticks / n;
|
|
430
|
+
let seconds = 0;
|
|
431
|
+
let prevBpm = bpm0;
|
|
432
|
+
for (let i = 1; i <= n; i++) {
|
|
433
|
+
const progress = dt * i / totalSegmentTicks;
|
|
434
|
+
const curBpm = bpm0 + curveNormalizedAt(progress, slope) * (bpm1 - bpm0);
|
|
435
|
+
seconds += dt * 60 / this._ppqn * (1 / prevBpm + 1 / curBpm) / 2;
|
|
436
|
+
prevBpm = curBpm;
|
|
437
|
+
}
|
|
438
|
+
return seconds;
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Inverse of _ticksToSecondsCurve: given seconds into a curved segment,
|
|
442
|
+
* return ticks. Uses binary search since there's no closed-form inverse.
|
|
443
|
+
*/
|
|
444
|
+
_secondsToTicksCurve(seconds, bpm0, bpm1, totalSegmentTicks, slope) {
|
|
445
|
+
if (totalSegmentTicks === 0 || seconds === 0) return 0;
|
|
446
|
+
const iterations = Math.min(40, Math.max(1, Math.ceil(Math.log2(2 * totalSegmentTicks))));
|
|
447
|
+
let lo = 0;
|
|
448
|
+
let hi = totalSegmentTicks;
|
|
449
|
+
for (let i = 0; i < iterations; i++) {
|
|
450
|
+
const mid = (lo + hi) / 2;
|
|
451
|
+
const midSeconds = this._ticksToSecondsCurve(mid, bpm0, bpm1, totalSegmentTicks, slope);
|
|
452
|
+
if (midSeconds < seconds) {
|
|
453
|
+
lo = mid;
|
|
454
|
+
} else {
|
|
455
|
+
hi = mid;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return (lo + hi) / 2;
|
|
459
|
+
}
|
|
460
|
+
_entryIndexAt(tick) {
|
|
317
461
|
let lo = 0;
|
|
318
462
|
let hi = this._entries.length - 1;
|
|
319
463
|
while (lo < hi) {
|
|
@@ -324,16 +468,31 @@ var TempoMap = class {
|
|
|
324
468
|
hi = mid - 1;
|
|
325
469
|
}
|
|
326
470
|
}
|
|
327
|
-
return
|
|
471
|
+
return lo;
|
|
328
472
|
}
|
|
329
473
|
_recomputeCache(fromIndex) {
|
|
330
474
|
for (let i = Math.max(1, fromIndex); i < this._entries.length; i++) {
|
|
331
475
|
const prev = this._entries[i - 1];
|
|
332
476
|
const tickDelta = this._entries[i].tick - prev.tick;
|
|
333
|
-
const
|
|
477
|
+
const entry = this._entries[i];
|
|
478
|
+
let segmentSeconds;
|
|
479
|
+
if (entry.interpolation === "linear") {
|
|
480
|
+
segmentSeconds = this._ticksToSecondsLinear(tickDelta, prev.bpm, entry.bpm, tickDelta);
|
|
481
|
+
} else if (typeof entry.interpolation === "object") {
|
|
482
|
+
segmentSeconds = this._ticksToSecondsCurve(
|
|
483
|
+
tickDelta,
|
|
484
|
+
prev.bpm,
|
|
485
|
+
entry.bpm,
|
|
486
|
+
tickDelta,
|
|
487
|
+
entry.interpolation.slope
|
|
488
|
+
);
|
|
489
|
+
} else {
|
|
490
|
+
const secondsPerTick = 60 / (prev.bpm * this._ppqn);
|
|
491
|
+
segmentSeconds = tickDelta * secondsPerTick;
|
|
492
|
+
}
|
|
334
493
|
this._entries[i] = {
|
|
335
|
-
...
|
|
336
|
-
secondsAtTick: prev.secondsAtTick +
|
|
494
|
+
...entry,
|
|
495
|
+
secondsAtTick: prev.secondsAtTick + segmentSeconds
|
|
337
496
|
};
|
|
338
497
|
}
|
|
339
498
|
}
|
|
@@ -900,6 +1059,7 @@ var Transport = class _Transport {
|
|
|
900
1059
|
this._mutedTrackIds = /* @__PURE__ */ new Set();
|
|
901
1060
|
this._playing = false;
|
|
902
1061
|
this._loopEnabled = false;
|
|
1062
|
+
this._loopStartTick = 0;
|
|
903
1063
|
this._loopStartSeconds = 0;
|
|
904
1064
|
this._listeners = /* @__PURE__ */ new Map();
|
|
905
1065
|
this._audioContext = audioContext;
|
|
@@ -1117,6 +1277,7 @@ var Transport = class _Transport {
|
|
|
1117
1277
|
return;
|
|
1118
1278
|
}
|
|
1119
1279
|
this._loopEnabled = enabled;
|
|
1280
|
+
this._loopStartTick = startTick;
|
|
1120
1281
|
this._loopStartSeconds = this._tempoMap.ticksToSeconds(startTick);
|
|
1121
1282
|
this._scheduler.setLoop(enabled, startTick, endTick);
|
|
1122
1283
|
this._clipPlayer.setLoop(enabled, startTick, endTick);
|
|
@@ -1145,14 +1306,18 @@ var Transport = class _Transport {
|
|
|
1145
1306
|
const startTick = this._sampleTimeline.samplesToTicks(startSample);
|
|
1146
1307
|
const endTick = this._sampleTimeline.samplesToTicks(endSample);
|
|
1147
1308
|
this._loopEnabled = enabled;
|
|
1309
|
+
this._loopStartTick = startTick;
|
|
1148
1310
|
this._loopStartSeconds = this._tempoMap.ticksToSeconds(startTick);
|
|
1149
1311
|
this._clipPlayer.setLoopSamples(enabled, startSample, endSample);
|
|
1150
1312
|
this._scheduler.setLoop(enabled, startTick, endTick);
|
|
1151
1313
|
this._emit("loop");
|
|
1152
1314
|
}
|
|
1153
1315
|
// --- Tempo ---
|
|
1154
|
-
setTempo(bpm, atTick) {
|
|
1155
|
-
this._tempoMap.setTempo(bpm, atTick);
|
|
1316
|
+
setTempo(bpm, atTick, options) {
|
|
1317
|
+
this._tempoMap.setTempo(bpm, atTick, options);
|
|
1318
|
+
if (this._loopEnabled) {
|
|
1319
|
+
this._loopStartSeconds = this._tempoMap.ticksToSeconds(this._loopStartTick);
|
|
1320
|
+
}
|
|
1156
1321
|
this._emit("tempochange");
|
|
1157
1322
|
}
|
|
1158
1323
|
getTempo(atTick) {
|
|
@@ -1176,6 +1341,9 @@ var Transport = class _Transport {
|
|
|
1176
1341
|
}
|
|
1177
1342
|
clearTempos() {
|
|
1178
1343
|
this._tempoMap.clearTempos();
|
|
1344
|
+
if (this._loopEnabled) {
|
|
1345
|
+
this._loopStartSeconds = this._tempoMap.ticksToSeconds(this._loopStartTick);
|
|
1346
|
+
}
|
|
1179
1347
|
this._emit("tempochange");
|
|
1180
1348
|
}
|
|
1181
1349
|
barToTick(bar) {
|