@waveform-playlist/engine 7.1.3

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/dist/index.js ADDED
@@ -0,0 +1,489 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ PlaylistEngine: () => PlaylistEngine,
24
+ calculateDuration: () => calculateDuration,
25
+ calculateSplitPoint: () => calculateSplitPoint,
26
+ calculateViewportBounds: () => calculateViewportBounds,
27
+ calculateZoomScrollPosition: () => calculateZoomScrollPosition,
28
+ canSplitAt: () => canSplitAt,
29
+ clampSeekPosition: () => clampSeekPosition,
30
+ constrainBoundaryTrim: () => constrainBoundaryTrim,
31
+ constrainClipDrag: () => constrainClipDrag,
32
+ findClosestZoomIndex: () => findClosestZoomIndex,
33
+ getVisibleChunkIndices: () => getVisibleChunkIndices,
34
+ shouldUpdateViewport: () => shouldUpdateViewport,
35
+ splitClip: () => splitClip
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/operations/clipOperations.ts
40
+ var import_core = require("@waveform-playlist/core");
41
+ function constrainClipDrag(clip, deltaSamples, sortedClips, clipIndex) {
42
+ let delta = deltaSamples;
43
+ const minDelta = -clip.startSample;
44
+ delta = Math.max(delta, minDelta);
45
+ if (clipIndex > 0) {
46
+ const prevClip = sortedClips[clipIndex - 1];
47
+ const prevClipEnd = prevClip.startSample + prevClip.durationSamples;
48
+ const minDeltaPrev = prevClipEnd - clip.startSample;
49
+ delta = Math.max(delta, minDeltaPrev);
50
+ }
51
+ if (clipIndex < sortedClips.length - 1) {
52
+ const nextClip = sortedClips[clipIndex + 1];
53
+ const maxDeltaNext = nextClip.startSample - (clip.startSample + clip.durationSamples);
54
+ delta = Math.min(delta, maxDeltaNext);
55
+ }
56
+ return delta;
57
+ }
58
+ function constrainBoundaryTrim(clip, deltaSamples, boundary, sortedClips, clipIndex, minDurationSamples) {
59
+ let delta = deltaSamples;
60
+ if (boundary === "left") {
61
+ delta = Math.max(delta, -clip.startSample);
62
+ delta = Math.max(delta, -clip.offsetSamples);
63
+ if (clipIndex > 0) {
64
+ const prevClip = sortedClips[clipIndex - 1];
65
+ const prevClipEnd = prevClip.startSample + prevClip.durationSamples;
66
+ delta = Math.max(delta, prevClipEnd - clip.startSample);
67
+ }
68
+ delta = Math.min(delta, clip.durationSamples - minDurationSamples);
69
+ } else {
70
+ delta = Math.max(delta, minDurationSamples - clip.durationSamples);
71
+ delta = Math.min(delta, clip.sourceDurationSamples - clip.offsetSamples - clip.durationSamples);
72
+ if (clipIndex < sortedClips.length - 1) {
73
+ const nextClip = sortedClips[clipIndex + 1];
74
+ delta = Math.min(delta, nextClip.startSample - clip.startSample - clip.durationSamples);
75
+ }
76
+ }
77
+ return delta;
78
+ }
79
+ function calculateSplitPoint(splitSample, samplesPerPixel) {
80
+ return Math.floor(splitSample / samplesPerPixel) * samplesPerPixel;
81
+ }
82
+ function splitClip(clip, splitSample) {
83
+ const leftDuration = splitSample - clip.startSample;
84
+ const rightDuration = clip.durationSamples - leftDuration;
85
+ const leftName = clip.name ? `${clip.name} (1)` : void 0;
86
+ const rightName = clip.name ? `${clip.name} (2)` : void 0;
87
+ const left = (0, import_core.createClip)({
88
+ startSample: clip.startSample,
89
+ durationSamples: leftDuration,
90
+ offsetSamples: clip.offsetSamples,
91
+ sampleRate: clip.sampleRate,
92
+ sourceDurationSamples: clip.sourceDurationSamples,
93
+ gain: clip.gain,
94
+ name: leftName,
95
+ color: clip.color,
96
+ fadeIn: clip.fadeIn,
97
+ audioBuffer: clip.audioBuffer,
98
+ waveformData: clip.waveformData
99
+ });
100
+ const right = (0, import_core.createClip)({
101
+ startSample: splitSample,
102
+ durationSamples: rightDuration,
103
+ offsetSamples: clip.offsetSamples + leftDuration,
104
+ sampleRate: clip.sampleRate,
105
+ sourceDurationSamples: clip.sourceDurationSamples,
106
+ gain: clip.gain,
107
+ name: rightName,
108
+ color: clip.color,
109
+ fadeOut: clip.fadeOut,
110
+ audioBuffer: clip.audioBuffer,
111
+ waveformData: clip.waveformData
112
+ });
113
+ return { left, right };
114
+ }
115
+ function canSplitAt(clip, sample, minDurationSamples) {
116
+ const clipEnd = clip.startSample + clip.durationSamples;
117
+ if (sample <= clip.startSample || sample >= clipEnd) {
118
+ return false;
119
+ }
120
+ const leftDuration = sample - clip.startSample;
121
+ const rightDuration = clipEnd - sample;
122
+ return leftDuration >= minDurationSamples && rightDuration >= minDurationSamples;
123
+ }
124
+
125
+ // src/operations/viewportOperations.ts
126
+ function calculateViewportBounds(scrollLeft, containerWidth, bufferRatio = 1.5) {
127
+ const buffer = containerWidth * bufferRatio;
128
+ return {
129
+ visibleStart: Math.max(0, scrollLeft - buffer),
130
+ visibleEnd: scrollLeft + containerWidth + buffer
131
+ };
132
+ }
133
+ function getVisibleChunkIndices(totalWidth, chunkWidth, visibleStart, visibleEnd) {
134
+ const totalChunks = Math.ceil(totalWidth / chunkWidth);
135
+ const indices = [];
136
+ for (let i = 0; i < totalChunks; i++) {
137
+ const chunkLeft = i * chunkWidth;
138
+ const thisChunkWidth = Math.min(totalWidth - chunkLeft, chunkWidth);
139
+ const chunkEnd = chunkLeft + thisChunkWidth;
140
+ if (chunkEnd <= visibleStart || chunkLeft >= visibleEnd) {
141
+ continue;
142
+ }
143
+ indices.push(i);
144
+ }
145
+ return indices;
146
+ }
147
+ function shouldUpdateViewport(oldScrollLeft, newScrollLeft, threshold = 100) {
148
+ return Math.abs(oldScrollLeft - newScrollLeft) >= threshold;
149
+ }
150
+
151
+ // src/operations/timelineOperations.ts
152
+ function calculateDuration(tracks) {
153
+ let maxDuration = 0;
154
+ for (const track of tracks) {
155
+ for (const clip of track.clips) {
156
+ const clipEndSample = clip.startSample + clip.durationSamples;
157
+ const clipEnd = clipEndSample / clip.sampleRate;
158
+ maxDuration = Math.max(maxDuration, clipEnd);
159
+ }
160
+ }
161
+ return maxDuration;
162
+ }
163
+ function findClosestZoomIndex(targetSamplesPerPixel, zoomLevels) {
164
+ if (zoomLevels.length === 0) return 0;
165
+ let bestIndex = 0;
166
+ let bestDiff = Math.abs(zoomLevels[0] - targetSamplesPerPixel);
167
+ for (let i = 1; i < zoomLevels.length; i++) {
168
+ const diff = Math.abs(zoomLevels[i] - targetSamplesPerPixel);
169
+ if (diff < bestDiff) {
170
+ bestDiff = diff;
171
+ bestIndex = i;
172
+ }
173
+ }
174
+ return bestIndex;
175
+ }
176
+ function calculateZoomScrollPosition(oldSamplesPerPixel, newSamplesPerPixel, scrollLeft, containerWidth, sampleRate, controlWidth = 0) {
177
+ const centerPixel = scrollLeft + containerWidth / 2 - controlWidth;
178
+ const centerTime = centerPixel * oldSamplesPerPixel / sampleRate;
179
+ const newCenterPixel = centerTime * sampleRate / newSamplesPerPixel;
180
+ const newScrollLeft = newCenterPixel + controlWidth - containerWidth / 2;
181
+ return Math.max(0, newScrollLeft);
182
+ }
183
+ function clampSeekPosition(time, duration) {
184
+ return Math.max(0, Math.min(time, duration));
185
+ }
186
+
187
+ // src/PlaylistEngine.ts
188
+ var import_core2 = require("@waveform-playlist/core");
189
+ var DEFAULT_SAMPLE_RATE = 44100;
190
+ var DEFAULT_SAMPLES_PER_PIXEL = 1e3;
191
+ var DEFAULT_ZOOM_LEVELS = [256, 512, 1024, 2048, 4096, 8192];
192
+ var DEFAULT_MIN_DURATION_SECONDS = 0.1;
193
+ var PlaylistEngine = class {
194
+ constructor(options = {}) {
195
+ this._tracks = [];
196
+ this._currentTime = 0;
197
+ this._isPlaying = false;
198
+ this._selectedTrackId = null;
199
+ this._animFrameId = null;
200
+ this._disposed = false;
201
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
202
+ this._listeners = /* @__PURE__ */ new Map();
203
+ this._sampleRate = options.sampleRate ?? DEFAULT_SAMPLE_RATE;
204
+ this._zoomLevels = [...options.zoomLevels ?? DEFAULT_ZOOM_LEVELS];
205
+ this._adapter = options.adapter ?? null;
206
+ if (this._zoomLevels.length === 0) {
207
+ throw new Error("PlaylistEngine: zoomLevels must not be empty");
208
+ }
209
+ const initialSpp = options.samplesPerPixel ?? DEFAULT_SAMPLES_PER_PIXEL;
210
+ this._zoomIndex = findClosestZoomIndex(initialSpp, this._zoomLevels);
211
+ }
212
+ // ---------------------------------------------------------------------------
213
+ // State snapshot
214
+ // ---------------------------------------------------------------------------
215
+ getState() {
216
+ return {
217
+ tracks: this._tracks.map((t) => ({ ...t, clips: [...t.clips] })),
218
+ duration: calculateDuration(this._tracks),
219
+ currentTime: this._currentTime,
220
+ isPlaying: this._isPlaying,
221
+ samplesPerPixel: this._zoomLevels[this._zoomIndex],
222
+ sampleRate: this._sampleRate,
223
+ selectedTrackId: this._selectedTrackId,
224
+ zoomIndex: this._zoomIndex,
225
+ canZoomIn: this._zoomIndex > 0,
226
+ canZoomOut: this._zoomIndex < this._zoomLevels.length - 1
227
+ };
228
+ }
229
+ // ---------------------------------------------------------------------------
230
+ // Track Management
231
+ // ---------------------------------------------------------------------------
232
+ setTracks(tracks) {
233
+ this._tracks = [...tracks];
234
+ this._adapter?.setTracks(this._tracks);
235
+ this._emitStateChange();
236
+ }
237
+ addTrack(track) {
238
+ this._tracks = [...this._tracks, track];
239
+ this._adapter?.setTracks(this._tracks);
240
+ this._emitStateChange();
241
+ }
242
+ removeTrack(trackId) {
243
+ if (!this._tracks.some((t) => t.id === trackId)) return;
244
+ this._tracks = this._tracks.filter((t) => t.id !== trackId);
245
+ if (this._selectedTrackId === trackId) {
246
+ this._selectedTrackId = null;
247
+ }
248
+ this._adapter?.setTracks(this._tracks);
249
+ this._emitStateChange();
250
+ }
251
+ selectTrack(trackId) {
252
+ this._selectedTrackId = trackId;
253
+ this._emitStateChange();
254
+ }
255
+ // ---------------------------------------------------------------------------
256
+ // Clip Editing (delegates to operations/)
257
+ // ---------------------------------------------------------------------------
258
+ moveClip(trackId, clipId, deltaSamples) {
259
+ const track = this._tracks.find((t) => t.id === trackId);
260
+ if (!track) return;
261
+ const clipIndex = track.clips.findIndex((c) => c.id === clipId);
262
+ if (clipIndex === -1) return;
263
+ const clip = track.clips[clipIndex];
264
+ const sortedClips = (0, import_core2.sortClipsByTime)(track.clips);
265
+ const sortedIndex = sortedClips.findIndex((c) => c.id === clipId);
266
+ const constrainedDelta = constrainClipDrag(clip, deltaSamples, sortedClips, sortedIndex);
267
+ if (constrainedDelta === 0) return;
268
+ this._tracks = this._tracks.map((t) => {
269
+ if (t.id !== trackId) return t;
270
+ const newClips = t.clips.map(
271
+ (c, i) => i === clipIndex ? {
272
+ ...c,
273
+ startSample: Math.floor(c.startSample + constrainedDelta)
274
+ } : c
275
+ );
276
+ return { ...t, clips: newClips };
277
+ });
278
+ this._emitStateChange();
279
+ }
280
+ splitClip(trackId, clipId, atSample) {
281
+ const track = this._tracks.find((t) => t.id === trackId);
282
+ if (!track) return;
283
+ const clipIndex = track.clips.findIndex((c) => c.id === clipId);
284
+ if (clipIndex === -1) return;
285
+ const clip = track.clips[clipIndex];
286
+ const minDuration = Math.floor(DEFAULT_MIN_DURATION_SECONDS * this._sampleRate);
287
+ if (!canSplitAt(clip, atSample, minDuration)) return;
288
+ const { left, right } = splitClip(clip, atSample);
289
+ this._tracks = this._tracks.map((t) => {
290
+ if (t.id !== trackId) return t;
291
+ const newClips = [...t.clips];
292
+ newClips.splice(clipIndex, 1, left, right);
293
+ return { ...t, clips: newClips };
294
+ });
295
+ this._emitStateChange();
296
+ }
297
+ trimClip(trackId, clipId, boundary, deltaSamples) {
298
+ const track = this._tracks.find((t) => t.id === trackId);
299
+ if (!track) return;
300
+ const clipIndex = track.clips.findIndex((c) => c.id === clipId);
301
+ if (clipIndex === -1) return;
302
+ const clip = track.clips[clipIndex];
303
+ const sortedClips = (0, import_core2.sortClipsByTime)(track.clips);
304
+ const sortedIndex = sortedClips.findIndex((c) => c.id === clipId);
305
+ const minDuration = Math.floor(DEFAULT_MIN_DURATION_SECONDS * this._sampleRate);
306
+ const constrained = constrainBoundaryTrim(
307
+ clip,
308
+ deltaSamples,
309
+ boundary,
310
+ sortedClips,
311
+ sortedIndex,
312
+ minDuration
313
+ );
314
+ if (constrained === 0) return;
315
+ this._tracks = this._tracks.map((t) => {
316
+ if (t.id !== trackId) return t;
317
+ const newClips = t.clips.map((c, i) => {
318
+ if (i !== clipIndex) return c;
319
+ if (boundary === "left") {
320
+ return {
321
+ ...c,
322
+ startSample: c.startSample + constrained,
323
+ offsetSamples: c.offsetSamples + constrained,
324
+ durationSamples: c.durationSamples - constrained
325
+ };
326
+ } else {
327
+ return { ...c, durationSamples: c.durationSamples + constrained };
328
+ }
329
+ });
330
+ return { ...t, clips: newClips };
331
+ });
332
+ this._emitStateChange();
333
+ }
334
+ // ---------------------------------------------------------------------------
335
+ // Playback (delegates to adapter, no-ops without adapter)
336
+ // ---------------------------------------------------------------------------
337
+ async play(startTime, endTime) {
338
+ if (startTime !== void 0) {
339
+ const duration = calculateDuration(this._tracks);
340
+ this._currentTime = clampSeekPosition(startTime, duration);
341
+ }
342
+ if (this._adapter) {
343
+ await this._adapter.play(this._currentTime, endTime);
344
+ this._startTimeUpdateLoop();
345
+ }
346
+ this._isPlaying = true;
347
+ this._emit("play");
348
+ this._emitStateChange();
349
+ }
350
+ pause() {
351
+ this._isPlaying = false;
352
+ this._stopTimeUpdateLoop();
353
+ this._adapter?.pause();
354
+ if (this._adapter) {
355
+ this._currentTime = this._adapter.getCurrentTime();
356
+ }
357
+ this._emit("pause");
358
+ this._emitStateChange();
359
+ }
360
+ stop() {
361
+ this._isPlaying = false;
362
+ this._currentTime = 0;
363
+ this._stopTimeUpdateLoop();
364
+ this._adapter?.stop();
365
+ this._emit("stop");
366
+ this._emitStateChange();
367
+ }
368
+ seek(time) {
369
+ const duration = calculateDuration(this._tracks);
370
+ this._currentTime = clampSeekPosition(time, duration);
371
+ this._adapter?.seek(this._currentTime);
372
+ this._emitStateChange();
373
+ }
374
+ setMasterVolume(volume) {
375
+ this._adapter?.setMasterVolume(volume);
376
+ }
377
+ // ---------------------------------------------------------------------------
378
+ // Per-Track Audio (delegates to adapter)
379
+ // ---------------------------------------------------------------------------
380
+ setTrackVolume(trackId, volume) {
381
+ this._adapter?.setTrackVolume(trackId, volume);
382
+ }
383
+ setTrackMute(trackId, muted) {
384
+ this._adapter?.setTrackMute(trackId, muted);
385
+ }
386
+ setTrackSolo(trackId, soloed) {
387
+ this._adapter?.setTrackSolo(trackId, soloed);
388
+ }
389
+ setTrackPan(trackId, pan) {
390
+ this._adapter?.setTrackPan(trackId, pan);
391
+ }
392
+ // ---------------------------------------------------------------------------
393
+ // Zoom
394
+ // ---------------------------------------------------------------------------
395
+ zoomIn() {
396
+ if (this._zoomIndex > 0) {
397
+ this._zoomIndex--;
398
+ this._emitStateChange();
399
+ }
400
+ }
401
+ zoomOut() {
402
+ if (this._zoomIndex < this._zoomLevels.length - 1) {
403
+ this._zoomIndex++;
404
+ this._emitStateChange();
405
+ }
406
+ }
407
+ setZoomLevel(samplesPerPixel) {
408
+ const newIndex = findClosestZoomIndex(samplesPerPixel, this._zoomLevels);
409
+ if (newIndex === this._zoomIndex) return;
410
+ this._zoomIndex = newIndex;
411
+ this._emitStateChange();
412
+ }
413
+ // ---------------------------------------------------------------------------
414
+ // Events
415
+ // ---------------------------------------------------------------------------
416
+ on(event, listener) {
417
+ if (!this._listeners.has(event)) {
418
+ this._listeners.set(event, /* @__PURE__ */ new Set());
419
+ }
420
+ this._listeners.get(event).add(listener);
421
+ }
422
+ off(event, listener) {
423
+ this._listeners.get(event)?.delete(listener);
424
+ }
425
+ // ---------------------------------------------------------------------------
426
+ // Lifecycle
427
+ // ---------------------------------------------------------------------------
428
+ dispose() {
429
+ if (this._disposed) return;
430
+ this._disposed = true;
431
+ this._stopTimeUpdateLoop();
432
+ this._adapter?.dispose();
433
+ this._listeners.clear();
434
+ }
435
+ // ---------------------------------------------------------------------------
436
+ // Private helpers
437
+ // ---------------------------------------------------------------------------
438
+ _emit(event, ...args) {
439
+ const listeners = this._listeners.get(event);
440
+ if (listeners) {
441
+ for (const listener of listeners) {
442
+ try {
443
+ listener(...args);
444
+ } catch (error) {
445
+ console.warn("[waveform-playlist/engine] Error in event listener:", error);
446
+ }
447
+ }
448
+ }
449
+ }
450
+ _emitStateChange() {
451
+ this._emit("statechange", this.getState());
452
+ }
453
+ _startTimeUpdateLoop() {
454
+ if (typeof requestAnimationFrame === "undefined") return;
455
+ this._stopTimeUpdateLoop();
456
+ const tick = () => {
457
+ if (this._disposed || !this._isPlaying) return;
458
+ if (this._adapter) {
459
+ this._currentTime = this._adapter.getCurrentTime();
460
+ this._emit("timeupdate", this._currentTime);
461
+ }
462
+ this._animFrameId = requestAnimationFrame(tick);
463
+ };
464
+ this._animFrameId = requestAnimationFrame(tick);
465
+ }
466
+ _stopTimeUpdateLoop() {
467
+ if (this._animFrameId !== null && typeof cancelAnimationFrame !== "undefined") {
468
+ cancelAnimationFrame(this._animFrameId);
469
+ this._animFrameId = null;
470
+ }
471
+ }
472
+ };
473
+ // Annotate the CommonJS export names for ESM import in node:
474
+ 0 && (module.exports = {
475
+ PlaylistEngine,
476
+ calculateDuration,
477
+ calculateSplitPoint,
478
+ calculateViewportBounds,
479
+ calculateZoomScrollPosition,
480
+ canSplitAt,
481
+ clampSeekPosition,
482
+ constrainBoundaryTrim,
483
+ constrainClipDrag,
484
+ findClosestZoomIndex,
485
+ getVisibleChunkIndices,
486
+ shouldUpdateViewport,
487
+ splitClip
488
+ });
489
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/operations/clipOperations.ts","../src/operations/viewportOperations.ts","../src/operations/timelineOperations.ts","../src/PlaylistEngine.ts"],"sourcesContent":["// Operations (pure functions)\nexport * from './operations';\n\n// Engine class\nexport { PlaylistEngine } from './PlaylistEngine';\n\n// Engine types\nexport * from './types';\n","/**\n * Clip Operations\n *\n * Pure functions for constraining clip movement, boundary trimming,\n * and splitting clips on a timeline. All positions are in samples (integers).\n */\n\nimport type { AudioClip } from '@waveform-playlist/core';\nimport { createClip } from '@waveform-playlist/core';\n\n/**\n * Constrain clip movement delta to prevent overlaps with adjacent clips\n * and going before sample 0.\n *\n * @param clip - The clip being dragged\n * @param deltaSamples - Requested movement in samples (negative = left, positive = right)\n * @param sortedClips - All clips on the track, sorted by startSample\n * @param clipIndex - Index of the dragged clip in sortedClips\n * @returns Constrained delta that prevents overlaps\n */\nexport function constrainClipDrag(\n clip: AudioClip,\n deltaSamples: number,\n sortedClips: AudioClip[],\n clipIndex: number\n): number {\n let delta = deltaSamples;\n\n // Constraint 1: Cannot go before sample 0\n const minDelta = -clip.startSample;\n delta = Math.max(delta, minDelta);\n\n // Constraint 2: Cannot overlap previous clip\n if (clipIndex > 0) {\n const prevClip = sortedClips[clipIndex - 1];\n const prevClipEnd = prevClip.startSample + prevClip.durationSamples;\n // clip.startSample + delta >= prevClipEnd\n const minDeltaPrev = prevClipEnd - clip.startSample;\n delta = Math.max(delta, minDeltaPrev);\n }\n\n // Constraint 3: Cannot overlap next clip\n if (clipIndex < sortedClips.length - 1) {\n const nextClip = sortedClips[clipIndex + 1];\n // clip.startSample + clip.durationSamples + delta <= nextClip.startSample\n const maxDeltaNext = nextClip.startSample - (clip.startSample + clip.durationSamples);\n delta = Math.min(delta, maxDeltaNext);\n }\n\n return delta;\n}\n\n/**\n * Constrain boundary trim delta for left or right edge of a clip.\n *\n * LEFT boundary: delta moves the left edge (positive = shrink, negative = expand)\n * - startSample += delta, offsetSamples += delta, durationSamples -= delta\n *\n * RIGHT boundary: delta applied to durationSamples (positive = expand, negative = shrink)\n * - durationSamples += delta\n *\n * @param clip - The clip being trimmed\n * @param deltaSamples - Requested trim delta in samples\n * @param boundary - Which edge is being trimmed: 'left' or 'right'\n * @param sortedClips - All clips on the track, sorted by startSample\n * @param clipIndex - Index of the trimmed clip in sortedClips\n * @param minDurationSamples - Minimum allowed clip duration in samples\n * @returns Constrained delta\n */\nexport function constrainBoundaryTrim(\n clip: AudioClip,\n deltaSamples: number,\n boundary: 'left' | 'right',\n sortedClips: AudioClip[],\n clipIndex: number,\n minDurationSamples: number\n): number {\n let delta = deltaSamples;\n\n if (boundary === 'left') {\n // Constraint 1: startSample + delta >= 0\n delta = Math.max(delta, -clip.startSample);\n\n // Constraint 2: offsetSamples + delta >= 0\n delta = Math.max(delta, -clip.offsetSamples);\n\n // Constraint 3: Cannot overlap previous clip\n if (clipIndex > 0) {\n const prevClip = sortedClips[clipIndex - 1];\n const prevClipEnd = prevClip.startSample + prevClip.durationSamples;\n // startSample + delta >= prevClipEnd\n delta = Math.max(delta, prevClipEnd - clip.startSample);\n }\n\n // Constraint 4: durationSamples - delta >= minDurationSamples\n // delta <= durationSamples - minDurationSamples\n delta = Math.min(delta, clip.durationSamples - minDurationSamples);\n } else {\n // RIGHT boundary\n\n // Constraint 1: durationSamples + delta >= minDurationSamples\n // delta >= minDurationSamples - durationSamples\n delta = Math.max(delta, minDurationSamples - clip.durationSamples);\n\n // Constraint 2: offsetSamples + (durationSamples + delta) <= sourceDurationSamples\n // delta <= sourceDurationSamples - offsetSamples - durationSamples\n delta = Math.min(delta, clip.sourceDurationSamples - clip.offsetSamples - clip.durationSamples);\n\n // Constraint 3: startSample + (durationSamples + delta) <= nextClip.startSample\n if (clipIndex < sortedClips.length - 1) {\n const nextClip = sortedClips[clipIndex + 1];\n // delta <= nextClip.startSample - startSample - durationSamples\n delta = Math.min(delta, nextClip.startSample - clip.startSample - clip.durationSamples);\n }\n }\n\n return delta;\n}\n\n/**\n * Snap a split sample position to the nearest pixel boundary.\n *\n * @param splitSample - The sample position to snap\n * @param samplesPerPixel - Current zoom level (samples per pixel)\n * @returns Snapped sample position\n */\nexport function calculateSplitPoint(splitSample: number, samplesPerPixel: number): number {\n return Math.floor(splitSample / samplesPerPixel) * samplesPerPixel;\n}\n\n/**\n * Split a clip into two clips at the given sample position.\n *\n * The left clip retains the original fadeIn; the right clip retains the original fadeOut.\n * Both clips share the same waveformData reference.\n * If the clip has a name, suffixes \" (1)\" and \" (2)\" are appended.\n *\n * @param clip - The clip to split\n * @param splitSample - The timeline sample position where the split occurs\n * @returns Object with `left` and `right` AudioClip\n */\nexport function splitClip(\n clip: AudioClip,\n splitSample: number\n): { left: AudioClip; right: AudioClip } {\n const leftDuration = splitSample - clip.startSample;\n const rightDuration = clip.durationSamples - leftDuration;\n\n const leftName = clip.name ? `${clip.name} (1)` : undefined;\n const rightName = clip.name ? `${clip.name} (2)` : undefined;\n\n const left = createClip({\n startSample: clip.startSample,\n durationSamples: leftDuration,\n offsetSamples: clip.offsetSamples,\n sampleRate: clip.sampleRate,\n sourceDurationSamples: clip.sourceDurationSamples,\n gain: clip.gain,\n name: leftName,\n color: clip.color,\n fadeIn: clip.fadeIn,\n audioBuffer: clip.audioBuffer,\n waveformData: clip.waveformData,\n });\n\n const right = createClip({\n startSample: splitSample,\n durationSamples: rightDuration,\n offsetSamples: clip.offsetSamples + leftDuration,\n sampleRate: clip.sampleRate,\n sourceDurationSamples: clip.sourceDurationSamples,\n gain: clip.gain,\n name: rightName,\n color: clip.color,\n fadeOut: clip.fadeOut,\n audioBuffer: clip.audioBuffer,\n waveformData: clip.waveformData,\n });\n\n return { left, right };\n}\n\n/**\n * Check whether a clip can be split at the given sample position.\n *\n * The split point must be strictly inside the clip (not at start or end),\n * and both resulting clips must meet the minimum duration requirement.\n *\n * @param clip - The clip to check\n * @param sample - The timeline sample position to test\n * @param minDurationSamples - Minimum allowed clip duration in samples\n * @returns true if the split is valid\n */\nexport function canSplitAt(clip: AudioClip, sample: number, minDurationSamples: number): boolean {\n const clipEnd = clip.startSample + clip.durationSamples;\n\n // Must be strictly within clip bounds\n if (sample <= clip.startSample || sample >= clipEnd) {\n return false;\n }\n\n // Both resulting clips must meet minimum duration\n const leftDuration = sample - clip.startSample;\n const rightDuration = clipEnd - sample;\n\n return leftDuration >= minDurationSamples && rightDuration >= minDurationSamples;\n}\n","/**\n * Viewport operations for virtual scrolling.\n *\n * Pure math helpers that determine which portion of the timeline\n * is visible and which canvas chunks need to be mounted.\n */\n\n/**\n * Calculate the visible region with an overscan buffer for virtual scrolling.\n *\n * The buffer extends the visible range on both sides so that chunks are\n * mounted slightly before they scroll into view, preventing flicker.\n *\n * @param scrollLeft - Current horizontal scroll position in pixels\n * @param containerWidth - Width of the scroll container in pixels\n * @param bufferRatio - Multiplier for buffer size (default 1.5x container width)\n * @returns Object with visibleStart and visibleEnd in pixels\n */\nexport function calculateViewportBounds(\n scrollLeft: number,\n containerWidth: number,\n bufferRatio: number = 1.5\n): { visibleStart: number; visibleEnd: number } {\n const buffer = containerWidth * bufferRatio;\n return {\n visibleStart: Math.max(0, scrollLeft - buffer),\n visibleEnd: scrollLeft + containerWidth + buffer,\n };\n}\n\n/**\n * Get an array of chunk indices that overlap the visible viewport.\n *\n * Chunks are fixed-width segments of the total timeline width. Only chunks\n * that intersect [visibleStart, visibleEnd) are included. The last chunk\n * may be narrower than chunkWidth if totalWidth is not evenly divisible.\n *\n * @param totalWidth - Total width of the timeline in pixels\n * @param chunkWidth - Width of each chunk in pixels\n * @param visibleStart - Left edge of the visible region in pixels\n * @param visibleEnd - Right edge of the visible region in pixels\n * @returns Array of chunk indices (0-based) that are visible\n */\nexport function getVisibleChunkIndices(\n totalWidth: number,\n chunkWidth: number,\n visibleStart: number,\n visibleEnd: number\n): number[] {\n const totalChunks = Math.ceil(totalWidth / chunkWidth);\n const indices: number[] = [];\n\n for (let i = 0; i < totalChunks; i++) {\n const chunkLeft = i * chunkWidth;\n const thisChunkWidth = Math.min(totalWidth - chunkLeft, chunkWidth);\n const chunkEnd = chunkLeft + thisChunkWidth;\n\n if (chunkEnd <= visibleStart || chunkLeft >= visibleEnd) {\n continue;\n }\n\n indices.push(i);\n }\n\n return indices;\n}\n\n/**\n * Determine whether a scroll change is large enough to warrant\n * recalculating the viewport and re-rendering chunks.\n *\n * Small scroll movements are ignored to avoid excessive recomputation\n * during smooth scrolling.\n *\n * @param oldScrollLeft - Previous scroll position in pixels\n * @param newScrollLeft - Current scroll position in pixels\n * @param threshold - Minimum pixel delta to trigger an update (default 100)\n * @returns true if the scroll delta meets or exceeds the threshold\n */\nexport function shouldUpdateViewport(\n oldScrollLeft: number,\n newScrollLeft: number,\n threshold: number = 100\n): boolean {\n return Math.abs(oldScrollLeft - newScrollLeft) >= threshold;\n}\n","import type { ClipTrack } from '@waveform-playlist/core';\n\n/**\n * Calculate total timeline duration in seconds from all tracks/clips.\n * Iterates all clips, finds the furthest clip end (startSample + durationSamples),\n * converts to seconds using each clip's sampleRate.\n *\n * @param tracks - Array of clip tracks\n * @returns Duration in seconds\n */\nexport function calculateDuration(tracks: ClipTrack[]): number {\n let maxDuration = 0;\n for (const track of tracks) {\n for (const clip of track.clips) {\n const clipEndSample = clip.startSample + clip.durationSamples;\n const clipEnd = clipEndSample / clip.sampleRate;\n maxDuration = Math.max(maxDuration, clipEnd);\n }\n }\n return maxDuration;\n}\n\n/**\n * Find the zoom level index closest to a given samplesPerPixel.\n * Returns exact match if found, otherwise the index whose value is\n * nearest to the target (by absolute difference).\n *\n * @param targetSamplesPerPixel - The samplesPerPixel value to find\n * @param zoomLevels - Array of available zoom levels (samplesPerPixel values)\n * @returns Index into the zoomLevels array\n */\nexport function findClosestZoomIndex(targetSamplesPerPixel: number, zoomLevels: number[]): number {\n if (zoomLevels.length === 0) return 0;\n\n let bestIndex = 0;\n let bestDiff = Math.abs(zoomLevels[0] - targetSamplesPerPixel);\n\n for (let i = 1; i < zoomLevels.length; i++) {\n const diff = Math.abs(zoomLevels[i] - targetSamplesPerPixel);\n if (diff < bestDiff) {\n bestDiff = diff;\n bestIndex = i;\n }\n }\n\n return bestIndex;\n}\n\n/**\n * Keep viewport centered during zoom changes.\n * Calculates center time from old zoom, computes new pixel position at new zoom,\n * and returns new scrollLeft clamped to >= 0.\n *\n * @param oldSamplesPerPixel - Previous zoom level\n * @param newSamplesPerPixel - New zoom level\n * @param scrollLeft - Current horizontal scroll position\n * @param containerWidth - Viewport width in pixels\n * @param sampleRate - Audio sample rate\n * @param controlWidth - Width of track controls panel (defaults to 0)\n * @returns New scrollLeft value\n */\nexport function calculateZoomScrollPosition(\n oldSamplesPerPixel: number,\n newSamplesPerPixel: number,\n scrollLeft: number,\n containerWidth: number,\n sampleRate: number,\n controlWidth: number = 0\n): number {\n const centerPixel = scrollLeft + containerWidth / 2 - controlWidth;\n const centerTime = (centerPixel * oldSamplesPerPixel) / sampleRate;\n const newCenterPixel = (centerTime * sampleRate) / newSamplesPerPixel;\n const newScrollLeft = newCenterPixel + controlWidth - containerWidth / 2;\n return Math.max(0, newScrollLeft);\n}\n\n/**\n * Clamp a seek position to the valid range [0, duration].\n *\n * @param time - Requested seek time in seconds\n * @param duration - Maximum duration in seconds\n * @returns Clamped time value\n */\nexport function clampSeekPosition(time: number, duration: number): number {\n return Math.max(0, Math.min(time, duration));\n}\n","/**\n * PlaylistEngine — Stateful, framework-agnostic timeline engine.\n *\n * Composes pure operations from ./operations with an event emitter\n * and optional PlayoutAdapter for audio playback delegation.\n */\n\nimport type { AudioClip, ClipTrack } from '@waveform-playlist/core';\nimport { sortClipsByTime } from '@waveform-playlist/core';\nimport {\n constrainClipDrag,\n constrainBoundaryTrim,\n canSplitAt,\n splitClip as splitClipOp,\n} from './operations/clipOperations';\nimport {\n calculateDuration,\n clampSeekPosition,\n findClosestZoomIndex,\n} from './operations/timelineOperations';\nimport type { PlayoutAdapter, EngineState, EngineEvents, PlaylistEngineOptions } from './types';\n\nconst DEFAULT_SAMPLE_RATE = 44100;\nconst DEFAULT_SAMPLES_PER_PIXEL = 1000;\nconst DEFAULT_ZOOM_LEVELS = [256, 512, 1024, 2048, 4096, 8192];\nconst DEFAULT_MIN_DURATION_SECONDS = 0.1;\n\ntype EventName = keyof EngineEvents;\n\nexport class PlaylistEngine {\n private _tracks: ClipTrack[] = [];\n private _currentTime = 0;\n private _isPlaying = false;\n private _selectedTrackId: string | null = null;\n private _sampleRate: number;\n private _zoomLevels: number[];\n private _zoomIndex: number;\n private _adapter: PlayoutAdapter | null;\n private _animFrameId: number | null = null;\n private _disposed = false;\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n private _listeners: Map<string, Set<Function>> = new Map();\n\n constructor(options: PlaylistEngineOptions = {}) {\n this._sampleRate = options.sampleRate ?? DEFAULT_SAMPLE_RATE;\n this._zoomLevels = [...(options.zoomLevels ?? DEFAULT_ZOOM_LEVELS)];\n this._adapter = options.adapter ?? null;\n\n if (this._zoomLevels.length === 0) {\n throw new Error('PlaylistEngine: zoomLevels must not be empty');\n }\n\n const initialSpp = options.samplesPerPixel ?? DEFAULT_SAMPLES_PER_PIXEL;\n this._zoomIndex = findClosestZoomIndex(initialSpp, this._zoomLevels);\n }\n\n // ---------------------------------------------------------------------------\n // State snapshot\n // ---------------------------------------------------------------------------\n\n getState(): EngineState {\n return {\n tracks: this._tracks.map((t) => ({ ...t, clips: [...t.clips] })),\n duration: calculateDuration(this._tracks),\n currentTime: this._currentTime,\n isPlaying: this._isPlaying,\n samplesPerPixel: this._zoomLevels[this._zoomIndex],\n sampleRate: this._sampleRate,\n selectedTrackId: this._selectedTrackId,\n zoomIndex: this._zoomIndex,\n canZoomIn: this._zoomIndex > 0,\n canZoomOut: this._zoomIndex < this._zoomLevels.length - 1,\n };\n }\n\n // ---------------------------------------------------------------------------\n // Track Management\n // ---------------------------------------------------------------------------\n\n setTracks(tracks: ClipTrack[]): void {\n this._tracks = [...tracks];\n this._adapter?.setTracks(this._tracks);\n this._emitStateChange();\n }\n\n addTrack(track: ClipTrack): void {\n this._tracks = [...this._tracks, track];\n this._adapter?.setTracks(this._tracks);\n this._emitStateChange();\n }\n\n removeTrack(trackId: string): void {\n if (!this._tracks.some((t) => t.id === trackId)) return;\n this._tracks = this._tracks.filter((t) => t.id !== trackId);\n if (this._selectedTrackId === trackId) {\n this._selectedTrackId = null;\n }\n this._adapter?.setTracks(this._tracks);\n this._emitStateChange();\n }\n\n selectTrack(trackId: string | null): void {\n this._selectedTrackId = trackId;\n this._emitStateChange();\n }\n\n // ---------------------------------------------------------------------------\n // Clip Editing (delegates to operations/)\n // ---------------------------------------------------------------------------\n\n moveClip(trackId: string, clipId: string, deltaSamples: number): void {\n const track = this._tracks.find((t) => t.id === trackId);\n if (!track) return;\n\n const clipIndex = track.clips.findIndex((c: AudioClip) => c.id === clipId);\n if (clipIndex === -1) return;\n\n const clip = track.clips[clipIndex];\n const sortedClips = sortClipsByTime(track.clips);\n const sortedIndex = sortedClips.findIndex((c: AudioClip) => c.id === clipId);\n\n const constrainedDelta = constrainClipDrag(clip, deltaSamples, sortedClips, sortedIndex);\n\n if (constrainedDelta === 0) return;\n\n this._tracks = this._tracks.map((t) => {\n if (t.id !== trackId) return t;\n const newClips = t.clips.map((c: AudioClip, i: number) =>\n i === clipIndex\n ? {\n ...c,\n startSample: Math.floor(c.startSample + constrainedDelta),\n }\n : c\n );\n return { ...t, clips: newClips };\n });\n\n this._emitStateChange();\n }\n\n splitClip(trackId: string, clipId: string, atSample: number): void {\n const track = this._tracks.find((t) => t.id === trackId);\n if (!track) return;\n\n const clipIndex = track.clips.findIndex((c: AudioClip) => c.id === clipId);\n if (clipIndex === -1) return;\n\n const clip = track.clips[clipIndex];\n const minDuration = Math.floor(DEFAULT_MIN_DURATION_SECONDS * this._sampleRate);\n\n if (!canSplitAt(clip, atSample, minDuration)) return;\n\n const { left, right } = splitClipOp(clip, atSample);\n\n this._tracks = this._tracks.map((t) => {\n if (t.id !== trackId) return t;\n const newClips = [...t.clips];\n newClips.splice(clipIndex, 1, left, right);\n return { ...t, clips: newClips };\n });\n\n this._emitStateChange();\n }\n\n trimClip(\n trackId: string,\n clipId: string,\n boundary: 'left' | 'right',\n deltaSamples: number\n ): void {\n const track = this._tracks.find((t) => t.id === trackId);\n if (!track) return;\n\n const clipIndex = track.clips.findIndex((c: AudioClip) => c.id === clipId);\n if (clipIndex === -1) return;\n\n const clip = track.clips[clipIndex];\n const sortedClips = sortClipsByTime(track.clips);\n const sortedIndex = sortedClips.findIndex((c: AudioClip) => c.id === clipId);\n const minDuration = Math.floor(DEFAULT_MIN_DURATION_SECONDS * this._sampleRate);\n\n const constrained = constrainBoundaryTrim(\n clip,\n deltaSamples,\n boundary,\n sortedClips,\n sortedIndex,\n minDuration\n );\n\n if (constrained === 0) return;\n\n this._tracks = this._tracks.map((t) => {\n if (t.id !== trackId) return t;\n const newClips = t.clips.map((c: AudioClip, i: number) => {\n if (i !== clipIndex) return c;\n if (boundary === 'left') {\n return {\n ...c,\n startSample: c.startSample + constrained,\n offsetSamples: c.offsetSamples + constrained,\n durationSamples: c.durationSamples - constrained,\n };\n } else {\n return { ...c, durationSamples: c.durationSamples + constrained };\n }\n });\n return { ...t, clips: newClips };\n });\n\n this._emitStateChange();\n }\n\n // ---------------------------------------------------------------------------\n // Playback (delegates to adapter, no-ops without adapter)\n // ---------------------------------------------------------------------------\n\n async play(startTime?: number, endTime?: number): Promise<void> {\n if (startTime !== undefined) {\n const duration = calculateDuration(this._tracks);\n this._currentTime = clampSeekPosition(startTime, duration);\n }\n\n if (this._adapter) {\n await this._adapter.play(this._currentTime, endTime);\n this._startTimeUpdateLoop();\n }\n\n this._isPlaying = true;\n this._emit('play');\n this._emitStateChange();\n }\n\n pause(): void {\n this._isPlaying = false;\n this._stopTimeUpdateLoop();\n this._adapter?.pause();\n if (this._adapter) {\n this._currentTime = this._adapter.getCurrentTime();\n }\n this._emit('pause');\n this._emitStateChange();\n }\n\n stop(): void {\n this._isPlaying = false;\n this._currentTime = 0;\n this._stopTimeUpdateLoop();\n this._adapter?.stop();\n this._emit('stop');\n this._emitStateChange();\n }\n\n seek(time: number): void {\n const duration = calculateDuration(this._tracks);\n this._currentTime = clampSeekPosition(time, duration);\n this._adapter?.seek(this._currentTime);\n this._emitStateChange();\n }\n\n setMasterVolume(volume: number): void {\n this._adapter?.setMasterVolume(volume);\n }\n\n // ---------------------------------------------------------------------------\n // Per-Track Audio (delegates to adapter)\n // ---------------------------------------------------------------------------\n\n setTrackVolume(trackId: string, volume: number): void {\n this._adapter?.setTrackVolume(trackId, volume);\n }\n\n setTrackMute(trackId: string, muted: boolean): void {\n this._adapter?.setTrackMute(trackId, muted);\n }\n\n setTrackSolo(trackId: string, soloed: boolean): void {\n this._adapter?.setTrackSolo(trackId, soloed);\n }\n\n setTrackPan(trackId: string, pan: number): void {\n this._adapter?.setTrackPan(trackId, pan);\n }\n\n // ---------------------------------------------------------------------------\n // Zoom\n // ---------------------------------------------------------------------------\n\n zoomIn(): void {\n if (this._zoomIndex > 0) {\n this._zoomIndex--;\n this._emitStateChange();\n }\n }\n\n zoomOut(): void {\n if (this._zoomIndex < this._zoomLevels.length - 1) {\n this._zoomIndex++;\n this._emitStateChange();\n }\n }\n\n setZoomLevel(samplesPerPixel: number): void {\n const newIndex = findClosestZoomIndex(samplesPerPixel, this._zoomLevels);\n if (newIndex === this._zoomIndex) return;\n this._zoomIndex = newIndex;\n this._emitStateChange();\n }\n\n // ---------------------------------------------------------------------------\n // Events\n // ---------------------------------------------------------------------------\n\n on<K extends EventName>(event: K, listener: EngineEvents[K]): void {\n if (!this._listeners.has(event)) {\n this._listeners.set(event, new Set());\n }\n this._listeners.get(event)!.add(listener);\n }\n\n off<K extends EventName>(event: K, listener: EngineEvents[K]): void {\n this._listeners.get(event)?.delete(listener);\n }\n\n // ---------------------------------------------------------------------------\n // Lifecycle\n // ---------------------------------------------------------------------------\n\n dispose(): void {\n if (this._disposed) return;\n this._disposed = true;\n this._stopTimeUpdateLoop();\n this._adapter?.dispose();\n this._listeners.clear();\n }\n\n // ---------------------------------------------------------------------------\n // Private helpers\n // ---------------------------------------------------------------------------\n\n private _emit(event: string, ...args: unknown[]): void {\n const listeners = this._listeners.get(event);\n if (listeners) {\n for (const listener of listeners) {\n try {\n listener(...args);\n } catch (error) {\n console.warn('[waveform-playlist/engine] Error in event listener:', error);\n }\n }\n }\n }\n\n private _emitStateChange(): void {\n this._emit('statechange', this.getState());\n }\n\n private _startTimeUpdateLoop(): void {\n // Guard for Node.js / SSR environments where RAF is unavailable\n if (typeof requestAnimationFrame === 'undefined') return;\n\n this._stopTimeUpdateLoop();\n\n const tick = () => {\n if (this._disposed || !this._isPlaying) return;\n if (this._adapter) {\n this._currentTime = this._adapter.getCurrentTime();\n this._emit('timeupdate', this._currentTime);\n }\n this._animFrameId = requestAnimationFrame(tick);\n };\n\n this._animFrameId = requestAnimationFrame(tick);\n }\n\n private _stopTimeUpdateLoop(): void {\n if (this._animFrameId !== null && typeof cancelAnimationFrame !== 'undefined') {\n cancelAnimationFrame(this._animFrameId);\n this._animFrameId = null;\n }\n }\n}\n"],"mappings":"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","names":["import_core"]}