@dawcore/components 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +260 -10
- package/dist/index.d.ts +260 -10
- package/dist/index.js +1138 -84
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1159 -108
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
package/dist/index.mjs
CHANGED
|
@@ -551,7 +551,7 @@ DawTransportElement = __decorateClass([
|
|
|
551
551
|
|
|
552
552
|
// src/elements/daw-play-button.ts
|
|
553
553
|
import { html as html3 } from "lit";
|
|
554
|
-
import { customElement as customElement6 } from "lit/decorators.js";
|
|
554
|
+
import { customElement as customElement6, state } from "lit/decorators.js";
|
|
555
555
|
|
|
556
556
|
// src/elements/daw-transport-button.ts
|
|
557
557
|
import { LitElement as LitElement6, css as css3 } from "lit";
|
|
@@ -581,9 +581,40 @@ DawTransportButton.styles = css3`
|
|
|
581
581
|
|
|
582
582
|
// src/elements/daw-play-button.ts
|
|
583
583
|
var DawPlayButtonElement = class extends DawTransportButton {
|
|
584
|
+
constructor() {
|
|
585
|
+
super(...arguments);
|
|
586
|
+
this._isRecording = false;
|
|
587
|
+
this._targetRef = null;
|
|
588
|
+
this._onRecStart = () => {
|
|
589
|
+
this._isRecording = true;
|
|
590
|
+
};
|
|
591
|
+
this._onRecEnd = () => {
|
|
592
|
+
this._isRecording = false;
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
connectedCallback() {
|
|
596
|
+
super.connectedCallback();
|
|
597
|
+
requestAnimationFrame(() => {
|
|
598
|
+
const target = this.target;
|
|
599
|
+
if (!target) return;
|
|
600
|
+
this._targetRef = target;
|
|
601
|
+
target.addEventListener("daw-recording-start", this._onRecStart);
|
|
602
|
+
target.addEventListener("daw-recording-complete", this._onRecEnd);
|
|
603
|
+
target.addEventListener("daw-recording-error", this._onRecEnd);
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
disconnectedCallback() {
|
|
607
|
+
super.disconnectedCallback();
|
|
608
|
+
if (this._targetRef) {
|
|
609
|
+
this._targetRef.removeEventListener("daw-recording-start", this._onRecStart);
|
|
610
|
+
this._targetRef.removeEventListener("daw-recording-complete", this._onRecEnd);
|
|
611
|
+
this._targetRef.removeEventListener("daw-recording-error", this._onRecEnd);
|
|
612
|
+
this._targetRef = null;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
584
615
|
render() {
|
|
585
616
|
return html3`
|
|
586
|
-
<button part="button" @click=${this._onClick}>
|
|
617
|
+
<button part="button" ?disabled=${this._isRecording} @click=${this._onClick}>
|
|
587
618
|
<slot>Play</slot>
|
|
588
619
|
</button>
|
|
589
620
|
`;
|
|
@@ -599,17 +630,53 @@ var DawPlayButtonElement = class extends DawTransportButton {
|
|
|
599
630
|
target.play();
|
|
600
631
|
}
|
|
601
632
|
};
|
|
633
|
+
__decorateClass([
|
|
634
|
+
state()
|
|
635
|
+
], DawPlayButtonElement.prototype, "_isRecording", 2);
|
|
602
636
|
DawPlayButtonElement = __decorateClass([
|
|
603
637
|
customElement6("daw-play-button")
|
|
604
638
|
], DawPlayButtonElement);
|
|
605
639
|
|
|
606
640
|
// src/elements/daw-pause-button.ts
|
|
607
|
-
import { html as html4 } from "lit";
|
|
608
|
-
import { customElement as customElement7 } from "lit/decorators.js";
|
|
641
|
+
import { html as html4, css as css4 } from "lit";
|
|
642
|
+
import { customElement as customElement7, state as state2 } from "lit/decorators.js";
|
|
609
643
|
var DawPauseButtonElement = class extends DawTransportButton {
|
|
644
|
+
constructor() {
|
|
645
|
+
super(...arguments);
|
|
646
|
+
this._isPaused = false;
|
|
647
|
+
this._isRecording = false;
|
|
648
|
+
this._targetRef = null;
|
|
649
|
+
this._onRecStart = () => {
|
|
650
|
+
this._isRecording = true;
|
|
651
|
+
};
|
|
652
|
+
this._onRecEnd = () => {
|
|
653
|
+
this._isRecording = false;
|
|
654
|
+
this._isPaused = false;
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
connectedCallback() {
|
|
658
|
+
super.connectedCallback();
|
|
659
|
+
requestAnimationFrame(() => {
|
|
660
|
+
const target = this.target;
|
|
661
|
+
if (!target) return;
|
|
662
|
+
this._targetRef = target;
|
|
663
|
+
target.addEventListener("daw-recording-start", this._onRecStart);
|
|
664
|
+
target.addEventListener("daw-recording-complete", this._onRecEnd);
|
|
665
|
+
target.addEventListener("daw-recording-error", this._onRecEnd);
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
disconnectedCallback() {
|
|
669
|
+
super.disconnectedCallback();
|
|
670
|
+
if (this._targetRef) {
|
|
671
|
+
this._targetRef.removeEventListener("daw-recording-start", this._onRecStart);
|
|
672
|
+
this._targetRef.removeEventListener("daw-recording-complete", this._onRecEnd);
|
|
673
|
+
this._targetRef.removeEventListener("daw-recording-error", this._onRecEnd);
|
|
674
|
+
this._targetRef = null;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
610
677
|
render() {
|
|
611
678
|
return html4`
|
|
612
|
-
<button part="button" @click=${this._onClick}>
|
|
679
|
+
<button part="button" ?data-paused=${this._isPaused} @click=${this._onClick}>
|
|
613
680
|
<slot>Pause</slot>
|
|
614
681
|
</button>
|
|
615
682
|
`;
|
|
@@ -622,9 +689,36 @@ var DawPauseButtonElement = class extends DawTransportButton {
|
|
|
622
689
|
);
|
|
623
690
|
return;
|
|
624
691
|
}
|
|
625
|
-
|
|
692
|
+
if (this._isRecording) {
|
|
693
|
+
if (this._isPaused) {
|
|
694
|
+
target.resumeRecording();
|
|
695
|
+
target.play(target.currentTime);
|
|
696
|
+
this._isPaused = false;
|
|
697
|
+
} else {
|
|
698
|
+
target.pauseRecording();
|
|
699
|
+
target.pause();
|
|
700
|
+
this._isPaused = true;
|
|
701
|
+
}
|
|
702
|
+
} else {
|
|
703
|
+
target.pause();
|
|
704
|
+
}
|
|
626
705
|
}
|
|
627
706
|
};
|
|
707
|
+
DawPauseButtonElement.styles = [
|
|
708
|
+
DawTransportButton.styles,
|
|
709
|
+
css4`
|
|
710
|
+
button[data-paused] {
|
|
711
|
+
background: rgba(255, 255, 255, 0.1);
|
|
712
|
+
border-color: var(--daw-controls-text, #e0d4c8);
|
|
713
|
+
}
|
|
714
|
+
`
|
|
715
|
+
];
|
|
716
|
+
__decorateClass([
|
|
717
|
+
state2()
|
|
718
|
+
], DawPauseButtonElement.prototype, "_isPaused", 2);
|
|
719
|
+
__decorateClass([
|
|
720
|
+
state2()
|
|
721
|
+
], DawPauseButtonElement.prototype, "_isRecording", 2);
|
|
628
722
|
DawPauseButtonElement = __decorateClass([
|
|
629
723
|
customElement7("daw-pause-button")
|
|
630
724
|
], DawPauseButtonElement);
|
|
@@ -648,6 +742,9 @@ var DawStopButtonElement = class extends DawTransportButton {
|
|
|
648
742
|
);
|
|
649
743
|
return;
|
|
650
744
|
}
|
|
745
|
+
if (target.isRecording) {
|
|
746
|
+
target.stopRecording();
|
|
747
|
+
}
|
|
651
748
|
target.stop();
|
|
652
749
|
}
|
|
653
750
|
};
|
|
@@ -656,8 +753,8 @@ DawStopButtonElement = __decorateClass([
|
|
|
656
753
|
], DawStopButtonElement);
|
|
657
754
|
|
|
658
755
|
// src/elements/daw-editor.ts
|
|
659
|
-
import { LitElement as LitElement8, html as html7, css as
|
|
660
|
-
import { customElement as customElement10, property as property6, state } from "lit/decorators.js";
|
|
756
|
+
import { LitElement as LitElement8, html as html7, css as css7 } from "lit";
|
|
757
|
+
import { customElement as customElement10, property as property6, state as state3 } from "lit/decorators.js";
|
|
661
758
|
import { createClipFromSeconds as createClipFromSeconds2, createTrack as createTrack2, clipPixelWidth } from "@waveform-playlist/core";
|
|
662
759
|
|
|
663
760
|
// src/workers/peaksWorker.ts
|
|
@@ -979,20 +1076,22 @@ function extractPeaks(waveformData, samplesPerPixel, isMono, offsetSamples, dura
|
|
|
979
1076
|
|
|
980
1077
|
// src/workers/peakPipeline.ts
|
|
981
1078
|
var PeakPipeline = class {
|
|
982
|
-
constructor() {
|
|
1079
|
+
constructor(baseScale = 128, bits = 16) {
|
|
983
1080
|
this._worker = null;
|
|
984
1081
|
this._cache = /* @__PURE__ */ new WeakMap();
|
|
985
1082
|
this._inflight = /* @__PURE__ */ new WeakMap();
|
|
1083
|
+
this._baseScale = baseScale;
|
|
1084
|
+
this._bits = bits;
|
|
986
1085
|
}
|
|
987
1086
|
/**
|
|
988
1087
|
* Generate PeakData for a clip from its AudioBuffer.
|
|
989
1088
|
* Uses cached WaveformData when available; otherwise generates via worker.
|
|
990
|
-
*
|
|
1089
|
+
* Worker generates at baseScale (default 128); extractPeaks resamples to the requested zoom.
|
|
991
1090
|
*/
|
|
992
|
-
async generatePeaks(audioBuffer, samplesPerPixel, isMono) {
|
|
993
|
-
const waveformData = await this._getWaveformData(audioBuffer
|
|
1091
|
+
async generatePeaks(audioBuffer, samplesPerPixel, isMono, offsetSamples, durationSamples) {
|
|
1092
|
+
const waveformData = await this._getWaveformData(audioBuffer);
|
|
994
1093
|
try {
|
|
995
|
-
return extractPeaks(waveformData, samplesPerPixel, isMono);
|
|
1094
|
+
return extractPeaks(waveformData, samplesPerPixel, isMono, offsetSamples, durationSamples);
|
|
996
1095
|
} catch (err) {
|
|
997
1096
|
console.warn("[dawcore] extractPeaks failed: " + String(err));
|
|
998
1097
|
throw err;
|
|
@@ -1004,14 +1103,24 @@ var PeakPipeline = class {
|
|
|
1004
1103
|
* Returns a new Map of clipId → PeakData. Clips without cached data or where
|
|
1005
1104
|
* the target scale is finer than the cached base are skipped.
|
|
1006
1105
|
*/
|
|
1007
|
-
reextractPeaks(clipBuffers, samplesPerPixel, isMono) {
|
|
1106
|
+
reextractPeaks(clipBuffers, samplesPerPixel, isMono, clipOffsets) {
|
|
1008
1107
|
const result = /* @__PURE__ */ new Map();
|
|
1009
1108
|
for (const [clipId, audioBuffer] of clipBuffers) {
|
|
1010
1109
|
const cached = this._cache.get(audioBuffer);
|
|
1011
1110
|
if (cached) {
|
|
1012
1111
|
if (samplesPerPixel < cached.scale) continue;
|
|
1013
1112
|
try {
|
|
1014
|
-
|
|
1113
|
+
const offsets = clipOffsets?.get(clipId);
|
|
1114
|
+
result.set(
|
|
1115
|
+
clipId,
|
|
1116
|
+
extractPeaks(
|
|
1117
|
+
cached,
|
|
1118
|
+
samplesPerPixel,
|
|
1119
|
+
isMono,
|
|
1120
|
+
offsets?.offsetSamples,
|
|
1121
|
+
offsets?.durationSamples
|
|
1122
|
+
)
|
|
1123
|
+
);
|
|
1015
1124
|
} catch (err) {
|
|
1016
1125
|
console.warn("[dawcore] reextractPeaks failed for clip " + clipId + ": " + String(err));
|
|
1017
1126
|
}
|
|
@@ -1023,9 +1132,9 @@ var PeakPipeline = class {
|
|
|
1023
1132
|
this._worker?.terminate();
|
|
1024
1133
|
this._worker = null;
|
|
1025
1134
|
}
|
|
1026
|
-
async _getWaveformData(audioBuffer
|
|
1135
|
+
async _getWaveformData(audioBuffer) {
|
|
1027
1136
|
const cached = this._cache.get(audioBuffer);
|
|
1028
|
-
if (cached
|
|
1137
|
+
if (cached) return cached;
|
|
1029
1138
|
const inflight = this._inflight.get(audioBuffer);
|
|
1030
1139
|
if (inflight) return inflight;
|
|
1031
1140
|
if (!this._worker) {
|
|
@@ -1039,8 +1148,8 @@ var PeakPipeline = class {
|
|
|
1039
1148
|
channels,
|
|
1040
1149
|
length: audioBuffer.length,
|
|
1041
1150
|
sampleRate: audioBuffer.sampleRate,
|
|
1042
|
-
scale:
|
|
1043
|
-
bits:
|
|
1151
|
+
scale: this._baseScale,
|
|
1152
|
+
bits: this._bits,
|
|
1044
1153
|
splitChannels: true
|
|
1045
1154
|
}).then((waveformData) => {
|
|
1046
1155
|
this._cache.set(audioBuffer, waveformData);
|
|
@@ -1057,7 +1166,7 @@ var PeakPipeline = class {
|
|
|
1057
1166
|
};
|
|
1058
1167
|
|
|
1059
1168
|
// src/elements/daw-track-controls.ts
|
|
1060
|
-
import { LitElement as LitElement7, html as html6, css as
|
|
1169
|
+
import { LitElement as LitElement7, html as html6, css as css5 } from "lit";
|
|
1061
1170
|
import { customElement as customElement9, property as property5 } from "lit/decorators.js";
|
|
1062
1171
|
var DawTrackControlsElement = class extends LitElement7 {
|
|
1063
1172
|
constructor() {
|
|
@@ -1157,11 +1266,11 @@ var DawTrackControlsElement = class extends LitElement7 {
|
|
|
1157
1266
|
`;
|
|
1158
1267
|
}
|
|
1159
1268
|
};
|
|
1160
|
-
DawTrackControlsElement.styles =
|
|
1269
|
+
DawTrackControlsElement.styles = css5`
|
|
1161
1270
|
:host {
|
|
1162
1271
|
display: flex;
|
|
1163
1272
|
flex-direction: column;
|
|
1164
|
-
justify-content:
|
|
1273
|
+
justify-content: flex-start;
|
|
1165
1274
|
box-sizing: border-box;
|
|
1166
1275
|
padding: 6px 8px;
|
|
1167
1276
|
background: var(--daw-controls-background, #0f0f1a);
|
|
@@ -1169,13 +1278,14 @@ DawTrackControlsElement.styles = css4`
|
|
|
1169
1278
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
1170
1279
|
font-family: system-ui, sans-serif;
|
|
1171
1280
|
font-size: 11px;
|
|
1281
|
+
overflow: hidden;
|
|
1172
1282
|
}
|
|
1173
1283
|
.header {
|
|
1174
1284
|
display: flex;
|
|
1175
1285
|
align-items: center;
|
|
1176
1286
|
justify-content: space-between;
|
|
1177
1287
|
gap: 4px;
|
|
1178
|
-
margin-bottom:
|
|
1288
|
+
margin-bottom: 3px;
|
|
1179
1289
|
}
|
|
1180
1290
|
.name {
|
|
1181
1291
|
flex: 1;
|
|
@@ -1202,7 +1312,7 @@ DawTrackControlsElement.styles = css4`
|
|
|
1202
1312
|
.buttons {
|
|
1203
1313
|
display: flex;
|
|
1204
1314
|
gap: 3px;
|
|
1205
|
-
margin-bottom:
|
|
1315
|
+
margin-bottom: 3px;
|
|
1206
1316
|
}
|
|
1207
1317
|
.btn {
|
|
1208
1318
|
background: rgba(255, 255, 255, 0.06);
|
|
@@ -1232,7 +1342,7 @@ DawTrackControlsElement.styles = css4`
|
|
|
1232
1342
|
display: flex;
|
|
1233
1343
|
align-items: center;
|
|
1234
1344
|
gap: 4px;
|
|
1235
|
-
height:
|
|
1345
|
+
height: 16px;
|
|
1236
1346
|
}
|
|
1237
1347
|
.slider-label {
|
|
1238
1348
|
width: 50px;
|
|
@@ -1312,8 +1422,8 @@ DawTrackControlsElement = __decorateClass([
|
|
|
1312
1422
|
], DawTrackControlsElement);
|
|
1313
1423
|
|
|
1314
1424
|
// src/styles/theme.ts
|
|
1315
|
-
import { css as
|
|
1316
|
-
var hostStyles =
|
|
1425
|
+
import { css as css6 } from "lit";
|
|
1426
|
+
var hostStyles = css6`
|
|
1317
1427
|
:host {
|
|
1318
1428
|
--daw-wave-color: #c49a6c;
|
|
1319
1429
|
--daw-progress-color: #63c75f;
|
|
@@ -1329,6 +1439,77 @@ var hostStyles = css5`
|
|
|
1329
1439
|
--daw-clip-header-text: #e0d4c8;
|
|
1330
1440
|
}
|
|
1331
1441
|
`;
|
|
1442
|
+
var clipStyles = css6`
|
|
1443
|
+
.clip-container {
|
|
1444
|
+
position: absolute;
|
|
1445
|
+
overflow: hidden;
|
|
1446
|
+
}
|
|
1447
|
+
.clip-header {
|
|
1448
|
+
position: relative;
|
|
1449
|
+
z-index: 1;
|
|
1450
|
+
height: 20px;
|
|
1451
|
+
background: var(--daw-clip-header-background, rgba(0, 0, 0, 0.4));
|
|
1452
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
1453
|
+
display: flex;
|
|
1454
|
+
align-items: center;
|
|
1455
|
+
padding: 0 6px;
|
|
1456
|
+
user-select: none;
|
|
1457
|
+
-webkit-user-drag: none;
|
|
1458
|
+
}
|
|
1459
|
+
.clip-header span {
|
|
1460
|
+
font-size: 10px;
|
|
1461
|
+
font-weight: 500;
|
|
1462
|
+
letter-spacing: 0.02em;
|
|
1463
|
+
font-family: system-ui, sans-serif;
|
|
1464
|
+
color: var(--daw-clip-header-text, #e0d4c8);
|
|
1465
|
+
white-space: nowrap;
|
|
1466
|
+
overflow: hidden;
|
|
1467
|
+
text-overflow: ellipsis;
|
|
1468
|
+
opacity: 0.8;
|
|
1469
|
+
}
|
|
1470
|
+
.clip-boundary {
|
|
1471
|
+
position: absolute;
|
|
1472
|
+
top: 0;
|
|
1473
|
+
width: 8px;
|
|
1474
|
+
height: 100%;
|
|
1475
|
+
z-index: 2;
|
|
1476
|
+
cursor: col-resize;
|
|
1477
|
+
background: transparent;
|
|
1478
|
+
border: none;
|
|
1479
|
+
touch-action: none;
|
|
1480
|
+
user-select: none;
|
|
1481
|
+
-webkit-user-drag: none;
|
|
1482
|
+
transition: background 0.1s, border-color 0.1s;
|
|
1483
|
+
}
|
|
1484
|
+
.clip-boundary[data-boundary-edge='left'] {
|
|
1485
|
+
left: 0;
|
|
1486
|
+
}
|
|
1487
|
+
.clip-boundary[data-boundary-edge='right'] {
|
|
1488
|
+
right: 0;
|
|
1489
|
+
}
|
|
1490
|
+
.clip-boundary[data-boundary-edge='left']:hover {
|
|
1491
|
+
background: rgba(255, 255, 255, 0.2);
|
|
1492
|
+
border-left: 2px solid rgba(255, 255, 255, 0.5);
|
|
1493
|
+
}
|
|
1494
|
+
.clip-boundary[data-boundary-edge='right']:hover {
|
|
1495
|
+
background: rgba(255, 255, 255, 0.2);
|
|
1496
|
+
border-right: 2px solid rgba(255, 255, 255, 0.5);
|
|
1497
|
+
}
|
|
1498
|
+
.clip-boundary[data-boundary-edge='left'].dragging {
|
|
1499
|
+
background: rgba(255, 255, 255, 0.4);
|
|
1500
|
+
border-left: 2px solid rgba(255, 255, 255, 0.8);
|
|
1501
|
+
}
|
|
1502
|
+
.clip-boundary[data-boundary-edge='right'].dragging {
|
|
1503
|
+
background: rgba(255, 255, 255, 0.4);
|
|
1504
|
+
border-right: 2px solid rgba(255, 255, 255, 0.8);
|
|
1505
|
+
}
|
|
1506
|
+
.clip-header[data-interactive] {
|
|
1507
|
+
cursor: grab;
|
|
1508
|
+
}
|
|
1509
|
+
.clip-header[data-interactive]:active {
|
|
1510
|
+
cursor: grabbing;
|
|
1511
|
+
}
|
|
1512
|
+
`;
|
|
1332
1513
|
|
|
1333
1514
|
// src/controllers/viewport-controller.ts
|
|
1334
1515
|
var OVERSCAN_MULTIPLIER = 1.5;
|
|
@@ -1511,6 +1692,9 @@ var RecordingController = class {
|
|
|
1511
1692
|
}
|
|
1512
1693
|
const channelCount = stream.getAudioTracks()[0]?.getSettings()?.channelCount ?? 1;
|
|
1513
1694
|
const startSample = options.startSample ?? Math.floor(this._host._currentTime * this._host.effectiveSampleRate);
|
|
1695
|
+
const outputLatency = rawCtx.outputLatency ?? 0;
|
|
1696
|
+
const lookAhead = context.lookAhead ?? 0;
|
|
1697
|
+
const latencySamples = Math.floor((outputLatency + lookAhead) * rawCtx.sampleRate);
|
|
1514
1698
|
const source = context.createMediaStreamSource(stream);
|
|
1515
1699
|
const workletNode = context.createAudioWorkletNode("recording-processor", {
|
|
1516
1700
|
channelCount,
|
|
@@ -1537,6 +1721,8 @@ var RecordingController = class {
|
|
|
1537
1721
|
channelCount,
|
|
1538
1722
|
bits,
|
|
1539
1723
|
isFirstMessage: true,
|
|
1724
|
+
latencySamples,
|
|
1725
|
+
wasOverdub: options.overdub ?? false,
|
|
1540
1726
|
_onTrackEnded: onTrackEnded,
|
|
1541
1727
|
_audioTrack: audioTrack
|
|
1542
1728
|
};
|
|
@@ -1557,6 +1743,9 @@ var RecordingController = class {
|
|
|
1557
1743
|
})
|
|
1558
1744
|
);
|
|
1559
1745
|
this._host.requestUpdate();
|
|
1746
|
+
if (options.overdub && typeof this._host.play === "function") {
|
|
1747
|
+
await this._host.play(this._host._currentTime);
|
|
1748
|
+
}
|
|
1560
1749
|
} catch (err) {
|
|
1561
1750
|
this._cleanupSession(trackId);
|
|
1562
1751
|
console.warn("[dawcore] RecordingController: Failed to start recording: " + String(err));
|
|
@@ -1569,11 +1758,28 @@ var RecordingController = class {
|
|
|
1569
1758
|
);
|
|
1570
1759
|
}
|
|
1571
1760
|
}
|
|
1761
|
+
pauseRecording(trackId) {
|
|
1762
|
+
const id = trackId ?? [...this._sessions.keys()][0];
|
|
1763
|
+
if (!id) return;
|
|
1764
|
+
const session = this._sessions.get(id);
|
|
1765
|
+
if (!session) return;
|
|
1766
|
+
session.workletNode.port.postMessage({ command: "pause" });
|
|
1767
|
+
}
|
|
1768
|
+
resumeRecording(trackId) {
|
|
1769
|
+
const id = trackId ?? [...this._sessions.keys()][0];
|
|
1770
|
+
if (!id) return;
|
|
1771
|
+
const session = this._sessions.get(id);
|
|
1772
|
+
if (!session) return;
|
|
1773
|
+
session.workletNode.port.postMessage({ command: "resume" });
|
|
1774
|
+
}
|
|
1572
1775
|
stopRecording(trackId) {
|
|
1573
1776
|
const id = trackId ?? [...this._sessions.keys()][0];
|
|
1574
1777
|
if (!id) return;
|
|
1575
1778
|
const session = this._sessions.get(id);
|
|
1576
1779
|
if (!session) return;
|
|
1780
|
+
if (session.wasOverdub && typeof this._host.stop === "function") {
|
|
1781
|
+
this._host.stop();
|
|
1782
|
+
}
|
|
1577
1783
|
session.workletNode.port.postMessage({ command: "stop" });
|
|
1578
1784
|
session.source.disconnect();
|
|
1579
1785
|
session.workletNode.disconnect();
|
|
@@ -1591,7 +1797,8 @@ var RecordingController = class {
|
|
|
1591
1797
|
);
|
|
1592
1798
|
return;
|
|
1593
1799
|
}
|
|
1594
|
-
const
|
|
1800
|
+
const context = getGlobalContext();
|
|
1801
|
+
const stopCtx = context.rawContext;
|
|
1595
1802
|
const channelData = session.chunks.map((chunkArr) => concatenateAudioData(chunkArr));
|
|
1596
1803
|
const audioBuffer = createAudioBuffer(
|
|
1597
1804
|
stopCtx,
|
|
@@ -1599,7 +1806,21 @@ var RecordingController = class {
|
|
|
1599
1806
|
this._host.effectiveSampleRate,
|
|
1600
1807
|
session.channelCount
|
|
1601
1808
|
);
|
|
1602
|
-
const
|
|
1809
|
+
const latencyOffsetSamples = session.latencySamples;
|
|
1810
|
+
const effectiveDuration = Math.max(0, audioBuffer.length - latencyOffsetSamples);
|
|
1811
|
+
if (effectiveDuration === 0) {
|
|
1812
|
+
console.warn("[dawcore] RecordingController: Recording too short for latency compensation");
|
|
1813
|
+
this._sessions.delete(id);
|
|
1814
|
+
this._host.requestUpdate();
|
|
1815
|
+
this._host.dispatchEvent(
|
|
1816
|
+
new CustomEvent("daw-recording-error", {
|
|
1817
|
+
bubbles: true,
|
|
1818
|
+
composed: true,
|
|
1819
|
+
detail: { trackId: id, error: new Error("Recording too short to save") }
|
|
1820
|
+
})
|
|
1821
|
+
);
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1603
1824
|
const event = new CustomEvent("daw-recording-complete", {
|
|
1604
1825
|
bubbles: true,
|
|
1605
1826
|
composed: true,
|
|
@@ -1608,14 +1829,21 @@ var RecordingController = class {
|
|
|
1608
1829
|
trackId: id,
|
|
1609
1830
|
audioBuffer,
|
|
1610
1831
|
startSample: session.startSample,
|
|
1611
|
-
durationSamples
|
|
1832
|
+
durationSamples: effectiveDuration,
|
|
1833
|
+
offsetSamples: latencyOffsetSamples
|
|
1612
1834
|
}
|
|
1613
1835
|
});
|
|
1614
1836
|
const notPrevented = this._host.dispatchEvent(event);
|
|
1615
1837
|
this._sessions.delete(id);
|
|
1616
1838
|
this._host.requestUpdate();
|
|
1617
1839
|
if (notPrevented) {
|
|
1618
|
-
this._createClipFromRecording(
|
|
1840
|
+
this._createClipFromRecording(
|
|
1841
|
+
id,
|
|
1842
|
+
audioBuffer,
|
|
1843
|
+
session.startSample,
|
|
1844
|
+
effectiveDuration,
|
|
1845
|
+
latencyOffsetSamples
|
|
1846
|
+
);
|
|
1619
1847
|
}
|
|
1620
1848
|
}
|
|
1621
1849
|
// Session fields are mutated in place on the hot path (~60fps worklet messages).
|
|
@@ -1646,7 +1874,9 @@ var RecordingController = class {
|
|
|
1646
1874
|
);
|
|
1647
1875
|
const newPeakCount = Math.floor(session.peaks[ch].length / 2);
|
|
1648
1876
|
const waveformSelector = `daw-waveform[data-recording-track="${trackId}"][data-recording-channel="${ch}"]`;
|
|
1649
|
-
const waveformEl = this._host.shadowRoot?.querySelector(
|
|
1877
|
+
const waveformEl = this._host.shadowRoot?.querySelector(
|
|
1878
|
+
waveformSelector
|
|
1879
|
+
);
|
|
1650
1880
|
if (waveformEl) {
|
|
1651
1881
|
if (session.isFirstMessage) {
|
|
1652
1882
|
waveformEl.peaks = session.peaks[ch];
|
|
@@ -1665,9 +1895,15 @@ var RecordingController = class {
|
|
|
1665
1895
|
this._host.requestUpdate();
|
|
1666
1896
|
}
|
|
1667
1897
|
}
|
|
1668
|
-
_createClipFromRecording(trackId, audioBuffer, startSample, durationSamples) {
|
|
1898
|
+
_createClipFromRecording(trackId, audioBuffer, startSample, durationSamples, offsetSamples = 0) {
|
|
1669
1899
|
if (typeof this._host._addRecordedClip === "function") {
|
|
1670
|
-
this._host._addRecordedClip(
|
|
1900
|
+
this._host._addRecordedClip(
|
|
1901
|
+
trackId,
|
|
1902
|
+
audioBuffer,
|
|
1903
|
+
startSample,
|
|
1904
|
+
durationSamples,
|
|
1905
|
+
offsetSamples
|
|
1906
|
+
);
|
|
1671
1907
|
} else {
|
|
1672
1908
|
console.warn(
|
|
1673
1909
|
'[dawcore] RecordingController: host does not implement _addRecordedClip \u2014 clip not created for track "' + trackId + '"'
|
|
@@ -1698,6 +1934,11 @@ var RecordingController = class {
|
|
|
1698
1934
|
|
|
1699
1935
|
// src/interactions/pointer-handler.ts
|
|
1700
1936
|
import { pixelsToSeconds } from "@waveform-playlist/core";
|
|
1937
|
+
|
|
1938
|
+
// src/interactions/constants.ts
|
|
1939
|
+
var DRAG_THRESHOLD = 3;
|
|
1940
|
+
|
|
1941
|
+
// src/interactions/pointer-handler.ts
|
|
1701
1942
|
var PointerHandler = class {
|
|
1702
1943
|
constructor(host) {
|
|
1703
1944
|
this._isDragging = false;
|
|
@@ -1706,6 +1947,34 @@ var PointerHandler = class {
|
|
|
1706
1947
|
// Cached from onPointerDown to avoid forced layout reflows at 60fps during drag
|
|
1707
1948
|
this._timelineRect = null;
|
|
1708
1949
|
this.onPointerDown = (e) => {
|
|
1950
|
+
const clipHandler = this._host._clipHandler;
|
|
1951
|
+
if (clipHandler) {
|
|
1952
|
+
const target = e.composedPath()[0];
|
|
1953
|
+
if (target && clipHandler.tryHandle(target, e)) {
|
|
1954
|
+
e.preventDefault();
|
|
1955
|
+
this._timeline = this._host.shadowRoot?.querySelector(".timeline");
|
|
1956
|
+
if (this._timeline) {
|
|
1957
|
+
this._timeline.setPointerCapture(e.pointerId);
|
|
1958
|
+
const onMove = (me) => clipHandler.onPointerMove(me);
|
|
1959
|
+
const onUp = (ue) => {
|
|
1960
|
+
clipHandler.onPointerUp(ue);
|
|
1961
|
+
this._timeline?.removeEventListener("pointermove", onMove);
|
|
1962
|
+
this._timeline?.removeEventListener("pointerup", onUp);
|
|
1963
|
+
try {
|
|
1964
|
+
this._timeline?.releasePointerCapture(ue.pointerId);
|
|
1965
|
+
} catch (err) {
|
|
1966
|
+
console.warn(
|
|
1967
|
+
"[dawcore] releasePointerCapture failed (may already be released): " + String(err)
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
this._timeline = null;
|
|
1971
|
+
};
|
|
1972
|
+
this._timeline.addEventListener("pointermove", onMove);
|
|
1973
|
+
this._timeline.addEventListener("pointerup", onUp);
|
|
1974
|
+
}
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1709
1978
|
this._timeline = this._host.shadowRoot?.querySelector(".timeline");
|
|
1710
1979
|
if (!this._timeline) return;
|
|
1711
1980
|
this._timelineRect = this._timeline.getBoundingClientRect();
|
|
@@ -1718,7 +1987,7 @@ var PointerHandler = class {
|
|
|
1718
1987
|
this._onPointerMove = (e) => {
|
|
1719
1988
|
if (!this._timeline) return;
|
|
1720
1989
|
const currentPx = this._pxFromPointer(e);
|
|
1721
|
-
if (!this._isDragging && Math.abs(currentPx - this._dragStartPx) >
|
|
1990
|
+
if (!this._isDragging && Math.abs(currentPx - this._dragStartPx) > DRAG_THRESHOLD) {
|
|
1722
1991
|
this._isDragging = true;
|
|
1723
1992
|
}
|
|
1724
1993
|
if (this._isDragging) {
|
|
@@ -1853,6 +2122,261 @@ var PointerHandler = class {
|
|
|
1853
2122
|
}
|
|
1854
2123
|
};
|
|
1855
2124
|
|
|
2125
|
+
// src/interactions/clip-pointer-handler.ts
|
|
2126
|
+
var ClipPointerHandler = class {
|
|
2127
|
+
constructor(host) {
|
|
2128
|
+
this._mode = null;
|
|
2129
|
+
this._clipId = "";
|
|
2130
|
+
this._trackId = "";
|
|
2131
|
+
this._startPx = 0;
|
|
2132
|
+
this._isDragging = false;
|
|
2133
|
+
this._lastDeltaPx = 0;
|
|
2134
|
+
this._cumulativeDeltaSamples = 0;
|
|
2135
|
+
// Trim visual feedback: snapshot of original clip state
|
|
2136
|
+
this._clipContainer = null;
|
|
2137
|
+
this._boundaryEl = null;
|
|
2138
|
+
this._originalLeft = 0;
|
|
2139
|
+
this._originalWidth = 0;
|
|
2140
|
+
this._originalOffsetSamples = 0;
|
|
2141
|
+
this._originalDurationSamples = 0;
|
|
2142
|
+
this._host = host;
|
|
2143
|
+
}
|
|
2144
|
+
/** Returns true if a drag interaction is currently in progress. */
|
|
2145
|
+
get isActive() {
|
|
2146
|
+
return this._mode !== null;
|
|
2147
|
+
}
|
|
2148
|
+
/**
|
|
2149
|
+
* Attempts to handle a pointerdown event on the given target element.
|
|
2150
|
+
* Returns true if the target is a recognized clip interaction element.
|
|
2151
|
+
*/
|
|
2152
|
+
tryHandle(target, e) {
|
|
2153
|
+
if (!this._host.interactiveClips) return false;
|
|
2154
|
+
const boundary = target.closest?.(".clip-boundary");
|
|
2155
|
+
const header = target.closest?.(".clip-header");
|
|
2156
|
+
if (boundary && boundary.dataset.boundaryEdge !== void 0) {
|
|
2157
|
+
const clipId = boundary.dataset.clipId;
|
|
2158
|
+
const trackId = boundary.dataset.trackId;
|
|
2159
|
+
const edge = boundary.dataset.boundaryEdge;
|
|
2160
|
+
if (!clipId || !trackId || edge !== "left" && edge !== "right") return false;
|
|
2161
|
+
this._beginDrag(edge === "left" ? "trim-left" : "trim-right", clipId, trackId, e);
|
|
2162
|
+
this._boundaryEl = boundary;
|
|
2163
|
+
return true;
|
|
2164
|
+
}
|
|
2165
|
+
if (header && header.dataset.interactive !== void 0) {
|
|
2166
|
+
const clipId = header.dataset.clipId;
|
|
2167
|
+
const trackId = header.dataset.trackId;
|
|
2168
|
+
if (!clipId || !trackId) return false;
|
|
2169
|
+
this._beginDrag("move", clipId, trackId, e);
|
|
2170
|
+
return true;
|
|
2171
|
+
}
|
|
2172
|
+
return false;
|
|
2173
|
+
}
|
|
2174
|
+
_beginDrag(mode, clipId, trackId, e) {
|
|
2175
|
+
this._mode = mode;
|
|
2176
|
+
this._clipId = clipId;
|
|
2177
|
+
this._trackId = trackId;
|
|
2178
|
+
this._startPx = e.clientX;
|
|
2179
|
+
this._isDragging = false;
|
|
2180
|
+
this._lastDeltaPx = 0;
|
|
2181
|
+
this._cumulativeDeltaSamples = 0;
|
|
2182
|
+
if (this._host.engine) {
|
|
2183
|
+
this._host.engine.beginTransaction();
|
|
2184
|
+
} else {
|
|
2185
|
+
console.warn(
|
|
2186
|
+
"[dawcore] beginDrag: engine unavailable, drag mutations will not be grouped for undo"
|
|
2187
|
+
);
|
|
2188
|
+
}
|
|
2189
|
+
if (mode === "trim-left" || mode === "trim-right") {
|
|
2190
|
+
const container = this._host.shadowRoot?.querySelector(
|
|
2191
|
+
`.clip-container[data-clip-id="${clipId}"]`
|
|
2192
|
+
);
|
|
2193
|
+
if (container) {
|
|
2194
|
+
this._clipContainer = container;
|
|
2195
|
+
this._originalLeft = parseFloat(container.style.left) || 0;
|
|
2196
|
+
this._originalWidth = parseFloat(container.style.width) || 0;
|
|
2197
|
+
} else {
|
|
2198
|
+
console.warn("[dawcore] clip container not found for trim visual feedback: " + clipId);
|
|
2199
|
+
}
|
|
2200
|
+
const engine = this._host.engine;
|
|
2201
|
+
if (engine) {
|
|
2202
|
+
const bounds = engine.getClipBounds(trackId, clipId);
|
|
2203
|
+
if (bounds) {
|
|
2204
|
+
this._originalOffsetSamples = bounds.offsetSamples;
|
|
2205
|
+
this._originalDurationSamples = bounds.durationSamples;
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
/** Processes pointermove events during an active drag. */
|
|
2211
|
+
onPointerMove(e) {
|
|
2212
|
+
if (this._mode === null) return;
|
|
2213
|
+
const totalDeltaPx = e.clientX - this._startPx;
|
|
2214
|
+
if (!this._isDragging && Math.abs(totalDeltaPx) > DRAG_THRESHOLD) {
|
|
2215
|
+
this._isDragging = true;
|
|
2216
|
+
if (this._boundaryEl) {
|
|
2217
|
+
this._boundaryEl.classList.add("dragging");
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
if (!this._isDragging) return;
|
|
2221
|
+
const engine = this._host.engine;
|
|
2222
|
+
if (!engine) return;
|
|
2223
|
+
if (this._mode === "move") {
|
|
2224
|
+
const incrementalDeltaPx = totalDeltaPx - this._lastDeltaPx;
|
|
2225
|
+
this._lastDeltaPx = totalDeltaPx;
|
|
2226
|
+
const incrementalDeltaSamples = Math.round(incrementalDeltaPx * this._host.samplesPerPixel);
|
|
2227
|
+
const applied = engine.moveClip(this._trackId, this._clipId, incrementalDeltaSamples, true);
|
|
2228
|
+
this._cumulativeDeltaSamples += applied;
|
|
2229
|
+
} else {
|
|
2230
|
+
const boundary = this._mode === "trim-left" ? "left" : "right";
|
|
2231
|
+
const rawDeltaSamples = Math.round(totalDeltaPx * this._host.samplesPerPixel);
|
|
2232
|
+
const deltaSamples = engine.constrainTrimDelta(
|
|
2233
|
+
this._trackId,
|
|
2234
|
+
this._clipId,
|
|
2235
|
+
boundary,
|
|
2236
|
+
rawDeltaSamples
|
|
2237
|
+
);
|
|
2238
|
+
const deltaPx = Math.round(deltaSamples / this._host.samplesPerPixel);
|
|
2239
|
+
this._cumulativeDeltaSamples = deltaSamples;
|
|
2240
|
+
if (this._clipContainer) {
|
|
2241
|
+
if (this._mode === "trim-left") {
|
|
2242
|
+
const newLeft = this._originalLeft + deltaPx;
|
|
2243
|
+
const newWidth = this._originalWidth - deltaPx;
|
|
2244
|
+
if (newWidth > 0) {
|
|
2245
|
+
this._clipContainer.style.left = newLeft + "px";
|
|
2246
|
+
this._clipContainer.style.width = newWidth + "px";
|
|
2247
|
+
const newOffset = this._originalOffsetSamples + deltaSamples;
|
|
2248
|
+
const newDuration = this._originalDurationSamples - deltaSamples;
|
|
2249
|
+
if (this._updateWaveformPeaks(newOffset, newDuration)) {
|
|
2250
|
+
const waveforms = this._clipContainer.querySelectorAll("daw-waveform");
|
|
2251
|
+
for (const wf of waveforms) {
|
|
2252
|
+
wf.style.left = "0px";
|
|
2253
|
+
}
|
|
2254
|
+
} else {
|
|
2255
|
+
const waveforms = this._clipContainer.querySelectorAll("daw-waveform");
|
|
2256
|
+
for (const wf of waveforms) {
|
|
2257
|
+
wf.style.left = -deltaPx + "px";
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
} else {
|
|
2262
|
+
const newWidth = this._originalWidth + deltaPx;
|
|
2263
|
+
if (newWidth > 0) {
|
|
2264
|
+
this._clipContainer.style.width = newWidth + "px";
|
|
2265
|
+
const newDuration = this._originalDurationSamples + deltaSamples;
|
|
2266
|
+
this._updateWaveformPeaks(this._originalOffsetSamples, newDuration);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
/** Processes pointerup events to finalize and dispatch result events. */
|
|
2273
|
+
onPointerUp(_e) {
|
|
2274
|
+
if (this._mode === null) return;
|
|
2275
|
+
try {
|
|
2276
|
+
if (!this._isDragging || this._cumulativeDeltaSamples === 0) {
|
|
2277
|
+
this._restoreTrimVisual();
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
const engine = this._host.engine;
|
|
2281
|
+
if (this._mode === "move") {
|
|
2282
|
+
if (engine) {
|
|
2283
|
+
engine.updateTrack(this._trackId);
|
|
2284
|
+
this._host.dispatchEvent(
|
|
2285
|
+
new CustomEvent("daw-clip-move", {
|
|
2286
|
+
bubbles: true,
|
|
2287
|
+
composed: true,
|
|
2288
|
+
detail: {
|
|
2289
|
+
trackId: this._trackId,
|
|
2290
|
+
clipId: this._clipId,
|
|
2291
|
+
deltaSamples: this._cumulativeDeltaSamples
|
|
2292
|
+
}
|
|
2293
|
+
})
|
|
2294
|
+
);
|
|
2295
|
+
} else {
|
|
2296
|
+
console.warn(
|
|
2297
|
+
"[dawcore] engine unavailable at move drop \u2014 audio may be out of sync for track " + this._trackId
|
|
2298
|
+
);
|
|
2299
|
+
}
|
|
2300
|
+
} else {
|
|
2301
|
+
this._restoreTrimVisual();
|
|
2302
|
+
const boundary = this._mode === "trim-left" ? "left" : "right";
|
|
2303
|
+
if (engine) {
|
|
2304
|
+
engine.trimClip(this._trackId, this._clipId, boundary, this._cumulativeDeltaSamples);
|
|
2305
|
+
this._host.dispatchEvent(
|
|
2306
|
+
new CustomEvent("daw-clip-trim", {
|
|
2307
|
+
bubbles: true,
|
|
2308
|
+
composed: true,
|
|
2309
|
+
detail: {
|
|
2310
|
+
trackId: this._trackId,
|
|
2311
|
+
clipId: this._clipId,
|
|
2312
|
+
boundary,
|
|
2313
|
+
deltaSamples: this._cumulativeDeltaSamples
|
|
2314
|
+
}
|
|
2315
|
+
})
|
|
2316
|
+
);
|
|
2317
|
+
} else {
|
|
2318
|
+
console.warn(
|
|
2319
|
+
"[dawcore] engine unavailable at trim drop \u2014 trim not applied for clip " + this._clipId
|
|
2320
|
+
);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
} finally {
|
|
2324
|
+
if (this._isDragging && this._cumulativeDeltaSamples !== 0) {
|
|
2325
|
+
this._host.engine?.commitTransaction();
|
|
2326
|
+
} else {
|
|
2327
|
+
this._host.engine?.abortTransaction();
|
|
2328
|
+
}
|
|
2329
|
+
this._reset();
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
/** Re-extract peaks from cache and set on waveform elements during trim drag.
|
|
2333
|
+
* Returns true if peaks were successfully updated. */
|
|
2334
|
+
_updateWaveformPeaks(offsetSamples, durationSamples) {
|
|
2335
|
+
if (!this._clipContainer || durationSamples <= 0) return false;
|
|
2336
|
+
const peakSlice = this._host.reextractClipPeaks(this._clipId, offsetSamples, durationSamples);
|
|
2337
|
+
if (!peakSlice) return false;
|
|
2338
|
+
const waveforms = this._clipContainer.querySelectorAll("daw-waveform");
|
|
2339
|
+
for (let i = 0; i < waveforms.length; i++) {
|
|
2340
|
+
const wf = waveforms[i];
|
|
2341
|
+
const channelPeaks = peakSlice.data[i];
|
|
2342
|
+
if (channelPeaks) {
|
|
2343
|
+
wf.peaks = channelPeaks;
|
|
2344
|
+
wf.length = peakSlice.length;
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
return true;
|
|
2348
|
+
}
|
|
2349
|
+
/** Restore clip container CSS to original values after trim visual preview. */
|
|
2350
|
+
_restoreTrimVisual() {
|
|
2351
|
+
if (this._clipContainer) {
|
|
2352
|
+
this._clipContainer.style.left = this._originalLeft + "px";
|
|
2353
|
+
this._clipContainer.style.width = this._originalWidth + "px";
|
|
2354
|
+
const waveforms = this._clipContainer.querySelectorAll("daw-waveform");
|
|
2355
|
+
for (const wf of waveforms) {
|
|
2356
|
+
wf.style.left = "0px";
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
_reset() {
|
|
2361
|
+
if (this._boundaryEl) {
|
|
2362
|
+
this._boundaryEl.classList.remove("dragging");
|
|
2363
|
+
this._boundaryEl = null;
|
|
2364
|
+
}
|
|
2365
|
+
this._mode = null;
|
|
2366
|
+
this._clipId = "";
|
|
2367
|
+
this._trackId = "";
|
|
2368
|
+
this._startPx = 0;
|
|
2369
|
+
this._isDragging = false;
|
|
2370
|
+
this._lastDeltaPx = 0;
|
|
2371
|
+
this._cumulativeDeltaSamples = 0;
|
|
2372
|
+
this._clipContainer = null;
|
|
2373
|
+
this._originalLeft = 0;
|
|
2374
|
+
this._originalWidth = 0;
|
|
2375
|
+
this._originalOffsetSamples = 0;
|
|
2376
|
+
this._originalDurationSamples = 0;
|
|
2377
|
+
}
|
|
2378
|
+
};
|
|
2379
|
+
|
|
1856
2380
|
// src/interactions/file-loader.ts
|
|
1857
2381
|
import { createClipFromSeconds, createTrack } from "@waveform-playlist/core";
|
|
1858
2382
|
async function loadFiles(host, files) {
|
|
@@ -1887,10 +2411,16 @@ async function loadFiles(host, files) {
|
|
|
1887
2411
|
sourceDuration: audioBuffer.duration
|
|
1888
2412
|
});
|
|
1889
2413
|
host._clipBuffers = new Map(host._clipBuffers).set(clip.id, audioBuffer);
|
|
2414
|
+
host._clipOffsets.set(clip.id, {
|
|
2415
|
+
offsetSamples: clip.offsetSamples,
|
|
2416
|
+
durationSamples: clip.durationSamples
|
|
2417
|
+
});
|
|
1890
2418
|
const peakData = await host._peakPipeline.generatePeaks(
|
|
1891
2419
|
audioBuffer,
|
|
1892
2420
|
host.samplesPerPixel,
|
|
1893
|
-
host.mono
|
|
2421
|
+
host.mono,
|
|
2422
|
+
clip.offsetSamples,
|
|
2423
|
+
clip.durationSamples
|
|
1894
2424
|
);
|
|
1895
2425
|
host._peaksData = new Map(host._peaksData).set(clip.id, peakData);
|
|
1896
2426
|
const trackId = crypto.randomUUID();
|
|
@@ -1949,17 +2479,31 @@ async function loadFiles(host, files) {
|
|
|
1949
2479
|
|
|
1950
2480
|
// src/interactions/recording-clip.ts
|
|
1951
2481
|
import { createClip } from "@waveform-playlist/core";
|
|
1952
|
-
function addRecordedClip(host, trackId, buf, startSample, durSamples) {
|
|
2482
|
+
function addRecordedClip(host, trackId, buf, startSample, durSamples, offsetSamples = 0) {
|
|
2483
|
+
let trimmedBuf = buf;
|
|
2484
|
+
if (offsetSamples > 0 && offsetSamples < buf.length) {
|
|
2485
|
+
const trimmed = new AudioBuffer({
|
|
2486
|
+
numberOfChannels: buf.numberOfChannels,
|
|
2487
|
+
length: durSamples,
|
|
2488
|
+
sampleRate: buf.sampleRate
|
|
2489
|
+
});
|
|
2490
|
+
for (let ch = 0; ch < buf.numberOfChannels; ch++) {
|
|
2491
|
+
const source = buf.getChannelData(ch);
|
|
2492
|
+
trimmed.copyToChannel(source.subarray(offsetSamples, offsetSamples + durSamples), ch);
|
|
2493
|
+
}
|
|
2494
|
+
trimmedBuf = trimmed;
|
|
2495
|
+
}
|
|
1953
2496
|
const clip = createClip({
|
|
1954
|
-
audioBuffer:
|
|
2497
|
+
audioBuffer: trimmedBuf,
|
|
1955
2498
|
startSample,
|
|
1956
2499
|
durationSamples: durSamples,
|
|
1957
2500
|
offsetSamples: 0,
|
|
2501
|
+
// offset already applied by slicing
|
|
1958
2502
|
gain: 1,
|
|
1959
2503
|
name: "Recording"
|
|
1960
2504
|
});
|
|
1961
|
-
host._clipBuffers = new Map(host._clipBuffers).set(clip.id,
|
|
1962
|
-
host._peakPipeline.generatePeaks(
|
|
2505
|
+
host._clipBuffers = new Map(host._clipBuffers).set(clip.id, trimmedBuf);
|
|
2506
|
+
host._peakPipeline.generatePeaks(trimmedBuf, host.samplesPerPixel, host.mono).then((pd) => {
|
|
1963
2507
|
host._peaksData = new Map(host._peaksData).set(clip.id, pd);
|
|
1964
2508
|
const t = host._engineTracks.get(trackId);
|
|
1965
2509
|
if (!t) {
|
|
@@ -1992,7 +2536,12 @@ function addRecordedClip(host, trackId, buf, startSample, durSamples) {
|
|
|
1992
2536
|
});
|
|
1993
2537
|
}
|
|
1994
2538
|
host._recomputeDuration();
|
|
1995
|
-
host.
|
|
2539
|
+
const updatedTrack = host._engineTracks.get(trackId);
|
|
2540
|
+
if (host._engine?.updateTrack && updatedTrack) {
|
|
2541
|
+
host._engine.updateTrack(trackId, updatedTrack);
|
|
2542
|
+
} else {
|
|
2543
|
+
host._engine?.setTracks([...host._engineTracks.values()]);
|
|
2544
|
+
}
|
|
1996
2545
|
}).catch((err) => {
|
|
1997
2546
|
console.warn("[dawcore] Failed to generate peaks for recorded clip: " + String(err));
|
|
1998
2547
|
const next = new Map(host._clipBuffers);
|
|
@@ -2010,6 +2559,169 @@ function addRecordedClip(host, trackId, buf, startSample, durSamples) {
|
|
|
2010
2559
|
});
|
|
2011
2560
|
}
|
|
2012
2561
|
|
|
2562
|
+
// src/interactions/split-handler.ts
|
|
2563
|
+
function splitAtPlayhead(host) {
|
|
2564
|
+
const wasPlaying = host.isPlaying;
|
|
2565
|
+
const time = host.currentTime;
|
|
2566
|
+
if (!canSplitAtTime(host, time)) return false;
|
|
2567
|
+
if (wasPlaying) {
|
|
2568
|
+
host.stop();
|
|
2569
|
+
}
|
|
2570
|
+
let result;
|
|
2571
|
+
try {
|
|
2572
|
+
result = performSplit(host, time);
|
|
2573
|
+
} catch (err) {
|
|
2574
|
+
console.warn("[dawcore] splitAtPlayhead failed: " + String(err));
|
|
2575
|
+
result = false;
|
|
2576
|
+
}
|
|
2577
|
+
if (wasPlaying) {
|
|
2578
|
+
host.play(time);
|
|
2579
|
+
}
|
|
2580
|
+
return result;
|
|
2581
|
+
}
|
|
2582
|
+
function canSplitAtTime(host, time) {
|
|
2583
|
+
const { engine } = host;
|
|
2584
|
+
if (!engine) return false;
|
|
2585
|
+
const state5 = engine.getState();
|
|
2586
|
+
if (!state5.selectedTrackId) return false;
|
|
2587
|
+
const track = state5.tracks.find((t) => t.id === state5.selectedTrackId);
|
|
2588
|
+
if (!track) return false;
|
|
2589
|
+
const atSample = Math.round(time * host.effectiveSampleRate);
|
|
2590
|
+
return !!findClipAtSample(track.clips, atSample);
|
|
2591
|
+
}
|
|
2592
|
+
function performSplit(host, time) {
|
|
2593
|
+
const { engine } = host;
|
|
2594
|
+
if (!engine) return false;
|
|
2595
|
+
const stateBefore = engine.getState();
|
|
2596
|
+
const { selectedTrackId, tracks } = stateBefore;
|
|
2597
|
+
if (!selectedTrackId) return false;
|
|
2598
|
+
const track = tracks.find((t) => t.id === selectedTrackId);
|
|
2599
|
+
if (!track) return false;
|
|
2600
|
+
const atSample = Math.round(time * host.effectiveSampleRate);
|
|
2601
|
+
const clip = findClipAtSample(track.clips, atSample);
|
|
2602
|
+
if (!clip) return false;
|
|
2603
|
+
const originalClipId = clip.id;
|
|
2604
|
+
const clipIdsBefore = new Set(track.clips.map((c) => c.id));
|
|
2605
|
+
engine.splitClip(selectedTrackId, originalClipId, atSample);
|
|
2606
|
+
const stateAfter = engine.getState();
|
|
2607
|
+
const trackAfter = stateAfter.tracks.find((t) => t.id === selectedTrackId);
|
|
2608
|
+
if (!trackAfter) {
|
|
2609
|
+
console.warn(
|
|
2610
|
+
'[dawcore] splitAtPlayhead: track "' + selectedTrackId + '" disappeared after split'
|
|
2611
|
+
);
|
|
2612
|
+
return false;
|
|
2613
|
+
}
|
|
2614
|
+
const newClips = trackAfter.clips.filter((c) => !clipIdsBefore.has(c.id));
|
|
2615
|
+
if (newClips.length !== 2) {
|
|
2616
|
+
if (newClips.length > 0) {
|
|
2617
|
+
console.warn(
|
|
2618
|
+
"[dawcore] splitAtPlayhead: expected 2 new clips after split but got " + newClips.length
|
|
2619
|
+
);
|
|
2620
|
+
}
|
|
2621
|
+
return false;
|
|
2622
|
+
}
|
|
2623
|
+
const sorted = [...newClips].sort((a, b) => a.startSample - b.startSample);
|
|
2624
|
+
const leftClipId = sorted[0].id;
|
|
2625
|
+
const rightClipId = sorted[1].id;
|
|
2626
|
+
host.dispatchEvent(
|
|
2627
|
+
new CustomEvent("daw-clip-split", {
|
|
2628
|
+
bubbles: true,
|
|
2629
|
+
composed: true,
|
|
2630
|
+
detail: {
|
|
2631
|
+
trackId: selectedTrackId,
|
|
2632
|
+
originalClipId,
|
|
2633
|
+
leftClipId,
|
|
2634
|
+
rightClipId
|
|
2635
|
+
}
|
|
2636
|
+
})
|
|
2637
|
+
);
|
|
2638
|
+
return true;
|
|
2639
|
+
}
|
|
2640
|
+
function findClipAtSample(clips, atSample) {
|
|
2641
|
+
return clips.find(
|
|
2642
|
+
(c) => atSample > c.startSample && atSample < c.startSample + c.durationSamples
|
|
2643
|
+
);
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
// src/interactions/clip-peak-sync.ts
|
|
2647
|
+
function syncPeaksForChangedClips(host, tracks) {
|
|
2648
|
+
const currentClipIds = /* @__PURE__ */ new Set();
|
|
2649
|
+
for (const track of tracks) {
|
|
2650
|
+
for (const clip of track.clips) {
|
|
2651
|
+
currentClipIds.add(clip.id);
|
|
2652
|
+
const cached = host._clipOffsets.get(clip.id);
|
|
2653
|
+
const needsPeaks = !host._peaksData.has(clip.id) || !cached || cached.offsetSamples !== clip.offsetSamples || cached.durationSamples !== clip.durationSamples;
|
|
2654
|
+
if (!needsPeaks) continue;
|
|
2655
|
+
const audioBuffer = clip.audioBuffer ?? host._clipBuffers.get(clip.id) ?? findAudioBufferForClip(host, clip, track);
|
|
2656
|
+
if (!audioBuffer) {
|
|
2657
|
+
console.warn(
|
|
2658
|
+
"[dawcore] syncPeaksForChangedClips: no AudioBuffer for clip " + clip.id + " \u2014 waveform will be blank"
|
|
2659
|
+
);
|
|
2660
|
+
continue;
|
|
2661
|
+
}
|
|
2662
|
+
host._clipBuffers = new Map(host._clipBuffers).set(clip.id, audioBuffer);
|
|
2663
|
+
host._clipOffsets.set(clip.id, {
|
|
2664
|
+
offsetSamples: clip.offsetSamples,
|
|
2665
|
+
durationSamples: clip.durationSamples
|
|
2666
|
+
});
|
|
2667
|
+
host._peakPipeline.generatePeaks(
|
|
2668
|
+
audioBuffer,
|
|
2669
|
+
host.samplesPerPixel,
|
|
2670
|
+
host.mono,
|
|
2671
|
+
clip.offsetSamples,
|
|
2672
|
+
clip.durationSamples
|
|
2673
|
+
).then((peakData) => {
|
|
2674
|
+
host._peaksData = new Map(host._peaksData).set(clip.id, peakData);
|
|
2675
|
+
}).catch((err) => {
|
|
2676
|
+
console.warn(
|
|
2677
|
+
"[dawcore] Failed to generate peaks for clip " + clip.id + ": " + String(err)
|
|
2678
|
+
);
|
|
2679
|
+
});
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
cleanupOrphanedClipData(host, currentClipIds);
|
|
2683
|
+
}
|
|
2684
|
+
function cleanupOrphanedClipData(host, currentClipIds) {
|
|
2685
|
+
let buffersChanged = false;
|
|
2686
|
+
let peaksChanged = false;
|
|
2687
|
+
for (const id of host._clipBuffers.keys()) {
|
|
2688
|
+
if (!currentClipIds.has(id)) {
|
|
2689
|
+
host._clipBuffers.delete(id);
|
|
2690
|
+
buffersChanged = true;
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
let offsetsChanged = false;
|
|
2694
|
+
for (const id of host._clipOffsets.keys()) {
|
|
2695
|
+
if (!currentClipIds.has(id)) {
|
|
2696
|
+
host._clipOffsets.delete(id);
|
|
2697
|
+
offsetsChanged = true;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
for (const id of host._peaksData.keys()) {
|
|
2701
|
+
if (!currentClipIds.has(id)) {
|
|
2702
|
+
host._peaksData.delete(id);
|
|
2703
|
+
peaksChanged = true;
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
if (buffersChanged) {
|
|
2707
|
+
host._clipBuffers = new Map(host._clipBuffers);
|
|
2708
|
+
}
|
|
2709
|
+
if (offsetsChanged) {
|
|
2710
|
+
host._clipOffsets = new Map(host._clipOffsets);
|
|
2711
|
+
}
|
|
2712
|
+
if (peaksChanged) {
|
|
2713
|
+
host._peaksData = new Map(host._peaksData);
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
function findAudioBufferForClip(host, clip, track) {
|
|
2717
|
+
for (const sibling of track.clips) {
|
|
2718
|
+
if (sibling.id === clip.id) continue;
|
|
2719
|
+
const buf = host._clipBuffers.get(sibling.id);
|
|
2720
|
+
if (buf) return buf;
|
|
2721
|
+
}
|
|
2722
|
+
return null;
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2013
2725
|
// src/elements/daw-editor.ts
|
|
2014
2726
|
var DawEditorElement = class extends LitElement8 {
|
|
2015
2727
|
constructor() {
|
|
@@ -2021,6 +2733,9 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2021
2733
|
this.barWidth = 1;
|
|
2022
2734
|
this.barGap = 0;
|
|
2023
2735
|
this.fileDrop = false;
|
|
2736
|
+
this.clipHeaders = false;
|
|
2737
|
+
this.clipHeaderHeight = 20;
|
|
2738
|
+
this.interactiveClips = false;
|
|
2024
2739
|
this.sampleRate = 48e3;
|
|
2025
2740
|
/** Resolved sample rate — falls back to sampleRate property until first audio decode. */
|
|
2026
2741
|
this._resolvedSampleRate = null;
|
|
@@ -2037,14 +2752,15 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2037
2752
|
this._currentTime = 0;
|
|
2038
2753
|
this._engine = null;
|
|
2039
2754
|
this._enginePromise = null;
|
|
2040
|
-
this._audioInitialized = false;
|
|
2041
2755
|
this._audioCache = /* @__PURE__ */ new Map();
|
|
2042
2756
|
this._clipBuffers = /* @__PURE__ */ new Map();
|
|
2757
|
+
this._clipOffsets = /* @__PURE__ */ new Map();
|
|
2043
2758
|
this._peakPipeline = new PeakPipeline();
|
|
2044
2759
|
this._trackElements = /* @__PURE__ */ new Map();
|
|
2045
2760
|
this._childObserver = null;
|
|
2046
2761
|
this._audioResume = new AudioResumeController(this);
|
|
2047
2762
|
this._recordingController = new RecordingController(this);
|
|
2763
|
+
this._clipPointer = new ClipPointerHandler(this);
|
|
2048
2764
|
this._pointer = new PointerHandler(this);
|
|
2049
2765
|
this._viewport = (() => {
|
|
2050
2766
|
const v = new ViewportController(this);
|
|
@@ -2088,6 +2804,19 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2088
2804
|
this._onTrackControl = (e) => {
|
|
2089
2805
|
const { trackId, prop, value } = e.detail ?? {};
|
|
2090
2806
|
if (!trackId || !prop || !DawEditorElement._CONTROL_PROPS.has(prop)) return;
|
|
2807
|
+
if (this._selectedTrackId !== trackId) {
|
|
2808
|
+
this._setSelectedTrackId(trackId);
|
|
2809
|
+
if (this._engine) {
|
|
2810
|
+
this._engine.selectTrack(trackId);
|
|
2811
|
+
}
|
|
2812
|
+
this.dispatchEvent(
|
|
2813
|
+
new CustomEvent("daw-track-select", {
|
|
2814
|
+
bubbles: true,
|
|
2815
|
+
composed: true,
|
|
2816
|
+
detail: { trackId }
|
|
2817
|
+
})
|
|
2818
|
+
);
|
|
2819
|
+
}
|
|
2091
2820
|
const oldDescriptor = this._tracks.get(trackId);
|
|
2092
2821
|
if (oldDescriptor) {
|
|
2093
2822
|
const descriptor = { ...oldDescriptor, [prop]: value };
|
|
@@ -2148,6 +2877,28 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2148
2877
|
// --- Recording ---
|
|
2149
2878
|
this.recordingStream = null;
|
|
2150
2879
|
}
|
|
2880
|
+
get _clipHandler() {
|
|
2881
|
+
return this.interactiveClips ? this._clipPointer : null;
|
|
2882
|
+
}
|
|
2883
|
+
get engine() {
|
|
2884
|
+
return this._engine;
|
|
2885
|
+
}
|
|
2886
|
+
/** Re-extract peaks for a clip at new offset/duration from cached WaveformData. */
|
|
2887
|
+
reextractClipPeaks(clipId, offsetSamples, durationSamples) {
|
|
2888
|
+
const buf = this._clipBuffers.get(clipId);
|
|
2889
|
+
if (!buf) return null;
|
|
2890
|
+
const singleClipBuffers = /* @__PURE__ */ new Map([[clipId, buf]]);
|
|
2891
|
+
const singleClipOffsets = /* @__PURE__ */ new Map([[clipId, { offsetSamples, durationSamples }]]);
|
|
2892
|
+
const result = this._peakPipeline.reextractPeaks(
|
|
2893
|
+
singleClipBuffers,
|
|
2894
|
+
this.samplesPerPixel,
|
|
2895
|
+
this.mono,
|
|
2896
|
+
singleClipOffsets
|
|
2897
|
+
);
|
|
2898
|
+
const peakData = result.get(clipId);
|
|
2899
|
+
if (!peakData) return null;
|
|
2900
|
+
return { data: peakData.data, length: peakData.length };
|
|
2901
|
+
}
|
|
2151
2902
|
get effectiveSampleRate() {
|
|
2152
2903
|
return this._resolvedSampleRate ?? this.sampleRate;
|
|
2153
2904
|
}
|
|
@@ -2222,6 +2973,7 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2222
2973
|
this._trackElements.clear();
|
|
2223
2974
|
this._audioCache.clear();
|
|
2224
2975
|
this._clipBuffers.clear();
|
|
2976
|
+
this._clipOffsets.clear();
|
|
2225
2977
|
this._peakPipeline.terminate();
|
|
2226
2978
|
try {
|
|
2227
2979
|
this._disposeEngine();
|
|
@@ -2233,17 +2985,19 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2233
2985
|
if (changedProperties.has("eagerResume")) {
|
|
2234
2986
|
this._audioResume.target = this.eagerResume;
|
|
2235
2987
|
}
|
|
2988
|
+
if (changedProperties.has("samplesPerPixel") && this._isPlaying) {
|
|
2989
|
+
this._startPlayhead();
|
|
2990
|
+
}
|
|
2236
2991
|
if (changedProperties.has("samplesPerPixel") && this._clipBuffers.size > 0) {
|
|
2237
|
-
const
|
|
2992
|
+
const re = this._peakPipeline.reextractPeaks(
|
|
2238
2993
|
this._clipBuffers,
|
|
2239
2994
|
this.samplesPerPixel,
|
|
2240
|
-
this.mono
|
|
2995
|
+
this.mono,
|
|
2996
|
+
this._clipOffsets
|
|
2241
2997
|
);
|
|
2242
|
-
if (
|
|
2998
|
+
if (re.size > 0) {
|
|
2243
2999
|
const next = new Map(this._peaksData);
|
|
2244
|
-
for (const [
|
|
2245
|
-
next.set(clipId, peakData);
|
|
2246
|
-
}
|
|
3000
|
+
for (const [id, pd] of re) next.set(id, pd);
|
|
2247
3001
|
this._peaksData = next;
|
|
2248
3002
|
}
|
|
2249
3003
|
}
|
|
@@ -2255,6 +3009,7 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2255
3009
|
const nextPeaks = new Map(this._peaksData);
|
|
2256
3010
|
for (const clip of removedTrack.clips) {
|
|
2257
3011
|
this._clipBuffers.delete(clip.id);
|
|
3012
|
+
this._clipOffsets.delete(clip.id);
|
|
2258
3013
|
nextPeaks.delete(clip.id);
|
|
2259
3014
|
}
|
|
2260
3015
|
this._peaksData = nextPeaks;
|
|
@@ -2333,10 +3088,16 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2333
3088
|
sourceDuration: audioBuffer.duration
|
|
2334
3089
|
});
|
|
2335
3090
|
this._clipBuffers = new Map(this._clipBuffers).set(clip.id, audioBuffer);
|
|
3091
|
+
this._clipOffsets.set(clip.id, {
|
|
3092
|
+
offsetSamples: clip.offsetSamples,
|
|
3093
|
+
durationSamples: clip.durationSamples
|
|
3094
|
+
});
|
|
2336
3095
|
const peakData = await this._peakPipeline.generatePeaks(
|
|
2337
3096
|
audioBuffer,
|
|
2338
3097
|
this.samplesPerPixel,
|
|
2339
|
-
this.mono
|
|
3098
|
+
this.mono,
|
|
3099
|
+
clip.offsetSamples,
|
|
3100
|
+
clip.durationSamples
|
|
2340
3101
|
);
|
|
2341
3102
|
this._peaksData = new Map(this._peaksData).set(clip.id, peakData);
|
|
2342
3103
|
clips.push(clip);
|
|
@@ -2428,10 +3189,20 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2428
3189
|
samplesPerPixel: this.samplesPerPixel,
|
|
2429
3190
|
zoomLevels: [256, 512, 1024, 2048, 4096, 8192, this.samplesPerPixel].filter((v, i, a) => a.indexOf(v) === i).sort((a, b) => a - b)
|
|
2430
3191
|
});
|
|
3192
|
+
let lastTracksVersion = -1;
|
|
2431
3193
|
engine.on("statechange", (engineState) => {
|
|
2432
3194
|
this._isPlaying = engineState.isPlaying;
|
|
2433
3195
|
this._duration = engineState.duration;
|
|
2434
3196
|
this._selectedTrackId = engineState.selectedTrackId;
|
|
3197
|
+
if (engineState.tracksVersion !== lastTracksVersion) {
|
|
3198
|
+
lastTracksVersion = engineState.tracksVersion;
|
|
3199
|
+
const nextTracks = /* @__PURE__ */ new Map();
|
|
3200
|
+
for (const track of engineState.tracks) {
|
|
3201
|
+
nextTracks.set(track.id, track);
|
|
3202
|
+
}
|
|
3203
|
+
this._engineTracks = nextTracks;
|
|
3204
|
+
syncPeaksForChangedClips(this, engineState.tracks);
|
|
3205
|
+
}
|
|
2435
3206
|
});
|
|
2436
3207
|
engine.on("timeupdate", (time) => {
|
|
2437
3208
|
this._currentTime = time;
|
|
@@ -2454,14 +3225,11 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2454
3225
|
return loadFiles(this, files);
|
|
2455
3226
|
}
|
|
2456
3227
|
// --- Playback ---
|
|
2457
|
-
async play() {
|
|
3228
|
+
async play(startTime) {
|
|
2458
3229
|
try {
|
|
2459
3230
|
const engine = await this._ensureEngine();
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
this._audioInitialized = true;
|
|
2463
|
-
}
|
|
2464
|
-
engine.play();
|
|
3231
|
+
await engine.init();
|
|
3232
|
+
engine.play(startTime);
|
|
2465
3233
|
this._startPlayhead();
|
|
2466
3234
|
this.dispatchEvent(new CustomEvent("daw-play", { bubbles: true, composed: true }));
|
|
2467
3235
|
} catch (err) {
|
|
@@ -2487,19 +3255,90 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2487
3255
|
this._stopPlayhead();
|
|
2488
3256
|
this.dispatchEvent(new CustomEvent("daw-stop", { bubbles: true, composed: true }));
|
|
2489
3257
|
}
|
|
3258
|
+
/** Toggle between play and pause. */
|
|
3259
|
+
togglePlayPause() {
|
|
3260
|
+
if (this._isPlaying) {
|
|
3261
|
+
this.pause();
|
|
3262
|
+
} else {
|
|
3263
|
+
this.play();
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
2490
3266
|
seekTo(time) {
|
|
2491
|
-
if (!this._engine)
|
|
2492
|
-
|
|
2493
|
-
|
|
3267
|
+
if (!this._engine) {
|
|
3268
|
+
console.warn("[dawcore] seekTo: engine not ready, call ignored");
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3271
|
+
if (this._isPlaying) {
|
|
3272
|
+
this.stop();
|
|
3273
|
+
this.play(time);
|
|
3274
|
+
} else {
|
|
3275
|
+
this._engine.seek(time);
|
|
3276
|
+
this._currentTime = time;
|
|
3277
|
+
this._stopPlayhead();
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
/** Undo the last structural edit. */
|
|
3281
|
+
undo() {
|
|
3282
|
+
if (!this._engine) {
|
|
3283
|
+
console.warn("[dawcore] undo: engine not ready, call ignored");
|
|
3284
|
+
return;
|
|
3285
|
+
}
|
|
3286
|
+
this._engine.undo();
|
|
3287
|
+
}
|
|
3288
|
+
/** Redo the last undone edit. */
|
|
3289
|
+
redo() {
|
|
3290
|
+
if (!this._engine) {
|
|
3291
|
+
console.warn("[dawcore] redo: engine not ready, call ignored");
|
|
3292
|
+
return;
|
|
3293
|
+
}
|
|
3294
|
+
this._engine.redo();
|
|
3295
|
+
}
|
|
3296
|
+
/** Whether undo is available. */
|
|
3297
|
+
get canUndo() {
|
|
3298
|
+
return this._engine?.canUndo ?? false;
|
|
3299
|
+
}
|
|
3300
|
+
/** Whether redo is available. */
|
|
3301
|
+
get canRedo() {
|
|
3302
|
+
return this._engine?.canRedo ?? false;
|
|
3303
|
+
}
|
|
3304
|
+
/** Split the clip under the playhead on the selected track. */
|
|
3305
|
+
splitAtPlayhead() {
|
|
3306
|
+
return splitAtPlayhead({
|
|
3307
|
+
effectiveSampleRate: this.effectiveSampleRate,
|
|
3308
|
+
currentTime: this._currentTime,
|
|
3309
|
+
isPlaying: this._isPlaying,
|
|
3310
|
+
engine: this._engine,
|
|
3311
|
+
dispatchEvent: (e) => this.dispatchEvent(e),
|
|
3312
|
+
stop: () => {
|
|
3313
|
+
this._engine?.stop();
|
|
3314
|
+
this._stopPlayhead();
|
|
3315
|
+
},
|
|
3316
|
+
// Call engine.play directly (synchronous) — not the async editor play()
|
|
3317
|
+
// which yields to microtask queue via await engine.init(). Engine is
|
|
3318
|
+
// already initialized at split time; the async gap causes audio desync.
|
|
3319
|
+
play: (time) => {
|
|
3320
|
+
this._engine?.play(time);
|
|
3321
|
+
this._startPlayhead();
|
|
3322
|
+
}
|
|
3323
|
+
});
|
|
3324
|
+
}
|
|
3325
|
+
get currentTime() {
|
|
3326
|
+
return this._currentTime;
|
|
2494
3327
|
}
|
|
2495
3328
|
get isRecording() {
|
|
2496
3329
|
return this._recordingController.isRecording;
|
|
2497
3330
|
}
|
|
3331
|
+
pauseRecording() {
|
|
3332
|
+
this._recordingController.pauseRecording();
|
|
3333
|
+
}
|
|
3334
|
+
resumeRecording() {
|
|
3335
|
+
this._recordingController.resumeRecording();
|
|
3336
|
+
}
|
|
2498
3337
|
stopRecording() {
|
|
2499
3338
|
this._recordingController.stopRecording();
|
|
2500
3339
|
}
|
|
2501
|
-
_addRecordedClip(trackId, buf, startSample, durSamples) {
|
|
2502
|
-
addRecordedClip(this, trackId, buf, startSample, durSamples);
|
|
3340
|
+
_addRecordedClip(trackId, buf, startSample, durSamples, offsetSamples = 0) {
|
|
3341
|
+
addRecordedClip(this, trackId, buf, startSample, durSamples, offsetSamples);
|
|
2503
3342
|
}
|
|
2504
3343
|
async startRecording(stream, options) {
|
|
2505
3344
|
const s = stream ?? this.recordingStream;
|
|
@@ -2512,15 +3351,19 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2512
3351
|
_renderRecordingPreview(trackId, chH) {
|
|
2513
3352
|
const rs = this._recordingController.getSession(trackId);
|
|
2514
3353
|
if (!rs) return "";
|
|
3354
|
+
const audibleSamples = Math.max(0, rs.totalSamples - rs.latencySamples);
|
|
3355
|
+
if (audibleSamples === 0) return "";
|
|
3356
|
+
const latencyPixels = Math.floor(rs.latencySamples / this.samplesPerPixel);
|
|
2515
3357
|
const left = Math.floor(rs.startSample / this.samplesPerPixel);
|
|
2516
|
-
const w = Math.floor(
|
|
2517
|
-
return rs.peaks.map(
|
|
2518
|
-
(
|
|
3358
|
+
const w = Math.floor(audibleSamples / this.samplesPerPixel);
|
|
3359
|
+
return rs.peaks.map((chPeaks, ch) => {
|
|
3360
|
+
const slicedPeaks = latencyPixels > 0 ? chPeaks.slice(latencyPixels * 2) : chPeaks;
|
|
3361
|
+
return html7`
|
|
2519
3362
|
<daw-waveform
|
|
2520
3363
|
data-recording-track=${trackId}
|
|
2521
3364
|
data-recording-channel=${ch}
|
|
2522
3365
|
style="position:absolute;left:${left}px;top:${ch * chH}px;"
|
|
2523
|
-
.peaks=${
|
|
3366
|
+
.peaks=${slicedPeaks}
|
|
2524
3367
|
.length=${w}
|
|
2525
3368
|
.waveHeight=${chH}
|
|
2526
3369
|
.barWidth=${this.barWidth}
|
|
@@ -2529,8 +3372,8 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2529
3372
|
.visibleEnd=${this._viewport.visibleEnd}
|
|
2530
3373
|
.originX=${left}
|
|
2531
3374
|
></daw-waveform>
|
|
2532
|
-
|
|
2533
|
-
);
|
|
3375
|
+
`;
|
|
3376
|
+
});
|
|
2534
3377
|
}
|
|
2535
3378
|
// --- Playhead ---
|
|
2536
3379
|
_startPlayhead() {
|
|
@@ -2572,13 +3415,14 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2572
3415
|
const orderedTracks = this._getOrderedTracks().map(([trackId, track]) => {
|
|
2573
3416
|
const descriptor = this._tracks.get(trackId);
|
|
2574
3417
|
const firstPeaks = track.clips.map((c) => this._peaksData.get(c.id)).find((p) => p && p.data.length > 0);
|
|
2575
|
-
const
|
|
3418
|
+
const recSession = this._recordingController.getSession(trackId);
|
|
3419
|
+
const numChannels = firstPeaks ? firstPeaks.data.length : recSession ? recSession.channelCount : 1;
|
|
2576
3420
|
return {
|
|
2577
3421
|
trackId,
|
|
2578
3422
|
track,
|
|
2579
3423
|
descriptor,
|
|
2580
3424
|
numChannels,
|
|
2581
|
-
trackHeight: this.waveHeight * numChannels
|
|
3425
|
+
trackHeight: this.waveHeight * numChannels + (this.clipHeaders ? this.clipHeaderHeight : 0)
|
|
2582
3426
|
};
|
|
2583
3427
|
});
|
|
2584
3428
|
return html7`
|
|
@@ -2632,21 +3476,47 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2632
3476
|
);
|
|
2633
3477
|
const clipLeft = Math.floor(clip.startSample / this.samplesPerPixel);
|
|
2634
3478
|
const channels = peakData?.data ?? [new Int16Array(0)];
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
3479
|
+
const hdrH = this.clipHeaders ? this.clipHeaderHeight : 0;
|
|
3480
|
+
const chH = this.waveHeight;
|
|
3481
|
+
return html7` <div
|
|
3482
|
+
class="clip-container"
|
|
3483
|
+
style="left:${clipLeft}px;top:0;width:${width}px;height:${t.trackHeight}px;"
|
|
3484
|
+
data-clip-id=${clip.id}
|
|
3485
|
+
>
|
|
3486
|
+
${hdrH > 0 ? html7`<div
|
|
3487
|
+
class="clip-header"
|
|
3488
|
+
data-clip-id=${clip.id}
|
|
3489
|
+
data-track-id=${t.trackId}
|
|
3490
|
+
?data-interactive=${this.interactiveClips}
|
|
3491
|
+
>
|
|
3492
|
+
<span>${clip.name || t.descriptor?.name || ""}</span>
|
|
3493
|
+
</div>` : ""}
|
|
3494
|
+
${channels.map(
|
|
3495
|
+
(chPeaks, chIdx) => html7` <daw-waveform
|
|
3496
|
+
style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;"
|
|
3497
|
+
.peaks=${chPeaks}
|
|
3498
|
+
.length=${peakData?.length ?? width}
|
|
3499
|
+
.waveHeight=${chH}
|
|
3500
|
+
.barWidth=${this.barWidth}
|
|
3501
|
+
.barGap=${this.barGap}
|
|
3502
|
+
.visibleStart=${this._viewport.visibleStart}
|
|
3503
|
+
.visibleEnd=${this._viewport.visibleEnd}
|
|
3504
|
+
.originX=${clipLeft}
|
|
3505
|
+
></daw-waveform>`
|
|
3506
|
+
)}
|
|
3507
|
+
${this.interactiveClips ? html7` <div
|
|
3508
|
+
class="clip-boundary"
|
|
3509
|
+
data-boundary-edge="left"
|
|
3510
|
+
data-clip-id=${clip.id}
|
|
3511
|
+
data-track-id=${t.trackId}
|
|
3512
|
+
></div>
|
|
3513
|
+
<div
|
|
3514
|
+
class="clip-boundary"
|
|
3515
|
+
data-boundary-edge="right"
|
|
3516
|
+
data-clip-id=${clip.id}
|
|
3517
|
+
data-track-id=${t.trackId}
|
|
3518
|
+
></div>` : ""}
|
|
3519
|
+
</div>`;
|
|
2650
3520
|
})}
|
|
2651
3521
|
${this._renderRecordingPreview(t.trackId, channelHeight)}
|
|
2652
3522
|
</div>
|
|
@@ -2660,7 +3530,7 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2660
3530
|
};
|
|
2661
3531
|
DawEditorElement.styles = [
|
|
2662
3532
|
hostStyles,
|
|
2663
|
-
|
|
3533
|
+
css7`
|
|
2664
3534
|
:host {
|
|
2665
3535
|
display: flex;
|
|
2666
3536
|
position: relative;
|
|
@@ -2694,7 +3564,8 @@ DawEditorElement.styles = [
|
|
|
2694
3564
|
outline: 2px dashed var(--daw-selection-color, rgba(99, 199, 95, 0.3));
|
|
2695
3565
|
outline-offset: -2px;
|
|
2696
3566
|
}
|
|
2697
|
-
|
|
3567
|
+
`,
|
|
3568
|
+
clipStyles
|
|
2698
3569
|
];
|
|
2699
3570
|
DawEditorElement._CONTROL_PROPS = /* @__PURE__ */ new Set(["volume", "pan", "muted", "soloed"]);
|
|
2700
3571
|
__decorateClass([
|
|
@@ -2718,29 +3589,38 @@ __decorateClass([
|
|
|
2718
3589
|
__decorateClass([
|
|
2719
3590
|
property6({ type: Boolean, attribute: "file-drop" })
|
|
2720
3591
|
], DawEditorElement.prototype, "fileDrop", 2);
|
|
3592
|
+
__decorateClass([
|
|
3593
|
+
property6({ type: Boolean, attribute: "clip-headers" })
|
|
3594
|
+
], DawEditorElement.prototype, "clipHeaders", 2);
|
|
3595
|
+
__decorateClass([
|
|
3596
|
+
property6({ type: Number, attribute: "clip-header-height" })
|
|
3597
|
+
], DawEditorElement.prototype, "clipHeaderHeight", 2);
|
|
3598
|
+
__decorateClass([
|
|
3599
|
+
property6({ type: Boolean, attribute: "interactive-clips" })
|
|
3600
|
+
], DawEditorElement.prototype, "interactiveClips", 2);
|
|
2721
3601
|
__decorateClass([
|
|
2722
3602
|
property6({ type: Number, attribute: "sample-rate" })
|
|
2723
3603
|
], DawEditorElement.prototype, "sampleRate", 2);
|
|
2724
3604
|
__decorateClass([
|
|
2725
|
-
|
|
3605
|
+
state3()
|
|
2726
3606
|
], DawEditorElement.prototype, "_tracks", 2);
|
|
2727
3607
|
__decorateClass([
|
|
2728
|
-
|
|
3608
|
+
state3()
|
|
2729
3609
|
], DawEditorElement.prototype, "_engineTracks", 2);
|
|
2730
3610
|
__decorateClass([
|
|
2731
|
-
|
|
3611
|
+
state3()
|
|
2732
3612
|
], DawEditorElement.prototype, "_peaksData", 2);
|
|
2733
3613
|
__decorateClass([
|
|
2734
|
-
|
|
3614
|
+
state3()
|
|
2735
3615
|
], DawEditorElement.prototype, "_isPlaying", 2);
|
|
2736
3616
|
__decorateClass([
|
|
2737
|
-
|
|
3617
|
+
state3()
|
|
2738
3618
|
], DawEditorElement.prototype, "_duration", 2);
|
|
2739
3619
|
__decorateClass([
|
|
2740
|
-
|
|
3620
|
+
state3()
|
|
2741
3621
|
], DawEditorElement.prototype, "_selectedTrackId", 2);
|
|
2742
3622
|
__decorateClass([
|
|
2743
|
-
|
|
3623
|
+
state3()
|
|
2744
3624
|
], DawEditorElement.prototype, "_dragOver", 2);
|
|
2745
3625
|
__decorateClass([
|
|
2746
3626
|
property6({ attribute: "eager-resume" })
|
|
@@ -2750,7 +3630,7 @@ DawEditorElement = __decorateClass([
|
|
|
2750
3630
|
], DawEditorElement);
|
|
2751
3631
|
|
|
2752
3632
|
// src/elements/daw-ruler.ts
|
|
2753
|
-
import { LitElement as LitElement9, html as html8, css as
|
|
3633
|
+
import { LitElement as LitElement9, html as html8, css as css8 } from "lit";
|
|
2754
3634
|
import { customElement as customElement11, property as property7 } from "lit/decorators.js";
|
|
2755
3635
|
|
|
2756
3636
|
// src/utils/time-format.ts
|
|
@@ -2883,7 +3763,7 @@ var DawRulerElement = class extends LitElement9 {
|
|
|
2883
3763
|
}
|
|
2884
3764
|
}
|
|
2885
3765
|
};
|
|
2886
|
-
DawRulerElement.styles =
|
|
3766
|
+
DawRulerElement.styles = css8`
|
|
2887
3767
|
:host {
|
|
2888
3768
|
display: block;
|
|
2889
3769
|
position: relative;
|
|
@@ -2921,7 +3801,7 @@ DawRulerElement = __decorateClass([
|
|
|
2921
3801
|
], DawRulerElement);
|
|
2922
3802
|
|
|
2923
3803
|
// src/elements/daw-selection.ts
|
|
2924
|
-
import { LitElement as LitElement10, html as html9, css as
|
|
3804
|
+
import { LitElement as LitElement10, html as html9, css as css9 } from "lit";
|
|
2925
3805
|
import { customElement as customElement12, property as property8 } from "lit/decorators.js";
|
|
2926
3806
|
var DawSelectionElement = class extends LitElement10 {
|
|
2927
3807
|
constructor() {
|
|
@@ -2936,7 +3816,7 @@ var DawSelectionElement = class extends LitElement10 {
|
|
|
2936
3816
|
return html9`<div style="left: ${left}px; width: ${width}px;"></div>`;
|
|
2937
3817
|
}
|
|
2938
3818
|
};
|
|
2939
|
-
DawSelectionElement.styles =
|
|
3819
|
+
DawSelectionElement.styles = css9`
|
|
2940
3820
|
:host {
|
|
2941
3821
|
position: absolute;
|
|
2942
3822
|
top: 0;
|
|
@@ -2963,8 +3843,8 @@ DawSelectionElement = __decorateClass([
|
|
|
2963
3843
|
], DawSelectionElement);
|
|
2964
3844
|
|
|
2965
3845
|
// src/elements/daw-record-button.ts
|
|
2966
|
-
import { html as html10, css as
|
|
2967
|
-
import { customElement as customElement13, state as
|
|
3846
|
+
import { html as html10, css as css10 } from "lit";
|
|
3847
|
+
import { customElement as customElement13, state as state4 } from "lit/decorators.js";
|
|
2968
3848
|
var DawRecordButtonElement = class extends DawTransportButton {
|
|
2969
3849
|
constructor() {
|
|
2970
3850
|
super(...arguments);
|
|
@@ -2982,7 +3862,7 @@ var DawRecordButtonElement = class extends DawTransportButton {
|
|
|
2982
3862
|
}
|
|
2983
3863
|
connectedCallback() {
|
|
2984
3864
|
super.connectedCallback();
|
|
2985
|
-
this._listenToTarget();
|
|
3865
|
+
requestAnimationFrame(() => this._listenToTarget());
|
|
2986
3866
|
}
|
|
2987
3867
|
disconnectedCallback() {
|
|
2988
3868
|
super.disconnectedCallback();
|
|
@@ -3007,11 +3887,12 @@ var DawRecordButtonElement = class extends DawTransportButton {
|
|
|
3007
3887
|
render() {
|
|
3008
3888
|
return html10`
|
|
3009
3889
|
<button part="button" ?data-recording=${this._isRecording} @click=${this._onClick}>
|
|
3010
|
-
<slot
|
|
3890
|
+
<slot>Record</slot>
|
|
3011
3891
|
</button>
|
|
3012
3892
|
`;
|
|
3013
3893
|
}
|
|
3014
3894
|
_onClick() {
|
|
3895
|
+
if (this._isRecording) return;
|
|
3015
3896
|
const target = this.target;
|
|
3016
3897
|
if (!target) {
|
|
3017
3898
|
console.warn(
|
|
@@ -3019,32 +3900,201 @@ var DawRecordButtonElement = class extends DawTransportButton {
|
|
|
3019
3900
|
);
|
|
3020
3901
|
return;
|
|
3021
3902
|
}
|
|
3022
|
-
|
|
3023
|
-
target.stopRecording();
|
|
3024
|
-
} else {
|
|
3025
|
-
target.startRecording(target.recordingStream);
|
|
3026
|
-
}
|
|
3903
|
+
target.startRecording(target.recordingStream);
|
|
3027
3904
|
}
|
|
3028
3905
|
};
|
|
3029
3906
|
DawRecordButtonElement.styles = [
|
|
3030
3907
|
DawTransportButton.styles,
|
|
3031
|
-
|
|
3908
|
+
css10`
|
|
3032
3909
|
button[data-recording] {
|
|
3033
3910
|
color: #d08070;
|
|
3034
3911
|
border-color: #d08070;
|
|
3912
|
+
background: rgba(208, 128, 112, 0.15);
|
|
3035
3913
|
}
|
|
3036
3914
|
`
|
|
3037
3915
|
];
|
|
3038
3916
|
__decorateClass([
|
|
3039
|
-
|
|
3917
|
+
state4()
|
|
3040
3918
|
], DawRecordButtonElement.prototype, "_isRecording", 2);
|
|
3041
3919
|
DawRecordButtonElement = __decorateClass([
|
|
3042
3920
|
customElement13("daw-record-button")
|
|
3043
3921
|
], DawRecordButtonElement);
|
|
3922
|
+
|
|
3923
|
+
// src/elements/daw-keyboard-shortcuts.ts
|
|
3924
|
+
import { LitElement as LitElement11 } from "lit";
|
|
3925
|
+
import { customElement as customElement14, property as property9 } from "lit/decorators.js";
|
|
3926
|
+
import { handleKeyboardEvent } from "@waveform-playlist/core";
|
|
3927
|
+
var DawKeyboardShortcutsElement = class extends LitElement11 {
|
|
3928
|
+
constructor() {
|
|
3929
|
+
super(...arguments);
|
|
3930
|
+
this.playback = false;
|
|
3931
|
+
this.splitting = false;
|
|
3932
|
+
this.undo = false;
|
|
3933
|
+
// --- JS properties for remapping ---
|
|
3934
|
+
this.playbackShortcuts = null;
|
|
3935
|
+
this.splittingShortcuts = null;
|
|
3936
|
+
this.undoShortcuts = null;
|
|
3937
|
+
/** Additional custom shortcuts. */
|
|
3938
|
+
this.customShortcuts = [];
|
|
3939
|
+
this._editor = null;
|
|
3940
|
+
this._cachedShortcuts = null;
|
|
3941
|
+
// --- Event handler ---
|
|
3942
|
+
this._onKeyDown = (e) => {
|
|
3943
|
+
const shortcuts = this.shortcuts;
|
|
3944
|
+
if (shortcuts.length === 0) return;
|
|
3945
|
+
try {
|
|
3946
|
+
handleKeyboardEvent(e, shortcuts, true);
|
|
3947
|
+
} catch (err) {
|
|
3948
|
+
console.warn("[dawcore] Keyboard shortcut failed (key=" + e.key + "): " + String(err));
|
|
3949
|
+
const target = this._editor ?? this;
|
|
3950
|
+
target.dispatchEvent(
|
|
3951
|
+
new CustomEvent("daw-error", {
|
|
3952
|
+
bubbles: true,
|
|
3953
|
+
composed: true,
|
|
3954
|
+
detail: { operation: "keyboard-shortcut", key: e.key, error: err }
|
|
3955
|
+
})
|
|
3956
|
+
);
|
|
3957
|
+
}
|
|
3958
|
+
};
|
|
3959
|
+
}
|
|
3960
|
+
/** All active shortcuts (read-only, cached). */
|
|
3961
|
+
get shortcuts() {
|
|
3962
|
+
if (!this._cachedShortcuts) {
|
|
3963
|
+
this._cachedShortcuts = this._buildShortcuts();
|
|
3964
|
+
}
|
|
3965
|
+
return this._cachedShortcuts;
|
|
3966
|
+
}
|
|
3967
|
+
/** Invalidate cached shortcuts when Lit properties change. */
|
|
3968
|
+
updated() {
|
|
3969
|
+
this._cachedShortcuts = null;
|
|
3970
|
+
}
|
|
3971
|
+
// --- Lifecycle ---
|
|
3972
|
+
connectedCallback() {
|
|
3973
|
+
super.connectedCallback();
|
|
3974
|
+
this._editor = this.closest("daw-editor");
|
|
3975
|
+
if (!this._editor) {
|
|
3976
|
+
console.warn(
|
|
3977
|
+
"[dawcore] <daw-keyboard-shortcuts> must be placed inside a <daw-editor>. Preset shortcuts (playback, splitting, undo) will be inactive; only customShortcuts will fire."
|
|
3978
|
+
);
|
|
3979
|
+
}
|
|
3980
|
+
document.addEventListener("keydown", this._onKeyDown);
|
|
3981
|
+
}
|
|
3982
|
+
disconnectedCallback() {
|
|
3983
|
+
super.disconnectedCallback();
|
|
3984
|
+
document.removeEventListener("keydown", this._onKeyDown);
|
|
3985
|
+
this._editor = null;
|
|
3986
|
+
}
|
|
3987
|
+
// No shadow DOM — render-less element
|
|
3988
|
+
createRenderRoot() {
|
|
3989
|
+
return this;
|
|
3990
|
+
}
|
|
3991
|
+
// --- Shortcut building ---
|
|
3992
|
+
_buildShortcuts() {
|
|
3993
|
+
const editor = this._editor;
|
|
3994
|
+
if (!editor) return this.customShortcuts;
|
|
3995
|
+
const result = [];
|
|
3996
|
+
if (this.playback) {
|
|
3997
|
+
const map = this.playbackShortcuts;
|
|
3998
|
+
result.push(
|
|
3999
|
+
this._makeShortcut(
|
|
4000
|
+
map?.playPause ?? { key: " ", ctrlKey: false, metaKey: false },
|
|
4001
|
+
() => editor.togglePlayPause(),
|
|
4002
|
+
"Play/Pause"
|
|
4003
|
+
),
|
|
4004
|
+
this._makeShortcut(
|
|
4005
|
+
map?.stop ?? { key: "Escape", ctrlKey: false, metaKey: false },
|
|
4006
|
+
() => editor.stop(),
|
|
4007
|
+
"Stop"
|
|
4008
|
+
),
|
|
4009
|
+
this._makeShortcut(
|
|
4010
|
+
map?.rewindToStart ?? { key: "0", ctrlKey: false, metaKey: false },
|
|
4011
|
+
() => editor.seekTo(0),
|
|
4012
|
+
"Rewind to start"
|
|
4013
|
+
)
|
|
4014
|
+
);
|
|
4015
|
+
}
|
|
4016
|
+
if (this.splitting) {
|
|
4017
|
+
const map = this.splittingShortcuts;
|
|
4018
|
+
const binding = map?.splitAtPlayhead ?? {
|
|
4019
|
+
key: "s",
|
|
4020
|
+
ctrlKey: false,
|
|
4021
|
+
metaKey: false,
|
|
4022
|
+
altKey: false
|
|
4023
|
+
};
|
|
4024
|
+
result.push(this._makeShortcut(binding, () => editor.splitAtPlayhead(), "Split at playhead"));
|
|
4025
|
+
}
|
|
4026
|
+
if (this.undo) {
|
|
4027
|
+
const map = this.undoShortcuts;
|
|
4028
|
+
const undoBinding = map?.undo ?? { key: "z" };
|
|
4029
|
+
const redoBinding = map?.redo ?? { key: "z", shiftKey: true };
|
|
4030
|
+
if (undoBinding.ctrlKey === void 0 && undoBinding.metaKey === void 0) {
|
|
4031
|
+
const undoShift = undoBinding.shiftKey === void 0 ? { shiftKey: false } : {};
|
|
4032
|
+
result.push(
|
|
4033
|
+
this._makeShortcut(
|
|
4034
|
+
{ ...undoBinding, ctrlKey: true, ...undoShift },
|
|
4035
|
+
() => editor.undo(),
|
|
4036
|
+
"Undo"
|
|
4037
|
+
),
|
|
4038
|
+
this._makeShortcut(
|
|
4039
|
+
{ ...undoBinding, metaKey: true, ...undoShift },
|
|
4040
|
+
() => editor.undo(),
|
|
4041
|
+
"Undo"
|
|
4042
|
+
)
|
|
4043
|
+
);
|
|
4044
|
+
} else {
|
|
4045
|
+
result.push(this._makeShortcut(undoBinding, () => editor.undo(), "Undo"));
|
|
4046
|
+
}
|
|
4047
|
+
if (redoBinding.ctrlKey === void 0 && redoBinding.metaKey === void 0) {
|
|
4048
|
+
const redoShift = redoBinding.shiftKey === void 0 ? { shiftKey: true } : {};
|
|
4049
|
+
result.push(
|
|
4050
|
+
this._makeShortcut(
|
|
4051
|
+
{ ...redoBinding, ctrlKey: true, ...redoShift },
|
|
4052
|
+
() => editor.redo(),
|
|
4053
|
+
"Redo"
|
|
4054
|
+
),
|
|
4055
|
+
this._makeShortcut(
|
|
4056
|
+
{ ...redoBinding, metaKey: true, ...redoShift },
|
|
4057
|
+
() => editor.redo(),
|
|
4058
|
+
"Redo"
|
|
4059
|
+
)
|
|
4060
|
+
);
|
|
4061
|
+
} else {
|
|
4062
|
+
result.push(this._makeShortcut(redoBinding, () => editor.redo(), "Redo"));
|
|
4063
|
+
}
|
|
4064
|
+
}
|
|
4065
|
+
result.push(...this.customShortcuts);
|
|
4066
|
+
return result;
|
|
4067
|
+
}
|
|
4068
|
+
_makeShortcut(binding, action, description) {
|
|
4069
|
+
return {
|
|
4070
|
+
key: binding.key,
|
|
4071
|
+
...binding.ctrlKey !== void 0 && { ctrlKey: binding.ctrlKey },
|
|
4072
|
+
...binding.shiftKey !== void 0 && { shiftKey: binding.shiftKey },
|
|
4073
|
+
...binding.metaKey !== void 0 && { metaKey: binding.metaKey },
|
|
4074
|
+
...binding.altKey !== void 0 && { altKey: binding.altKey },
|
|
4075
|
+
action,
|
|
4076
|
+
description
|
|
4077
|
+
};
|
|
4078
|
+
}
|
|
4079
|
+
};
|
|
4080
|
+
__decorateClass([
|
|
4081
|
+
property9({ type: Boolean })
|
|
4082
|
+
], DawKeyboardShortcutsElement.prototype, "playback", 2);
|
|
4083
|
+
__decorateClass([
|
|
4084
|
+
property9({ type: Boolean })
|
|
4085
|
+
], DawKeyboardShortcutsElement.prototype, "splitting", 2);
|
|
4086
|
+
__decorateClass([
|
|
4087
|
+
property9({ type: Boolean })
|
|
4088
|
+
], DawKeyboardShortcutsElement.prototype, "undo", 2);
|
|
4089
|
+
DawKeyboardShortcutsElement = __decorateClass([
|
|
4090
|
+
customElement14("daw-keyboard-shortcuts")
|
|
4091
|
+
], DawKeyboardShortcutsElement);
|
|
3044
4092
|
export {
|
|
3045
4093
|
AudioResumeController,
|
|
4094
|
+
ClipPointerHandler,
|
|
3046
4095
|
DawClipElement,
|
|
3047
4096
|
DawEditorElement,
|
|
4097
|
+
DawKeyboardShortcutsElement,
|
|
3048
4098
|
DawPauseButtonElement,
|
|
3049
4099
|
DawPlayButtonElement,
|
|
3050
4100
|
DawPlayheadElement,
|
|
@@ -3057,6 +4107,7 @@ export {
|
|
|
3057
4107
|
DawTransportButton,
|
|
3058
4108
|
DawTransportElement,
|
|
3059
4109
|
DawWaveformElement,
|
|
3060
|
-
RecordingController
|
|
4110
|
+
RecordingController,
|
|
4111
|
+
splitAtPlayhead
|
|
3061
4112
|
};
|
|
3062
4113
|
//# sourceMappingURL=index.mjs.map
|