@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/dist/index.js CHANGED
@@ -20,12 +20,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ANNOTATION_LINK_THRESHOLD_TICKS: () => ANNOTATION_LINK_THRESHOLD_TICKS,
24
+ DEFAULT_ANNOTATION_SHORTCUTS: () => DEFAULT_ANNOTATION_SHORTCUTS,
23
25
  DEFAULT_SPECTROGRAM_COLOR_MAP: () => DEFAULT_SPECTROGRAM_COLOR_MAP,
24
26
  InteractionState: () => InteractionState,
27
+ LINK_THRESHOLD: () => LINK_THRESHOLD,
25
28
  MAX_CANVAS_WIDTH: () => MAX_CANVAS_WIDTH,
29
+ MIN_ANNOTATION_DURATION: () => MIN_ANNOTATION_DURATION,
26
30
  MIN_PIXELS_PER_UNIT: () => MIN_PIXELS_PER_UNIT,
27
31
  PPQN: () => PPQN,
28
32
  SPECTROGRAM_DEFAULTS: () => SPECTROGRAM_DEFAULTS,
33
+ annotationMinDurationTicks: () => annotationMinDurationTicks,
29
34
  appendPeaks: () => appendPeaks,
30
35
  appendToAudioBuffer: () => appendToAudioBuffer,
31
36
  applyFadeIn: () => applyFadeIn,
@@ -33,6 +38,7 @@ __export(index_exports, {
33
38
  audibleLatencySamples: () => audibleLatencySamples,
34
39
  buildSpectrogramCanvasId: () => buildSpectrogramCanvasId,
35
40
  calculateDuration: () => calculateDuration,
41
+ carveClipRange: () => carveClipRange,
36
42
  clipDurationTime: () => clipDurationTime,
37
43
  clipEndTime: () => clipEndTime,
38
44
  clipOffsetTime: () => clipOffsetTime,
@@ -49,6 +55,7 @@ __export(index_exports, {
49
55
  createTrack: () => createTrack,
50
56
  dBToNormalized: () => dBToNormalized,
51
57
  detectMeterChanges: () => detectMeterChanges,
58
+ enumerateMicrophones: () => enumerateMicrophones,
52
59
  exponentialCurve: () => exponentialCurve,
53
60
  findGaps: () => findGaps,
54
61
  gainToDb: () => gainToDb,
@@ -61,11 +68,13 @@ __export(index_exports, {
61
68
  handleKeyboardEvent: () => handleKeyboardEvent,
62
69
  linearCurve: () => linearCurve,
63
70
  logarithmicCurve: () => logarithmicCurve,
71
+ matchesKeyBinding: () => matchesKeyBinding,
64
72
  normalizedToDb: () => normalizedToDb,
65
73
  parseSpectrogramCanvasId: () => parseSpectrogramCanvasId,
66
74
  pixelsToSamples: () => pixelsToSamples,
67
75
  pixelsToSeconds: () => pixelsToSeconds,
68
76
  probeRangeSupport: () => probeRangeSupport,
77
+ resolveAnnotationShortcuts: () => resolveAnnotationShortcuts,
69
78
  resolveRecordingOffsetSamples: () => resolveRecordingOffsetSamples,
70
79
  sCurveCurve: () => sCurveCurve,
71
80
  samplesToPixels: () => samplesToPixels,
@@ -79,8 +88,11 @@ __export(index_exports, {
79
88
  sortClipsByTime: () => sortClipsByTime,
80
89
  ticksPerBar: () => ticksPerBar,
81
90
  ticksPerBeat: () => ticksPerBeat,
91
+ ticksToBarBeat: () => ticksToBarBeat,
82
92
  ticksToSamples: () => ticksToSamples,
83
- trackChannelCount: () => trackChannelCount
93
+ trackChannelCount: () => trackChannelCount,
94
+ updateAnnotationBoundaries: () => updateAnnotationBoundaries,
95
+ watchMicrophoneDevices: () => watchMicrophoneDevices
84
96
  });
85
97
  module.exports = __toCommonJS(index_exports);
86
98
 
@@ -542,6 +554,37 @@ function computeMusicalTicks(params) {
542
554
  };
543
555
  return result;
544
556
  }
557
+ function ticksToBarBeat(tick, meterEntries, ppqn) {
558
+ if (ppqn <= 0) return { bar: 1, beat: 1 };
559
+ const entries = meterEntries.length > 0 ? meterEntries : [{ tick: 0, numerator: 4, denominator: 4 }];
560
+ let barOffset = 0;
561
+ for (let i = 0; i < entries.length; i++) {
562
+ const meter = entries[i];
563
+ const segmentStart = meter.tick;
564
+ const segmentEnd = i + 1 < entries.length ? entries[i + 1].tick : Number.MAX_SAFE_INTEGER;
565
+ const ts2 = [meter.numerator, meter.denominator];
566
+ const tpBar2 = ticksPerBar(ts2, ppqn);
567
+ const tpBeat2 = ticksPerBeat(ts2, ppqn);
568
+ if (tick >= segmentStart && tick < segmentEnd) {
569
+ const offset2 = tick - segmentStart;
570
+ const barIndexInSegment2 = Math.floor(offset2 / tpBar2);
571
+ const beatInBar2 = Math.floor(offset2 % tpBar2 / tpBeat2);
572
+ return { bar: barOffset + barIndexInSegment2 + 1, beat: beatInBar2 + 1 };
573
+ }
574
+ if (segmentEnd !== Number.MAX_SAFE_INTEGER) {
575
+ const segmentLen = segmentEnd - segmentStart;
576
+ barOffset += Math.floor(segmentLen / tpBar2);
577
+ }
578
+ }
579
+ const first = entries[0];
580
+ const ts = [first.numerator, first.denominator];
581
+ const tpBar = ticksPerBar(ts, ppqn);
582
+ const tpBeat = ticksPerBeat(ts, ppqn);
583
+ const offset = tick - first.tick;
584
+ const barIndexInSegment = Math.floor(offset / tpBar);
585
+ const beatInBar = Math.floor(offset % tpBar / tpBeat);
586
+ return { bar: barIndexInSegment + 1, beat: beatInBar + 1 };
587
+ }
545
588
  function snapTickToGrid(tick, snapTo, meterEntries, ppqn = 960) {
546
589
  if (snapTo === "off") return tick;
547
590
  let meter = meterEntries[0] ?? { tick: 0, numerator: 4, denominator: 4 };
@@ -721,6 +764,44 @@ var SPECTROGRAM_DEFAULTS = {
721
764
  };
722
765
  var DEFAULT_SPECTROGRAM_COLOR_MAP = "viridis";
723
766
 
767
+ // src/utils/carveClipRange.ts
768
+ function carveClipRange(clips, rangeStart, rangeEnd) {
769
+ if (!(rangeEnd > rangeStart)) {
770
+ return [...clips];
771
+ }
772
+ const result = [];
773
+ for (const clip of clips) {
774
+ const clipStart = clip.startSample;
775
+ const clipEnd = clip.startSample + clip.durationSamples;
776
+ if (clipEnd <= rangeStart || clipStart >= rangeEnd) {
777
+ result.push(clip);
778
+ continue;
779
+ }
780
+ const keepHead = clipStart < rangeStart;
781
+ const keepTail = clipEnd > rangeEnd;
782
+ if (keepHead) {
783
+ result.push({
784
+ ...clip,
785
+ durationSamples: rangeStart - clipStart
786
+ });
787
+ }
788
+ if (keepTail) {
789
+ const carvedFromClipStart = rangeEnd - clipStart;
790
+ const tail = {
791
+ ...clip,
792
+ // Head + tail from one clip must not share an id
793
+ id: keepHead ? `${clip.id}-carve-${rangeEnd}` : clip.id,
794
+ startSample: rangeEnd,
795
+ durationSamples: clipEnd - rangeEnd,
796
+ offsetSamples: clip.offsetSamples + carvedFromClipStart
797
+ };
798
+ delete tail.startTick;
799
+ result.push(tail);
800
+ }
801
+ }
802
+ return result;
803
+ }
804
+
724
805
  // src/clipTimeHelpers.ts
725
806
  function clipStartTime(clip) {
726
807
  return clip.startSample / clip.sampleRate;
@@ -848,6 +929,14 @@ function applyFadeOut(param, startTime, duration, type = "linear", startValue =
848
929
  }
849
930
 
850
931
  // src/keyboard.ts
932
+ function matchesKeyBinding(event, binding) {
933
+ const keyMatch = event.key.toLowerCase() === binding.key.toLowerCase() || event.key === binding.key;
934
+ const ctrlMatch = binding.ctrlKey === void 0 || event.ctrlKey === binding.ctrlKey;
935
+ const shiftMatch = binding.shiftKey === void 0 || event.shiftKey === binding.shiftKey;
936
+ const metaMatch = binding.metaKey === void 0 || event.metaKey === binding.metaKey;
937
+ const altMatch = binding.altKey === void 0 || event.altKey === binding.altKey;
938
+ return keyMatch && ctrlMatch && shiftMatch && metaMatch && altMatch;
939
+ }
851
940
  function handleKeyboardEvent(event, shortcuts, enabled) {
852
941
  if (!enabled) return;
853
942
  if (event.repeat) return;
@@ -855,14 +944,7 @@ function handleKeyboardEvent(event, shortcuts, enabled) {
855
944
  if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) {
856
945
  return;
857
946
  }
858
- const matchingShortcut = shortcuts.find((shortcut) => {
859
- const keyMatch = event.key.toLowerCase() === shortcut.key.toLowerCase() || event.key === shortcut.key;
860
- const ctrlMatch = shortcut.ctrlKey === void 0 || event.ctrlKey === shortcut.ctrlKey;
861
- const shiftMatch = shortcut.shiftKey === void 0 || event.shiftKey === shortcut.shiftKey;
862
- const metaMatch = shortcut.metaKey === void 0 || event.metaKey === shortcut.metaKey;
863
- const altMatch = shortcut.altKey === void 0 || event.altKey === shortcut.altKey;
864
- return keyMatch && ctrlMatch && shiftMatch && metaMatch && altMatch;
865
- });
947
+ const matchingShortcut = shortcuts.find((shortcut) => matchesKeyBinding(event, shortcut));
866
948
  if (matchingShortcut) {
867
949
  if (matchingShortcut.preventDefault !== false) {
868
950
  event.preventDefault();
@@ -910,14 +992,191 @@ async function probeRangeSupport(url, fetchImpl = fetch) {
910
992
  controller.abort();
911
993
  }
912
994
  }
995
+
996
+ // src/enumerateMicrophones.ts
997
+ var UNSUPPORTED = Object.freeze({
998
+ supported: false,
999
+ hasLabels: false,
1000
+ devices: []
1001
+ });
1002
+ function resolveMediaDevices(mediaDevices) {
1003
+ if (mediaDevices) return mediaDevices;
1004
+ return typeof navigator !== "undefined" ? navigator.mediaDevices : void 0;
1005
+ }
1006
+ async function enumerateMicrophones(mediaDevices) {
1007
+ const md = resolveMediaDevices(mediaDevices);
1008
+ if (!md || typeof md.enumerateDevices !== "function") {
1009
+ return UNSUPPORTED;
1010
+ }
1011
+ const all = await md.enumerateDevices();
1012
+ const inputs = all.filter((device) => device.kind === "audioinput");
1013
+ const hasLabels = inputs.some((device) => device.label.length > 0);
1014
+ return {
1015
+ supported: true,
1016
+ hasLabels,
1017
+ devices: inputs.map((device, index) => ({
1018
+ deviceId: device.deviceId,
1019
+ label: device.label || // Pre-permission fallbacks: Chrome redacts deviceId to '' as well,
1020
+ // so fall through to a positional name when there's nothing to slice.
1021
+ (device.deviceId ? `Microphone ${device.deviceId.slice(0, 8)}` : `Microphone ${index + 1}`),
1022
+ groupId: device.groupId
1023
+ }))
1024
+ };
1025
+ }
1026
+ function watchMicrophoneDevices(listener, mediaDevices) {
1027
+ const md = resolveMediaDevices(mediaDevices);
1028
+ if (!md || typeof md.enumerateDevices !== "function") {
1029
+ queueMicrotask(() => listener(UNSUPPORTED));
1030
+ return () => {
1031
+ };
1032
+ }
1033
+ let active = true;
1034
+ const deliver = () => {
1035
+ enumerateMicrophones(md).then((result) => {
1036
+ if (active) listener(result);
1037
+ }).catch((err) => {
1038
+ console.warn("[waveform-playlist] Microphone enumeration failed:", String(err));
1039
+ });
1040
+ };
1041
+ md.addEventListener("devicechange", deliver);
1042
+ deliver();
1043
+ return () => {
1044
+ active = false;
1045
+ md.removeEventListener("devicechange", deliver);
1046
+ };
1047
+ }
1048
+
1049
+ // src/annotations/boundaries.ts
1050
+ var LINK_THRESHOLD = 0.01;
1051
+ var MIN_ANNOTATION_DURATION = 0.1;
1052
+ var ANNOTATION_LINK_THRESHOLD_TICKS = 0.5;
1053
+ function annotationMinDurationTicks(ppqn) {
1054
+ return Math.max(1, Math.round(ppqn / 32));
1055
+ }
1056
+ function updateAnnotationBoundaries(params, options = {}) {
1057
+ const { annotationIndex, newTime, isDraggingStart, annotations, duration, linkEndpoints } = params;
1058
+ const linkThreshold = options.linkThreshold ?? LINK_THRESHOLD;
1059
+ const minDuration = options.minDuration ?? MIN_ANNOTATION_DURATION;
1060
+ const updatedAnnotations = [...annotations];
1061
+ const annotation = annotations[annotationIndex];
1062
+ if (isDraggingStart) {
1063
+ const constrainedStart = Math.min(annotation.end - minDuration, Math.max(0, newTime));
1064
+ const delta = constrainedStart - annotation.start;
1065
+ updatedAnnotations[annotationIndex] = { ...annotation, start: constrainedStart };
1066
+ if (linkEndpoints && annotationIndex > 0) {
1067
+ const prevAnnotation = updatedAnnotations[annotationIndex - 1];
1068
+ if (Math.abs(prevAnnotation.end - annotation.start) < linkThreshold) {
1069
+ updatedAnnotations[annotationIndex - 1] = {
1070
+ ...prevAnnotation,
1071
+ end: Math.max(prevAnnotation.start + minDuration, prevAnnotation.end + delta)
1072
+ };
1073
+ } else if (constrainedStart <= prevAnnotation.end) {
1074
+ updatedAnnotations[annotationIndex] = {
1075
+ ...updatedAnnotations[annotationIndex],
1076
+ start: prevAnnotation.end
1077
+ };
1078
+ }
1079
+ } else if (!linkEndpoints && annotationIndex > 0 && constrainedStart < updatedAnnotations[annotationIndex - 1].end) {
1080
+ updatedAnnotations[annotationIndex - 1] = {
1081
+ ...updatedAnnotations[annotationIndex - 1],
1082
+ end: constrainedStart
1083
+ };
1084
+ }
1085
+ } else {
1086
+ const constrainedEnd = Math.max(annotation.start + minDuration, Math.min(newTime, duration));
1087
+ const delta = constrainedEnd - annotation.end;
1088
+ updatedAnnotations[annotationIndex] = { ...annotation, end: constrainedEnd };
1089
+ if (linkEndpoints && annotationIndex < updatedAnnotations.length - 1) {
1090
+ const nextAnnotation = updatedAnnotations[annotationIndex + 1];
1091
+ if (Math.abs(nextAnnotation.start - annotation.end) < linkThreshold) {
1092
+ const newStart = nextAnnotation.start + delta;
1093
+ updatedAnnotations[annotationIndex + 1] = {
1094
+ ...nextAnnotation,
1095
+ start: Math.min(nextAnnotation.end - minDuration, newStart)
1096
+ };
1097
+ let currentIndex = annotationIndex + 1;
1098
+ while (currentIndex < updatedAnnotations.length - 1) {
1099
+ const current = updatedAnnotations[currentIndex];
1100
+ const next = updatedAnnotations[currentIndex + 1];
1101
+ if (Math.abs(next.start - current.end) < linkThreshold) {
1102
+ const nextDelta = current.end - annotations[currentIndex].end;
1103
+ updatedAnnotations[currentIndex + 1] = {
1104
+ ...next,
1105
+ start: Math.min(next.end - minDuration, next.start + nextDelta)
1106
+ };
1107
+ currentIndex++;
1108
+ } else {
1109
+ break;
1110
+ }
1111
+ }
1112
+ } else if (constrainedEnd >= nextAnnotation.start) {
1113
+ updatedAnnotations[annotationIndex] = {
1114
+ ...updatedAnnotations[annotationIndex],
1115
+ end: nextAnnotation.start
1116
+ };
1117
+ }
1118
+ } else if (!linkEndpoints && annotationIndex < updatedAnnotations.length - 1 && constrainedEnd > updatedAnnotations[annotationIndex + 1].start) {
1119
+ const nextAnnotation = updatedAnnotations[annotationIndex + 1];
1120
+ updatedAnnotations[annotationIndex + 1] = { ...nextAnnotation, start: constrainedEnd };
1121
+ let currentIndex = annotationIndex + 1;
1122
+ while (currentIndex < updatedAnnotations.length - 1) {
1123
+ const current = updatedAnnotations[currentIndex];
1124
+ const next = updatedAnnotations[currentIndex + 1];
1125
+ if (current.end > next.start) {
1126
+ updatedAnnotations[currentIndex + 1] = { ...next, start: current.end };
1127
+ currentIndex++;
1128
+ } else {
1129
+ break;
1130
+ }
1131
+ }
1132
+ }
1133
+ }
1134
+ return updatedAnnotations;
1135
+ }
1136
+
1137
+ // src/annotations/shortcuts.ts
1138
+ var noMods = { ctrlKey: false, metaKey: false };
1139
+ var DEFAULT_ANNOTATION_SHORTCUTS = {
1140
+ selectPrevious: [
1141
+ { key: "ArrowUp", ...noMods },
1142
+ { key: "ArrowLeft", ...noMods }
1143
+ ],
1144
+ selectNext: [
1145
+ { key: "ArrowDown", ...noMods },
1146
+ { key: "ArrowRight", ...noMods }
1147
+ ],
1148
+ selectFirst: [{ key: "Home", ...noMods }],
1149
+ selectLast: [{ key: "End", ...noMods }],
1150
+ clearSelection: [{ key: "Escape", ...noMods }],
1151
+ moveStartEarlier: [{ key: "[", ...noMods }],
1152
+ moveStartLater: [{ key: "]", ...noMods }],
1153
+ // '{' / '}' are what event.key reports for Shift+[ / Shift+] — no explicit
1154
+ // shiftKey needed; the key value itself encodes it.
1155
+ moveEndEarlier: [{ key: "{", ...noMods }],
1156
+ moveEndLater: [{ key: "}", ...noMods }],
1157
+ playActive: [{ key: "Enter", ...noMods }]
1158
+ };
1159
+ var ALL_ACTIONS = Object.keys(DEFAULT_ANNOTATION_SHORTCUTS);
1160
+ function resolveAnnotationShortcuts(remap) {
1161
+ return ALL_ACTIONS.flatMap((action) => {
1162
+ const override = remap?.[action];
1163
+ const bindings = override ? [override] : DEFAULT_ANNOTATION_SHORTCUTS[action];
1164
+ return bindings.map((binding) => ({ action, binding }));
1165
+ });
1166
+ }
913
1167
  // Annotate the CommonJS export names for ESM import in node:
914
1168
  0 && (module.exports = {
1169
+ ANNOTATION_LINK_THRESHOLD_TICKS,
1170
+ DEFAULT_ANNOTATION_SHORTCUTS,
915
1171
  DEFAULT_SPECTROGRAM_COLOR_MAP,
916
1172
  InteractionState,
1173
+ LINK_THRESHOLD,
917
1174
  MAX_CANVAS_WIDTH,
1175
+ MIN_ANNOTATION_DURATION,
918
1176
  MIN_PIXELS_PER_UNIT,
919
1177
  PPQN,
920
1178
  SPECTROGRAM_DEFAULTS,
1179
+ annotationMinDurationTicks,
921
1180
  appendPeaks,
922
1181
  appendToAudioBuffer,
923
1182
  applyFadeIn,
@@ -925,6 +1184,7 @@ async function probeRangeSupport(url, fetchImpl = fetch) {
925
1184
  audibleLatencySamples,
926
1185
  buildSpectrogramCanvasId,
927
1186
  calculateDuration,
1187
+ carveClipRange,
928
1188
  clipDurationTime,
929
1189
  clipEndTime,
930
1190
  clipOffsetTime,
@@ -941,6 +1201,7 @@ async function probeRangeSupport(url, fetchImpl = fetch) {
941
1201
  createTrack,
942
1202
  dBToNormalized,
943
1203
  detectMeterChanges,
1204
+ enumerateMicrophones,
944
1205
  exponentialCurve,
945
1206
  findGaps,
946
1207
  gainToDb,
@@ -953,11 +1214,13 @@ async function probeRangeSupport(url, fetchImpl = fetch) {
953
1214
  handleKeyboardEvent,
954
1215
  linearCurve,
955
1216
  logarithmicCurve,
1217
+ matchesKeyBinding,
956
1218
  normalizedToDb,
957
1219
  parseSpectrogramCanvasId,
958
1220
  pixelsToSamples,
959
1221
  pixelsToSeconds,
960
1222
  probeRangeSupport,
1223
+ resolveAnnotationShortcuts,
961
1224
  resolveRecordingOffsetSamples,
962
1225
  sCurveCurve,
963
1226
  samplesToPixels,
@@ -971,7 +1234,10 @@ async function probeRangeSupport(url, fetchImpl = fetch) {
971
1234
  sortClipsByTime,
972
1235
  ticksPerBar,
973
1236
  ticksPerBeat,
1237
+ ticksToBarBeat,
974
1238
  ticksToSamples,
975
- trackChannelCount
1239
+ trackChannelCount,
1240
+ updateAnnotationBoundaries,
1241
+ watchMicrophoneDevices
976
1242
  });
977
1243
  //# sourceMappingURL=index.js.map