@waveform-playlist/core 12.5.0 → 12.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +86 -0
- package/dist/index.d.mts +158 -1
- package/dist/index.d.ts +158 -1
- package/dist/index.js +276 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +263 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -456,6 +456,37 @@ function computeMusicalTicks(params) {
|
|
|
456
456
|
};
|
|
457
457
|
return result;
|
|
458
458
|
}
|
|
459
|
+
function ticksToBarBeat(tick, meterEntries, ppqn) {
|
|
460
|
+
if (ppqn <= 0) return { bar: 1, beat: 1 };
|
|
461
|
+
const entries = meterEntries.length > 0 ? meterEntries : [{ tick: 0, numerator: 4, denominator: 4 }];
|
|
462
|
+
let barOffset = 0;
|
|
463
|
+
for (let i = 0; i < entries.length; i++) {
|
|
464
|
+
const meter = entries[i];
|
|
465
|
+
const segmentStart = meter.tick;
|
|
466
|
+
const segmentEnd = i + 1 < entries.length ? entries[i + 1].tick : Number.MAX_SAFE_INTEGER;
|
|
467
|
+
const ts2 = [meter.numerator, meter.denominator];
|
|
468
|
+
const tpBar2 = ticksPerBar(ts2, ppqn);
|
|
469
|
+
const tpBeat2 = ticksPerBeat(ts2, ppqn);
|
|
470
|
+
if (tick >= segmentStart && tick < segmentEnd) {
|
|
471
|
+
const offset2 = tick - segmentStart;
|
|
472
|
+
const barIndexInSegment2 = Math.floor(offset2 / tpBar2);
|
|
473
|
+
const beatInBar2 = Math.floor(offset2 % tpBar2 / tpBeat2);
|
|
474
|
+
return { bar: barOffset + barIndexInSegment2 + 1, beat: beatInBar2 + 1 };
|
|
475
|
+
}
|
|
476
|
+
if (segmentEnd !== Number.MAX_SAFE_INTEGER) {
|
|
477
|
+
const segmentLen = segmentEnd - segmentStart;
|
|
478
|
+
barOffset += Math.floor(segmentLen / tpBar2);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const first = entries[0];
|
|
482
|
+
const ts = [first.numerator, first.denominator];
|
|
483
|
+
const tpBar = ticksPerBar(ts, ppqn);
|
|
484
|
+
const tpBeat = ticksPerBeat(ts, ppqn);
|
|
485
|
+
const offset = tick - first.tick;
|
|
486
|
+
const barIndexInSegment = Math.floor(offset / tpBar);
|
|
487
|
+
const beatInBar = Math.floor(offset % tpBar / tpBeat);
|
|
488
|
+
return { bar: barIndexInSegment + 1, beat: beatInBar + 1 };
|
|
489
|
+
}
|
|
459
490
|
function snapTickToGrid(tick, snapTo, meterEntries, ppqn = 960) {
|
|
460
491
|
if (snapTo === "off") return tick;
|
|
461
492
|
let meter = meterEntries[0] ?? { tick: 0, numerator: 4, denominator: 4 };
|
|
@@ -635,6 +666,44 @@ var SPECTROGRAM_DEFAULTS = {
|
|
|
635
666
|
};
|
|
636
667
|
var DEFAULT_SPECTROGRAM_COLOR_MAP = "viridis";
|
|
637
668
|
|
|
669
|
+
// src/utils/carveClipRange.ts
|
|
670
|
+
function carveClipRange(clips, rangeStart, rangeEnd) {
|
|
671
|
+
if (!(rangeEnd > rangeStart)) {
|
|
672
|
+
return [...clips];
|
|
673
|
+
}
|
|
674
|
+
const result = [];
|
|
675
|
+
for (const clip of clips) {
|
|
676
|
+
const clipStart = clip.startSample;
|
|
677
|
+
const clipEnd = clip.startSample + clip.durationSamples;
|
|
678
|
+
if (clipEnd <= rangeStart || clipStart >= rangeEnd) {
|
|
679
|
+
result.push(clip);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
const keepHead = clipStart < rangeStart;
|
|
683
|
+
const keepTail = clipEnd > rangeEnd;
|
|
684
|
+
if (keepHead) {
|
|
685
|
+
result.push({
|
|
686
|
+
...clip,
|
|
687
|
+
durationSamples: rangeStart - clipStart
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
if (keepTail) {
|
|
691
|
+
const carvedFromClipStart = rangeEnd - clipStart;
|
|
692
|
+
const tail = {
|
|
693
|
+
...clip,
|
|
694
|
+
// Head + tail from one clip must not share an id
|
|
695
|
+
id: keepHead ? `${clip.id}-carve-${rangeEnd}` : clip.id,
|
|
696
|
+
startSample: rangeEnd,
|
|
697
|
+
durationSamples: clipEnd - rangeEnd,
|
|
698
|
+
offsetSamples: clip.offsetSamples + carvedFromClipStart
|
|
699
|
+
};
|
|
700
|
+
delete tail.startTick;
|
|
701
|
+
result.push(tail);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return result;
|
|
705
|
+
}
|
|
706
|
+
|
|
638
707
|
// src/clipTimeHelpers.ts
|
|
639
708
|
function clipStartTime(clip) {
|
|
640
709
|
return clip.startSample / clip.sampleRate;
|
|
@@ -762,6 +831,14 @@ function applyFadeOut(param, startTime, duration, type = "linear", startValue =
|
|
|
762
831
|
}
|
|
763
832
|
|
|
764
833
|
// src/keyboard.ts
|
|
834
|
+
function matchesKeyBinding(event, binding) {
|
|
835
|
+
const keyMatch = event.key.toLowerCase() === binding.key.toLowerCase() || event.key === binding.key;
|
|
836
|
+
const ctrlMatch = binding.ctrlKey === void 0 || event.ctrlKey === binding.ctrlKey;
|
|
837
|
+
const shiftMatch = binding.shiftKey === void 0 || event.shiftKey === binding.shiftKey;
|
|
838
|
+
const metaMatch = binding.metaKey === void 0 || event.metaKey === binding.metaKey;
|
|
839
|
+
const altMatch = binding.altKey === void 0 || event.altKey === binding.altKey;
|
|
840
|
+
return keyMatch && ctrlMatch && shiftMatch && metaMatch && altMatch;
|
|
841
|
+
}
|
|
765
842
|
function handleKeyboardEvent(event, shortcuts, enabled) {
|
|
766
843
|
if (!enabled) return;
|
|
767
844
|
if (event.repeat) return;
|
|
@@ -769,14 +846,7 @@ function handleKeyboardEvent(event, shortcuts, enabled) {
|
|
|
769
846
|
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) {
|
|
770
847
|
return;
|
|
771
848
|
}
|
|
772
|
-
const matchingShortcut = shortcuts.find((shortcut) =>
|
|
773
|
-
const keyMatch = event.key.toLowerCase() === shortcut.key.toLowerCase() || event.key === shortcut.key;
|
|
774
|
-
const ctrlMatch = shortcut.ctrlKey === void 0 || event.ctrlKey === shortcut.ctrlKey;
|
|
775
|
-
const shiftMatch = shortcut.shiftKey === void 0 || event.shiftKey === shortcut.shiftKey;
|
|
776
|
-
const metaMatch = shortcut.metaKey === void 0 || event.metaKey === shortcut.metaKey;
|
|
777
|
-
const altMatch = shortcut.altKey === void 0 || event.altKey === shortcut.altKey;
|
|
778
|
-
return keyMatch && ctrlMatch && shiftMatch && metaMatch && altMatch;
|
|
779
|
-
});
|
|
849
|
+
const matchingShortcut = shortcuts.find((shortcut) => matchesKeyBinding(event, shortcut));
|
|
780
850
|
if (matchingShortcut) {
|
|
781
851
|
if (matchingShortcut.preventDefault !== false) {
|
|
782
852
|
event.preventDefault();
|
|
@@ -824,13 +894,190 @@ async function probeRangeSupport(url, fetchImpl = fetch) {
|
|
|
824
894
|
controller.abort();
|
|
825
895
|
}
|
|
826
896
|
}
|
|
897
|
+
|
|
898
|
+
// src/enumerateMicrophones.ts
|
|
899
|
+
var UNSUPPORTED = Object.freeze({
|
|
900
|
+
supported: false,
|
|
901
|
+
hasLabels: false,
|
|
902
|
+
devices: []
|
|
903
|
+
});
|
|
904
|
+
function resolveMediaDevices(mediaDevices) {
|
|
905
|
+
if (mediaDevices) return mediaDevices;
|
|
906
|
+
return typeof navigator !== "undefined" ? navigator.mediaDevices : void 0;
|
|
907
|
+
}
|
|
908
|
+
async function enumerateMicrophones(mediaDevices) {
|
|
909
|
+
const md = resolveMediaDevices(mediaDevices);
|
|
910
|
+
if (!md || typeof md.enumerateDevices !== "function") {
|
|
911
|
+
return UNSUPPORTED;
|
|
912
|
+
}
|
|
913
|
+
const all = await md.enumerateDevices();
|
|
914
|
+
const inputs = all.filter((device) => device.kind === "audioinput");
|
|
915
|
+
const hasLabels = inputs.some((device) => device.label.length > 0);
|
|
916
|
+
return {
|
|
917
|
+
supported: true,
|
|
918
|
+
hasLabels,
|
|
919
|
+
devices: inputs.map((device, index) => ({
|
|
920
|
+
deviceId: device.deviceId,
|
|
921
|
+
label: device.label || // Pre-permission fallbacks: Chrome redacts deviceId to '' as well,
|
|
922
|
+
// so fall through to a positional name when there's nothing to slice.
|
|
923
|
+
(device.deviceId ? `Microphone ${device.deviceId.slice(0, 8)}` : `Microphone ${index + 1}`),
|
|
924
|
+
groupId: device.groupId
|
|
925
|
+
}))
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
function watchMicrophoneDevices(listener, mediaDevices) {
|
|
929
|
+
const md = resolveMediaDevices(mediaDevices);
|
|
930
|
+
if (!md || typeof md.enumerateDevices !== "function") {
|
|
931
|
+
queueMicrotask(() => listener(UNSUPPORTED));
|
|
932
|
+
return () => {
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
let active = true;
|
|
936
|
+
const deliver = () => {
|
|
937
|
+
enumerateMicrophones(md).then((result) => {
|
|
938
|
+
if (active) listener(result);
|
|
939
|
+
}).catch((err) => {
|
|
940
|
+
console.warn("[waveform-playlist] Microphone enumeration failed:", String(err));
|
|
941
|
+
});
|
|
942
|
+
};
|
|
943
|
+
md.addEventListener("devicechange", deliver);
|
|
944
|
+
deliver();
|
|
945
|
+
return () => {
|
|
946
|
+
active = false;
|
|
947
|
+
md.removeEventListener("devicechange", deliver);
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// src/annotations/boundaries.ts
|
|
952
|
+
var LINK_THRESHOLD = 0.01;
|
|
953
|
+
var MIN_ANNOTATION_DURATION = 0.1;
|
|
954
|
+
var ANNOTATION_LINK_THRESHOLD_TICKS = 0.5;
|
|
955
|
+
function annotationMinDurationTicks(ppqn) {
|
|
956
|
+
return Math.max(1, Math.round(ppqn / 32));
|
|
957
|
+
}
|
|
958
|
+
function updateAnnotationBoundaries(params, options = {}) {
|
|
959
|
+
const { annotationIndex, newTime, isDraggingStart, annotations, duration, linkEndpoints } = params;
|
|
960
|
+
const linkThreshold = options.linkThreshold ?? LINK_THRESHOLD;
|
|
961
|
+
const minDuration = options.minDuration ?? MIN_ANNOTATION_DURATION;
|
|
962
|
+
const updatedAnnotations = [...annotations];
|
|
963
|
+
const annotation = annotations[annotationIndex];
|
|
964
|
+
if (isDraggingStart) {
|
|
965
|
+
const constrainedStart = Math.min(annotation.end - minDuration, Math.max(0, newTime));
|
|
966
|
+
const delta = constrainedStart - annotation.start;
|
|
967
|
+
updatedAnnotations[annotationIndex] = { ...annotation, start: constrainedStart };
|
|
968
|
+
if (linkEndpoints && annotationIndex > 0) {
|
|
969
|
+
const prevAnnotation = updatedAnnotations[annotationIndex - 1];
|
|
970
|
+
if (Math.abs(prevAnnotation.end - annotation.start) < linkThreshold) {
|
|
971
|
+
updatedAnnotations[annotationIndex - 1] = {
|
|
972
|
+
...prevAnnotation,
|
|
973
|
+
end: Math.max(prevAnnotation.start + minDuration, prevAnnotation.end + delta)
|
|
974
|
+
};
|
|
975
|
+
} else if (constrainedStart <= prevAnnotation.end) {
|
|
976
|
+
updatedAnnotations[annotationIndex] = {
|
|
977
|
+
...updatedAnnotations[annotationIndex],
|
|
978
|
+
start: prevAnnotation.end
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
} else if (!linkEndpoints && annotationIndex > 0 && constrainedStart < updatedAnnotations[annotationIndex - 1].end) {
|
|
982
|
+
updatedAnnotations[annotationIndex - 1] = {
|
|
983
|
+
...updatedAnnotations[annotationIndex - 1],
|
|
984
|
+
end: constrainedStart
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
} else {
|
|
988
|
+
const constrainedEnd = Math.max(annotation.start + minDuration, Math.min(newTime, duration));
|
|
989
|
+
const delta = constrainedEnd - annotation.end;
|
|
990
|
+
updatedAnnotations[annotationIndex] = { ...annotation, end: constrainedEnd };
|
|
991
|
+
if (linkEndpoints && annotationIndex < updatedAnnotations.length - 1) {
|
|
992
|
+
const nextAnnotation = updatedAnnotations[annotationIndex + 1];
|
|
993
|
+
if (Math.abs(nextAnnotation.start - annotation.end) < linkThreshold) {
|
|
994
|
+
const newStart = nextAnnotation.start + delta;
|
|
995
|
+
updatedAnnotations[annotationIndex + 1] = {
|
|
996
|
+
...nextAnnotation,
|
|
997
|
+
start: Math.min(nextAnnotation.end - minDuration, newStart)
|
|
998
|
+
};
|
|
999
|
+
let currentIndex = annotationIndex + 1;
|
|
1000
|
+
while (currentIndex < updatedAnnotations.length - 1) {
|
|
1001
|
+
const current = updatedAnnotations[currentIndex];
|
|
1002
|
+
const next = updatedAnnotations[currentIndex + 1];
|
|
1003
|
+
if (Math.abs(next.start - current.end) < linkThreshold) {
|
|
1004
|
+
const nextDelta = current.end - annotations[currentIndex].end;
|
|
1005
|
+
updatedAnnotations[currentIndex + 1] = {
|
|
1006
|
+
...next,
|
|
1007
|
+
start: Math.min(next.end - minDuration, next.start + nextDelta)
|
|
1008
|
+
};
|
|
1009
|
+
currentIndex++;
|
|
1010
|
+
} else {
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
} else if (constrainedEnd >= nextAnnotation.start) {
|
|
1015
|
+
updatedAnnotations[annotationIndex] = {
|
|
1016
|
+
...updatedAnnotations[annotationIndex],
|
|
1017
|
+
end: nextAnnotation.start
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
} else if (!linkEndpoints && annotationIndex < updatedAnnotations.length - 1 && constrainedEnd > updatedAnnotations[annotationIndex + 1].start) {
|
|
1021
|
+
const nextAnnotation = updatedAnnotations[annotationIndex + 1];
|
|
1022
|
+
updatedAnnotations[annotationIndex + 1] = { ...nextAnnotation, start: constrainedEnd };
|
|
1023
|
+
let currentIndex = annotationIndex + 1;
|
|
1024
|
+
while (currentIndex < updatedAnnotations.length - 1) {
|
|
1025
|
+
const current = updatedAnnotations[currentIndex];
|
|
1026
|
+
const next = updatedAnnotations[currentIndex + 1];
|
|
1027
|
+
if (current.end > next.start) {
|
|
1028
|
+
updatedAnnotations[currentIndex + 1] = { ...next, start: current.end };
|
|
1029
|
+
currentIndex++;
|
|
1030
|
+
} else {
|
|
1031
|
+
break;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return updatedAnnotations;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// src/annotations/shortcuts.ts
|
|
1040
|
+
var noMods = { ctrlKey: false, metaKey: false };
|
|
1041
|
+
var DEFAULT_ANNOTATION_SHORTCUTS = {
|
|
1042
|
+
selectPrevious: [
|
|
1043
|
+
{ key: "ArrowUp", ...noMods },
|
|
1044
|
+
{ key: "ArrowLeft", ...noMods }
|
|
1045
|
+
],
|
|
1046
|
+
selectNext: [
|
|
1047
|
+
{ key: "ArrowDown", ...noMods },
|
|
1048
|
+
{ key: "ArrowRight", ...noMods }
|
|
1049
|
+
],
|
|
1050
|
+
selectFirst: [{ key: "Home", ...noMods }],
|
|
1051
|
+
selectLast: [{ key: "End", ...noMods }],
|
|
1052
|
+
clearSelection: [{ key: "Escape", ...noMods }],
|
|
1053
|
+
moveStartEarlier: [{ key: "[", ...noMods }],
|
|
1054
|
+
moveStartLater: [{ key: "]", ...noMods }],
|
|
1055
|
+
// '{' / '}' are what event.key reports for Shift+[ / Shift+] — no explicit
|
|
1056
|
+
// shiftKey needed; the key value itself encodes it.
|
|
1057
|
+
moveEndEarlier: [{ key: "{", ...noMods }],
|
|
1058
|
+
moveEndLater: [{ key: "}", ...noMods }],
|
|
1059
|
+
playActive: [{ key: "Enter", ...noMods }]
|
|
1060
|
+
};
|
|
1061
|
+
var ALL_ACTIONS = Object.keys(DEFAULT_ANNOTATION_SHORTCUTS);
|
|
1062
|
+
function resolveAnnotationShortcuts(remap) {
|
|
1063
|
+
return ALL_ACTIONS.flatMap((action) => {
|
|
1064
|
+
const override = remap?.[action];
|
|
1065
|
+
const bindings = override ? [override] : DEFAULT_ANNOTATION_SHORTCUTS[action];
|
|
1066
|
+
return bindings.map((binding) => ({ action, binding }));
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
827
1069
|
export {
|
|
1070
|
+
ANNOTATION_LINK_THRESHOLD_TICKS,
|
|
1071
|
+
DEFAULT_ANNOTATION_SHORTCUTS,
|
|
828
1072
|
DEFAULT_SPECTROGRAM_COLOR_MAP,
|
|
829
1073
|
InteractionState,
|
|
1074
|
+
LINK_THRESHOLD,
|
|
830
1075
|
MAX_CANVAS_WIDTH,
|
|
1076
|
+
MIN_ANNOTATION_DURATION,
|
|
831
1077
|
MIN_PIXELS_PER_UNIT,
|
|
832
1078
|
PPQN,
|
|
833
1079
|
SPECTROGRAM_DEFAULTS,
|
|
1080
|
+
annotationMinDurationTicks,
|
|
834
1081
|
appendPeaks,
|
|
835
1082
|
appendToAudioBuffer,
|
|
836
1083
|
applyFadeIn,
|
|
@@ -838,6 +1085,7 @@ export {
|
|
|
838
1085
|
audibleLatencySamples,
|
|
839
1086
|
buildSpectrogramCanvasId,
|
|
840
1087
|
calculateDuration,
|
|
1088
|
+
carveClipRange,
|
|
841
1089
|
clipDurationTime,
|
|
842
1090
|
clipEndTime,
|
|
843
1091
|
clipOffsetTime,
|
|
@@ -854,6 +1102,7 @@ export {
|
|
|
854
1102
|
createTrack,
|
|
855
1103
|
dBToNormalized,
|
|
856
1104
|
detectMeterChanges,
|
|
1105
|
+
enumerateMicrophones,
|
|
857
1106
|
exponentialCurve,
|
|
858
1107
|
findGaps,
|
|
859
1108
|
gainToDb,
|
|
@@ -866,11 +1115,13 @@ export {
|
|
|
866
1115
|
handleKeyboardEvent,
|
|
867
1116
|
linearCurve,
|
|
868
1117
|
logarithmicCurve,
|
|
1118
|
+
matchesKeyBinding,
|
|
869
1119
|
normalizedToDb,
|
|
870
1120
|
parseSpectrogramCanvasId,
|
|
871
1121
|
pixelsToSamples,
|
|
872
1122
|
pixelsToSeconds,
|
|
873
1123
|
probeRangeSupport,
|
|
1124
|
+
resolveAnnotationShortcuts,
|
|
874
1125
|
resolveRecordingOffsetSamples,
|
|
875
1126
|
sCurveCurve,
|
|
876
1127
|
samplesToPixels,
|
|
@@ -884,7 +1135,10 @@ export {
|
|
|
884
1135
|
sortClipsByTime,
|
|
885
1136
|
ticksPerBar,
|
|
886
1137
|
ticksPerBeat,
|
|
1138
|
+
ticksToBarBeat,
|
|
887
1139
|
ticksToSamples,
|
|
888
|
-
trackChannelCount
|
|
1140
|
+
trackChannelCount,
|
|
1141
|
+
updateAnnotationBoundaries,
|
|
1142
|
+
watchMicrophoneDevices
|
|
889
1143
|
};
|
|
890
1144
|
//# sourceMappingURL=index.mjs.map
|