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