@dawcore/components 0.0.1

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,3111 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var __decorateClass = (decorators, target, key, kind) => {
30
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
31
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
32
+ if (decorator = decorators[i])
33
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
34
+ if (kind && result) __defProp(target, key, result);
35
+ return result;
36
+ };
37
+
38
+ // src/index.ts
39
+ var index_exports = {};
40
+ __export(index_exports, {
41
+ AudioResumeController: () => AudioResumeController,
42
+ DawClipElement: () => DawClipElement,
43
+ DawEditorElement: () => DawEditorElement,
44
+ DawPauseButtonElement: () => DawPauseButtonElement,
45
+ DawPlayButtonElement: () => DawPlayButtonElement,
46
+ DawPlayheadElement: () => DawPlayheadElement,
47
+ DawRecordButtonElement: () => DawRecordButtonElement,
48
+ DawRulerElement: () => DawRulerElement,
49
+ DawSelectionElement: () => DawSelectionElement,
50
+ DawStopButtonElement: () => DawStopButtonElement,
51
+ DawTrackControlsElement: () => DawTrackControlsElement,
52
+ DawTrackElement: () => DawTrackElement,
53
+ DawTransportButton: () => DawTransportButton,
54
+ DawTransportElement: () => DawTransportElement,
55
+ DawWaveformElement: () => DawWaveformElement,
56
+ RecordingController: () => RecordingController
57
+ });
58
+ module.exports = __toCommonJS(index_exports);
59
+
60
+ // src/elements/daw-clip.ts
61
+ var import_lit = require("lit");
62
+ var import_decorators = require("lit/decorators.js");
63
+ var DawClipElement = class extends import_lit.LitElement {
64
+ constructor() {
65
+ super(...arguments);
66
+ this.src = "";
67
+ this.peaksSrc = "";
68
+ this.start = 0;
69
+ this.duration = 0;
70
+ this.offset = 0;
71
+ this.gain = 1;
72
+ this.name = "";
73
+ this.color = "";
74
+ this.fadeIn = 0;
75
+ this.fadeOut = 0;
76
+ this.fadeType = "linear";
77
+ this.clipId = crypto.randomUUID();
78
+ }
79
+ // Light DOM — no visual rendering, just a data container
80
+ createRenderRoot() {
81
+ return this;
82
+ }
83
+ };
84
+ __decorateClass([
85
+ (0, import_decorators.property)()
86
+ ], DawClipElement.prototype, "src", 2);
87
+ __decorateClass([
88
+ (0, import_decorators.property)({ attribute: "peaks-src" })
89
+ ], DawClipElement.prototype, "peaksSrc", 2);
90
+ __decorateClass([
91
+ (0, import_decorators.property)({ type: Number })
92
+ ], DawClipElement.prototype, "start", 2);
93
+ __decorateClass([
94
+ (0, import_decorators.property)({ type: Number })
95
+ ], DawClipElement.prototype, "duration", 2);
96
+ __decorateClass([
97
+ (0, import_decorators.property)({ type: Number })
98
+ ], DawClipElement.prototype, "offset", 2);
99
+ __decorateClass([
100
+ (0, import_decorators.property)({ type: Number })
101
+ ], DawClipElement.prototype, "gain", 2);
102
+ __decorateClass([
103
+ (0, import_decorators.property)()
104
+ ], DawClipElement.prototype, "name", 2);
105
+ __decorateClass([
106
+ (0, import_decorators.property)()
107
+ ], DawClipElement.prototype, "color", 2);
108
+ __decorateClass([
109
+ (0, import_decorators.property)({ type: Number, attribute: "fade-in" })
110
+ ], DawClipElement.prototype, "fadeIn", 2);
111
+ __decorateClass([
112
+ (0, import_decorators.property)({ type: Number, attribute: "fade-out" })
113
+ ], DawClipElement.prototype, "fadeOut", 2);
114
+ __decorateClass([
115
+ (0, import_decorators.property)({ attribute: "fade-type" })
116
+ ], DawClipElement.prototype, "fadeType", 2);
117
+ DawClipElement = __decorateClass([
118
+ (0, import_decorators.customElement)("daw-clip")
119
+ ], DawClipElement);
120
+
121
+ // src/elements/daw-track.ts
122
+ var import_lit2 = require("lit");
123
+ var import_decorators2 = require("lit/decorators.js");
124
+ var DawTrackElement = class extends import_lit2.LitElement {
125
+ constructor() {
126
+ super(...arguments);
127
+ this.src = "";
128
+ this.name = "";
129
+ this.volume = 1;
130
+ this.pan = 0;
131
+ this.muted = false;
132
+ this.soloed = false;
133
+ this.trackId = crypto.randomUUID();
134
+ // Track removal is detected by the editor's MutationObserver,
135
+ // not by dispatching from disconnectedCallback (detached elements
136
+ // cannot bubble events to ancestors).
137
+ this._hasRendered = false;
138
+ }
139
+ // Light DOM so <daw-clip> children are queryable.
140
+ createRenderRoot() {
141
+ return this;
142
+ }
143
+ connectedCallback() {
144
+ super.connectedCallback();
145
+ setTimeout(() => {
146
+ this.dispatchEvent(
147
+ new CustomEvent("daw-track-connected", {
148
+ bubbles: true,
149
+ composed: true,
150
+ detail: { trackId: this.trackId, element: this }
151
+ })
152
+ );
153
+ }, 0);
154
+ }
155
+ updated(changed) {
156
+ if (!this._hasRendered) {
157
+ this._hasRendered = true;
158
+ return;
159
+ }
160
+ const trackProps = ["volume", "pan", "muted", "soloed", "src", "name"];
161
+ const hasTrackChange = trackProps.some((p) => changed.has(p));
162
+ if (hasTrackChange) {
163
+ this.dispatchEvent(
164
+ new CustomEvent("daw-track-update", {
165
+ bubbles: true,
166
+ composed: true,
167
+ detail: { trackId: this.trackId }
168
+ })
169
+ );
170
+ }
171
+ }
172
+ };
173
+ __decorateClass([
174
+ (0, import_decorators2.property)()
175
+ ], DawTrackElement.prototype, "src", 2);
176
+ __decorateClass([
177
+ (0, import_decorators2.property)()
178
+ ], DawTrackElement.prototype, "name", 2);
179
+ __decorateClass([
180
+ (0, import_decorators2.property)({ type: Number })
181
+ ], DawTrackElement.prototype, "volume", 2);
182
+ __decorateClass([
183
+ (0, import_decorators2.property)({ type: Number })
184
+ ], DawTrackElement.prototype, "pan", 2);
185
+ __decorateClass([
186
+ (0, import_decorators2.property)({ type: Boolean })
187
+ ], DawTrackElement.prototype, "muted", 2);
188
+ __decorateClass([
189
+ (0, import_decorators2.property)({ type: Boolean })
190
+ ], DawTrackElement.prototype, "soloed", 2);
191
+ DawTrackElement = __decorateClass([
192
+ (0, import_decorators2.customElement)("daw-track")
193
+ ], DawTrackElement);
194
+
195
+ // src/elements/daw-waveform.ts
196
+ var import_lit3 = require("lit");
197
+ var import_decorators3 = require("lit/decorators.js");
198
+
199
+ // src/utils/peak-rendering.ts
200
+ function aggregatePeaks(data, bits, startIndex, endIndex) {
201
+ if (startIndex * 2 + 1 >= data.length) {
202
+ return null;
203
+ }
204
+ const maxValue = 2 ** (bits - 1);
205
+ let minPeak = data[startIndex * 2] / maxValue;
206
+ let maxPeak = data[startIndex * 2 + 1] / maxValue;
207
+ for (let p = startIndex + 1; p < endIndex; p++) {
208
+ if (p * 2 + 1 >= data.length) break;
209
+ const pMin = data[p * 2] / maxValue;
210
+ const pMax = data[p * 2 + 1] / maxValue;
211
+ if (pMin < minPeak) minPeak = pMin;
212
+ if (pMax > maxPeak) maxPeak = pMax;
213
+ }
214
+ return { min: minPeak, max: maxPeak };
215
+ }
216
+ function calculateBarRects(x, barWidth, halfHeight, minPeak, maxPeak, drawMode) {
217
+ const min = Math.abs(minPeak * halfHeight);
218
+ const max = Math.abs(maxPeak * halfHeight);
219
+ if (drawMode === "normal") {
220
+ return [{ x, y: halfHeight - max, width: barWidth, height: max + min }];
221
+ }
222
+ return [
223
+ { x, y: 0, width: barWidth, height: halfHeight - max },
224
+ { x, y: halfHeight + min, width: barWidth, height: halfHeight - min }
225
+ ];
226
+ }
227
+
228
+ // src/utils/viewport.ts
229
+ function getVisibleChunkIndices(totalWidth, chunkWidth, visibleStart, visibleEnd, originX = 0) {
230
+ const totalChunks = Math.ceil(totalWidth / chunkWidth);
231
+ const indices = [];
232
+ for (let i = 0; i < totalChunks; i++) {
233
+ const chunkStart = originX + i * chunkWidth;
234
+ const chunkEnd = chunkStart + chunkWidth;
235
+ if (chunkEnd > visibleStart && chunkStart < visibleEnd) {
236
+ indices.push(i);
237
+ }
238
+ }
239
+ return indices;
240
+ }
241
+
242
+ // src/elements/daw-waveform.ts
243
+ var MAX_CANVAS_WIDTH = 1e3;
244
+ var LAYOUT_PROPS = /* @__PURE__ */ new Set(["length", "waveHeight", "barWidth", "barGap"]);
245
+ function groupDirtyByChunk(dirtyPixels, step) {
246
+ const dirtyByChunk = /* @__PURE__ */ new Map();
247
+ for (const peakIdx of dirtyPixels) {
248
+ const barPixel = Math.floor(peakIdx / step) * step;
249
+ const chunkIdx = Math.floor(barPixel / MAX_CANVAS_WIDTH);
250
+ const existing = dirtyByChunk.get(chunkIdx);
251
+ if (existing) {
252
+ dirtyByChunk.set(chunkIdx, {
253
+ min: Math.min(existing.min, barPixel),
254
+ max: Math.max(existing.max, barPixel)
255
+ });
256
+ } else {
257
+ dirtyByChunk.set(chunkIdx, { min: barPixel, max: barPixel });
258
+ }
259
+ }
260
+ return dirtyByChunk;
261
+ }
262
+ var DawWaveformElement = class extends import_lit3.LitElement {
263
+ constructor() {
264
+ super(...arguments);
265
+ this._peaks = new Int16Array(0);
266
+ this._dirtyPixels = /* @__PURE__ */ new Set();
267
+ this._drawScheduled = false;
268
+ this._rafId = 0;
269
+ /** Chunk indices visible in the last draw pass — used to detect new chunks on scroll. */
270
+ this._drawnChunks = /* @__PURE__ */ new Set();
271
+ this.length = 0;
272
+ this.waveHeight = 128;
273
+ this.barWidth = 1;
274
+ this.barGap = 0;
275
+ this.visibleStart = -Infinity;
276
+ this.visibleEnd = Infinity;
277
+ this.originX = 0;
278
+ }
279
+ set peaks(value) {
280
+ this._peaks = value;
281
+ this._markAllDirty();
282
+ this.requestUpdate();
283
+ }
284
+ get peaks() {
285
+ return this._peaks;
286
+ }
287
+ /**
288
+ * Replace the internal peaks reference without marking all dirty.
289
+ * Use with updatePeaks() for incremental recording updates where
290
+ * appendPeaks() returns a new array but only the tail changed.
291
+ */
292
+ setPeaksQuiet(value) {
293
+ this._peaks = value;
294
+ }
295
+ get bits() {
296
+ return this._peaks instanceof Int8Array ? 8 : 16;
297
+ }
298
+ _getVisibleChunkIndices() {
299
+ return getVisibleChunkIndices(
300
+ this.length,
301
+ MAX_CANVAS_WIDTH,
302
+ this.visibleStart,
303
+ this.visibleEnd,
304
+ this.originX
305
+ );
306
+ }
307
+ /**
308
+ * Mark a range of peak indices as dirty for incremental redraw.
309
+ * The caller must have already updated the underlying peaks array.
310
+ * Does NOT trigger a Lit re-render — bypasses Lit entirely.
311
+ */
312
+ updatePeaks(startIndex, endIndex) {
313
+ const peakCount = Math.floor(this._peaks.length / 2);
314
+ const clampedStart = Math.max(0, startIndex);
315
+ const clampedEnd = Math.min(peakCount, endIndex);
316
+ for (let i = clampedStart; i < clampedEnd; i++) {
317
+ this._dirtyPixels.add(i);
318
+ }
319
+ this._scheduleDraw();
320
+ }
321
+ _markAllDirty() {
322
+ const peakCount = Math.floor(this._peaks.length / 2);
323
+ for (let i = 0; i < peakCount; i++) {
324
+ this._dirtyPixels.add(i);
325
+ }
326
+ this._scheduleDraw();
327
+ }
328
+ _scheduleDraw() {
329
+ if (!this._drawScheduled) {
330
+ this._drawScheduled = true;
331
+ this._rafId = requestAnimationFrame(() => {
332
+ this._drawScheduled = false;
333
+ this._drawDirty();
334
+ });
335
+ }
336
+ }
337
+ _drawDirty() {
338
+ if (this._dirtyPixels.size === 0 || this.length === 0 || this._peaks.length === 0) {
339
+ this._dirtyPixels.clear();
340
+ return;
341
+ }
342
+ const canvases = this.shadowRoot?.querySelectorAll("canvas");
343
+ if (!canvases || canvases.length === 0) {
344
+ return;
345
+ }
346
+ const step = this.barWidth + this.barGap;
347
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
348
+ const halfHeight = this.waveHeight / 2;
349
+ const bits = this.bits;
350
+ const waveColor = getComputedStyle(this).getPropertyValue("--daw-wave-color").trim() || "#c49a6c";
351
+ const dirtyByChunk = groupDirtyByChunk(this._dirtyPixels, step);
352
+ this._drawnChunks.clear();
353
+ for (const canvas of canvases) {
354
+ const chunkIdx = Number(canvas.dataset.index);
355
+ this._drawnChunks.add(chunkIdx);
356
+ const range = dirtyByChunk.get(chunkIdx);
357
+ if (!range) continue;
358
+ this._drawChunk(canvas, chunkIdx, range, step, dpr, halfHeight, bits, waveColor);
359
+ }
360
+ this._dirtyPixels.clear();
361
+ }
362
+ _drawChunk(canvas, chunkIdx, range, step, dpr, halfHeight, bits, waveColor) {
363
+ const ctx = canvas.getContext("2d");
364
+ if (!ctx) return;
365
+ const globalOffset = chunkIdx * MAX_CANVAS_WIDTH;
366
+ const clearStart = Math.max(0, range.min - globalOffset);
367
+ const clearEnd = range.max - globalOffset + this.barWidth;
368
+ const clearWidth = clearEnd - clearStart;
369
+ const firstBar = range.min;
370
+ ctx.resetTransform();
371
+ ctx.clearRect(clearStart * dpr, 0, clearWidth * dpr, canvas.height);
372
+ ctx.scale(dpr, dpr);
373
+ ctx.fillStyle = waveColor;
374
+ const canvasWidth = Math.min(MAX_CANVAS_WIDTH, this.length - globalOffset);
375
+ const regionEnd = Math.min(globalOffset + clearEnd, globalOffset + canvasWidth);
376
+ for (let bar = Math.max(0, firstBar); bar < regionEnd; bar += step) {
377
+ const peak = aggregatePeaks(this._peaks, bits, bar, bar + step);
378
+ if (!peak) continue;
379
+ const rects = calculateBarRects(
380
+ bar - globalOffset,
381
+ this.barWidth,
382
+ halfHeight,
383
+ peak.min,
384
+ peak.max,
385
+ "normal"
386
+ );
387
+ for (const r of rects) {
388
+ ctx.fillRect(r.x, r.y, r.width, r.height);
389
+ }
390
+ }
391
+ }
392
+ connectedCallback() {
393
+ super.connectedCallback();
394
+ if (this._dirtyPixels.size > 0) {
395
+ this._scheduleDraw();
396
+ }
397
+ }
398
+ disconnectedCallback() {
399
+ super.disconnectedCallback();
400
+ if (this._drawScheduled) {
401
+ cancelAnimationFrame(this._rafId);
402
+ this._drawScheduled = false;
403
+ }
404
+ }
405
+ render() {
406
+ const indices = this._getVisibleChunkIndices();
407
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
408
+ return import_lit3.html`
409
+ <div class="container" style="width: ${this.length}px; height: ${this.waveHeight}px;">
410
+ ${indices.map((i) => {
411
+ const width = Math.min(MAX_CANVAS_WIDTH, this.length - i * MAX_CANVAS_WIDTH);
412
+ return import_lit3.html`
413
+ <canvas
414
+ data-index=${i}
415
+ width=${width * dpr}
416
+ height=${this.waveHeight * dpr}
417
+ style="left: ${i * MAX_CANVAS_WIDTH}px; width: ${width}px; height: ${this.waveHeight}px;"
418
+ ></canvas>
419
+ `;
420
+ })}
421
+ </div>
422
+ `;
423
+ }
424
+ /** Mark peaks dirty only for chunks that weren't drawn in the previous frame. */
425
+ _markNewChunksDirty() {
426
+ const currentIndices = this._getVisibleChunkIndices();
427
+ const peakCount = Math.floor(this._peaks.length / 2);
428
+ for (const chunkIdx of currentIndices) {
429
+ if (!this._drawnChunks.has(chunkIdx)) {
430
+ const start = chunkIdx * MAX_CANVAS_WIDTH;
431
+ const end = Math.min(start + MAX_CANVAS_WIDTH, peakCount);
432
+ for (let i = start; i < end; i++) {
433
+ this._dirtyPixels.add(i);
434
+ }
435
+ }
436
+ }
437
+ if (this._dirtyPixels.size > 0) {
438
+ this._scheduleDraw();
439
+ }
440
+ }
441
+ updated(changedProperties) {
442
+ const needsFullDirty = [...changedProperties.keys()].some((key) => LAYOUT_PROPS.has(key));
443
+ if (needsFullDirty) {
444
+ this._markAllDirty();
445
+ return;
446
+ }
447
+ if (changedProperties.has("visibleStart") || changedProperties.has("visibleEnd") || changedProperties.has("originX")) {
448
+ this._markNewChunksDirty();
449
+ }
450
+ }
451
+ };
452
+ DawWaveformElement.styles = import_lit3.css`
453
+ :host {
454
+ display: block;
455
+ position: relative;
456
+ }
457
+ .container {
458
+ position: relative;
459
+ }
460
+ canvas {
461
+ position: absolute;
462
+ top: 0;
463
+ }
464
+ `;
465
+ __decorateClass([
466
+ (0, import_decorators3.property)({ type: Number, attribute: false })
467
+ ], DawWaveformElement.prototype, "length", 2);
468
+ __decorateClass([
469
+ (0, import_decorators3.property)({ type: Number, attribute: false })
470
+ ], DawWaveformElement.prototype, "waveHeight", 2);
471
+ __decorateClass([
472
+ (0, import_decorators3.property)({ type: Number, attribute: false })
473
+ ], DawWaveformElement.prototype, "barWidth", 2);
474
+ __decorateClass([
475
+ (0, import_decorators3.property)({ type: Number, attribute: false })
476
+ ], DawWaveformElement.prototype, "barGap", 2);
477
+ __decorateClass([
478
+ (0, import_decorators3.property)({ type: Number, attribute: false })
479
+ ], DawWaveformElement.prototype, "visibleStart", 2);
480
+ __decorateClass([
481
+ (0, import_decorators3.property)({ type: Number, attribute: false })
482
+ ], DawWaveformElement.prototype, "visibleEnd", 2);
483
+ __decorateClass([
484
+ (0, import_decorators3.property)({ type: Number, attribute: false })
485
+ ], DawWaveformElement.prototype, "originX", 2);
486
+ DawWaveformElement = __decorateClass([
487
+ (0, import_decorators3.customElement)("daw-waveform")
488
+ ], DawWaveformElement);
489
+
490
+ // src/elements/daw-playhead.ts
491
+ var import_lit4 = require("lit");
492
+ var import_decorators4 = require("lit/decorators.js");
493
+
494
+ // src/controllers/animation-controller.ts
495
+ var AnimationController = class {
496
+ constructor(host) {
497
+ this._rafId = null;
498
+ this._callback = null;
499
+ host.addController(this);
500
+ }
501
+ start(callback) {
502
+ this.stop();
503
+ this._callback = callback;
504
+ const loop = () => {
505
+ this._callback?.();
506
+ this._rafId = requestAnimationFrame(loop);
507
+ };
508
+ this._rafId = requestAnimationFrame(loop);
509
+ }
510
+ stop() {
511
+ if (this._rafId !== null) {
512
+ cancelAnimationFrame(this._rafId);
513
+ this._rafId = null;
514
+ }
515
+ this._callback = null;
516
+ }
517
+ hostConnected() {
518
+ }
519
+ hostDisconnected() {
520
+ this.stop();
521
+ }
522
+ };
523
+
524
+ // src/elements/daw-playhead.ts
525
+ var DawPlayheadElement = class extends import_lit4.LitElement {
526
+ constructor() {
527
+ super(...arguments);
528
+ this._animation = new AnimationController(this);
529
+ this._line = null;
530
+ }
531
+ render() {
532
+ return import_lit4.html`<div></div>`;
533
+ }
534
+ firstUpdated() {
535
+ this._line = this.shadowRoot.querySelector("div");
536
+ }
537
+ startAnimation(getTime, sampleRate, samplesPerPixel) {
538
+ this._animation.start(() => {
539
+ const time = getTime();
540
+ const px = time * sampleRate / samplesPerPixel;
541
+ if (this._line) {
542
+ this._line.style.transform = `translate3d(${px}px, 0, 0)`;
543
+ }
544
+ });
545
+ }
546
+ stopAnimation(time, sampleRate, samplesPerPixel) {
547
+ this._animation.stop();
548
+ const px = time * sampleRate / samplesPerPixel;
549
+ if (this._line) {
550
+ this._line.style.transform = `translate3d(${px}px, 0, 0)`;
551
+ }
552
+ }
553
+ };
554
+ DawPlayheadElement.styles = import_lit4.css`
555
+ :host {
556
+ position: absolute;
557
+ top: 0;
558
+ bottom: 0;
559
+ left: 0;
560
+ pointer-events: none;
561
+ z-index: 10;
562
+ }
563
+ div {
564
+ position: absolute;
565
+ top: 0;
566
+ bottom: 0;
567
+ width: 1px;
568
+ background: var(--daw-playhead-color, #d08070);
569
+ will-change: transform;
570
+ }
571
+ `;
572
+ DawPlayheadElement = __decorateClass([
573
+ (0, import_decorators4.customElement)("daw-playhead")
574
+ ], DawPlayheadElement);
575
+
576
+ // src/elements/daw-transport.ts
577
+ var import_lit5 = require("lit");
578
+ var import_decorators5 = require("lit/decorators.js");
579
+ var DawTransportElement = class extends import_lit5.LitElement {
580
+ constructor() {
581
+ super(...arguments);
582
+ this.for = "";
583
+ }
584
+ get target() {
585
+ return this.for ? document.getElementById(this.for) : null;
586
+ }
587
+ // Light DOM — button children stay in consumer's DOM.
588
+ // No render() needed; light DOM elements don't use <slot>.
589
+ createRenderRoot() {
590
+ return this;
591
+ }
592
+ };
593
+ __decorateClass([
594
+ (0, import_decorators5.property)()
595
+ ], DawTransportElement.prototype, "for", 2);
596
+ DawTransportElement = __decorateClass([
597
+ (0, import_decorators5.customElement)("daw-transport")
598
+ ], DawTransportElement);
599
+
600
+ // src/elements/daw-play-button.ts
601
+ var import_lit7 = require("lit");
602
+ var import_decorators6 = require("lit/decorators.js");
603
+
604
+ // src/elements/daw-transport-button.ts
605
+ var import_lit6 = require("lit");
606
+ var DawTransportButton = class extends import_lit6.LitElement {
607
+ get target() {
608
+ const transport = this.closest("daw-transport");
609
+ return transport?.target ?? null;
610
+ }
611
+ };
612
+ DawTransportButton.styles = import_lit6.css`
613
+ button {
614
+ cursor: pointer;
615
+ background: var(--daw-controls-background, #1a1a2e);
616
+ color: var(--daw-controls-text, #e0d4c8);
617
+ border: 1px solid currentColor;
618
+ padding: 4px 8px;
619
+ font: inherit;
620
+ }
621
+ button:hover {
622
+ opacity: 0.8;
623
+ }
624
+ button:disabled {
625
+ opacity: 0.4;
626
+ cursor: default;
627
+ }
628
+ `;
629
+
630
+ // src/elements/daw-play-button.ts
631
+ var DawPlayButtonElement = class extends DawTransportButton {
632
+ render() {
633
+ return import_lit7.html`
634
+ <button part="button" @click=${this._onClick}>
635
+ <slot>Play</slot>
636
+ </button>
637
+ `;
638
+ }
639
+ _onClick() {
640
+ const target = this.target;
641
+ if (!target) {
642
+ console.warn(
643
+ '[dawcore] <daw-play-button> has no target. Check <daw-transport for="..."> references a valid <daw-editor> id.'
644
+ );
645
+ return;
646
+ }
647
+ target.play();
648
+ }
649
+ };
650
+ DawPlayButtonElement = __decorateClass([
651
+ (0, import_decorators6.customElement)("daw-play-button")
652
+ ], DawPlayButtonElement);
653
+
654
+ // src/elements/daw-pause-button.ts
655
+ var import_lit8 = require("lit");
656
+ var import_decorators7 = require("lit/decorators.js");
657
+ var DawPauseButtonElement = class extends DawTransportButton {
658
+ render() {
659
+ return import_lit8.html`
660
+ <button part="button" @click=${this._onClick}>
661
+ <slot>Pause</slot>
662
+ </button>
663
+ `;
664
+ }
665
+ _onClick() {
666
+ const target = this.target;
667
+ if (!target) {
668
+ console.warn(
669
+ '[dawcore] <daw-pause-button> has no target. Check <daw-transport for="..."> references a valid <daw-editor> id.'
670
+ );
671
+ return;
672
+ }
673
+ target.pause();
674
+ }
675
+ };
676
+ DawPauseButtonElement = __decorateClass([
677
+ (0, import_decorators7.customElement)("daw-pause-button")
678
+ ], DawPauseButtonElement);
679
+
680
+ // src/elements/daw-stop-button.ts
681
+ var import_lit9 = require("lit");
682
+ var import_decorators8 = require("lit/decorators.js");
683
+ var DawStopButtonElement = class extends DawTransportButton {
684
+ render() {
685
+ return import_lit9.html`
686
+ <button part="button" @click=${this._onClick}>
687
+ <slot>Stop</slot>
688
+ </button>
689
+ `;
690
+ }
691
+ _onClick() {
692
+ const target = this.target;
693
+ if (!target) {
694
+ console.warn(
695
+ '[dawcore] <daw-stop-button> has no target. Check <daw-transport for="..."> references a valid <daw-editor> id.'
696
+ );
697
+ return;
698
+ }
699
+ target.stop();
700
+ }
701
+ };
702
+ DawStopButtonElement = __decorateClass([
703
+ (0, import_decorators8.customElement)("daw-stop-button")
704
+ ], DawStopButtonElement);
705
+
706
+ // src/elements/daw-editor.ts
707
+ var import_lit12 = require("lit");
708
+ var import_decorators10 = require("lit/decorators.js");
709
+ var import_core4 = require("@waveform-playlist/core");
710
+
711
+ // src/workers/peaksWorker.ts
712
+ var import_waveform_data = __toESM(require("waveform-data"));
713
+ var workerSource = `
714
+ "use strict";
715
+
716
+ var INT8_MAX = 127;
717
+ var INT8_MIN = -128;
718
+ var INT16_MAX = 32767;
719
+ var INT16_MIN = -32768;
720
+
721
+ function calculateWaveformDataLength(audio_sample_count, scale) {
722
+ var data_length = Math.floor(audio_sample_count / scale);
723
+ var samples_remaining = audio_sample_count - (data_length * scale);
724
+ if (samples_remaining > 0) {
725
+ data_length++;
726
+ }
727
+ return data_length;
728
+ }
729
+
730
+ function generateWaveformData(options) {
731
+ var scale = options.scale;
732
+ var amplitude_scale = options.amplitude_scale;
733
+ var split_channels = options.split_channels;
734
+ var length = options.length;
735
+ var sample_rate = options.sample_rate;
736
+ var channels = options.channels.map(function(channel) {
737
+ return new Float32Array(channel);
738
+ });
739
+ var output_channels = split_channels ? channels.length : 1;
740
+ var header_size = 24;
741
+ var data_length = calculateWaveformDataLength(length, scale);
742
+ var bytes_per_sample = options.bits === 8 ? 1 : 2;
743
+ var total_size = header_size + data_length * 2 * bytes_per_sample * output_channels;
744
+ var buffer = new ArrayBuffer(total_size);
745
+ var data_view = new DataView(buffer);
746
+
747
+ var scale_counter = 0;
748
+ var offset = header_size;
749
+
750
+ var min_value = new Array(output_channels);
751
+ var max_value = new Array(output_channels);
752
+
753
+ for (var channel = 0; channel < output_channels; channel++) {
754
+ min_value[channel] = Infinity;
755
+ max_value[channel] = -Infinity;
756
+ }
757
+
758
+ var range_min = options.bits === 8 ? INT8_MIN : INT16_MIN;
759
+ var range_max = options.bits === 8 ? INT8_MAX : INT16_MAX;
760
+
761
+ data_view.setInt32(0, 2, true);
762
+ data_view.setUint32(4, options.bits === 8, true);
763
+ data_view.setInt32(8, sample_rate, true);
764
+ data_view.setInt32(12, scale, true);
765
+ data_view.setInt32(16, data_length, true);
766
+ data_view.setInt32(20, output_channels, true);
767
+
768
+ for (var i = 0; i < length; i++) {
769
+ var sample = 0;
770
+
771
+ if (output_channels === 1) {
772
+ for (var ch = 0; ch < channels.length; ++ch) {
773
+ sample += channels[ch][i];
774
+ }
775
+ sample = Math.floor(range_max * sample * amplitude_scale / channels.length);
776
+
777
+ if (sample < min_value[0]) {
778
+ min_value[0] = sample;
779
+ if (min_value[0] < range_min) {
780
+ min_value[0] = range_min;
781
+ }
782
+ }
783
+ if (sample > max_value[0]) {
784
+ max_value[0] = sample;
785
+ if (max_value[0] > range_max) {
786
+ max_value[0] = range_max;
787
+ }
788
+ }
789
+ }
790
+ else {
791
+ for (var ch2 = 0; ch2 < output_channels; ++ch2) {
792
+ sample = Math.floor(range_max * channels[ch2][i] * amplitude_scale);
793
+
794
+ if (sample < min_value[ch2]) {
795
+ min_value[ch2] = sample;
796
+ if (min_value[ch2] < range_min) {
797
+ min_value[ch2] = range_min;
798
+ }
799
+ }
800
+ if (sample > max_value[ch2]) {
801
+ max_value[ch2] = sample;
802
+ if (max_value[ch2] > range_max) {
803
+ max_value[ch2] = range_max;
804
+ }
805
+ }
806
+ }
807
+ }
808
+
809
+ if (++scale_counter === scale) {
810
+ for (var ch3 = 0; ch3 < output_channels; ch3++) {
811
+ if (options.bits === 8) {
812
+ data_view.setInt8(offset++, min_value[ch3]);
813
+ data_view.setInt8(offset++, max_value[ch3]);
814
+ }
815
+ else {
816
+ data_view.setInt16(offset, min_value[ch3], true);
817
+ data_view.setInt16(offset + 2, max_value[ch3], true);
818
+ offset += 4;
819
+ }
820
+ min_value[ch3] = Infinity;
821
+ max_value[ch3] = -Infinity;
822
+ }
823
+ scale_counter = 0;
824
+ }
825
+ }
826
+
827
+ if (scale_counter > 0) {
828
+ for (var ch4 = 0; ch4 < output_channels; ch4++) {
829
+ if (options.bits === 8) {
830
+ data_view.setInt8(offset++, min_value[ch4]);
831
+ data_view.setInt8(offset++, max_value[ch4]);
832
+ }
833
+ else {
834
+ data_view.setInt16(offset, min_value[ch4], true);
835
+ data_view.setInt16(offset + 2, max_value[ch4], true);
836
+ offset += 4;
837
+ }
838
+ }
839
+ }
840
+
841
+ return buffer;
842
+ }
843
+
844
+ self.onmessage = function(e) {
845
+ var msg = e.data;
846
+ try {
847
+ var result = generateWaveformData({
848
+ scale: msg.scale,
849
+ bits: msg.bits,
850
+ amplitude_scale: msg.amplitude_scale,
851
+ split_channels: msg.split_channels,
852
+ length: msg.length,
853
+ sample_rate: msg.sample_rate,
854
+ channels: msg.channels
855
+ });
856
+ self.postMessage({ id: msg.id, buffer: result }, [result]);
857
+ } catch (err) {
858
+ self.postMessage({ id: msg.id, error: err.message || String(err) });
859
+ }
860
+ };
861
+ `;
862
+ function createPeaksWorker() {
863
+ let worker;
864
+ try {
865
+ const blob = new Blob([workerSource], { type: "application/javascript" });
866
+ const url = URL.createObjectURL(blob);
867
+ worker = new Worker(url);
868
+ URL.revokeObjectURL(url);
869
+ } catch (err) {
870
+ console.warn("[dawcore] Failed to create peaks worker (CSP restriction?): " + String(err));
871
+ return {
872
+ generate() {
873
+ return Promise.reject(
874
+ new Error(
875
+ "Peaks worker unavailable (CSP may block blob: URLs). Add blob: to worker-src directive."
876
+ )
877
+ );
878
+ },
879
+ terminate() {
880
+ }
881
+ };
882
+ }
883
+ const pending = /* @__PURE__ */ new Map();
884
+ let terminated = false;
885
+ let idCounter = 0;
886
+ worker.onmessage = (e) => {
887
+ const msg = e.data;
888
+ const entry = pending.get(msg.id);
889
+ if (!entry) {
890
+ console.warn("[dawcore] Received worker message for unknown id: " + String(msg.id));
891
+ return;
892
+ }
893
+ pending.delete(msg.id);
894
+ if (msg.error) {
895
+ entry.reject(new Error(msg.error));
896
+ } else {
897
+ try {
898
+ const waveformData = import_waveform_data.default.create(msg.buffer);
899
+ entry.resolve(waveformData);
900
+ } catch (err) {
901
+ entry.reject(err);
902
+ }
903
+ }
904
+ };
905
+ worker.onerror = (e) => {
906
+ const reason = e.error ?? new Error(e.message);
907
+ console.warn("[dawcore] Peaks worker crashed: " + String(reason));
908
+ terminated = true;
909
+ worker.terminate();
910
+ for (const [, entry] of pending) {
911
+ entry.reject(reason);
912
+ }
913
+ pending.clear();
914
+ };
915
+ return {
916
+ generate(params) {
917
+ if (terminated) return Promise.reject(new Error("Worker terminated"));
918
+ const messageId = String(++idCounter);
919
+ return new Promise((resolve, reject) => {
920
+ pending.set(messageId, { resolve, reject });
921
+ worker.postMessage(
922
+ {
923
+ id: messageId,
924
+ scale: params.scale,
925
+ bits: params.bits,
926
+ amplitude_scale: 1,
927
+ split_channels: params.splitChannels,
928
+ length: params.length,
929
+ sample_rate: params.sampleRate,
930
+ channels: params.channels
931
+ },
932
+ params.channels
933
+ // Transfer ownership
934
+ );
935
+ });
936
+ },
937
+ terminate() {
938
+ terminated = true;
939
+ worker.terminate();
940
+ for (const [, entry] of pending) {
941
+ entry.reject(new Error("Worker terminated"));
942
+ }
943
+ pending.clear();
944
+ }
945
+ };
946
+ }
947
+
948
+ // src/workers/waveformDataUtils.ts
949
+ function sliceAndResample(waveformData, samplesPerPixel, offsetSamples, durationSamples) {
950
+ let processedData = waveformData;
951
+ if (offsetSamples !== void 0 && durationSamples !== void 0) {
952
+ if (processedData.scale !== samplesPerPixel) {
953
+ const sourceScale = waveformData.scale;
954
+ const ratio = samplesPerPixel / sourceScale;
955
+ const targetStart = Math.floor(offsetSamples / samplesPerPixel);
956
+ const targetEnd = Math.ceil((offsetSamples + durationSamples) / samplesPerPixel);
957
+ const sourceStart = Math.max(0, Math.floor(targetStart * ratio));
958
+ const sourceEnd = Math.min(waveformData.length, Math.ceil(targetEnd * ratio));
959
+ if (sourceStart >= sourceEnd) {
960
+ return null;
961
+ }
962
+ processedData = processedData.slice({
963
+ startIndex: sourceStart,
964
+ endIndex: sourceEnd
965
+ });
966
+ processedData = processedData.resample({ scale: samplesPerPixel });
967
+ } else {
968
+ const startIndex = Math.floor(offsetSamples / samplesPerPixel);
969
+ const endIndex = Math.ceil((offsetSamples + durationSamples) / samplesPerPixel);
970
+ processedData = processedData.slice({ startIndex, endIndex });
971
+ }
972
+ } else if (processedData.scale !== samplesPerPixel) {
973
+ processedData = processedData.resample({ scale: samplesPerPixel });
974
+ }
975
+ return processedData;
976
+ }
977
+ function extractPeaks(waveformData, samplesPerPixel, isMono, offsetSamples, durationSamples) {
978
+ const processedData = sliceAndResample(
979
+ waveformData,
980
+ samplesPerPixel,
981
+ offsetSamples,
982
+ durationSamples
983
+ );
984
+ if (processedData === null) {
985
+ const bits2 = waveformData.bits;
986
+ const numChannels2 = isMono ? 1 : waveformData.channels;
987
+ const emptyData = Array.from(
988
+ { length: numChannels2 },
989
+ () => bits2 === 8 ? new Int8Array(0) : new Int16Array(0)
990
+ );
991
+ return { length: 0, data: emptyData, bits: bits2 };
992
+ }
993
+ const numChannels = processedData.channels;
994
+ const bits = processedData.bits;
995
+ const channelPeaks = [];
996
+ for (let c = 0; c < numChannels; c++) {
997
+ const channel = processedData.channel(c);
998
+ const minArray = channel.min_array();
999
+ const maxArray = channel.max_array();
1000
+ const len = minArray.length;
1001
+ const peaks = bits === 8 ? new Int8Array(len * 2) : new Int16Array(len * 2);
1002
+ for (let i = 0; i < len; i++) {
1003
+ peaks[i * 2] = minArray[i];
1004
+ peaks[i * 2 + 1] = maxArray[i];
1005
+ }
1006
+ channelPeaks.push(peaks);
1007
+ }
1008
+ if (isMono && channelPeaks.length > 1) {
1009
+ const weight = 1 / channelPeaks.length;
1010
+ const numPeaks = channelPeaks[0].length / 2;
1011
+ const monoPeaks = bits === 8 ? new Int8Array(numPeaks * 2) : new Int16Array(numPeaks * 2);
1012
+ for (let i = 0; i < numPeaks; i++) {
1013
+ let min = 0;
1014
+ let max = 0;
1015
+ for (let c = 0; c < channelPeaks.length; c++) {
1016
+ min += weight * channelPeaks[c][i * 2];
1017
+ max += weight * channelPeaks[c][i * 2 + 1];
1018
+ }
1019
+ monoPeaks[i * 2] = min;
1020
+ monoPeaks[i * 2 + 1] = max;
1021
+ }
1022
+ return { length: numPeaks, data: [monoPeaks], bits };
1023
+ }
1024
+ const peakLength = channelPeaks.length > 0 ? channelPeaks[0].length / 2 : 0;
1025
+ return { length: peakLength, data: channelPeaks, bits };
1026
+ }
1027
+
1028
+ // src/workers/peakPipeline.ts
1029
+ var PeakPipeline = class {
1030
+ constructor() {
1031
+ this._worker = null;
1032
+ this._cache = /* @__PURE__ */ new WeakMap();
1033
+ this._inflight = /* @__PURE__ */ new WeakMap();
1034
+ }
1035
+ /**
1036
+ * Generate PeakData for a clip from its AudioBuffer.
1037
+ * Uses cached WaveformData when available; otherwise generates via worker.
1038
+ * The worker generates at `scale` (= samplesPerPixel) for exact rendering.
1039
+ */
1040
+ async generatePeaks(audioBuffer, samplesPerPixel, isMono) {
1041
+ const waveformData = await this._getWaveformData(audioBuffer, samplesPerPixel);
1042
+ try {
1043
+ return extractPeaks(waveformData, samplesPerPixel, isMono);
1044
+ } catch (err) {
1045
+ console.warn("[dawcore] extractPeaks failed: " + String(err));
1046
+ throw err;
1047
+ }
1048
+ }
1049
+ /**
1050
+ * Re-extract peaks for all clips at a new zoom level using cached WaveformData.
1051
+ * Only works for zoom levels coarser than (or equal to) the cached base scale.
1052
+ * Returns a new Map of clipId → PeakData. Clips without cached data or where
1053
+ * the target scale is finer than the cached base are skipped.
1054
+ */
1055
+ reextractPeaks(clipBuffers, samplesPerPixel, isMono) {
1056
+ const result = /* @__PURE__ */ new Map();
1057
+ for (const [clipId, audioBuffer] of clipBuffers) {
1058
+ const cached = this._cache.get(audioBuffer);
1059
+ if (cached) {
1060
+ if (samplesPerPixel < cached.scale) continue;
1061
+ try {
1062
+ result.set(clipId, extractPeaks(cached, samplesPerPixel, isMono));
1063
+ } catch (err) {
1064
+ console.warn("[dawcore] reextractPeaks failed for clip " + clipId + ": " + String(err));
1065
+ }
1066
+ }
1067
+ }
1068
+ return result;
1069
+ }
1070
+ terminate() {
1071
+ this._worker?.terminate();
1072
+ this._worker = null;
1073
+ }
1074
+ async _getWaveformData(audioBuffer, samplesPerPixel) {
1075
+ const cached = this._cache.get(audioBuffer);
1076
+ if (cached && cached.scale <= samplesPerPixel) return cached;
1077
+ const inflight = this._inflight.get(audioBuffer);
1078
+ if (inflight) return inflight;
1079
+ if (!this._worker) {
1080
+ this._worker = createPeaksWorker();
1081
+ }
1082
+ const channels = [];
1083
+ for (let c = 0; c < audioBuffer.numberOfChannels; c++) {
1084
+ channels.push(audioBuffer.getChannelData(c).slice().buffer);
1085
+ }
1086
+ const promise = this._worker.generate({
1087
+ channels,
1088
+ length: audioBuffer.length,
1089
+ sampleRate: audioBuffer.sampleRate,
1090
+ scale: samplesPerPixel,
1091
+ bits: 16,
1092
+ splitChannels: true
1093
+ }).then((waveformData) => {
1094
+ this._cache.set(audioBuffer, waveformData);
1095
+ this._inflight.delete(audioBuffer);
1096
+ return waveformData;
1097
+ }).catch((err) => {
1098
+ this._inflight.delete(audioBuffer);
1099
+ console.warn("[dawcore] Peak generation via worker failed: " + String(err));
1100
+ throw err;
1101
+ });
1102
+ this._inflight.set(audioBuffer, promise);
1103
+ return promise;
1104
+ }
1105
+ };
1106
+
1107
+ // src/elements/daw-track-controls.ts
1108
+ var import_lit10 = require("lit");
1109
+ var import_decorators9 = require("lit/decorators.js");
1110
+ var DawTrackControlsElement = class extends import_lit10.LitElement {
1111
+ constructor() {
1112
+ super(...arguments);
1113
+ this.trackId = null;
1114
+ this.trackName = "";
1115
+ this.volume = 1;
1116
+ this.pan = 0;
1117
+ this.muted = false;
1118
+ this.soloed = false;
1119
+ this._onVolumeInput = (e) => {
1120
+ const value = Number(e.target.value);
1121
+ if (Number.isFinite(value)) this._dispatchControl("volume", value);
1122
+ };
1123
+ this._onPanInput = (e) => {
1124
+ const value = Number(e.target.value);
1125
+ if (Number.isFinite(value)) this._dispatchControl("pan", value);
1126
+ };
1127
+ this._onMuteClick = () => {
1128
+ this._dispatchControl("muted", !this.muted);
1129
+ };
1130
+ this._onSoloClick = () => {
1131
+ this._dispatchControl("soloed", !this.soloed);
1132
+ };
1133
+ this._onRemoveClick = () => {
1134
+ if (!this.trackId) return;
1135
+ this.dispatchEvent(
1136
+ new CustomEvent("daw-track-remove", {
1137
+ bubbles: true,
1138
+ composed: true,
1139
+ detail: { trackId: this.trackId }
1140
+ })
1141
+ );
1142
+ };
1143
+ }
1144
+ _dispatchControl(prop, value) {
1145
+ if (!this.trackId) return;
1146
+ this.dispatchEvent(
1147
+ new CustomEvent("daw-track-control", {
1148
+ bubbles: true,
1149
+ composed: true,
1150
+ detail: { trackId: this.trackId, prop, value }
1151
+ })
1152
+ );
1153
+ }
1154
+ render() {
1155
+ const volPercent = Math.round(this.volume * 100);
1156
+ const panPercent = Math.round(Math.abs(this.pan) * 100);
1157
+ const panDisplay = this.pan === 0 ? "C" : (this.pan > 0 ? "R" : "L") + panPercent;
1158
+ return import_lit10.html`
1159
+ <div class="header">
1160
+ <span class="name" title=${this.trackName}>${this.trackName || "Untitled"}</span>
1161
+ <button class="remove-btn" @click=${this._onRemoveClick} title="Remove track">
1162
+ &times;
1163
+ </button>
1164
+ </div>
1165
+ <div class="buttons">
1166
+ <button
1167
+ class="btn ${this.muted ? "muted-active" : ""}"
1168
+ @click=${this._onMuteClick}
1169
+ title="Mute"
1170
+ >
1171
+ M
1172
+ </button>
1173
+ <button class="btn ${this.soloed ? "active" : ""}" @click=${this._onSoloClick} title="Solo">
1174
+ S
1175
+ </button>
1176
+ </div>
1177
+ <div class="slider-row">
1178
+ <span class="slider-label">
1179
+ <span class="slider-label-name">Vol</span>
1180
+ <span class="slider-label-value">${volPercent}%</span>
1181
+ </span>
1182
+ <input
1183
+ type="range"
1184
+ min="0"
1185
+ max="1"
1186
+ step="0.01"
1187
+ .value=${String(this.volume)}
1188
+ @input=${this._onVolumeInput}
1189
+ />
1190
+ </div>
1191
+ <div class="slider-row">
1192
+ <span class="slider-label">
1193
+ <span class="slider-label-name">Pan</span>
1194
+ <span class="slider-label-value">${panDisplay}</span>
1195
+ </span>
1196
+ <input
1197
+ type="range"
1198
+ min="-1"
1199
+ max="1"
1200
+ step="0.01"
1201
+ .value=${String(this.pan)}
1202
+ @input=${this._onPanInput}
1203
+ />
1204
+ </div>
1205
+ `;
1206
+ }
1207
+ };
1208
+ DawTrackControlsElement.styles = import_lit10.css`
1209
+ :host {
1210
+ display: flex;
1211
+ flex-direction: column;
1212
+ justify-content: center;
1213
+ box-sizing: border-box;
1214
+ padding: 6px 8px;
1215
+ background: var(--daw-controls-background, #0f0f1a);
1216
+ color: var(--daw-controls-text, #c49a6c);
1217
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
1218
+ font-family: system-ui, sans-serif;
1219
+ font-size: 11px;
1220
+ }
1221
+ .header {
1222
+ display: flex;
1223
+ align-items: center;
1224
+ justify-content: space-between;
1225
+ gap: 4px;
1226
+ margin-bottom: 6px;
1227
+ }
1228
+ .name {
1229
+ flex: 1;
1230
+ overflow: hidden;
1231
+ text-overflow: ellipsis;
1232
+ white-space: nowrap;
1233
+ font-weight: 600;
1234
+ font-size: 11px;
1235
+ }
1236
+ .remove-btn {
1237
+ background: none;
1238
+ border: none;
1239
+ color: var(--daw-controls-text, #c49a6c);
1240
+ cursor: pointer;
1241
+ padding: 0 2px;
1242
+ font-size: 14px;
1243
+ line-height: 1;
1244
+ opacity: 0.4;
1245
+ }
1246
+ .remove-btn:hover {
1247
+ opacity: 1;
1248
+ color: #d08070;
1249
+ }
1250
+ .buttons {
1251
+ display: flex;
1252
+ gap: 3px;
1253
+ margin-bottom: 6px;
1254
+ }
1255
+ .btn {
1256
+ background: rgba(255, 255, 255, 0.06);
1257
+ border: 1px solid rgba(255, 255, 255, 0.1);
1258
+ border-radius: 3px;
1259
+ color: var(--daw-controls-text, #c49a6c);
1260
+ cursor: pointer;
1261
+ font-size: 10px;
1262
+ font-weight: 600;
1263
+ padding: 2px 8px;
1264
+ text-align: center;
1265
+ }
1266
+ .btn:hover {
1267
+ background: rgba(255, 255, 255, 0.12);
1268
+ }
1269
+ .btn.active {
1270
+ background: rgba(99, 199, 95, 0.25);
1271
+ border-color: rgba(99, 199, 95, 0.5);
1272
+ color: #63c75f;
1273
+ }
1274
+ .btn.muted-active {
1275
+ background: rgba(208, 128, 112, 0.25);
1276
+ border-color: rgba(208, 128, 112, 0.5);
1277
+ color: #d08070;
1278
+ }
1279
+ .slider-row {
1280
+ display: flex;
1281
+ align-items: center;
1282
+ gap: 4px;
1283
+ height: 20px;
1284
+ }
1285
+ .slider-label {
1286
+ width: 50px;
1287
+ font-size: 9px;
1288
+ text-transform: uppercase;
1289
+ letter-spacing: 0.5px;
1290
+ opacity: 0.6;
1291
+ flex-shrink: 0;
1292
+ display: flex;
1293
+ justify-content: space-between;
1294
+ }
1295
+ .slider-label-name {
1296
+ opacity: 0.5;
1297
+ }
1298
+ .slider-label-value {
1299
+ font-family: 'Courier New', monospace;
1300
+ }
1301
+ input[type='range'] {
1302
+ flex: 1;
1303
+ min-width: 0;
1304
+ height: 20px;
1305
+ margin: 0;
1306
+ -webkit-appearance: none;
1307
+ appearance: none;
1308
+ background: transparent;
1309
+ cursor: pointer;
1310
+ }
1311
+ input[type='range']::-webkit-slider-runnable-track {
1312
+ height: 3px;
1313
+ background: rgba(255, 255, 255, 0.12);
1314
+ border-radius: 2px;
1315
+ }
1316
+ input[type='range']::-webkit-slider-thumb {
1317
+ -webkit-appearance: none;
1318
+ width: 12px;
1319
+ height: 12px;
1320
+ border-radius: 50%;
1321
+ background: var(--daw-controls-text, #c49a6c);
1322
+ margin-top: -4.5px;
1323
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
1324
+ }
1325
+ input[type='range']::-moz-range-track {
1326
+ height: 3px;
1327
+ background: rgba(255, 255, 255, 0.12);
1328
+ border-radius: 2px;
1329
+ border: none;
1330
+ }
1331
+ input[type='range']::-moz-range-thumb {
1332
+ width: 12px;
1333
+ height: 12px;
1334
+ border-radius: 50%;
1335
+ background: var(--daw-controls-text, #c49a6c);
1336
+ border: none;
1337
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
1338
+ }
1339
+ `;
1340
+ __decorateClass([
1341
+ (0, import_decorators9.property)({ attribute: false })
1342
+ ], DawTrackControlsElement.prototype, "trackId", 2);
1343
+ __decorateClass([
1344
+ (0, import_decorators9.property)({ attribute: false })
1345
+ ], DawTrackControlsElement.prototype, "trackName", 2);
1346
+ __decorateClass([
1347
+ (0, import_decorators9.property)({ type: Number, attribute: false })
1348
+ ], DawTrackControlsElement.prototype, "volume", 2);
1349
+ __decorateClass([
1350
+ (0, import_decorators9.property)({ type: Number, attribute: false })
1351
+ ], DawTrackControlsElement.prototype, "pan", 2);
1352
+ __decorateClass([
1353
+ (0, import_decorators9.property)({ type: Boolean, attribute: false })
1354
+ ], DawTrackControlsElement.prototype, "muted", 2);
1355
+ __decorateClass([
1356
+ (0, import_decorators9.property)({ type: Boolean, attribute: false })
1357
+ ], DawTrackControlsElement.prototype, "soloed", 2);
1358
+ DawTrackControlsElement = __decorateClass([
1359
+ (0, import_decorators9.customElement)("daw-track-controls")
1360
+ ], DawTrackControlsElement);
1361
+
1362
+ // src/styles/theme.ts
1363
+ var import_lit11 = require("lit");
1364
+ var hostStyles = import_lit11.css`
1365
+ :host {
1366
+ --daw-wave-color: #c49a6c;
1367
+ --daw-progress-color: #63c75f;
1368
+ --daw-playhead-color: #d08070;
1369
+ --daw-background: #1a1a2e;
1370
+ --daw-track-background: #16213e;
1371
+ --daw-ruler-color: #c49a6c;
1372
+ --daw-ruler-background: #0f0f1a;
1373
+ --daw-controls-background: #1a1a2e;
1374
+ --daw-controls-text: #e0d4c8;
1375
+ --daw-selection-color: rgba(99, 199, 95, 0.3);
1376
+ --daw-clip-header-background: rgba(0, 0, 0, 0.4);
1377
+ --daw-clip-header-text: #e0d4c8;
1378
+ }
1379
+ `;
1380
+
1381
+ // src/controllers/viewport-controller.ts
1382
+ var OVERSCAN_MULTIPLIER = 1.5;
1383
+ var SCROLL_THRESHOLD = 100;
1384
+ var ViewportController = class {
1385
+ constructor(host) {
1386
+ this._scrollContainer = null;
1387
+ this._lastScrollLeft = 0;
1388
+ // Permissive defaults: render everything until scroll container is attached
1389
+ this.visibleStart = -Infinity;
1390
+ this.visibleEnd = Infinity;
1391
+ this.containerWidth = 0;
1392
+ /** CSS selector for the scroll container inside the host's Shadow DOM. */
1393
+ this.scrollSelector = "";
1394
+ this._onScroll = () => {
1395
+ if (!this._scrollContainer) return;
1396
+ const { scrollLeft, clientWidth } = this._scrollContainer;
1397
+ if (Math.abs(scrollLeft - this._lastScrollLeft) >= SCROLL_THRESHOLD) {
1398
+ this._update(scrollLeft, clientWidth);
1399
+ this._host.requestUpdate();
1400
+ }
1401
+ };
1402
+ this._host = host;
1403
+ host.addController(this);
1404
+ }
1405
+ hostConnected() {
1406
+ requestAnimationFrame(() => {
1407
+ if (!this._host.isConnected) return;
1408
+ const container = this.scrollSelector ? this._host.shadowRoot?.querySelector(this.scrollSelector) : this._host;
1409
+ if (container) {
1410
+ this._attachScrollContainer(container);
1411
+ } else if (this.scrollSelector) {
1412
+ console.warn(
1413
+ '[dawcore] ViewportController: scroll container not found for "' + this.scrollSelector + '"'
1414
+ );
1415
+ }
1416
+ });
1417
+ }
1418
+ hostDisconnected() {
1419
+ this._scrollContainer?.removeEventListener("scroll", this._onScroll);
1420
+ this._scrollContainer = null;
1421
+ }
1422
+ _attachScrollContainer(container) {
1423
+ this._scrollContainer?.removeEventListener("scroll", this._onScroll);
1424
+ this._scrollContainer = container;
1425
+ container.addEventListener("scroll", this._onScroll, { passive: true });
1426
+ this._update(container.scrollLeft, container.clientWidth);
1427
+ this._host.requestUpdate();
1428
+ }
1429
+ _update(scrollLeft, containerWidth) {
1430
+ this._lastScrollLeft = scrollLeft;
1431
+ this.containerWidth = containerWidth;
1432
+ const buffer = containerWidth * OVERSCAN_MULTIPLIER;
1433
+ this.visibleStart = scrollLeft - buffer;
1434
+ this.visibleEnd = scrollLeft + containerWidth + buffer;
1435
+ }
1436
+ };
1437
+
1438
+ // src/controllers/audio-resume-controller.ts
1439
+ var import_playout = require("@waveform-playlist/playout");
1440
+ var AudioResumeController = class {
1441
+ constructor(host) {
1442
+ this._target = null;
1443
+ this._attached = false;
1444
+ this._generation = 0;
1445
+ this._onGesture = (e) => {
1446
+ (0, import_playout.resumeGlobalAudioContext)().catch((err) => {
1447
+ console.warn(
1448
+ "[dawcore] AudioResumeController: eager resume failed, will retry on play: " + String(err)
1449
+ );
1450
+ });
1451
+ const otherType = e.type === "pointerdown" ? "keydown" : "pointerdown";
1452
+ this._target?.removeEventListener(otherType, this._onGesture, {
1453
+ capture: true
1454
+ });
1455
+ this._target = null;
1456
+ };
1457
+ this._host = host;
1458
+ host.addController(this);
1459
+ }
1460
+ hostConnected() {
1461
+ const gen = ++this._generation;
1462
+ requestAnimationFrame(() => {
1463
+ if (gen !== this._generation) return;
1464
+ if (!this._host.isConnected || this._attached || this.target === void 0) return;
1465
+ let resolvedTarget;
1466
+ try {
1467
+ resolvedTarget = this._resolveTarget();
1468
+ } catch (err) {
1469
+ console.warn(
1470
+ '[dawcore] AudioResumeController: failed to resolve target "' + this.target + '": ' + String(err)
1471
+ );
1472
+ return;
1473
+ }
1474
+ if (!resolvedTarget) return;
1475
+ this._target = resolvedTarget;
1476
+ this._attached = true;
1477
+ resolvedTarget.addEventListener("pointerdown", this._onGesture, {
1478
+ once: true,
1479
+ capture: true
1480
+ });
1481
+ resolvedTarget.addEventListener("keydown", this._onGesture, {
1482
+ once: true,
1483
+ capture: true
1484
+ });
1485
+ });
1486
+ }
1487
+ hostDisconnected() {
1488
+ this._removeListeners();
1489
+ this._attached = false;
1490
+ }
1491
+ _resolveTarget() {
1492
+ const t = this.target;
1493
+ if (t === void 0) return null;
1494
+ if (t === "") return this._host;
1495
+ if (t === "document") return document;
1496
+ const el = document.querySelector(t);
1497
+ if (!el) {
1498
+ console.warn(
1499
+ '[dawcore] AudioResumeController: target "' + t + '" not found in DOM at attach time, falling back to host element. Ensure the target exists before <daw-editor> connects.'
1500
+ );
1501
+ return this._host;
1502
+ }
1503
+ return el;
1504
+ }
1505
+ _removeListeners() {
1506
+ if (!this._target) return;
1507
+ this._target.removeEventListener("pointerdown", this._onGesture, {
1508
+ capture: true
1509
+ });
1510
+ this._target.removeEventListener("keydown", this._onGesture, {
1511
+ capture: true
1512
+ });
1513
+ this._target = null;
1514
+ }
1515
+ };
1516
+
1517
+ // src/controllers/recording-controller.ts
1518
+ var import_playout2 = require("@waveform-playlist/playout");
1519
+ var import_worklets = require("@waveform-playlist/worklets");
1520
+ var import_recording = require("@waveform-playlist/recording");
1521
+ var RecordingController = class {
1522
+ constructor(host) {
1523
+ this._sessions = /* @__PURE__ */ new Map();
1524
+ this._workletLoaded = false;
1525
+ this._host = host;
1526
+ host.addController(this);
1527
+ }
1528
+ hostConnected() {
1529
+ }
1530
+ hostDisconnected() {
1531
+ for (const trackId of [...this._sessions.keys()]) {
1532
+ this._cleanupSession(trackId);
1533
+ }
1534
+ }
1535
+ get isRecording() {
1536
+ return this._sessions.size > 0;
1537
+ }
1538
+ getSession(trackId) {
1539
+ return this._sessions.get(trackId);
1540
+ }
1541
+ async startRecording(stream, options = {}) {
1542
+ const trackId = options.trackId ?? this._host._selectedTrackId;
1543
+ if (!trackId) {
1544
+ console.warn("[dawcore] RecordingController: No track selected for recording");
1545
+ return;
1546
+ }
1547
+ if (this._sessions.has(trackId)) {
1548
+ console.warn('[dawcore] RecordingController: Already recording on track "' + trackId + '"');
1549
+ return;
1550
+ }
1551
+ const bits = options.bits ?? 16;
1552
+ const context = (0, import_playout2.getGlobalContext)();
1553
+ const rawCtx = context.rawContext;
1554
+ this._host.resolveAudioContextSampleRate(rawCtx.sampleRate);
1555
+ try {
1556
+ if (!this._workletLoaded) {
1557
+ await rawCtx.audioWorklet.addModule(import_worklets.recordingProcessorUrl);
1558
+ this._workletLoaded = true;
1559
+ }
1560
+ const channelCount = stream.getAudioTracks()[0]?.getSettings()?.channelCount ?? 1;
1561
+ const startSample = options.startSample ?? Math.floor(this._host._currentTime * this._host.effectiveSampleRate);
1562
+ const source = context.createMediaStreamSource(stream);
1563
+ const workletNode = context.createAudioWorkletNode("recording-processor", {
1564
+ channelCount,
1565
+ channelCountMode: "explicit"
1566
+ });
1567
+ const audioTrack = stream.getAudioTracks()[0] ?? null;
1568
+ const onTrackEnded = audioTrack ? () => {
1569
+ if (this._sessions.has(trackId)) {
1570
+ this.stopRecording(trackId);
1571
+ }
1572
+ } : null;
1573
+ const session = {
1574
+ trackId,
1575
+ stream,
1576
+ source,
1577
+ workletNode,
1578
+ chunks: Array.from({ length: channelCount }, () => []),
1579
+ totalSamples: 0,
1580
+ peaks: Array.from(
1581
+ { length: channelCount },
1582
+ () => bits === 8 ? new Int8Array(0) : new Int16Array(0)
1583
+ ),
1584
+ startSample,
1585
+ channelCount,
1586
+ bits,
1587
+ isFirstMessage: true,
1588
+ _onTrackEnded: onTrackEnded,
1589
+ _audioTrack: audioTrack
1590
+ };
1591
+ this._sessions.set(trackId, session);
1592
+ workletNode.port.onmessage = (e) => {
1593
+ this._onWorkletMessage(trackId, e.data);
1594
+ };
1595
+ source.connect(workletNode);
1596
+ workletNode.port.postMessage({ command: "start", channelCount });
1597
+ if (audioTrack && onTrackEnded) {
1598
+ audioTrack.addEventListener("ended", onTrackEnded);
1599
+ }
1600
+ this._host.dispatchEvent(
1601
+ new CustomEvent("daw-recording-start", {
1602
+ bubbles: true,
1603
+ composed: true,
1604
+ detail: { trackId, stream }
1605
+ })
1606
+ );
1607
+ this._host.requestUpdate();
1608
+ } catch (err) {
1609
+ this._cleanupSession(trackId);
1610
+ console.warn("[dawcore] RecordingController: Failed to start recording: " + String(err));
1611
+ this._host.dispatchEvent(
1612
+ new CustomEvent("daw-recording-error", {
1613
+ bubbles: true,
1614
+ composed: true,
1615
+ detail: { trackId, error: err }
1616
+ })
1617
+ );
1618
+ }
1619
+ }
1620
+ stopRecording(trackId) {
1621
+ const id = trackId ?? [...this._sessions.keys()][0];
1622
+ if (!id) return;
1623
+ const session = this._sessions.get(id);
1624
+ if (!session) return;
1625
+ session.workletNode.port.postMessage({ command: "stop" });
1626
+ session.source.disconnect();
1627
+ session.workletNode.disconnect();
1628
+ this._removeTrackEndedListener(session);
1629
+ if (session.totalSamples === 0) {
1630
+ console.warn("[dawcore] RecordingController: No audio data captured");
1631
+ this._sessions.delete(id);
1632
+ this._host.requestUpdate();
1633
+ this._host.dispatchEvent(
1634
+ new CustomEvent("daw-recording-error", {
1635
+ bubbles: true,
1636
+ composed: true,
1637
+ detail: { trackId: id, error: new Error("No audio data captured") }
1638
+ })
1639
+ );
1640
+ return;
1641
+ }
1642
+ const stopCtx = (0, import_playout2.getGlobalContext)().rawContext;
1643
+ const channelData = session.chunks.map((chunkArr) => (0, import_recording.concatenateAudioData)(chunkArr));
1644
+ const audioBuffer = (0, import_recording.createAudioBuffer)(
1645
+ stopCtx,
1646
+ channelData,
1647
+ this._host.effectiveSampleRate,
1648
+ session.channelCount
1649
+ );
1650
+ const durationSamples = audioBuffer.length;
1651
+ const event = new CustomEvent("daw-recording-complete", {
1652
+ bubbles: true,
1653
+ composed: true,
1654
+ cancelable: true,
1655
+ detail: {
1656
+ trackId: id,
1657
+ audioBuffer,
1658
+ startSample: session.startSample,
1659
+ durationSamples
1660
+ }
1661
+ });
1662
+ const notPrevented = this._host.dispatchEvent(event);
1663
+ this._sessions.delete(id);
1664
+ this._host.requestUpdate();
1665
+ if (notPrevented) {
1666
+ this._createClipFromRecording(id, audioBuffer, session.startSample, durationSamples);
1667
+ }
1668
+ }
1669
+ // Session fields are mutated in place on the hot path (~60fps worklet messages).
1670
+ // This is intentional — creating new session objects + Map entries per message
1671
+ // would cause significant GC pressure. Mutations are confined to the controller's
1672
+ // private map and do not affect Lit's reactive rendering.
1673
+ _onWorkletMessage(trackId, data) {
1674
+ const session = this._sessions.get(trackId);
1675
+ if (!session) return;
1676
+ const { channels } = data;
1677
+ if (!channels || channels.length === 0 || !channels[0]) return;
1678
+ const samplesProcessedBefore = session.totalSamples;
1679
+ for (let ch = 0; ch < session.channelCount; ch++) {
1680
+ if (channels[ch]) {
1681
+ session.chunks[ch].push(channels[ch]);
1682
+ }
1683
+ }
1684
+ session.totalSamples += channels[0].length;
1685
+ for (let ch = 0; ch < session.channelCount; ch++) {
1686
+ if (!channels[ch]) continue;
1687
+ const oldPeakCount = Math.floor(session.peaks[ch].length / 2);
1688
+ session.peaks[ch] = (0, import_recording.appendPeaks)(
1689
+ session.peaks[ch],
1690
+ channels[ch],
1691
+ this._host.samplesPerPixel,
1692
+ samplesProcessedBefore,
1693
+ session.bits
1694
+ );
1695
+ const newPeakCount = Math.floor(session.peaks[ch].length / 2);
1696
+ const waveformSelector = `daw-waveform[data-recording-track="${trackId}"][data-recording-channel="${ch}"]`;
1697
+ const waveformEl = this._host.shadowRoot?.querySelector(waveformSelector);
1698
+ if (waveformEl) {
1699
+ if (session.isFirstMessage) {
1700
+ waveformEl.peaks = session.peaks[ch];
1701
+ } else {
1702
+ waveformEl.setPeaksQuiet(session.peaks[ch]);
1703
+ waveformEl.updatePeaks(Math.max(0, oldPeakCount - 1), newPeakCount);
1704
+ }
1705
+ }
1706
+ }
1707
+ session.isFirstMessage = false;
1708
+ const newPixelWidth = Math.floor(session.totalSamples / this._host.samplesPerPixel);
1709
+ const oldPixelWidth = Math.floor(
1710
+ (session.totalSamples - channels[0].length) / this._host.samplesPerPixel
1711
+ );
1712
+ if (newPixelWidth > oldPixelWidth) {
1713
+ this._host.requestUpdate();
1714
+ }
1715
+ }
1716
+ _createClipFromRecording(trackId, audioBuffer, startSample, durationSamples) {
1717
+ if (typeof this._host._addRecordedClip === "function") {
1718
+ this._host._addRecordedClip(trackId, audioBuffer, startSample, durationSamples);
1719
+ } else {
1720
+ console.warn(
1721
+ '[dawcore] RecordingController: host does not implement _addRecordedClip \u2014 clip not created for track "' + trackId + '"'
1722
+ );
1723
+ }
1724
+ }
1725
+ _removeTrackEndedListener(session) {
1726
+ if (session._audioTrack && session._onTrackEnded) {
1727
+ session._audioTrack.removeEventListener("ended", session._onTrackEnded);
1728
+ }
1729
+ }
1730
+ _cleanupSession(trackId) {
1731
+ const session = this._sessions.get(trackId);
1732
+ if (!session) return;
1733
+ try {
1734
+ this._removeTrackEndedListener(session);
1735
+ session.workletNode.port.postMessage({ command: "stop" });
1736
+ session.source.disconnect();
1737
+ session.workletNode.disconnect();
1738
+ } catch (err) {
1739
+ console.warn(
1740
+ '[dawcore] RecordingController: disconnect error during cleanup for track "' + trackId + '": ' + String(err)
1741
+ );
1742
+ }
1743
+ this._sessions.delete(trackId);
1744
+ }
1745
+ };
1746
+
1747
+ // src/interactions/pointer-handler.ts
1748
+ var import_core = require("@waveform-playlist/core");
1749
+ var PointerHandler = class {
1750
+ constructor(host) {
1751
+ this._isDragging = false;
1752
+ this._dragStartPx = 0;
1753
+ this._timeline = null;
1754
+ // Cached from onPointerDown to avoid forced layout reflows at 60fps during drag
1755
+ this._timelineRect = null;
1756
+ this.onPointerDown = (e) => {
1757
+ this._timeline = this._host.shadowRoot?.querySelector(".timeline");
1758
+ if (!this._timeline) return;
1759
+ this._timelineRect = this._timeline.getBoundingClientRect();
1760
+ this._dragStartPx = this._pxFromPointer(e);
1761
+ this._isDragging = false;
1762
+ this._timeline.setPointerCapture(e.pointerId);
1763
+ this._timeline.addEventListener("pointermove", this._onPointerMove);
1764
+ this._timeline.addEventListener("pointerup", this._onPointerUp);
1765
+ };
1766
+ this._onPointerMove = (e) => {
1767
+ if (!this._timeline) return;
1768
+ const currentPx = this._pxFromPointer(e);
1769
+ if (!this._isDragging && Math.abs(currentPx - this._dragStartPx) > 3) {
1770
+ this._isDragging = true;
1771
+ }
1772
+ if (this._isDragging) {
1773
+ const h = this._host;
1774
+ const startTime = (0, import_core.pixelsToSeconds)(
1775
+ this._dragStartPx,
1776
+ h.samplesPerPixel,
1777
+ h.effectiveSampleRate
1778
+ );
1779
+ const endTime = (0, import_core.pixelsToSeconds)(currentPx, h.samplesPerPixel, h.effectiveSampleRate);
1780
+ h._selectionStartTime = Math.min(startTime, endTime);
1781
+ h._selectionEndTime = Math.max(startTime, endTime);
1782
+ const sel = h.shadowRoot?.querySelector("daw-selection");
1783
+ if (sel) {
1784
+ sel.startPx = h._selectionStartTime * h.effectiveSampleRate / h.samplesPerPixel;
1785
+ sel.endPx = h._selectionEndTime * h.effectiveSampleRate / h.samplesPerPixel;
1786
+ }
1787
+ }
1788
+ };
1789
+ this._onPointerUp = (e) => {
1790
+ if (!this._timeline) return;
1791
+ try {
1792
+ this._timeline.releasePointerCapture(e.pointerId);
1793
+ } catch (err) {
1794
+ console.warn(
1795
+ "[dawcore] releasePointerCapture failed (may already be released): " + String(err)
1796
+ );
1797
+ }
1798
+ this._timeline.removeEventListener("pointermove", this._onPointerMove);
1799
+ this._timeline.removeEventListener("pointerup", this._onPointerUp);
1800
+ try {
1801
+ if (this._isDragging) {
1802
+ this._finalizeSelection();
1803
+ } else {
1804
+ this._handleSeekClick(e);
1805
+ }
1806
+ } catch (err) {
1807
+ console.warn("[dawcore] Pointer interaction failed: " + String(err));
1808
+ } finally {
1809
+ this._isDragging = false;
1810
+ this._timeline = null;
1811
+ this._timelineRect = null;
1812
+ }
1813
+ };
1814
+ this._host = host;
1815
+ }
1816
+ _pxFromPointer(e) {
1817
+ if (!this._timelineRect) {
1818
+ console.warn("[dawcore] _pxFromPointer called without timeline reference");
1819
+ return 0;
1820
+ }
1821
+ return e.clientX - this._timelineRect.left;
1822
+ }
1823
+ _finalizeSelection() {
1824
+ const h = this._host;
1825
+ if (h._engine) {
1826
+ h._engine.setSelection(h._selectionStartTime, h._selectionEndTime);
1827
+ }
1828
+ h.dispatchEvent(
1829
+ new CustomEvent("daw-selection", {
1830
+ bubbles: true,
1831
+ composed: true,
1832
+ detail: { start: h._selectionStartTime, end: h._selectionEndTime }
1833
+ })
1834
+ );
1835
+ h.requestUpdate();
1836
+ }
1837
+ _handleSeekClick(e) {
1838
+ const h = this._host;
1839
+ const px = this._pxFromPointer(e);
1840
+ const time = (0, import_core.pixelsToSeconds)(px, h.samplesPerPixel, h.effectiveSampleRate);
1841
+ h._selectionStartTime = 0;
1842
+ h._selectionEndTime = 0;
1843
+ if (this._timeline) {
1844
+ const trackRows = this._timeline.querySelectorAll(".track-row");
1845
+ for (const row of trackRows) {
1846
+ const rowRect = row.getBoundingClientRect();
1847
+ if (e.clientY >= rowRect.top && e.clientY < rowRect.bottom) {
1848
+ const trackId = row.dataset.trackId;
1849
+ if (trackId) {
1850
+ this._selectTrack(trackId);
1851
+ }
1852
+ break;
1853
+ }
1854
+ }
1855
+ }
1856
+ const wasPlaying = h._isPlaying;
1857
+ if (h._engine) {
1858
+ h._engine.setSelection(0, 0);
1859
+ if (wasPlaying) {
1860
+ h._engine.stop();
1861
+ h._engine.play(time);
1862
+ h._startPlayhead();
1863
+ } else {
1864
+ h._engine.seek(time);
1865
+ }
1866
+ }
1867
+ h._currentTime = time;
1868
+ if (!wasPlaying) {
1869
+ h._stopPlayhead();
1870
+ }
1871
+ h.dispatchEvent(
1872
+ new CustomEvent("daw-seek", {
1873
+ bubbles: true,
1874
+ composed: true,
1875
+ detail: { time }
1876
+ })
1877
+ );
1878
+ h.requestUpdate();
1879
+ }
1880
+ _selectTrack(trackId) {
1881
+ const h = this._host;
1882
+ if (h._engine) {
1883
+ try {
1884
+ h._engine.selectTrack(trackId);
1885
+ } catch (err) {
1886
+ console.warn(
1887
+ "[dawcore] selectTrack via engine failed, falling back to local: " + String(err)
1888
+ );
1889
+ h._setSelectedTrackId(trackId);
1890
+ }
1891
+ } else {
1892
+ h._setSelectedTrackId(trackId);
1893
+ }
1894
+ h.dispatchEvent(
1895
+ new CustomEvent("daw-track-select", {
1896
+ bubbles: true,
1897
+ composed: true,
1898
+ detail: { trackId }
1899
+ })
1900
+ );
1901
+ }
1902
+ };
1903
+
1904
+ // src/interactions/file-loader.ts
1905
+ var import_core2 = require("@waveform-playlist/core");
1906
+ async function loadFiles(host, files) {
1907
+ if (!files) {
1908
+ console.warn("[dawcore] loadFiles called with null/undefined");
1909
+ return { loaded: [], failed: [] };
1910
+ }
1911
+ const fileArray = Array.from(files);
1912
+ const loaded = [];
1913
+ const failed = [];
1914
+ for (const file of fileArray) {
1915
+ if (file.type && !file.type.startsWith("audio/")) {
1916
+ failed.push({ file, error: new Error("Non-audio MIME type: " + file.type) });
1917
+ console.warn("[dawcore] Skipping non-audio file: " + file.name + " (" + file.type + ")");
1918
+ continue;
1919
+ }
1920
+ const blobUrl = URL.createObjectURL(file);
1921
+ try {
1922
+ const audioBuffer = await host._fetchAndDecode(blobUrl);
1923
+ URL.revokeObjectURL(blobUrl);
1924
+ host._audioCache.delete(blobUrl);
1925
+ host._resolvedSampleRate = audioBuffer.sampleRate;
1926
+ const name = file.name.replace(/\.\w+$/, "");
1927
+ const clip = (0, import_core2.createClipFromSeconds)({
1928
+ audioBuffer,
1929
+ startTime: 0,
1930
+ duration: audioBuffer.duration,
1931
+ offset: 0,
1932
+ gain: 1,
1933
+ name,
1934
+ sampleRate: audioBuffer.sampleRate,
1935
+ sourceDuration: audioBuffer.duration
1936
+ });
1937
+ host._clipBuffers = new Map(host._clipBuffers).set(clip.id, audioBuffer);
1938
+ const peakData = await host._peakPipeline.generatePeaks(
1939
+ audioBuffer,
1940
+ host.samplesPerPixel,
1941
+ host.mono
1942
+ );
1943
+ host._peaksData = new Map(host._peaksData).set(clip.id, peakData);
1944
+ const trackId = crypto.randomUUID();
1945
+ const track = (0, import_core2.createTrack)({ name, clips: [clip] });
1946
+ track.id = trackId;
1947
+ host._tracks = new Map(host._tracks).set(trackId, {
1948
+ name,
1949
+ src: "",
1950
+ volume: 1,
1951
+ pan: 0,
1952
+ muted: false,
1953
+ soloed: false,
1954
+ clips: [
1955
+ {
1956
+ src: "",
1957
+ start: 0,
1958
+ duration: audioBuffer.duration,
1959
+ offset: 0,
1960
+ gain: 1,
1961
+ name,
1962
+ fadeIn: 0,
1963
+ fadeOut: 0,
1964
+ fadeType: "linear"
1965
+ }
1966
+ ]
1967
+ });
1968
+ host._engineTracks = new Map(host._engineTracks).set(trackId, track);
1969
+ host._recomputeDuration();
1970
+ const engine = await host._ensureEngine();
1971
+ engine.setTracks([...host._engineTracks.values()]);
1972
+ loaded.push(trackId);
1973
+ host.dispatchEvent(
1974
+ new CustomEvent("daw-track-ready", {
1975
+ bubbles: true,
1976
+ composed: true,
1977
+ detail: { trackId }
1978
+ })
1979
+ );
1980
+ } catch (err) {
1981
+ URL.revokeObjectURL(blobUrl);
1982
+ console.warn("[dawcore] Failed to load file: " + file.name + " \u2014 " + String(err));
1983
+ failed.push({ file, error: err });
1984
+ if (host.isConnected) {
1985
+ host.dispatchEvent(
1986
+ new CustomEvent("daw-files-load-error", {
1987
+ bubbles: true,
1988
+ composed: true,
1989
+ detail: { file, error: err }
1990
+ })
1991
+ );
1992
+ }
1993
+ }
1994
+ }
1995
+ return { loaded, failed };
1996
+ }
1997
+
1998
+ // src/interactions/recording-clip.ts
1999
+ var import_core3 = require("@waveform-playlist/core");
2000
+ function addRecordedClip(host, trackId, buf, startSample, durSamples) {
2001
+ const clip = (0, import_core3.createClip)({
2002
+ audioBuffer: buf,
2003
+ startSample,
2004
+ durationSamples: durSamples,
2005
+ offsetSamples: 0,
2006
+ gain: 1,
2007
+ name: "Recording"
2008
+ });
2009
+ host._clipBuffers = new Map(host._clipBuffers).set(clip.id, buf);
2010
+ host._peakPipeline.generatePeaks(buf, host.samplesPerPixel, host.mono).then((pd) => {
2011
+ host._peaksData = new Map(host._peaksData).set(clip.id, pd);
2012
+ const t = host._engineTracks.get(trackId);
2013
+ if (!t) {
2014
+ const next = new Map(host._clipBuffers);
2015
+ next.delete(clip.id);
2016
+ host._clipBuffers = next;
2017
+ return;
2018
+ }
2019
+ host._engineTracks = new Map(host._engineTracks).set(trackId, {
2020
+ ...t,
2021
+ clips: [...t.clips, clip]
2022
+ });
2023
+ const desc = host._tracks.get(trackId);
2024
+ if (desc) {
2025
+ const sr = host.effectiveSampleRate;
2026
+ const clipDesc = {
2027
+ src: "",
2028
+ start: startSample / sr,
2029
+ duration: durSamples / sr,
2030
+ offset: 0,
2031
+ gain: 1,
2032
+ name: "Recording",
2033
+ fadeIn: 0,
2034
+ fadeOut: 0,
2035
+ fadeType: "linear"
2036
+ };
2037
+ host._tracks = new Map(host._tracks).set(trackId, {
2038
+ ...desc,
2039
+ clips: [...desc.clips, clipDesc]
2040
+ });
2041
+ }
2042
+ host._recomputeDuration();
2043
+ host._engine?.setTracks([...host._engineTracks.values()]);
2044
+ }).catch((err) => {
2045
+ console.warn("[dawcore] Failed to generate peaks for recorded clip: " + String(err));
2046
+ const next = new Map(host._clipBuffers);
2047
+ next.delete(clip.id);
2048
+ host._clipBuffers = next;
2049
+ if (host.isConnected) {
2050
+ host.dispatchEvent(
2051
+ new CustomEvent("daw-error", {
2052
+ bubbles: true,
2053
+ composed: true,
2054
+ detail: { operation: "recording-peaks", error: err }
2055
+ })
2056
+ );
2057
+ }
2058
+ });
2059
+ }
2060
+
2061
+ // src/elements/daw-editor.ts
2062
+ var DawEditorElement = class extends import_lit12.LitElement {
2063
+ constructor() {
2064
+ super(...arguments);
2065
+ this.samplesPerPixel = 1024;
2066
+ this.waveHeight = 128;
2067
+ this.timescale = false;
2068
+ this.mono = false;
2069
+ this.barWidth = 1;
2070
+ this.barGap = 0;
2071
+ this.fileDrop = false;
2072
+ this.sampleRate = 48e3;
2073
+ /** Resolved sample rate — falls back to sampleRate property until first audio decode. */
2074
+ this._resolvedSampleRate = null;
2075
+ this._tracks = /* @__PURE__ */ new Map();
2076
+ this._engineTracks = /* @__PURE__ */ new Map();
2077
+ this._peaksData = /* @__PURE__ */ new Map();
2078
+ this._isPlaying = false;
2079
+ this._duration = 0;
2080
+ this._selectedTrackId = null;
2081
+ this._dragOver = false;
2082
+ // Not @state — updated directly to avoid 60fps Lit re-renders
2083
+ this._selectionStartTime = 0;
2084
+ this._selectionEndTime = 0;
2085
+ this._currentTime = 0;
2086
+ this._engine = null;
2087
+ this._enginePromise = null;
2088
+ this._audioInitialized = false;
2089
+ this._audioCache = /* @__PURE__ */ new Map();
2090
+ this._clipBuffers = /* @__PURE__ */ new Map();
2091
+ this._peakPipeline = new PeakPipeline();
2092
+ this._trackElements = /* @__PURE__ */ new Map();
2093
+ this._childObserver = null;
2094
+ this._audioResume = new AudioResumeController(this);
2095
+ this._recordingController = new RecordingController(this);
2096
+ this._pointer = new PointerHandler(this);
2097
+ this._viewport = (() => {
2098
+ const v = new ViewportController(this);
2099
+ v.scrollSelector = ".scroll-area";
2100
+ return v;
2101
+ })();
2102
+ // --- Track Events ---
2103
+ this._onTrackConnected = (e) => {
2104
+ const trackId = e.detail?.trackId;
2105
+ const trackEl = e.detail?.element;
2106
+ if (!trackId || !(trackEl instanceof HTMLElement)) {
2107
+ console.warn("[dawcore] Invalid daw-track-connected event detail: " + String(e.detail));
2108
+ return;
2109
+ }
2110
+ const descriptor = this._readTrackDescriptor(trackEl);
2111
+ this._tracks = new Map(this._tracks).set(trackId, descriptor);
2112
+ this._trackElements.set(trackId, trackEl);
2113
+ this._loadTrack(trackId, descriptor);
2114
+ };
2115
+ this._onTrackUpdate = (e) => {
2116
+ const trackId = e.detail?.trackId;
2117
+ if (!trackId) return;
2118
+ const trackEl = e.target.closest("daw-track");
2119
+ if (!trackEl) return;
2120
+ const oldDescriptor = this._tracks.get(trackId);
2121
+ const descriptor = this._readTrackDescriptor(trackEl);
2122
+ this._tracks = new Map(this._tracks).set(trackId, descriptor);
2123
+ if (this._engine) {
2124
+ if (oldDescriptor?.volume !== descriptor.volume)
2125
+ this._engine.setTrackVolume(trackId, descriptor.volume);
2126
+ if (oldDescriptor?.pan !== descriptor.pan) this._engine.setTrackPan(trackId, descriptor.pan);
2127
+ if (oldDescriptor?.muted !== descriptor.muted)
2128
+ this._engine.setTrackMute(trackId, descriptor.muted);
2129
+ if (oldDescriptor?.soloed !== descriptor.soloed)
2130
+ this._engine.setTrackSolo(trackId, descriptor.soloed);
2131
+ }
2132
+ if (oldDescriptor?.src !== descriptor.src) {
2133
+ this._loadTrack(trackId, descriptor);
2134
+ }
2135
+ };
2136
+ this._onTrackControl = (e) => {
2137
+ const { trackId, prop, value } = e.detail ?? {};
2138
+ if (!trackId || !prop || !DawEditorElement._CONTROL_PROPS.has(prop)) return;
2139
+ const oldDescriptor = this._tracks.get(trackId);
2140
+ if (oldDescriptor) {
2141
+ const descriptor = { ...oldDescriptor, [prop]: value };
2142
+ this._tracks = new Map(this._tracks).set(trackId, descriptor);
2143
+ if (this._engine) {
2144
+ if (prop === "volume")
2145
+ this._engine.setTrackVolume(trackId, Math.max(0, Math.min(1, Number(value))));
2146
+ if (prop === "pan")
2147
+ this._engine.setTrackPan(trackId, Math.max(-1, Math.min(1, Number(value))));
2148
+ if (prop === "muted") this._engine.setTrackMute(trackId, Boolean(value));
2149
+ if (prop === "soloed") this._engine.setTrackSolo(trackId, Boolean(value));
2150
+ }
2151
+ }
2152
+ };
2153
+ this._onTrackRemoveRequest = (e) => {
2154
+ const { trackId } = e.detail ?? {};
2155
+ if (!trackId) return;
2156
+ const trackEl = this._trackElements.get(trackId);
2157
+ if (trackEl) {
2158
+ trackEl.remove();
2159
+ } else {
2160
+ this._onTrackRemoved(trackId);
2161
+ }
2162
+ };
2163
+ // --- File Drop ---
2164
+ this._onDragOver = (e) => {
2165
+ if (!this.fileDrop) return;
2166
+ e.preventDefault();
2167
+ if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
2168
+ this._dragOver = true;
2169
+ };
2170
+ this._onDragLeave = (e) => {
2171
+ if (!this.fileDrop) return;
2172
+ const timeline = this.shadowRoot?.querySelector(".timeline");
2173
+ if (timeline && !timeline.contains(e.relatedTarget)) {
2174
+ this._dragOver = false;
2175
+ }
2176
+ };
2177
+ this._onDrop = async (e) => {
2178
+ if (!this.fileDrop) return;
2179
+ e.preventDefault();
2180
+ this._dragOver = false;
2181
+ const files = e.dataTransfer?.files;
2182
+ if (!files || files.length === 0) return;
2183
+ try {
2184
+ await this.loadFiles(files);
2185
+ } catch (err) {
2186
+ console.warn("[dawcore] File drop failed: " + String(err));
2187
+ this.dispatchEvent(
2188
+ new CustomEvent("daw-error", {
2189
+ bubbles: true,
2190
+ composed: true,
2191
+ detail: { operation: "file-drop", error: err }
2192
+ })
2193
+ );
2194
+ }
2195
+ };
2196
+ // --- Recording ---
2197
+ this.recordingStream = null;
2198
+ }
2199
+ get effectiveSampleRate() {
2200
+ return this._resolvedSampleRate ?? this.sampleRate;
2201
+ }
2202
+ resolveAudioContextSampleRate(rate) {
2203
+ if (!this._resolvedSampleRate) this._resolvedSampleRate = rate;
2204
+ }
2205
+ get _totalWidth() {
2206
+ return Math.ceil(this._duration * this.effectiveSampleRate / this.samplesPerPixel);
2207
+ }
2208
+ _setSelectedTrackId(trackId) {
2209
+ this._selectedTrackId = trackId;
2210
+ }
2211
+ get tracks() {
2212
+ return [...this._tracks.values()];
2213
+ }
2214
+ get selectedTrackId() {
2215
+ return this._selectedTrackId;
2216
+ }
2217
+ get selection() {
2218
+ if (this._selectionStartTime === 0 && this._selectionEndTime === 0) return null;
2219
+ return { start: this._selectionStartTime, end: this._selectionEndTime };
2220
+ }
2221
+ setSelection(start, end) {
2222
+ this._selectionStartTime = Math.min(start, end);
2223
+ this._selectionEndTime = Math.max(start, end);
2224
+ if (this._engine) {
2225
+ this._engine.setSelection(this._selectionStartTime, this._selectionEndTime);
2226
+ }
2227
+ this.requestUpdate();
2228
+ this.dispatchEvent(
2229
+ new CustomEvent("daw-selection", {
2230
+ bubbles: true,
2231
+ composed: true,
2232
+ detail: { start: this._selectionStartTime, end: this._selectionEndTime }
2233
+ })
2234
+ );
2235
+ }
2236
+ // --- Lifecycle ---
2237
+ connectedCallback() {
2238
+ super.connectedCallback();
2239
+ this.addEventListener("daw-track-connected", this._onTrackConnected);
2240
+ this.addEventListener("daw-track-update", this._onTrackUpdate);
2241
+ this.addEventListener("daw-track-control", this._onTrackControl);
2242
+ this.addEventListener("daw-track-remove", this._onTrackRemoveRequest);
2243
+ this._childObserver = new MutationObserver((mutations) => {
2244
+ for (const mutation of mutations) {
2245
+ for (const node of mutation.removedNodes) {
2246
+ if (node instanceof HTMLElement) {
2247
+ if (node.tagName === "DAW-TRACK") {
2248
+ this._onTrackRemoved(node.trackId);
2249
+ }
2250
+ const nested = node.querySelectorAll?.("daw-track");
2251
+ if (nested) {
2252
+ for (const track of nested) {
2253
+ this._onTrackRemoved(track.trackId);
2254
+ }
2255
+ }
2256
+ }
2257
+ }
2258
+ }
2259
+ });
2260
+ this._childObserver.observe(this, { childList: true, subtree: true });
2261
+ }
2262
+ disconnectedCallback() {
2263
+ super.disconnectedCallback();
2264
+ this.removeEventListener("daw-track-connected", this._onTrackConnected);
2265
+ this.removeEventListener("daw-track-update", this._onTrackUpdate);
2266
+ this.removeEventListener("daw-track-control", this._onTrackControl);
2267
+ this.removeEventListener("daw-track-remove", this._onTrackRemoveRequest);
2268
+ this._childObserver?.disconnect();
2269
+ this._childObserver = null;
2270
+ this._trackElements.clear();
2271
+ this._audioCache.clear();
2272
+ this._clipBuffers.clear();
2273
+ this._peakPipeline.terminate();
2274
+ try {
2275
+ this._disposeEngine();
2276
+ } catch (err) {
2277
+ console.warn("[dawcore] Error disposing engine: " + String(err));
2278
+ }
2279
+ }
2280
+ willUpdate(changedProperties) {
2281
+ if (changedProperties.has("eagerResume")) {
2282
+ this._audioResume.target = this.eagerResume;
2283
+ }
2284
+ if (changedProperties.has("samplesPerPixel") && this._clipBuffers.size > 0) {
2285
+ const reextracted = this._peakPipeline.reextractPeaks(
2286
+ this._clipBuffers,
2287
+ this.samplesPerPixel,
2288
+ this.mono
2289
+ );
2290
+ if (reextracted.size > 0) {
2291
+ const next = new Map(this._peaksData);
2292
+ for (const [clipId, peakData] of reextracted) {
2293
+ next.set(clipId, peakData);
2294
+ }
2295
+ this._peaksData = next;
2296
+ }
2297
+ }
2298
+ }
2299
+ _onTrackRemoved(trackId) {
2300
+ this._trackElements.delete(trackId);
2301
+ const removedTrack = this._engineTracks.get(trackId);
2302
+ if (removedTrack) {
2303
+ const nextPeaks = new Map(this._peaksData);
2304
+ for (const clip of removedTrack.clips) {
2305
+ this._clipBuffers.delete(clip.id);
2306
+ nextPeaks.delete(clip.id);
2307
+ }
2308
+ this._peaksData = nextPeaks;
2309
+ }
2310
+ const nextTracks = new Map(this._tracks);
2311
+ nextTracks.delete(trackId);
2312
+ this._tracks = nextTracks;
2313
+ const nextEngine = new Map(this._engineTracks);
2314
+ nextEngine.delete(trackId);
2315
+ this._engineTracks = nextEngine;
2316
+ this._recomputeDuration();
2317
+ if (this._engine) {
2318
+ this._engine.removeTrack(trackId);
2319
+ }
2320
+ if (nextEngine.size === 0) {
2321
+ this._currentTime = 0;
2322
+ this._stopPlayhead();
2323
+ }
2324
+ }
2325
+ _readTrackDescriptor(trackEl) {
2326
+ const clipEls = trackEl.querySelectorAll("daw-clip");
2327
+ const clips = [];
2328
+ if (clipEls.length === 0 && trackEl.src) {
2329
+ clips.push({
2330
+ src: trackEl.src,
2331
+ start: 0,
2332
+ duration: 0,
2333
+ offset: 0,
2334
+ gain: 1,
2335
+ name: trackEl.name || "",
2336
+ fadeIn: 0,
2337
+ fadeOut: 0,
2338
+ fadeType: "linear"
2339
+ });
2340
+ } else {
2341
+ for (const clipEl of clipEls) {
2342
+ clips.push({
2343
+ src: clipEl.src,
2344
+ start: clipEl.start,
2345
+ duration: clipEl.duration,
2346
+ offset: clipEl.offset,
2347
+ gain: clipEl.gain,
2348
+ name: clipEl.name,
2349
+ fadeIn: clipEl.fadeIn,
2350
+ fadeOut: clipEl.fadeOut,
2351
+ fadeType: clipEl.fadeType
2352
+ });
2353
+ }
2354
+ }
2355
+ return {
2356
+ name: trackEl.name || "Untitled",
2357
+ src: trackEl.src,
2358
+ volume: trackEl.volume,
2359
+ pan: trackEl.pan,
2360
+ muted: trackEl.muted,
2361
+ soloed: trackEl.soloed,
2362
+ clips
2363
+ };
2364
+ }
2365
+ // --- Audio Loading ---
2366
+ async _loadTrack(trackId, descriptor) {
2367
+ try {
2368
+ const clips = [];
2369
+ for (const clipDesc of descriptor.clips) {
2370
+ if (!clipDesc.src) continue;
2371
+ const audioBuffer = await this._fetchAndDecode(clipDesc.src);
2372
+ this._resolvedSampleRate = audioBuffer.sampleRate;
2373
+ const clip = (0, import_core4.createClipFromSeconds)({
2374
+ audioBuffer,
2375
+ startTime: clipDesc.start,
2376
+ duration: clipDesc.duration || audioBuffer.duration,
2377
+ offset: clipDesc.offset,
2378
+ gain: clipDesc.gain,
2379
+ name: clipDesc.name,
2380
+ sampleRate: audioBuffer.sampleRate,
2381
+ sourceDuration: audioBuffer.duration
2382
+ });
2383
+ this._clipBuffers = new Map(this._clipBuffers).set(clip.id, audioBuffer);
2384
+ const peakData = await this._peakPipeline.generatePeaks(
2385
+ audioBuffer,
2386
+ this.samplesPerPixel,
2387
+ this.mono
2388
+ );
2389
+ this._peaksData = new Map(this._peaksData).set(clip.id, peakData);
2390
+ clips.push(clip);
2391
+ }
2392
+ const track = (0, import_core4.createTrack)({
2393
+ name: descriptor.name,
2394
+ clips,
2395
+ volume: descriptor.volume,
2396
+ pan: descriptor.pan,
2397
+ muted: descriptor.muted,
2398
+ soloed: descriptor.soloed
2399
+ });
2400
+ track.id = trackId;
2401
+ this._engineTracks = new Map(this._engineTracks).set(trackId, track);
2402
+ this._recomputeDuration();
2403
+ const engine = await this._ensureEngine();
2404
+ engine.setTracks([...this._engineTracks.values()]);
2405
+ this.dispatchEvent(
2406
+ new CustomEvent("daw-track-ready", {
2407
+ bubbles: true,
2408
+ composed: true,
2409
+ detail: { trackId }
2410
+ })
2411
+ );
2412
+ } catch (err) {
2413
+ if (!this.isConnected) return;
2414
+ console.warn('[dawcore] Failed to load track "' + trackId + '": ' + String(err));
2415
+ this.dispatchEvent(
2416
+ new CustomEvent("daw-track-error", {
2417
+ bubbles: true,
2418
+ composed: true,
2419
+ detail: { trackId, error: err }
2420
+ })
2421
+ );
2422
+ }
2423
+ }
2424
+ async _fetchAndDecode(src) {
2425
+ if (this._audioCache.has(src)) {
2426
+ return this._audioCache.get(src);
2427
+ }
2428
+ const promise = (async () => {
2429
+ const response = await fetch(src);
2430
+ if (!response.ok) {
2431
+ throw new Error(
2432
+ 'Failed to fetch audio "' + src + '": ' + response.status + " " + response.statusText
2433
+ );
2434
+ }
2435
+ const arrayBuffer = await response.arrayBuffer();
2436
+ const { getGlobalAudioContext } = await import("@waveform-playlist/playout");
2437
+ return getGlobalAudioContext().decodeAudioData(arrayBuffer);
2438
+ })();
2439
+ this._audioCache.set(src, promise);
2440
+ try {
2441
+ return await promise;
2442
+ } catch (err) {
2443
+ this._audioCache.delete(src);
2444
+ throw err;
2445
+ }
2446
+ }
2447
+ _recomputeDuration() {
2448
+ let maxSample = 0;
2449
+ for (const track of this._engineTracks.values()) {
2450
+ for (const clip of track.clips) {
2451
+ const endSample = clip.startSample + clip.durationSamples;
2452
+ if (endSample > maxSample) maxSample = endSample;
2453
+ }
2454
+ }
2455
+ this._duration = maxSample / this.effectiveSampleRate;
2456
+ }
2457
+ // --- Engine ---
2458
+ _ensureEngine() {
2459
+ if (this._engine) return Promise.resolve(this._engine);
2460
+ if (this._enginePromise) return this._enginePromise;
2461
+ this._enginePromise = this._buildEngine().catch((err) => {
2462
+ this._enginePromise = null;
2463
+ throw err;
2464
+ });
2465
+ return this._enginePromise;
2466
+ }
2467
+ async _buildEngine() {
2468
+ const [{ PlaylistEngine }, { createToneAdapter }] = await Promise.all([
2469
+ import("@waveform-playlist/engine"),
2470
+ import("@waveform-playlist/playout")
2471
+ ]);
2472
+ const adapter = createToneAdapter();
2473
+ const engine = new PlaylistEngine({
2474
+ adapter,
2475
+ sampleRate: this.effectiveSampleRate,
2476
+ samplesPerPixel: this.samplesPerPixel,
2477
+ zoomLevels: [256, 512, 1024, 2048, 4096, 8192, this.samplesPerPixel].filter((v, i, a) => a.indexOf(v) === i).sort((a, b) => a - b)
2478
+ });
2479
+ engine.on("statechange", (engineState) => {
2480
+ this._isPlaying = engineState.isPlaying;
2481
+ this._duration = engineState.duration;
2482
+ this._selectedTrackId = engineState.selectedTrackId;
2483
+ });
2484
+ engine.on("timeupdate", (time) => {
2485
+ this._currentTime = time;
2486
+ });
2487
+ engine.on("stop", () => {
2488
+ this._currentTime = engine.getCurrentTime();
2489
+ this._stopPlayhead();
2490
+ });
2491
+ this._engine = engine;
2492
+ return engine;
2493
+ }
2494
+ _disposeEngine() {
2495
+ if (this._engine) {
2496
+ this._engine.dispose();
2497
+ this._engine = null;
2498
+ }
2499
+ this._enginePromise = null;
2500
+ }
2501
+ async loadFiles(files) {
2502
+ return loadFiles(this, files);
2503
+ }
2504
+ // --- Playback ---
2505
+ async play() {
2506
+ try {
2507
+ const engine = await this._ensureEngine();
2508
+ if (!this._audioInitialized) {
2509
+ await engine.init();
2510
+ this._audioInitialized = true;
2511
+ }
2512
+ engine.play();
2513
+ this._startPlayhead();
2514
+ this.dispatchEvent(new CustomEvent("daw-play", { bubbles: true, composed: true }));
2515
+ } catch (err) {
2516
+ console.warn("[dawcore] Playback failed: " + String(err));
2517
+ this.dispatchEvent(
2518
+ new CustomEvent("daw-error", {
2519
+ bubbles: true,
2520
+ composed: true,
2521
+ detail: { operation: "play", error: err }
2522
+ })
2523
+ );
2524
+ }
2525
+ }
2526
+ pause() {
2527
+ if (!this._engine) return;
2528
+ this._engine.pause();
2529
+ this._stopPlayhead();
2530
+ this.dispatchEvent(new CustomEvent("daw-pause", { bubbles: true, composed: true }));
2531
+ }
2532
+ stop() {
2533
+ if (!this._engine) return;
2534
+ this._engine.stop();
2535
+ this._stopPlayhead();
2536
+ this.dispatchEvent(new CustomEvent("daw-stop", { bubbles: true, composed: true }));
2537
+ }
2538
+ seekTo(time) {
2539
+ if (!this._engine) return;
2540
+ this._engine.seek(time);
2541
+ this._currentTime = time;
2542
+ }
2543
+ get isRecording() {
2544
+ return this._recordingController.isRecording;
2545
+ }
2546
+ stopRecording() {
2547
+ this._recordingController.stopRecording();
2548
+ }
2549
+ _addRecordedClip(trackId, buf, startSample, durSamples) {
2550
+ addRecordedClip(this, trackId, buf, startSample, durSamples);
2551
+ }
2552
+ async startRecording(stream, options) {
2553
+ const s = stream ?? this.recordingStream;
2554
+ if (!s) {
2555
+ console.warn("[dawcore] startRecording: no stream provided and recordingStream is null");
2556
+ return;
2557
+ }
2558
+ await this._recordingController.startRecording(s, options);
2559
+ }
2560
+ _renderRecordingPreview(trackId, chH) {
2561
+ const rs = this._recordingController.getSession(trackId);
2562
+ if (!rs) return "";
2563
+ const left = Math.floor(rs.startSample / this.samplesPerPixel);
2564
+ const w = Math.floor(rs.totalSamples / this.samplesPerPixel);
2565
+ return rs.peaks.map(
2566
+ (chPeaks, ch) => import_lit12.html`
2567
+ <daw-waveform
2568
+ data-recording-track=${trackId}
2569
+ data-recording-channel=${ch}
2570
+ style="position:absolute;left:${left}px;top:${ch * chH}px;"
2571
+ .peaks=${chPeaks}
2572
+ .length=${w}
2573
+ .waveHeight=${chH}
2574
+ .barWidth=${this.barWidth}
2575
+ .barGap=${this.barGap}
2576
+ .visibleStart=${this._viewport.visibleStart}
2577
+ .visibleEnd=${this._viewport.visibleEnd}
2578
+ .originX=${left}
2579
+ ></daw-waveform>
2580
+ `
2581
+ );
2582
+ }
2583
+ // --- Playhead ---
2584
+ _startPlayhead() {
2585
+ const playhead = this._getPlayhead();
2586
+ if (!playhead || !this._engine) return;
2587
+ const engine = this._engine;
2588
+ playhead.startAnimation(
2589
+ () => engine.getCurrentTime(),
2590
+ this.effectiveSampleRate,
2591
+ this.samplesPerPixel
2592
+ );
2593
+ }
2594
+ _stopPlayhead() {
2595
+ const playhead = this._getPlayhead();
2596
+ if (!playhead) return;
2597
+ playhead.stopAnimation(this._currentTime, this.effectiveSampleRate, this.samplesPerPixel);
2598
+ }
2599
+ _getPlayhead() {
2600
+ return this.shadowRoot?.querySelector("daw-playhead");
2601
+ }
2602
+ _getOrderedTracks() {
2603
+ const domOrder = [...this.querySelectorAll("daw-track")].map(
2604
+ (el) => el.trackId
2605
+ );
2606
+ return [...this._engineTracks.entries()].sort((a, b) => {
2607
+ const ai = domOrder.indexOf(a[0]);
2608
+ const bi = domOrder.indexOf(b[0]);
2609
+ if (ai === -1 && bi === -1) return 0;
2610
+ if (ai === -1) return 1;
2611
+ if (bi === -1) return -1;
2612
+ return ai - bi;
2613
+ });
2614
+ }
2615
+ // --- Render ---
2616
+ render() {
2617
+ const sr = this.effectiveSampleRate;
2618
+ const selStartPx = this._selectionStartTime * sr / this.samplesPerPixel;
2619
+ const selEndPx = this._selectionEndTime * sr / this.samplesPerPixel;
2620
+ const orderedTracks = this._getOrderedTracks().map(([trackId, track]) => {
2621
+ const descriptor = this._tracks.get(trackId);
2622
+ const firstPeaks = track.clips.map((c) => this._peaksData.get(c.id)).find((p) => p && p.data.length > 0);
2623
+ const numChannels = firstPeaks ? firstPeaks.data.length : 1;
2624
+ return {
2625
+ trackId,
2626
+ track,
2627
+ descriptor,
2628
+ numChannels,
2629
+ trackHeight: this.waveHeight * numChannels
2630
+ };
2631
+ });
2632
+ return import_lit12.html`
2633
+ ${orderedTracks.length > 0 ? import_lit12.html`<div class="controls-column">
2634
+ ${this.timescale ? import_lit12.html`<div style="height: 30px;"></div>` : ""}
2635
+ ${orderedTracks.map(
2636
+ (t) => import_lit12.html`
2637
+ <daw-track-controls
2638
+ style="height: ${t.trackHeight}px;"
2639
+ .trackId=${t.trackId}
2640
+ .trackName=${t.descriptor?.name ?? "Untitled"}
2641
+ .volume=${t.descriptor?.volume ?? 1}
2642
+ .pan=${t.descriptor?.pan ?? 0}
2643
+ .muted=${t.descriptor?.muted ?? false}
2644
+ .soloed=${t.descriptor?.soloed ?? false}
2645
+ ></daw-track-controls>
2646
+ `
2647
+ )}
2648
+ </div>` : ""}
2649
+ <div class="scroll-area">
2650
+ <div
2651
+ class="timeline ${this._dragOver ? "drag-over" : ""}"
2652
+ style="width: ${this._totalWidth > 0 ? this._totalWidth + "px" : "100%"};"
2653
+ data-playing=${this._isPlaying}
2654
+ @pointerdown=${this._pointer.onPointerDown}
2655
+ @dragover=${this._onDragOver}
2656
+ @dragleave=${this._onDragLeave}
2657
+ @drop=${this._onDrop}
2658
+ >
2659
+ ${orderedTracks.length > 0 && this.timescale ? import_lit12.html`<daw-ruler
2660
+ .samplesPerPixel=${this.samplesPerPixel}
2661
+ .sampleRate=${this.effectiveSampleRate}
2662
+ .duration=${this._duration}
2663
+ ></daw-ruler>` : ""}
2664
+ ${orderedTracks.length > 0 ? import_lit12.html`<daw-selection .startPx=${selStartPx} .endPx=${selEndPx}></daw-selection>
2665
+ <daw-playhead></daw-playhead>` : ""}
2666
+ ${orderedTracks.map((t) => {
2667
+ const channelHeight = this.waveHeight;
2668
+ return import_lit12.html`
2669
+ <div
2670
+ class="track-row ${t.trackId === this._selectedTrackId ? "selected" : ""}"
2671
+ style="height: ${t.trackHeight}px;"
2672
+ data-track-id=${t.trackId}
2673
+ >
2674
+ ${t.track.clips.map((clip) => {
2675
+ const peakData = this._peaksData.get(clip.id);
2676
+ const width = (0, import_core4.clipPixelWidth)(
2677
+ clip.startSample,
2678
+ clip.durationSamples,
2679
+ this.samplesPerPixel
2680
+ );
2681
+ const clipLeft = Math.floor(clip.startSample / this.samplesPerPixel);
2682
+ const channels = peakData?.data ?? [new Int16Array(0)];
2683
+ return channels.map(
2684
+ (channelPeaks, chIdx) => import_lit12.html`
2685
+ <daw-waveform
2686
+ style="position: absolute; left: ${clipLeft}px; top: ${chIdx * channelHeight}px;"
2687
+ .peaks=${channelPeaks}
2688
+ .length=${peakData?.length ?? width}
2689
+ .waveHeight=${channelHeight}
2690
+ .barWidth=${this.barWidth}
2691
+ .barGap=${this.barGap}
2692
+ .visibleStart=${this._viewport.visibleStart}
2693
+ .visibleEnd=${this._viewport.visibleEnd}
2694
+ .originX=${clipLeft}
2695
+ ></daw-waveform>
2696
+ `
2697
+ );
2698
+ })}
2699
+ ${this._renderRecordingPreview(t.trackId, channelHeight)}
2700
+ </div>
2701
+ `;
2702
+ })}
2703
+ </div>
2704
+ </div>
2705
+ <slot></slot>
2706
+ `;
2707
+ }
2708
+ };
2709
+ DawEditorElement.styles = [
2710
+ hostStyles,
2711
+ import_lit12.css`
2712
+ :host {
2713
+ display: flex;
2714
+ position: relative;
2715
+ background: var(--daw-background, #1a1a2e);
2716
+ overflow: hidden;
2717
+ }
2718
+ .controls-column {
2719
+ flex-shrink: 0;
2720
+ width: var(--daw-controls-width, 180px);
2721
+ }
2722
+ .scroll-area {
2723
+ flex: 1;
2724
+ overflow-x: auto;
2725
+ overflow-y: hidden;
2726
+ min-height: var(--daw-min-height, 200px);
2727
+ }
2728
+ .timeline {
2729
+ position: relative;
2730
+ min-height: 100%;
2731
+ cursor: text;
2732
+ }
2733
+ .track-row {
2734
+ position: relative;
2735
+ background: var(--daw-track-background, #16213e);
2736
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
2737
+ }
2738
+ .track-row.selected {
2739
+ background: rgba(99, 199, 95, 0.08);
2740
+ }
2741
+ .timeline.drag-over {
2742
+ outline: 2px dashed var(--daw-selection-color, rgba(99, 199, 95, 0.3));
2743
+ outline-offset: -2px;
2744
+ }
2745
+ `
2746
+ ];
2747
+ DawEditorElement._CONTROL_PROPS = /* @__PURE__ */ new Set(["volume", "pan", "muted", "soloed"]);
2748
+ __decorateClass([
2749
+ (0, import_decorators10.property)({ type: Number, attribute: "samples-per-pixel" })
2750
+ ], DawEditorElement.prototype, "samplesPerPixel", 2);
2751
+ __decorateClass([
2752
+ (0, import_decorators10.property)({ type: Number, attribute: "wave-height" })
2753
+ ], DawEditorElement.prototype, "waveHeight", 2);
2754
+ __decorateClass([
2755
+ (0, import_decorators10.property)({ type: Boolean })
2756
+ ], DawEditorElement.prototype, "timescale", 2);
2757
+ __decorateClass([
2758
+ (0, import_decorators10.property)({ type: Boolean })
2759
+ ], DawEditorElement.prototype, "mono", 2);
2760
+ __decorateClass([
2761
+ (0, import_decorators10.property)({ type: Number, attribute: "bar-width" })
2762
+ ], DawEditorElement.prototype, "barWidth", 2);
2763
+ __decorateClass([
2764
+ (0, import_decorators10.property)({ type: Number, attribute: "bar-gap" })
2765
+ ], DawEditorElement.prototype, "barGap", 2);
2766
+ __decorateClass([
2767
+ (0, import_decorators10.property)({ type: Boolean, attribute: "file-drop" })
2768
+ ], DawEditorElement.prototype, "fileDrop", 2);
2769
+ __decorateClass([
2770
+ (0, import_decorators10.property)({ type: Number, attribute: "sample-rate" })
2771
+ ], DawEditorElement.prototype, "sampleRate", 2);
2772
+ __decorateClass([
2773
+ (0, import_decorators10.state)()
2774
+ ], DawEditorElement.prototype, "_tracks", 2);
2775
+ __decorateClass([
2776
+ (0, import_decorators10.state)()
2777
+ ], DawEditorElement.prototype, "_engineTracks", 2);
2778
+ __decorateClass([
2779
+ (0, import_decorators10.state)()
2780
+ ], DawEditorElement.prototype, "_peaksData", 2);
2781
+ __decorateClass([
2782
+ (0, import_decorators10.state)()
2783
+ ], DawEditorElement.prototype, "_isPlaying", 2);
2784
+ __decorateClass([
2785
+ (0, import_decorators10.state)()
2786
+ ], DawEditorElement.prototype, "_duration", 2);
2787
+ __decorateClass([
2788
+ (0, import_decorators10.state)()
2789
+ ], DawEditorElement.prototype, "_selectedTrackId", 2);
2790
+ __decorateClass([
2791
+ (0, import_decorators10.state)()
2792
+ ], DawEditorElement.prototype, "_dragOver", 2);
2793
+ __decorateClass([
2794
+ (0, import_decorators10.property)({ attribute: "eager-resume" })
2795
+ ], DawEditorElement.prototype, "eagerResume", 2);
2796
+ DawEditorElement = __decorateClass([
2797
+ (0, import_decorators10.customElement)("daw-editor")
2798
+ ], DawEditorElement);
2799
+
2800
+ // src/elements/daw-ruler.ts
2801
+ var import_lit13 = require("lit");
2802
+ var import_decorators11 = require("lit/decorators.js");
2803
+
2804
+ // src/utils/time-format.ts
2805
+ function formatTime(milliseconds) {
2806
+ const seconds = Math.floor(milliseconds / 1e3);
2807
+ const s = seconds % 60;
2808
+ const m = (seconds - s) / 60;
2809
+ return `${m}:${String(s).padStart(2, "0")}`;
2810
+ }
2811
+
2812
+ // src/utils/smart-scale.ts
2813
+ var timeinfo = /* @__PURE__ */ new Map([
2814
+ [700, { marker: 1e3, bigStep: 500, smallStep: 100 }],
2815
+ [1500, { marker: 2e3, bigStep: 1e3, smallStep: 200 }],
2816
+ [2500, { marker: 2e3, bigStep: 1e3, smallStep: 500 }],
2817
+ [5e3, { marker: 5e3, bigStep: 1e3, smallStep: 500 }],
2818
+ [1e4, { marker: 1e4, bigStep: 5e3, smallStep: 1e3 }],
2819
+ [12e3, { marker: 15e3, bigStep: 5e3, smallStep: 1e3 }],
2820
+ [Infinity, { marker: 3e4, bigStep: 1e4, smallStep: 5e3 }]
2821
+ ]);
2822
+ function getScaleInfo(samplesPerPixel) {
2823
+ for (const [resolution, config] of timeinfo) {
2824
+ if (samplesPerPixel < resolution) {
2825
+ return config;
2826
+ }
2827
+ }
2828
+ return { marker: 3e4, bigStep: 1e4, smallStep: 5e3 };
2829
+ }
2830
+ function computeTemporalTicks(samplesPerPixel, sampleRate, duration, rulerHeight) {
2831
+ const widthX = Math.ceil(duration * sampleRate / samplesPerPixel);
2832
+ const config = getScaleInfo(samplesPerPixel);
2833
+ const { marker, bigStep, smallStep } = config;
2834
+ const canvasInfo = /* @__PURE__ */ new Map();
2835
+ const labels = [];
2836
+ const pixPerSec = sampleRate / samplesPerPixel;
2837
+ for (let counter = 0; ; counter += smallStep) {
2838
+ const pix = Math.floor(counter / 1e3 * pixPerSec);
2839
+ if (pix >= widthX) break;
2840
+ if (counter % marker === 0) {
2841
+ canvasInfo.set(pix, rulerHeight);
2842
+ labels.push({ pix, text: formatTime(counter) });
2843
+ } else if (counter % bigStep === 0) {
2844
+ canvasInfo.set(pix, Math.floor(rulerHeight / 2));
2845
+ } else if (counter % smallStep === 0) {
2846
+ canvasInfo.set(pix, Math.floor(rulerHeight / 5));
2847
+ }
2848
+ }
2849
+ return { widthX, canvasInfo, labels };
2850
+ }
2851
+
2852
+ // src/elements/daw-ruler.ts
2853
+ var MAX_CANVAS_WIDTH2 = 1e3;
2854
+ var DawRulerElement = class extends import_lit13.LitElement {
2855
+ constructor() {
2856
+ super(...arguments);
2857
+ this.samplesPerPixel = 1024;
2858
+ this.sampleRate = 48e3;
2859
+ this.duration = 0;
2860
+ this.rulerHeight = 30;
2861
+ this._tickData = null;
2862
+ }
2863
+ willUpdate() {
2864
+ if (this.duration > 0) {
2865
+ this._tickData = computeTemporalTicks(
2866
+ this.samplesPerPixel,
2867
+ this.sampleRate,
2868
+ this.duration,
2869
+ this.rulerHeight
2870
+ );
2871
+ } else {
2872
+ this._tickData = null;
2873
+ }
2874
+ }
2875
+ render() {
2876
+ if (!this._tickData) return import_lit13.html``;
2877
+ const { widthX, labels } = this._tickData;
2878
+ const totalChunks = Math.ceil(widthX / MAX_CANVAS_WIDTH2);
2879
+ const indices = Array.from({ length: totalChunks }, (_, i) => i);
2880
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
2881
+ return import_lit13.html`
2882
+ <div class="container" style="width: ${widthX}px; height: ${this.rulerHeight}px;">
2883
+ ${indices.map((i) => {
2884
+ const width = Math.min(MAX_CANVAS_WIDTH2, widthX - i * MAX_CANVAS_WIDTH2);
2885
+ return import_lit13.html`
2886
+ <canvas
2887
+ data-index=${i}
2888
+ width=${width * dpr}
2889
+ height=${this.rulerHeight * dpr}
2890
+ style="left: ${i * MAX_CANVAS_WIDTH2}px; width: ${width}px; height: ${this.rulerHeight}px;"
2891
+ ></canvas>
2892
+ `;
2893
+ })}
2894
+ ${labels.map(
2895
+ ({ pix, text }) => import_lit13.html`<span class="label" style="left: ${pix + 4}px;">${text}</span>`
2896
+ )}
2897
+ </div>
2898
+ `;
2899
+ }
2900
+ updated() {
2901
+ this._drawTicks();
2902
+ }
2903
+ _drawTicks() {
2904
+ if (!this._tickData) return;
2905
+ const canvases = this.shadowRoot?.querySelectorAll("canvas");
2906
+ if (!canvases) return;
2907
+ const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : 1;
2908
+ const rulerColor = getComputedStyle(this).getPropertyValue("--daw-ruler-color").trim() || "#c49a6c";
2909
+ for (const canvas of canvases) {
2910
+ const idx = Number(canvas.dataset.index);
2911
+ const ctx = canvas.getContext("2d");
2912
+ if (!ctx) continue;
2913
+ const canvasWidth = Math.min(
2914
+ MAX_CANVAS_WIDTH2,
2915
+ this._tickData.widthX - idx * MAX_CANVAS_WIDTH2
2916
+ );
2917
+ const globalOffset = idx * MAX_CANVAS_WIDTH2;
2918
+ ctx.resetTransform();
2919
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
2920
+ ctx.scale(dpr, dpr);
2921
+ ctx.strokeStyle = rulerColor;
2922
+ ctx.lineWidth = 1;
2923
+ for (const [pix, height] of this._tickData.canvasInfo) {
2924
+ const localX = pix - globalOffset;
2925
+ if (localX < 0 || localX >= canvasWidth) continue;
2926
+ ctx.beginPath();
2927
+ ctx.moveTo(localX + 0.5, this.rulerHeight);
2928
+ ctx.lineTo(localX + 0.5, this.rulerHeight - height);
2929
+ ctx.stroke();
2930
+ }
2931
+ }
2932
+ }
2933
+ };
2934
+ DawRulerElement.styles = import_lit13.css`
2935
+ :host {
2936
+ display: block;
2937
+ position: relative;
2938
+ background: var(--daw-ruler-background, #0f0f1a);
2939
+ }
2940
+ .container {
2941
+ position: relative;
2942
+ }
2943
+ canvas {
2944
+ position: absolute;
2945
+ top: 0;
2946
+ }
2947
+ .label {
2948
+ position: absolute;
2949
+ font-size: 0.7rem;
2950
+ white-space: nowrap;
2951
+ color: var(--daw-ruler-color, #c49a6c);
2952
+ top: 2px;
2953
+ }
2954
+ `;
2955
+ __decorateClass([
2956
+ (0, import_decorators11.property)({ type: Number, attribute: false })
2957
+ ], DawRulerElement.prototype, "samplesPerPixel", 2);
2958
+ __decorateClass([
2959
+ (0, import_decorators11.property)({ type: Number, attribute: false })
2960
+ ], DawRulerElement.prototype, "sampleRate", 2);
2961
+ __decorateClass([
2962
+ (0, import_decorators11.property)({ type: Number, attribute: false })
2963
+ ], DawRulerElement.prototype, "duration", 2);
2964
+ __decorateClass([
2965
+ (0, import_decorators11.property)({ type: Number, attribute: false })
2966
+ ], DawRulerElement.prototype, "rulerHeight", 2);
2967
+ DawRulerElement = __decorateClass([
2968
+ (0, import_decorators11.customElement)("daw-ruler")
2969
+ ], DawRulerElement);
2970
+
2971
+ // src/elements/daw-selection.ts
2972
+ var import_lit14 = require("lit");
2973
+ var import_decorators12 = require("lit/decorators.js");
2974
+ var DawSelectionElement = class extends import_lit14.LitElement {
2975
+ constructor() {
2976
+ super(...arguments);
2977
+ this.startPx = 0;
2978
+ this.endPx = 0;
2979
+ }
2980
+ render() {
2981
+ const left = Math.min(this.startPx, this.endPx);
2982
+ const width = Math.abs(this.endPx - this.startPx);
2983
+ if (width === 0) return import_lit14.html``;
2984
+ return import_lit14.html`<div style="left: ${left}px; width: ${width}px;"></div>`;
2985
+ }
2986
+ };
2987
+ DawSelectionElement.styles = import_lit14.css`
2988
+ :host {
2989
+ position: absolute;
2990
+ top: 0;
2991
+ bottom: 0;
2992
+ left: 0;
2993
+ pointer-events: none;
2994
+ z-index: 5;
2995
+ }
2996
+ div {
2997
+ position: absolute;
2998
+ top: 0;
2999
+ bottom: 0;
3000
+ background: var(--daw-selection-color, rgba(99, 199, 95, 0.3));
3001
+ }
3002
+ `;
3003
+ __decorateClass([
3004
+ (0, import_decorators12.property)({ type: Number, attribute: false })
3005
+ ], DawSelectionElement.prototype, "startPx", 2);
3006
+ __decorateClass([
3007
+ (0, import_decorators12.property)({ type: Number, attribute: false })
3008
+ ], DawSelectionElement.prototype, "endPx", 2);
3009
+ DawSelectionElement = __decorateClass([
3010
+ (0, import_decorators12.customElement)("daw-selection")
3011
+ ], DawSelectionElement);
3012
+
3013
+ // src/elements/daw-record-button.ts
3014
+ var import_lit15 = require("lit");
3015
+ var import_decorators13 = require("lit/decorators.js");
3016
+ var DawRecordButtonElement = class extends DawTransportButton {
3017
+ constructor() {
3018
+ super(...arguments);
3019
+ this._isRecording = false;
3020
+ this._targetRef = null;
3021
+ this._onStart = () => {
3022
+ this._isRecording = true;
3023
+ };
3024
+ this._onComplete = () => {
3025
+ this._isRecording = false;
3026
+ };
3027
+ this._onError = () => {
3028
+ this._isRecording = false;
3029
+ };
3030
+ }
3031
+ connectedCallback() {
3032
+ super.connectedCallback();
3033
+ this._listenToTarget();
3034
+ }
3035
+ disconnectedCallback() {
3036
+ super.disconnectedCallback();
3037
+ this._cleanupListeners();
3038
+ }
3039
+ _listenToTarget() {
3040
+ const target = this.target;
3041
+ if (!target) return;
3042
+ this._targetRef = target;
3043
+ target.addEventListener("daw-recording-start", this._onStart);
3044
+ target.addEventListener("daw-recording-complete", this._onComplete);
3045
+ target.addEventListener("daw-recording-error", this._onError);
3046
+ }
3047
+ _cleanupListeners() {
3048
+ if (this._targetRef) {
3049
+ this._targetRef.removeEventListener("daw-recording-start", this._onStart);
3050
+ this._targetRef.removeEventListener("daw-recording-complete", this._onComplete);
3051
+ this._targetRef.removeEventListener("daw-recording-error", this._onError);
3052
+ this._targetRef = null;
3053
+ }
3054
+ }
3055
+ render() {
3056
+ return import_lit15.html`
3057
+ <button part="button" ?data-recording=${this._isRecording} @click=${this._onClick}>
3058
+ <slot>${this._isRecording ? "Stop Rec" : "Record"}</slot>
3059
+ </button>
3060
+ `;
3061
+ }
3062
+ _onClick() {
3063
+ const target = this.target;
3064
+ if (!target) {
3065
+ console.warn(
3066
+ '[dawcore] <daw-record-button> has no target. Check <daw-transport for="..."> references a valid <daw-editor> id.'
3067
+ );
3068
+ return;
3069
+ }
3070
+ if (this._isRecording) {
3071
+ target.stopRecording();
3072
+ } else {
3073
+ target.startRecording(target.recordingStream);
3074
+ }
3075
+ }
3076
+ };
3077
+ DawRecordButtonElement.styles = [
3078
+ DawTransportButton.styles,
3079
+ import_lit15.css`
3080
+ button[data-recording] {
3081
+ color: #d08070;
3082
+ border-color: #d08070;
3083
+ }
3084
+ `
3085
+ ];
3086
+ __decorateClass([
3087
+ (0, import_decorators13.state)()
3088
+ ], DawRecordButtonElement.prototype, "_isRecording", 2);
3089
+ DawRecordButtonElement = __decorateClass([
3090
+ (0, import_decorators13.customElement)("daw-record-button")
3091
+ ], DawRecordButtonElement);
3092
+ // Annotate the CommonJS export names for ESM import in node:
3093
+ 0 && (module.exports = {
3094
+ AudioResumeController,
3095
+ DawClipElement,
3096
+ DawEditorElement,
3097
+ DawPauseButtonElement,
3098
+ DawPlayButtonElement,
3099
+ DawPlayheadElement,
3100
+ DawRecordButtonElement,
3101
+ DawRulerElement,
3102
+ DawSelectionElement,
3103
+ DawStopButtonElement,
3104
+ DawTrackControlsElement,
3105
+ DawTrackElement,
3106
+ DawTransportButton,
3107
+ DawTransportElement,
3108
+ DawWaveformElement,
3109
+ RecordingController
3110
+ });
3111
+ //# sourceMappingURL=index.js.map