@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.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