@dawcore/components 0.0.1 → 0.0.2
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 +186 -9
- package/dist/index.d.ts +186 -9
- package/dist/index.js +868 -81
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +890 -105
- 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,245 @@ 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 (mode === "trim-left" || mode === "trim-right") {
|
|
2183
|
+
const container = this._host.shadowRoot?.querySelector(
|
|
2184
|
+
`.clip-container[data-clip-id="${clipId}"]`
|
|
2185
|
+
);
|
|
2186
|
+
if (container) {
|
|
2187
|
+
this._clipContainer = container;
|
|
2188
|
+
this._originalLeft = parseFloat(container.style.left) || 0;
|
|
2189
|
+
this._originalWidth = parseFloat(container.style.width) || 0;
|
|
2190
|
+
} else {
|
|
2191
|
+
console.warn("[dawcore] clip container not found for trim visual feedback: " + clipId);
|
|
2192
|
+
}
|
|
2193
|
+
const engine = this._host.engine;
|
|
2194
|
+
if (engine) {
|
|
2195
|
+
const bounds = engine.getClipBounds(trackId, clipId);
|
|
2196
|
+
if (bounds) {
|
|
2197
|
+
this._originalOffsetSamples = bounds.offsetSamples;
|
|
2198
|
+
this._originalDurationSamples = bounds.durationSamples;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
/** Processes pointermove events during an active drag. */
|
|
2204
|
+
onPointerMove(e) {
|
|
2205
|
+
if (this._mode === null) return;
|
|
2206
|
+
const totalDeltaPx = e.clientX - this._startPx;
|
|
2207
|
+
if (!this._isDragging && Math.abs(totalDeltaPx) > DRAG_THRESHOLD) {
|
|
2208
|
+
this._isDragging = true;
|
|
2209
|
+
if (this._boundaryEl) {
|
|
2210
|
+
this._boundaryEl.classList.add("dragging");
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
if (!this._isDragging) return;
|
|
2214
|
+
const engine = this._host.engine;
|
|
2215
|
+
if (!engine) return;
|
|
2216
|
+
if (this._mode === "move") {
|
|
2217
|
+
const incrementalDeltaPx = totalDeltaPx - this._lastDeltaPx;
|
|
2218
|
+
this._lastDeltaPx = totalDeltaPx;
|
|
2219
|
+
const incrementalDeltaSamples = Math.round(incrementalDeltaPx * this._host.samplesPerPixel);
|
|
2220
|
+
this._cumulativeDeltaSamples += incrementalDeltaSamples;
|
|
2221
|
+
engine.moveClip(this._trackId, this._clipId, incrementalDeltaSamples, true);
|
|
2222
|
+
} else {
|
|
2223
|
+
const boundary = this._mode === "trim-left" ? "left" : "right";
|
|
2224
|
+
const rawDeltaSamples = Math.round(totalDeltaPx * this._host.samplesPerPixel);
|
|
2225
|
+
const deltaSamples = engine.constrainTrimDelta(
|
|
2226
|
+
this._trackId,
|
|
2227
|
+
this._clipId,
|
|
2228
|
+
boundary,
|
|
2229
|
+
rawDeltaSamples
|
|
2230
|
+
);
|
|
2231
|
+
const deltaPx = Math.round(deltaSamples / this._host.samplesPerPixel);
|
|
2232
|
+
this._cumulativeDeltaSamples = deltaSamples;
|
|
2233
|
+
if (this._clipContainer) {
|
|
2234
|
+
if (this._mode === "trim-left") {
|
|
2235
|
+
const newLeft = this._originalLeft + deltaPx;
|
|
2236
|
+
const newWidth = this._originalWidth - deltaPx;
|
|
2237
|
+
if (newWidth > 0) {
|
|
2238
|
+
this._clipContainer.style.left = newLeft + "px";
|
|
2239
|
+
this._clipContainer.style.width = newWidth + "px";
|
|
2240
|
+
const newOffset = this._originalOffsetSamples + deltaSamples;
|
|
2241
|
+
const newDuration = this._originalDurationSamples - deltaSamples;
|
|
2242
|
+
if (this._updateWaveformPeaks(newOffset, newDuration)) {
|
|
2243
|
+
const waveforms = this._clipContainer.querySelectorAll("daw-waveform");
|
|
2244
|
+
for (const wf of waveforms) {
|
|
2245
|
+
wf.style.left = "0px";
|
|
2246
|
+
}
|
|
2247
|
+
} else {
|
|
2248
|
+
const waveforms = this._clipContainer.querySelectorAll("daw-waveform");
|
|
2249
|
+
for (const wf of waveforms) {
|
|
2250
|
+
wf.style.left = -deltaPx + "px";
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
} else {
|
|
2255
|
+
const newWidth = this._originalWidth + deltaPx;
|
|
2256
|
+
if (newWidth > 0) {
|
|
2257
|
+
this._clipContainer.style.width = newWidth + "px";
|
|
2258
|
+
const newDuration = this._originalDurationSamples + deltaSamples;
|
|
2259
|
+
this._updateWaveformPeaks(this._originalOffsetSamples, newDuration);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
/** Processes pointerup events to finalize and dispatch result events. */
|
|
2266
|
+
onPointerUp(_e) {
|
|
2267
|
+
if (this._mode === null) return;
|
|
2268
|
+
try {
|
|
2269
|
+
if (!this._isDragging || this._cumulativeDeltaSamples === 0) {
|
|
2270
|
+
this._restoreTrimVisual();
|
|
2271
|
+
return;
|
|
2272
|
+
}
|
|
2273
|
+
const engine = this._host.engine;
|
|
2274
|
+
if (this._mode === "move") {
|
|
2275
|
+
if (engine) {
|
|
2276
|
+
engine.updateTrack(this._trackId);
|
|
2277
|
+
this._host.dispatchEvent(
|
|
2278
|
+
new CustomEvent("daw-clip-move", {
|
|
2279
|
+
bubbles: true,
|
|
2280
|
+
composed: true,
|
|
2281
|
+
detail: {
|
|
2282
|
+
trackId: this._trackId,
|
|
2283
|
+
clipId: this._clipId,
|
|
2284
|
+
deltaSamples: this._cumulativeDeltaSamples
|
|
2285
|
+
}
|
|
2286
|
+
})
|
|
2287
|
+
);
|
|
2288
|
+
} else {
|
|
2289
|
+
console.warn(
|
|
2290
|
+
"[dawcore] engine unavailable at move drop \u2014 audio may be out of sync for track " + this._trackId
|
|
2291
|
+
);
|
|
2292
|
+
}
|
|
2293
|
+
} else {
|
|
2294
|
+
this._restoreTrimVisual();
|
|
2295
|
+
const boundary = this._mode === "trim-left" ? "left" : "right";
|
|
2296
|
+
if (engine) {
|
|
2297
|
+
engine.trimClip(this._trackId, this._clipId, boundary, this._cumulativeDeltaSamples);
|
|
2298
|
+
this._host.dispatchEvent(
|
|
2299
|
+
new CustomEvent("daw-clip-trim", {
|
|
2300
|
+
bubbles: true,
|
|
2301
|
+
composed: true,
|
|
2302
|
+
detail: {
|
|
2303
|
+
trackId: this._trackId,
|
|
2304
|
+
clipId: this._clipId,
|
|
2305
|
+
boundary,
|
|
2306
|
+
deltaSamples: this._cumulativeDeltaSamples
|
|
2307
|
+
}
|
|
2308
|
+
})
|
|
2309
|
+
);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
} finally {
|
|
2313
|
+
this._reset();
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
/** Re-extract peaks from cache and set on waveform elements during trim drag.
|
|
2317
|
+
* Returns true if peaks were successfully updated. */
|
|
2318
|
+
_updateWaveformPeaks(offsetSamples, durationSamples) {
|
|
2319
|
+
if (!this._clipContainer || durationSamples <= 0) return false;
|
|
2320
|
+
const peakSlice = this._host.reextractClipPeaks(this._clipId, offsetSamples, durationSamples);
|
|
2321
|
+
if (!peakSlice) return false;
|
|
2322
|
+
const waveforms = this._clipContainer.querySelectorAll("daw-waveform");
|
|
2323
|
+
for (let i = 0; i < waveforms.length; i++) {
|
|
2324
|
+
const wf = waveforms[i];
|
|
2325
|
+
const channelPeaks = peakSlice.data[i];
|
|
2326
|
+
if (channelPeaks) {
|
|
2327
|
+
wf.peaks = channelPeaks;
|
|
2328
|
+
wf.length = peakSlice.length;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
return true;
|
|
2332
|
+
}
|
|
2333
|
+
/** Restore clip container CSS to original values after trim visual preview. */
|
|
2334
|
+
_restoreTrimVisual() {
|
|
2335
|
+
if (this._clipContainer) {
|
|
2336
|
+
this._clipContainer.style.left = this._originalLeft + "px";
|
|
2337
|
+
this._clipContainer.style.width = this._originalWidth + "px";
|
|
2338
|
+
const waveforms = this._clipContainer.querySelectorAll("daw-waveform");
|
|
2339
|
+
for (const wf of waveforms) {
|
|
2340
|
+
wf.style.left = "0px";
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
_reset() {
|
|
2345
|
+
if (this._boundaryEl) {
|
|
2346
|
+
this._boundaryEl.classList.remove("dragging");
|
|
2347
|
+
this._boundaryEl = null;
|
|
2348
|
+
}
|
|
2349
|
+
this._mode = null;
|
|
2350
|
+
this._clipId = "";
|
|
2351
|
+
this._trackId = "";
|
|
2352
|
+
this._startPx = 0;
|
|
2353
|
+
this._isDragging = false;
|
|
2354
|
+
this._lastDeltaPx = 0;
|
|
2355
|
+
this._cumulativeDeltaSamples = 0;
|
|
2356
|
+
this._clipContainer = null;
|
|
2357
|
+
this._originalLeft = 0;
|
|
2358
|
+
this._originalWidth = 0;
|
|
2359
|
+
this._originalOffsetSamples = 0;
|
|
2360
|
+
this._originalDurationSamples = 0;
|
|
2361
|
+
}
|
|
2362
|
+
};
|
|
2363
|
+
|
|
1856
2364
|
// src/interactions/file-loader.ts
|
|
1857
2365
|
import { createClipFromSeconds, createTrack } from "@waveform-playlist/core";
|
|
1858
2366
|
async function loadFiles(host, files) {
|
|
@@ -1887,10 +2395,16 @@ async function loadFiles(host, files) {
|
|
|
1887
2395
|
sourceDuration: audioBuffer.duration
|
|
1888
2396
|
});
|
|
1889
2397
|
host._clipBuffers = new Map(host._clipBuffers).set(clip.id, audioBuffer);
|
|
2398
|
+
host._clipOffsets.set(clip.id, {
|
|
2399
|
+
offsetSamples: clip.offsetSamples,
|
|
2400
|
+
durationSamples: clip.durationSamples
|
|
2401
|
+
});
|
|
1890
2402
|
const peakData = await host._peakPipeline.generatePeaks(
|
|
1891
2403
|
audioBuffer,
|
|
1892
2404
|
host.samplesPerPixel,
|
|
1893
|
-
host.mono
|
|
2405
|
+
host.mono,
|
|
2406
|
+
clip.offsetSamples,
|
|
2407
|
+
clip.durationSamples
|
|
1894
2408
|
);
|
|
1895
2409
|
host._peaksData = new Map(host._peaksData).set(clip.id, peakData);
|
|
1896
2410
|
const trackId = crypto.randomUUID();
|
|
@@ -1949,17 +2463,31 @@ async function loadFiles(host, files) {
|
|
|
1949
2463
|
|
|
1950
2464
|
// src/interactions/recording-clip.ts
|
|
1951
2465
|
import { createClip } from "@waveform-playlist/core";
|
|
1952
|
-
function addRecordedClip(host, trackId, buf, startSample, durSamples) {
|
|
2466
|
+
function addRecordedClip(host, trackId, buf, startSample, durSamples, offsetSamples = 0) {
|
|
2467
|
+
let trimmedBuf = buf;
|
|
2468
|
+
if (offsetSamples > 0 && offsetSamples < buf.length) {
|
|
2469
|
+
const trimmed = new AudioBuffer({
|
|
2470
|
+
numberOfChannels: buf.numberOfChannels,
|
|
2471
|
+
length: durSamples,
|
|
2472
|
+
sampleRate: buf.sampleRate
|
|
2473
|
+
});
|
|
2474
|
+
for (let ch = 0; ch < buf.numberOfChannels; ch++) {
|
|
2475
|
+
const source = buf.getChannelData(ch);
|
|
2476
|
+
trimmed.copyToChannel(source.subarray(offsetSamples, offsetSamples + durSamples), ch);
|
|
2477
|
+
}
|
|
2478
|
+
trimmedBuf = trimmed;
|
|
2479
|
+
}
|
|
1953
2480
|
const clip = createClip({
|
|
1954
|
-
audioBuffer:
|
|
2481
|
+
audioBuffer: trimmedBuf,
|
|
1955
2482
|
startSample,
|
|
1956
2483
|
durationSamples: durSamples,
|
|
1957
2484
|
offsetSamples: 0,
|
|
2485
|
+
// offset already applied by slicing
|
|
1958
2486
|
gain: 1,
|
|
1959
2487
|
name: "Recording"
|
|
1960
2488
|
});
|
|
1961
|
-
host._clipBuffers = new Map(host._clipBuffers).set(clip.id,
|
|
1962
|
-
host._peakPipeline.generatePeaks(
|
|
2489
|
+
host._clipBuffers = new Map(host._clipBuffers).set(clip.id, trimmedBuf);
|
|
2490
|
+
host._peakPipeline.generatePeaks(trimmedBuf, host.samplesPerPixel, host.mono).then((pd) => {
|
|
1963
2491
|
host._peaksData = new Map(host._peaksData).set(clip.id, pd);
|
|
1964
2492
|
const t = host._engineTracks.get(trackId);
|
|
1965
2493
|
if (!t) {
|
|
@@ -1992,7 +2520,12 @@ function addRecordedClip(host, trackId, buf, startSample, durSamples) {
|
|
|
1992
2520
|
});
|
|
1993
2521
|
}
|
|
1994
2522
|
host._recomputeDuration();
|
|
1995
|
-
host.
|
|
2523
|
+
const updatedTrack = host._engineTracks.get(trackId);
|
|
2524
|
+
if (host._engine?.updateTrack && updatedTrack) {
|
|
2525
|
+
host._engine.updateTrack(trackId, updatedTrack);
|
|
2526
|
+
} else {
|
|
2527
|
+
host._engine?.setTracks([...host._engineTracks.values()]);
|
|
2528
|
+
}
|
|
1996
2529
|
}).catch((err) => {
|
|
1997
2530
|
console.warn("[dawcore] Failed to generate peaks for recorded clip: " + String(err));
|
|
1998
2531
|
const next = new Map(host._clipBuffers);
|
|
@@ -2010,6 +2543,140 @@ function addRecordedClip(host, trackId, buf, startSample, durSamples) {
|
|
|
2010
2543
|
});
|
|
2011
2544
|
}
|
|
2012
2545
|
|
|
2546
|
+
// src/interactions/split-handler.ts
|
|
2547
|
+
function splitAtPlayhead(host) {
|
|
2548
|
+
const { engine } = host;
|
|
2549
|
+
if (!engine) return false;
|
|
2550
|
+
const stateBefore = engine.getState();
|
|
2551
|
+
const { selectedTrackId, tracks } = stateBefore;
|
|
2552
|
+
if (!selectedTrackId) return false;
|
|
2553
|
+
const track = tracks.find((t) => t.id === selectedTrackId);
|
|
2554
|
+
if (!track) return false;
|
|
2555
|
+
const atSample = Math.round(host.currentTime * host.effectiveSampleRate);
|
|
2556
|
+
const clip = findClipAtSample(track.clips, atSample);
|
|
2557
|
+
if (!clip) return false;
|
|
2558
|
+
const originalClipId = clip.id;
|
|
2559
|
+
const clipIdsBefore = new Set(track.clips.map((c) => c.id));
|
|
2560
|
+
engine.splitClip(selectedTrackId, originalClipId, atSample);
|
|
2561
|
+
const stateAfter = engine.getState();
|
|
2562
|
+
const trackAfter = stateAfter.tracks.find((t) => t.id === selectedTrackId);
|
|
2563
|
+
if (!trackAfter) {
|
|
2564
|
+
console.warn(
|
|
2565
|
+
'[dawcore] splitAtPlayhead: track "' + selectedTrackId + '" disappeared after split'
|
|
2566
|
+
);
|
|
2567
|
+
return false;
|
|
2568
|
+
}
|
|
2569
|
+
const newClips = trackAfter.clips.filter((c) => !clipIdsBefore.has(c.id));
|
|
2570
|
+
if (newClips.length !== 2) {
|
|
2571
|
+
if (newClips.length > 0) {
|
|
2572
|
+
console.warn(
|
|
2573
|
+
"[dawcore] splitAtPlayhead: expected 2 new clips after split but got " + newClips.length
|
|
2574
|
+
);
|
|
2575
|
+
}
|
|
2576
|
+
return false;
|
|
2577
|
+
}
|
|
2578
|
+
const sorted = [...newClips].sort((a, b) => a.startSample - b.startSample);
|
|
2579
|
+
const leftClipId = sorted[0].id;
|
|
2580
|
+
const rightClipId = sorted[1].id;
|
|
2581
|
+
host.dispatchEvent(
|
|
2582
|
+
new CustomEvent("daw-clip-split", {
|
|
2583
|
+
bubbles: true,
|
|
2584
|
+
composed: true,
|
|
2585
|
+
detail: {
|
|
2586
|
+
trackId: selectedTrackId,
|
|
2587
|
+
originalClipId,
|
|
2588
|
+
leftClipId,
|
|
2589
|
+
rightClipId
|
|
2590
|
+
}
|
|
2591
|
+
})
|
|
2592
|
+
);
|
|
2593
|
+
return true;
|
|
2594
|
+
}
|
|
2595
|
+
function findClipAtSample(clips, atSample) {
|
|
2596
|
+
return clips.find(
|
|
2597
|
+
(c) => atSample > c.startSample && atSample < c.startSample + c.durationSamples
|
|
2598
|
+
);
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
// src/interactions/clip-peak-sync.ts
|
|
2602
|
+
function syncPeaksForChangedClips(host, tracks) {
|
|
2603
|
+
const currentClipIds = /* @__PURE__ */ new Set();
|
|
2604
|
+
for (const track of tracks) {
|
|
2605
|
+
for (const clip of track.clips) {
|
|
2606
|
+
currentClipIds.add(clip.id);
|
|
2607
|
+
const cached = host._clipOffsets.get(clip.id);
|
|
2608
|
+
const needsPeaks = !host._peaksData.has(clip.id) || !cached || cached.offsetSamples !== clip.offsetSamples || cached.durationSamples !== clip.durationSamples;
|
|
2609
|
+
if (!needsPeaks) continue;
|
|
2610
|
+
const audioBuffer = clip.audioBuffer ?? host._clipBuffers.get(clip.id) ?? findAudioBufferForClip(host, clip, track);
|
|
2611
|
+
if (!audioBuffer) {
|
|
2612
|
+
console.warn(
|
|
2613
|
+
"[dawcore] syncPeaksForChangedClips: no AudioBuffer for clip " + clip.id + " \u2014 waveform will be blank"
|
|
2614
|
+
);
|
|
2615
|
+
continue;
|
|
2616
|
+
}
|
|
2617
|
+
host._clipBuffers = new Map(host._clipBuffers).set(clip.id, audioBuffer);
|
|
2618
|
+
host._clipOffsets.set(clip.id, {
|
|
2619
|
+
offsetSamples: clip.offsetSamples,
|
|
2620
|
+
durationSamples: clip.durationSamples
|
|
2621
|
+
});
|
|
2622
|
+
host._peakPipeline.generatePeaks(
|
|
2623
|
+
audioBuffer,
|
|
2624
|
+
host.samplesPerPixel,
|
|
2625
|
+
host.mono,
|
|
2626
|
+
clip.offsetSamples,
|
|
2627
|
+
clip.durationSamples
|
|
2628
|
+
).then((peakData) => {
|
|
2629
|
+
host._peaksData = new Map(host._peaksData).set(clip.id, peakData);
|
|
2630
|
+
}).catch((err) => {
|
|
2631
|
+
console.warn(
|
|
2632
|
+
"[dawcore] Failed to generate peaks for clip " + clip.id + ": " + String(err)
|
|
2633
|
+
);
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
cleanupOrphanedClipData(host, currentClipIds);
|
|
2638
|
+
}
|
|
2639
|
+
function cleanupOrphanedClipData(host, currentClipIds) {
|
|
2640
|
+
let buffersChanged = false;
|
|
2641
|
+
let peaksChanged = false;
|
|
2642
|
+
for (const id of host._clipBuffers.keys()) {
|
|
2643
|
+
if (!currentClipIds.has(id)) {
|
|
2644
|
+
host._clipBuffers.delete(id);
|
|
2645
|
+
buffersChanged = true;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
let offsetsChanged = false;
|
|
2649
|
+
for (const id of host._clipOffsets.keys()) {
|
|
2650
|
+
if (!currentClipIds.has(id)) {
|
|
2651
|
+
host._clipOffsets.delete(id);
|
|
2652
|
+
offsetsChanged = true;
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
for (const id of host._peaksData.keys()) {
|
|
2656
|
+
if (!currentClipIds.has(id)) {
|
|
2657
|
+
host._peaksData.delete(id);
|
|
2658
|
+
peaksChanged = true;
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
if (buffersChanged) {
|
|
2662
|
+
host._clipBuffers = new Map(host._clipBuffers);
|
|
2663
|
+
}
|
|
2664
|
+
if (offsetsChanged) {
|
|
2665
|
+
host._clipOffsets = new Map(host._clipOffsets);
|
|
2666
|
+
}
|
|
2667
|
+
if (peaksChanged) {
|
|
2668
|
+
host._peaksData = new Map(host._peaksData);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
function findAudioBufferForClip(host, clip, track) {
|
|
2672
|
+
for (const sibling of track.clips) {
|
|
2673
|
+
if (sibling.id === clip.id) continue;
|
|
2674
|
+
const buf = host._clipBuffers.get(sibling.id);
|
|
2675
|
+
if (buf) return buf;
|
|
2676
|
+
}
|
|
2677
|
+
return null;
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2013
2680
|
// src/elements/daw-editor.ts
|
|
2014
2681
|
var DawEditorElement = class extends LitElement8 {
|
|
2015
2682
|
constructor() {
|
|
@@ -2021,6 +2688,9 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2021
2688
|
this.barWidth = 1;
|
|
2022
2689
|
this.barGap = 0;
|
|
2023
2690
|
this.fileDrop = false;
|
|
2691
|
+
this.clipHeaders = false;
|
|
2692
|
+
this.clipHeaderHeight = 20;
|
|
2693
|
+
this.interactiveClips = false;
|
|
2024
2694
|
this.sampleRate = 48e3;
|
|
2025
2695
|
/** Resolved sample rate — falls back to sampleRate property until first audio decode. */
|
|
2026
2696
|
this._resolvedSampleRate = null;
|
|
@@ -2037,14 +2707,15 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2037
2707
|
this._currentTime = 0;
|
|
2038
2708
|
this._engine = null;
|
|
2039
2709
|
this._enginePromise = null;
|
|
2040
|
-
this._audioInitialized = false;
|
|
2041
2710
|
this._audioCache = /* @__PURE__ */ new Map();
|
|
2042
2711
|
this._clipBuffers = /* @__PURE__ */ new Map();
|
|
2712
|
+
this._clipOffsets = /* @__PURE__ */ new Map();
|
|
2043
2713
|
this._peakPipeline = new PeakPipeline();
|
|
2044
2714
|
this._trackElements = /* @__PURE__ */ new Map();
|
|
2045
2715
|
this._childObserver = null;
|
|
2046
2716
|
this._audioResume = new AudioResumeController(this);
|
|
2047
2717
|
this._recordingController = new RecordingController(this);
|
|
2718
|
+
this._clipPointer = new ClipPointerHandler(this);
|
|
2048
2719
|
this._pointer = new PointerHandler(this);
|
|
2049
2720
|
this._viewport = (() => {
|
|
2050
2721
|
const v = new ViewportController(this);
|
|
@@ -2145,9 +2816,42 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2145
2816
|
);
|
|
2146
2817
|
}
|
|
2147
2818
|
};
|
|
2819
|
+
this._onKeyDown = (e) => {
|
|
2820
|
+
if (!this.interactiveClips) return;
|
|
2821
|
+
if (e.key === "s" || e.key === "S") {
|
|
2822
|
+
if (e.ctrlKey || e.metaKey || e.altKey) return;
|
|
2823
|
+
const tag = e.target?.tagName;
|
|
2824
|
+
if (tag === "INPUT" || tag === "TEXTAREA") return;
|
|
2825
|
+
if (e.target?.isContentEditable) return;
|
|
2826
|
+
e.preventDefault();
|
|
2827
|
+
this.splitAtPlayhead();
|
|
2828
|
+
}
|
|
2829
|
+
};
|
|
2148
2830
|
// --- Recording ---
|
|
2149
2831
|
this.recordingStream = null;
|
|
2150
2832
|
}
|
|
2833
|
+
get _clipHandler() {
|
|
2834
|
+
return this.interactiveClips ? this._clipPointer : null;
|
|
2835
|
+
}
|
|
2836
|
+
get engine() {
|
|
2837
|
+
return this._engine;
|
|
2838
|
+
}
|
|
2839
|
+
/** Re-extract peaks for a clip at new offset/duration from cached WaveformData. */
|
|
2840
|
+
reextractClipPeaks(clipId, offsetSamples, durationSamples) {
|
|
2841
|
+
const buf = this._clipBuffers.get(clipId);
|
|
2842
|
+
if (!buf) return null;
|
|
2843
|
+
const singleClipBuffers = /* @__PURE__ */ new Map([[clipId, buf]]);
|
|
2844
|
+
const singleClipOffsets = /* @__PURE__ */ new Map([[clipId, { offsetSamples, durationSamples }]]);
|
|
2845
|
+
const result = this._peakPipeline.reextractPeaks(
|
|
2846
|
+
singleClipBuffers,
|
|
2847
|
+
this.samplesPerPixel,
|
|
2848
|
+
this.mono,
|
|
2849
|
+
singleClipOffsets
|
|
2850
|
+
);
|
|
2851
|
+
const peakData = result.get(clipId);
|
|
2852
|
+
if (!peakData) return null;
|
|
2853
|
+
return { data: peakData.data, length: peakData.length };
|
|
2854
|
+
}
|
|
2151
2855
|
get effectiveSampleRate() {
|
|
2152
2856
|
return this._resolvedSampleRate ?? this.sampleRate;
|
|
2153
2857
|
}
|
|
@@ -2188,6 +2892,10 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2188
2892
|
// --- Lifecycle ---
|
|
2189
2893
|
connectedCallback() {
|
|
2190
2894
|
super.connectedCallback();
|
|
2895
|
+
if (!this.hasAttribute("tabindex")) {
|
|
2896
|
+
this.setAttribute("tabindex", "0");
|
|
2897
|
+
}
|
|
2898
|
+
this.addEventListener("keydown", this._onKeyDown);
|
|
2191
2899
|
this.addEventListener("daw-track-connected", this._onTrackConnected);
|
|
2192
2900
|
this.addEventListener("daw-track-update", this._onTrackUpdate);
|
|
2193
2901
|
this.addEventListener("daw-track-control", this._onTrackControl);
|
|
@@ -2213,6 +2921,7 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2213
2921
|
}
|
|
2214
2922
|
disconnectedCallback() {
|
|
2215
2923
|
super.disconnectedCallback();
|
|
2924
|
+
this.removeEventListener("keydown", this._onKeyDown);
|
|
2216
2925
|
this.removeEventListener("daw-track-connected", this._onTrackConnected);
|
|
2217
2926
|
this.removeEventListener("daw-track-update", this._onTrackUpdate);
|
|
2218
2927
|
this.removeEventListener("daw-track-control", this._onTrackControl);
|
|
@@ -2222,6 +2931,7 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2222
2931
|
this._trackElements.clear();
|
|
2223
2932
|
this._audioCache.clear();
|
|
2224
2933
|
this._clipBuffers.clear();
|
|
2934
|
+
this._clipOffsets.clear();
|
|
2225
2935
|
this._peakPipeline.terminate();
|
|
2226
2936
|
try {
|
|
2227
2937
|
this._disposeEngine();
|
|
@@ -2233,17 +2943,19 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2233
2943
|
if (changedProperties.has("eagerResume")) {
|
|
2234
2944
|
this._audioResume.target = this.eagerResume;
|
|
2235
2945
|
}
|
|
2946
|
+
if (changedProperties.has("samplesPerPixel") && this._isPlaying) {
|
|
2947
|
+
this._startPlayhead();
|
|
2948
|
+
}
|
|
2236
2949
|
if (changedProperties.has("samplesPerPixel") && this._clipBuffers.size > 0) {
|
|
2237
|
-
const
|
|
2950
|
+
const re = this._peakPipeline.reextractPeaks(
|
|
2238
2951
|
this._clipBuffers,
|
|
2239
2952
|
this.samplesPerPixel,
|
|
2240
|
-
this.mono
|
|
2953
|
+
this.mono,
|
|
2954
|
+
this._clipOffsets
|
|
2241
2955
|
);
|
|
2242
|
-
if (
|
|
2956
|
+
if (re.size > 0) {
|
|
2243
2957
|
const next = new Map(this._peaksData);
|
|
2244
|
-
for (const [
|
|
2245
|
-
next.set(clipId, peakData);
|
|
2246
|
-
}
|
|
2958
|
+
for (const [id, pd] of re) next.set(id, pd);
|
|
2247
2959
|
this._peaksData = next;
|
|
2248
2960
|
}
|
|
2249
2961
|
}
|
|
@@ -2255,6 +2967,7 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2255
2967
|
const nextPeaks = new Map(this._peaksData);
|
|
2256
2968
|
for (const clip of removedTrack.clips) {
|
|
2257
2969
|
this._clipBuffers.delete(clip.id);
|
|
2970
|
+
this._clipOffsets.delete(clip.id);
|
|
2258
2971
|
nextPeaks.delete(clip.id);
|
|
2259
2972
|
}
|
|
2260
2973
|
this._peaksData = nextPeaks;
|
|
@@ -2333,10 +3046,16 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2333
3046
|
sourceDuration: audioBuffer.duration
|
|
2334
3047
|
});
|
|
2335
3048
|
this._clipBuffers = new Map(this._clipBuffers).set(clip.id, audioBuffer);
|
|
3049
|
+
this._clipOffsets.set(clip.id, {
|
|
3050
|
+
offsetSamples: clip.offsetSamples,
|
|
3051
|
+
durationSamples: clip.durationSamples
|
|
3052
|
+
});
|
|
2336
3053
|
const peakData = await this._peakPipeline.generatePeaks(
|
|
2337
3054
|
audioBuffer,
|
|
2338
3055
|
this.samplesPerPixel,
|
|
2339
|
-
this.mono
|
|
3056
|
+
this.mono,
|
|
3057
|
+
clip.offsetSamples,
|
|
3058
|
+
clip.durationSamples
|
|
2340
3059
|
);
|
|
2341
3060
|
this._peaksData = new Map(this._peaksData).set(clip.id, peakData);
|
|
2342
3061
|
clips.push(clip);
|
|
@@ -2428,10 +3147,20 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2428
3147
|
samplesPerPixel: this.samplesPerPixel,
|
|
2429
3148
|
zoomLevels: [256, 512, 1024, 2048, 4096, 8192, this.samplesPerPixel].filter((v, i, a) => a.indexOf(v) === i).sort((a, b) => a - b)
|
|
2430
3149
|
});
|
|
3150
|
+
let lastTracksVersion = -1;
|
|
2431
3151
|
engine.on("statechange", (engineState) => {
|
|
2432
3152
|
this._isPlaying = engineState.isPlaying;
|
|
2433
3153
|
this._duration = engineState.duration;
|
|
2434
3154
|
this._selectedTrackId = engineState.selectedTrackId;
|
|
3155
|
+
if (engineState.tracksVersion !== lastTracksVersion) {
|
|
3156
|
+
lastTracksVersion = engineState.tracksVersion;
|
|
3157
|
+
const nextTracks = /* @__PURE__ */ new Map();
|
|
3158
|
+
for (const track of engineState.tracks) {
|
|
3159
|
+
nextTracks.set(track.id, track);
|
|
3160
|
+
}
|
|
3161
|
+
this._engineTracks = nextTracks;
|
|
3162
|
+
syncPeaksForChangedClips(this, engineState.tracks);
|
|
3163
|
+
}
|
|
2435
3164
|
});
|
|
2436
3165
|
engine.on("timeupdate", (time) => {
|
|
2437
3166
|
this._currentTime = time;
|
|
@@ -2454,14 +3183,11 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2454
3183
|
return loadFiles(this, files);
|
|
2455
3184
|
}
|
|
2456
3185
|
// --- Playback ---
|
|
2457
|
-
async play() {
|
|
3186
|
+
async play(startTime) {
|
|
2458
3187
|
try {
|
|
2459
3188
|
const engine = await this._ensureEngine();
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
this._audioInitialized = true;
|
|
2463
|
-
}
|
|
2464
|
-
engine.play();
|
|
3189
|
+
await engine.init();
|
|
3190
|
+
engine.play(startTime);
|
|
2465
3191
|
this._startPlayhead();
|
|
2466
3192
|
this.dispatchEvent(new CustomEvent("daw-play", { bubbles: true, composed: true }));
|
|
2467
3193
|
} catch (err) {
|
|
@@ -2492,14 +3218,32 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2492
3218
|
this._engine.seek(time);
|
|
2493
3219
|
this._currentTime = time;
|
|
2494
3220
|
}
|
|
3221
|
+
/** Split the clip under the playhead on the selected track. */
|
|
3222
|
+
splitAtPlayhead() {
|
|
3223
|
+
return splitAtPlayhead({
|
|
3224
|
+
effectiveSampleRate: this.effectiveSampleRate,
|
|
3225
|
+
currentTime: this._currentTime,
|
|
3226
|
+
engine: this._engine,
|
|
3227
|
+
dispatchEvent: (e) => this.dispatchEvent(e)
|
|
3228
|
+
});
|
|
3229
|
+
}
|
|
3230
|
+
get currentTime() {
|
|
3231
|
+
return this._currentTime;
|
|
3232
|
+
}
|
|
2495
3233
|
get isRecording() {
|
|
2496
3234
|
return this._recordingController.isRecording;
|
|
2497
3235
|
}
|
|
3236
|
+
pauseRecording() {
|
|
3237
|
+
this._recordingController.pauseRecording();
|
|
3238
|
+
}
|
|
3239
|
+
resumeRecording() {
|
|
3240
|
+
this._recordingController.resumeRecording();
|
|
3241
|
+
}
|
|
2498
3242
|
stopRecording() {
|
|
2499
3243
|
this._recordingController.stopRecording();
|
|
2500
3244
|
}
|
|
2501
|
-
_addRecordedClip(trackId, buf, startSample, durSamples) {
|
|
2502
|
-
addRecordedClip(this, trackId, buf, startSample, durSamples);
|
|
3245
|
+
_addRecordedClip(trackId, buf, startSample, durSamples, offsetSamples = 0) {
|
|
3246
|
+
addRecordedClip(this, trackId, buf, startSample, durSamples, offsetSamples);
|
|
2503
3247
|
}
|
|
2504
3248
|
async startRecording(stream, options) {
|
|
2505
3249
|
const s = stream ?? this.recordingStream;
|
|
@@ -2512,15 +3256,19 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2512
3256
|
_renderRecordingPreview(trackId, chH) {
|
|
2513
3257
|
const rs = this._recordingController.getSession(trackId);
|
|
2514
3258
|
if (!rs) return "";
|
|
3259
|
+
const audibleSamples = Math.max(0, rs.totalSamples - rs.latencySamples);
|
|
3260
|
+
if (audibleSamples === 0) return "";
|
|
3261
|
+
const latencyPixels = Math.floor(rs.latencySamples / this.samplesPerPixel);
|
|
2515
3262
|
const left = Math.floor(rs.startSample / this.samplesPerPixel);
|
|
2516
|
-
const w = Math.floor(
|
|
2517
|
-
return rs.peaks.map(
|
|
2518
|
-
(
|
|
3263
|
+
const w = Math.floor(audibleSamples / this.samplesPerPixel);
|
|
3264
|
+
return rs.peaks.map((chPeaks, ch) => {
|
|
3265
|
+
const slicedPeaks = latencyPixels > 0 ? chPeaks.slice(latencyPixels * 2) : chPeaks;
|
|
3266
|
+
return html7`
|
|
2519
3267
|
<daw-waveform
|
|
2520
3268
|
data-recording-track=${trackId}
|
|
2521
3269
|
data-recording-channel=${ch}
|
|
2522
3270
|
style="position:absolute;left:${left}px;top:${ch * chH}px;"
|
|
2523
|
-
.peaks=${
|
|
3271
|
+
.peaks=${slicedPeaks}
|
|
2524
3272
|
.length=${w}
|
|
2525
3273
|
.waveHeight=${chH}
|
|
2526
3274
|
.barWidth=${this.barWidth}
|
|
@@ -2529,8 +3277,8 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2529
3277
|
.visibleEnd=${this._viewport.visibleEnd}
|
|
2530
3278
|
.originX=${left}
|
|
2531
3279
|
></daw-waveform>
|
|
2532
|
-
|
|
2533
|
-
);
|
|
3280
|
+
`;
|
|
3281
|
+
});
|
|
2534
3282
|
}
|
|
2535
3283
|
// --- Playhead ---
|
|
2536
3284
|
_startPlayhead() {
|
|
@@ -2572,13 +3320,14 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2572
3320
|
const orderedTracks = this._getOrderedTracks().map(([trackId, track]) => {
|
|
2573
3321
|
const descriptor = this._tracks.get(trackId);
|
|
2574
3322
|
const firstPeaks = track.clips.map((c) => this._peaksData.get(c.id)).find((p) => p && p.data.length > 0);
|
|
2575
|
-
const
|
|
3323
|
+
const recSession = this._recordingController.getSession(trackId);
|
|
3324
|
+
const numChannels = firstPeaks ? firstPeaks.data.length : recSession ? recSession.channelCount : 1;
|
|
2576
3325
|
return {
|
|
2577
3326
|
trackId,
|
|
2578
3327
|
track,
|
|
2579
3328
|
descriptor,
|
|
2580
3329
|
numChannels,
|
|
2581
|
-
trackHeight: this.waveHeight * numChannels
|
|
3330
|
+
trackHeight: this.waveHeight * numChannels + (this.clipHeaders ? this.clipHeaderHeight : 0)
|
|
2582
3331
|
};
|
|
2583
3332
|
});
|
|
2584
3333
|
return html7`
|
|
@@ -2632,21 +3381,47 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2632
3381
|
);
|
|
2633
3382
|
const clipLeft = Math.floor(clip.startSample / this.samplesPerPixel);
|
|
2634
3383
|
const channels = peakData?.data ?? [new Int16Array(0)];
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
3384
|
+
const hdrH = this.clipHeaders ? this.clipHeaderHeight : 0;
|
|
3385
|
+
const chH = this.waveHeight;
|
|
3386
|
+
return html7` <div
|
|
3387
|
+
class="clip-container"
|
|
3388
|
+
style="left:${clipLeft}px;top:0;width:${width}px;height:${t.trackHeight}px;"
|
|
3389
|
+
data-clip-id=${clip.id}
|
|
3390
|
+
>
|
|
3391
|
+
${hdrH > 0 ? html7`<div
|
|
3392
|
+
class="clip-header"
|
|
3393
|
+
data-clip-id=${clip.id}
|
|
3394
|
+
data-track-id=${t.trackId}
|
|
3395
|
+
?data-interactive=${this.interactiveClips}
|
|
3396
|
+
>
|
|
3397
|
+
<span>${clip.name || t.descriptor?.name || ""}</span>
|
|
3398
|
+
</div>` : ""}
|
|
3399
|
+
${channels.map(
|
|
3400
|
+
(chPeaks, chIdx) => html7` <daw-waveform
|
|
3401
|
+
style="position:absolute;left:0;top:${hdrH + chIdx * chH}px;"
|
|
3402
|
+
.peaks=${chPeaks}
|
|
3403
|
+
.length=${peakData?.length ?? width}
|
|
3404
|
+
.waveHeight=${chH}
|
|
3405
|
+
.barWidth=${this.barWidth}
|
|
3406
|
+
.barGap=${this.barGap}
|
|
3407
|
+
.visibleStart=${this._viewport.visibleStart}
|
|
3408
|
+
.visibleEnd=${this._viewport.visibleEnd}
|
|
3409
|
+
.originX=${clipLeft}
|
|
3410
|
+
></daw-waveform>`
|
|
3411
|
+
)}
|
|
3412
|
+
${this.interactiveClips ? html7` <div
|
|
3413
|
+
class="clip-boundary"
|
|
3414
|
+
data-boundary-edge="left"
|
|
3415
|
+
data-clip-id=${clip.id}
|
|
3416
|
+
data-track-id=${t.trackId}
|
|
3417
|
+
></div>
|
|
3418
|
+
<div
|
|
3419
|
+
class="clip-boundary"
|
|
3420
|
+
data-boundary-edge="right"
|
|
3421
|
+
data-clip-id=${clip.id}
|
|
3422
|
+
data-track-id=${t.trackId}
|
|
3423
|
+
></div>` : ""}
|
|
3424
|
+
</div>`;
|
|
2650
3425
|
})}
|
|
2651
3426
|
${this._renderRecordingPreview(t.trackId, channelHeight)}
|
|
2652
3427
|
</div>
|
|
@@ -2660,7 +3435,7 @@ var DawEditorElement = class extends LitElement8 {
|
|
|
2660
3435
|
};
|
|
2661
3436
|
DawEditorElement.styles = [
|
|
2662
3437
|
hostStyles,
|
|
2663
|
-
|
|
3438
|
+
css7`
|
|
2664
3439
|
:host {
|
|
2665
3440
|
display: flex;
|
|
2666
3441
|
position: relative;
|
|
@@ -2694,7 +3469,8 @@ DawEditorElement.styles = [
|
|
|
2694
3469
|
outline: 2px dashed var(--daw-selection-color, rgba(99, 199, 95, 0.3));
|
|
2695
3470
|
outline-offset: -2px;
|
|
2696
3471
|
}
|
|
2697
|
-
|
|
3472
|
+
`,
|
|
3473
|
+
clipStyles
|
|
2698
3474
|
];
|
|
2699
3475
|
DawEditorElement._CONTROL_PROPS = /* @__PURE__ */ new Set(["volume", "pan", "muted", "soloed"]);
|
|
2700
3476
|
__decorateClass([
|
|
@@ -2718,29 +3494,38 @@ __decorateClass([
|
|
|
2718
3494
|
__decorateClass([
|
|
2719
3495
|
property6({ type: Boolean, attribute: "file-drop" })
|
|
2720
3496
|
], DawEditorElement.prototype, "fileDrop", 2);
|
|
3497
|
+
__decorateClass([
|
|
3498
|
+
property6({ type: Boolean, attribute: "clip-headers" })
|
|
3499
|
+
], DawEditorElement.prototype, "clipHeaders", 2);
|
|
3500
|
+
__decorateClass([
|
|
3501
|
+
property6({ type: Number, attribute: "clip-header-height" })
|
|
3502
|
+
], DawEditorElement.prototype, "clipHeaderHeight", 2);
|
|
3503
|
+
__decorateClass([
|
|
3504
|
+
property6({ type: Boolean, attribute: "interactive-clips" })
|
|
3505
|
+
], DawEditorElement.prototype, "interactiveClips", 2);
|
|
2721
3506
|
__decorateClass([
|
|
2722
3507
|
property6({ type: Number, attribute: "sample-rate" })
|
|
2723
3508
|
], DawEditorElement.prototype, "sampleRate", 2);
|
|
2724
3509
|
__decorateClass([
|
|
2725
|
-
|
|
3510
|
+
state3()
|
|
2726
3511
|
], DawEditorElement.prototype, "_tracks", 2);
|
|
2727
3512
|
__decorateClass([
|
|
2728
|
-
|
|
3513
|
+
state3()
|
|
2729
3514
|
], DawEditorElement.prototype, "_engineTracks", 2);
|
|
2730
3515
|
__decorateClass([
|
|
2731
|
-
|
|
3516
|
+
state3()
|
|
2732
3517
|
], DawEditorElement.prototype, "_peaksData", 2);
|
|
2733
3518
|
__decorateClass([
|
|
2734
|
-
|
|
3519
|
+
state3()
|
|
2735
3520
|
], DawEditorElement.prototype, "_isPlaying", 2);
|
|
2736
3521
|
__decorateClass([
|
|
2737
|
-
|
|
3522
|
+
state3()
|
|
2738
3523
|
], DawEditorElement.prototype, "_duration", 2);
|
|
2739
3524
|
__decorateClass([
|
|
2740
|
-
|
|
3525
|
+
state3()
|
|
2741
3526
|
], DawEditorElement.prototype, "_selectedTrackId", 2);
|
|
2742
3527
|
__decorateClass([
|
|
2743
|
-
|
|
3528
|
+
state3()
|
|
2744
3529
|
], DawEditorElement.prototype, "_dragOver", 2);
|
|
2745
3530
|
__decorateClass([
|
|
2746
3531
|
property6({ attribute: "eager-resume" })
|
|
@@ -2750,7 +3535,7 @@ DawEditorElement = __decorateClass([
|
|
|
2750
3535
|
], DawEditorElement);
|
|
2751
3536
|
|
|
2752
3537
|
// src/elements/daw-ruler.ts
|
|
2753
|
-
import { LitElement as LitElement9, html as html8, css as
|
|
3538
|
+
import { LitElement as LitElement9, html as html8, css as css8 } from "lit";
|
|
2754
3539
|
import { customElement as customElement11, property as property7 } from "lit/decorators.js";
|
|
2755
3540
|
|
|
2756
3541
|
// src/utils/time-format.ts
|
|
@@ -2883,7 +3668,7 @@ var DawRulerElement = class extends LitElement9 {
|
|
|
2883
3668
|
}
|
|
2884
3669
|
}
|
|
2885
3670
|
};
|
|
2886
|
-
DawRulerElement.styles =
|
|
3671
|
+
DawRulerElement.styles = css8`
|
|
2887
3672
|
:host {
|
|
2888
3673
|
display: block;
|
|
2889
3674
|
position: relative;
|
|
@@ -2921,7 +3706,7 @@ DawRulerElement = __decorateClass([
|
|
|
2921
3706
|
], DawRulerElement);
|
|
2922
3707
|
|
|
2923
3708
|
// src/elements/daw-selection.ts
|
|
2924
|
-
import { LitElement as LitElement10, html as html9, css as
|
|
3709
|
+
import { LitElement as LitElement10, html as html9, css as css9 } from "lit";
|
|
2925
3710
|
import { customElement as customElement12, property as property8 } from "lit/decorators.js";
|
|
2926
3711
|
var DawSelectionElement = class extends LitElement10 {
|
|
2927
3712
|
constructor() {
|
|
@@ -2936,7 +3721,7 @@ var DawSelectionElement = class extends LitElement10 {
|
|
|
2936
3721
|
return html9`<div style="left: ${left}px; width: ${width}px;"></div>`;
|
|
2937
3722
|
}
|
|
2938
3723
|
};
|
|
2939
|
-
DawSelectionElement.styles =
|
|
3724
|
+
DawSelectionElement.styles = css9`
|
|
2940
3725
|
:host {
|
|
2941
3726
|
position: absolute;
|
|
2942
3727
|
top: 0;
|
|
@@ -2963,8 +3748,8 @@ DawSelectionElement = __decorateClass([
|
|
|
2963
3748
|
], DawSelectionElement);
|
|
2964
3749
|
|
|
2965
3750
|
// src/elements/daw-record-button.ts
|
|
2966
|
-
import { html as html10, css as
|
|
2967
|
-
import { customElement as customElement13, state as
|
|
3751
|
+
import { html as html10, css as css10 } from "lit";
|
|
3752
|
+
import { customElement as customElement13, state as state4 } from "lit/decorators.js";
|
|
2968
3753
|
var DawRecordButtonElement = class extends DawTransportButton {
|
|
2969
3754
|
constructor() {
|
|
2970
3755
|
super(...arguments);
|
|
@@ -2982,7 +3767,7 @@ var DawRecordButtonElement = class extends DawTransportButton {
|
|
|
2982
3767
|
}
|
|
2983
3768
|
connectedCallback() {
|
|
2984
3769
|
super.connectedCallback();
|
|
2985
|
-
this._listenToTarget();
|
|
3770
|
+
requestAnimationFrame(() => this._listenToTarget());
|
|
2986
3771
|
}
|
|
2987
3772
|
disconnectedCallback() {
|
|
2988
3773
|
super.disconnectedCallback();
|
|
@@ -3007,11 +3792,12 @@ var DawRecordButtonElement = class extends DawTransportButton {
|
|
|
3007
3792
|
render() {
|
|
3008
3793
|
return html10`
|
|
3009
3794
|
<button part="button" ?data-recording=${this._isRecording} @click=${this._onClick}>
|
|
3010
|
-
<slot
|
|
3795
|
+
<slot>Record</slot>
|
|
3011
3796
|
</button>
|
|
3012
3797
|
`;
|
|
3013
3798
|
}
|
|
3014
3799
|
_onClick() {
|
|
3800
|
+
if (this._isRecording) return;
|
|
3015
3801
|
const target = this.target;
|
|
3016
3802
|
if (!target) {
|
|
3017
3803
|
console.warn(
|
|
@@ -3019,30 +3805,28 @@ var DawRecordButtonElement = class extends DawTransportButton {
|
|
|
3019
3805
|
);
|
|
3020
3806
|
return;
|
|
3021
3807
|
}
|
|
3022
|
-
|
|
3023
|
-
target.stopRecording();
|
|
3024
|
-
} else {
|
|
3025
|
-
target.startRecording(target.recordingStream);
|
|
3026
|
-
}
|
|
3808
|
+
target.startRecording(target.recordingStream);
|
|
3027
3809
|
}
|
|
3028
3810
|
};
|
|
3029
3811
|
DawRecordButtonElement.styles = [
|
|
3030
3812
|
DawTransportButton.styles,
|
|
3031
|
-
|
|
3813
|
+
css10`
|
|
3032
3814
|
button[data-recording] {
|
|
3033
3815
|
color: #d08070;
|
|
3034
3816
|
border-color: #d08070;
|
|
3817
|
+
background: rgba(208, 128, 112, 0.15);
|
|
3035
3818
|
}
|
|
3036
3819
|
`
|
|
3037
3820
|
];
|
|
3038
3821
|
__decorateClass([
|
|
3039
|
-
|
|
3822
|
+
state4()
|
|
3040
3823
|
], DawRecordButtonElement.prototype, "_isRecording", 2);
|
|
3041
3824
|
DawRecordButtonElement = __decorateClass([
|
|
3042
3825
|
customElement13("daw-record-button")
|
|
3043
3826
|
], DawRecordButtonElement);
|
|
3044
3827
|
export {
|
|
3045
3828
|
AudioResumeController,
|
|
3829
|
+
ClipPointerHandler,
|
|
3046
3830
|
DawClipElement,
|
|
3047
3831
|
DawEditorElement,
|
|
3048
3832
|
DawPauseButtonElement,
|
|
@@ -3057,6 +3841,7 @@ export {
|
|
|
3057
3841
|
DawTransportButton,
|
|
3058
3842
|
DawTransportElement,
|
|
3059
3843
|
DawWaveformElement,
|
|
3060
|
-
RecordingController
|
|
3844
|
+
RecordingController,
|
|
3845
|
+
splitAtPlayhead
|
|
3061
3846
|
};
|
|
3062
3847
|
//# sourceMappingURL=index.mjs.map
|