@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 ADDED
@@ -0,0 +1,86 @@
1
+ # @waveform-playlist/core
2
+
3
+ Core types, interfaces and utilities for waveform-playlist — pure functions and TypeScript types with zero runtime dependencies.
4
+
5
+ Used by nearly every `@waveform-playlist/*` and `@dawcore/*` package (`engine`, `playout`, `media-element-playout`, `browser`, `ui-components`, `recording`, `annotations`, `spectrogram`, `midi`, `transport`, `dawcore`, `dawcore-spectrogram`, `dawcore-midi`, `webaudio-peaks`, `loaders`, and more). Most consumers get it transitively — install it directly only if you're building on the clip model, timeline math, or fade curves yourself.
6
+
7
+ ## Features
8
+
9
+ - **Clip & track types** — `AudioClip`, `ClipTrack`, `Timeline`, `Fade`/`FadeType`, `Peaks`/`Bits`/`PeakData`, `WaveformDataObject`
10
+ - **Sample-based timeline math** — `createClip`/`createClipFromSeconds`/`createClipFromTicks`, clip queries (`getClipsInRange`, `clipsOverlap`, `findGaps`), and second-conversion helpers (`clipStartTime`, `clipEndTime`, `clipPixelWidth`)
11
+ - **Fade curves** — linear, exponential, logarithmic, and S-curve generators for native Web Audio `AudioParam` (no Tone.js dependency)
12
+ - **Decibel utilities** — gain ↔ dB ↔ normalized-0-1 conversions for meters and volume controls
13
+ - **Peaks generation** — real-time min/max peak extraction during recording
14
+ - **Musical time** — PPQN tick math, bar/beat conversion, time-signature meter detection, grid snapping
15
+ - **Keyboard shortcuts** — a framework-agnostic shortcut matcher shared by the React and Web Components layers
16
+ - **Spectrogram canvas-ID contract** — canonical build/parse helpers for the `${clipId}-ch${n}-chunk${m}` canvas ID format shared by the worker pool and rendering layers
17
+ - **HTTP range support probing** — detect whether an audio host supports byte-range seeking
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install @waveform-playlist/core
23
+ ```
24
+
25
+ Zero dependencies — safe to install anywhere without pulling in Web Audio, React, or Tone.js.
26
+
27
+ ## Usage
28
+
29
+ ```typescript
30
+ import { createClipFromSeconds, clipStartTime, clipEndTime, gainToDb } from '@waveform-playlist/core';
31
+
32
+ // Build a clip from a decoded AudioBuffer, positioned at 2.5s on the timeline
33
+ const clip = createClipFromSeconds({
34
+ audioBuffer,
35
+ startTime: 2.5,
36
+ offset: 0,
37
+ fadeIn: { duration: 0.5, type: 'linear' },
38
+ });
39
+
40
+ clipStartTime(clip); // 2.5
41
+ clipEndTime(clip); // 2.5 + audioBuffer.duration
42
+
43
+ // Convert a linear gain value to dB for a Tone.js Volume node
44
+ gainToDb(0.5); // ≈ -6.02
45
+ ```
46
+
47
+ ```typescript
48
+ import { buildSpectrogramCanvasId, parseSpectrogramCanvasId } from '@waveform-playlist/core';
49
+
50
+ const id = buildSpectrogramCanvasId({ clipId: 'clip-1', channelIndex: 0, chunkIndex: 3 });
51
+ // "clip-1-ch0-chunk3"
52
+
53
+ parseSpectrogramCanvasId(id);
54
+ // { clipId: 'clip-1', channelIndex: 0, chunkIndex: 3 }
55
+ ```
56
+
57
+ ## API
58
+
59
+ | Module | Key exports |
60
+ |---|---|
61
+ | `types/` | `AudioClip`, `ClipTrack`, `Timeline`, `Track`, `Fade`, `FadeType`, `Peaks`, `Bits`, `PeakData`, `WaveformDataObject`, `SpectrogramConfig`, `RenderMode`, `ColorMapValue` |
62
+ | `clipTimeHelpers` | `clipStartTime`, `clipEndTime`, `clipOffsetTime`, `clipDurationTime`, `clipPixelWidth`, `trackChannelCount` |
63
+ | clip constructors (`types/clip`) | `createClip`, `createClipFromSeconds`, `createClipFromTicks`, `createTrack`, `createTimeline`, `getClipsInRange`, `getClipsAtSample`, `clipsOverlap`, `sortClipsByTime`, `findGaps` |
64
+ | `fades` | `applyFadeIn`, `applyFadeOut`, `generateCurve`, `linearCurve`, `exponentialCurve`, `logarithmicCurve`, `sCurveCurve` |
65
+ | `utils/dBUtils` | `gainToDb`, `dBToNormalized`, `normalizedToDb`, `gainToNormalized` |
66
+ | `utils/conversions` | `samplesToSeconds`, `secondsToSamples`, `samplesToPixels`, `pixelsToSamples`, `pixelsToSeconds`, `secondsToPixels` |
67
+ | `utils/beatsAndBars` | `PPQN`, `ticksPerBeat`, `ticksPerBar`, `ticksToSamples`, `samplesToTicks`, `snapToGrid` |
68
+ | `utils/musicalTicks` | `computeMusicalTicks`, `snapToTicks`, `snapTickToGrid`, `SnapTo`, `MusicalTick` |
69
+ | `utils/meterDetection` | `MeterEntry`, `detectMeterChanges` |
70
+ | `utils/peaksGenerator` | `generatePeaks`, `appendPeaks` |
71
+ | `utils/audioBufferUtils` | `concatenateAudioData`, `createAudioBuffer`, `appendToAudioBuffer`, `calculateDuration` |
72
+ | `utils/latency` | `resolveRecordingOffsetSamples` — override-vs-auto recording latency compensation (shared by React + dawcore) |
73
+ | `utils/carveClipRange` | `carveClipRange` — punch-in replace: carve a sample range out of a clip list (trim/remove/split) |
74
+ | `keyboard` | `KeyboardShortcut`, `handleKeyboardEvent`, `getShortcutLabel` |
75
+ | `spectrogramCanvasId` | `buildSpectrogramCanvasId`, `parseSpectrogramCanvasId`, `SpectrogramCanvasIdParts` |
76
+ | `probeRangeSupport` | `probeRangeSupport`, `RangeSupport` |
77
+ | `enumerateMicrophones` | `enumerateMicrophones`, `watchMicrophoneDevices`, `MicrophoneEnumeration` — permission-free device listing with `supported`/`hasLabels` flags (labels are browser-redacted pre-permission) |
78
+ | `constants` | `MAX_CANVAS_WIDTH` |
79
+
80
+ ## Examples & Documentation
81
+
82
+ - Guides: [naomiaro.github.io/waveform-playlist](https://naomiaro.github.io/waveform-playlist/)
83
+
84
+ ## License
85
+
86
+ MIT
package/dist/index.d.mts CHANGED
@@ -476,6 +476,10 @@ interface AnnotationData {
476
476
  end: number;
477
477
  lines: string[];
478
478
  language?: string;
479
+ /** Musical position (ticks). Authoritative when BOTH tick fields are set —
480
+ * start/end seconds are then a derived cache (clip startTick pattern). */
481
+ startTick?: number;
482
+ endTick?: number;
479
483
  }
480
484
  /**
481
485
  * Annotation format definition for parsing/serializing
@@ -775,6 +779,21 @@ declare const MIN_PIXELS_PER_UNIT = 8;
775
779
  * correct across meter changes.
776
780
  */
777
781
  declare function computeMusicalTicks(params: MusicalTickParams): MusicalTickData;
782
+ /**
783
+ * Convert an absolute tick to a 1-based bar.beat position, honoring meter
784
+ * changes. Bars/beats are counted per meter segment (the same walk
785
+ * `computeMusicalTicks` uses to build its `barOffset` accumulator). Beat is
786
+ * the integer beat the tick falls WITHIN (floor, 1-based) — a tick exactly on
787
+ * a bar line is beat 1.
788
+ *
789
+ * Guards: empty `meterEntries` → treat as 4/4 from tick 0. `ppqn <= 0` →
790
+ * returns `{ bar: 1, beat: 1 }` (division-by-zero guard, matches
791
+ * `computeMusicalTicks`'s ppqn guard).
792
+ */
793
+ declare function ticksToBarBeat(tick: number, meterEntries: MeterEntry[], ppqn: number): {
794
+ bar: number;
795
+ beat: number;
796
+ };
778
797
  /**
779
798
  * Snaps a tick position to the nearest grid boundary defined by `snapTo`.
780
799
  *
@@ -880,6 +899,37 @@ declare const SPECTROGRAM_DEFAULTS: Required<Omit<SpectrogramConfig, 'maxFrequen
880
899
  /** Default color map when none is specified. */
881
900
  declare const DEFAULT_SPECTROGRAM_COLOR_MAP: ColorMapValue;
882
901
 
902
+ /**
903
+ * Punch-in replace: carve a sample range out of a clip list.
904
+ *
905
+ * Used when a newly recorded clip lands at the playhead and must REPLACE any
906
+ * existing clip content between its start and end (issue #579): partial
907
+ * overlaps are trimmed, fully-covered clips are removed, and a clip that
908
+ * spans the whole range is split into two.
909
+ */
910
+
911
+ /**
912
+ * Return a new clip list with the sample range [rangeStart, rangeEnd)
913
+ * removed from every clip that overlaps it.
914
+ *
915
+ * - Clips outside the range are returned by reference (untouched).
916
+ * - Clips fully inside the range are dropped.
917
+ * - A clip overlapping only the range's start keeps its head (right-trim).
918
+ * - A clip overlapping only the range's end keeps its tail: its start moves
919
+ * to rangeEnd and `offsetSamples` advances by the carved amount.
920
+ * - A clip containing the whole range splits into head + tail; the tail is a
921
+ * new clip (deterministic id `<original>-carve-<rangeEnd>`) sharing the
922
+ * same audioBuffer.
923
+ *
924
+ * Clips whose `startSample` changes lose their `startTick` — a sample-space
925
+ * carve cannot recompute ticks, and the engine re-enriches clips without
926
+ * `startTick` on ingestion (see AudioClip.startTick).
927
+ *
928
+ * Pure and immutable: input clips are never mutated. An empty or inverted
929
+ * range returns the input list unchanged.
930
+ */
931
+ declare function carveClipRange(clips: readonly AudioClip[], rangeStart: number, rangeEnd: number): AudioClip[];
932
+
883
933
  /** Clip start position in seconds */
884
934
  declare function clipStartTime(clip: AudioClip): number;
885
935
  /** Clip end position in seconds (start + duration) */
@@ -994,6 +1044,13 @@ interface KeyboardShortcut {
994
1044
  description?: string;
995
1045
  preventDefault?: boolean;
996
1046
  }
1047
+ /** A key + modifier combination, without an action. Used for remapping maps. */
1048
+ type KeyBinding = Pick<KeyboardShortcut, 'key' | 'ctrlKey' | 'shiftKey' | 'metaKey' | 'altKey'>;
1049
+ /**
1050
+ * Does a keyboard event match a key binding?
1051
+ * `undefined` modifier = match any state; `false` = must NOT be pressed.
1052
+ */
1053
+ declare function matchesKeyBinding(event: KeyboardEvent, binding: KeyBinding): boolean;
997
1054
  /**
998
1055
  * Handle a keyboard event against a list of shortcuts.
999
1056
  * Pure function, no framework dependency.
@@ -1036,4 +1093,104 @@ type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
1036
1093
  */
1037
1094
  declare function probeRangeSupport(url: string, fetchImpl?: FetchLike): Promise<RangeSupport>;
1038
1095
 
1039
- export { type AnnotationAction, type AnnotationActionOptions, type AnnotationData, type AnnotationEventMap, type AnnotationFormat, type AnnotationListOptions, type AudioBuffer$1 as AudioBuffer, type AudioClip, type Bits, type ClipTrack, type ColorMapEntry, type ColorMapName, type ColorMapValue, type CreateClipOptions, type CreateClipOptionsSeconds, type CreateClipOptionsTicks, type CreateTrackOptions, DEFAULT_SPECTROGRAM_COLOR_MAP, type FFTSize, type Fade, type FadeConfig, type FadeType, type Gap, InteractionState, type KeyboardShortcut, MAX_CANVAS_WIDTH, MIN_PIXELS_PER_UNIT, type MeterEntry, type MidiNoteData, type MusicalTick, type MusicalTickData, type MusicalTickParams, PPQN, type PeakData, type Peaks, type PlaylistConfig, type PlayoutState, type RangeSupport, type RenderAnnotationItemProps, type RenderMode, SPECTROGRAM_DEFAULTS, type SnapTo, type SpectrogramCanvasIdParts, type SpectrogramComputeConfig, type SpectrogramConfig, type SpectrogramData, type SpectrogramDisplayConfig, type TickType, type TimeSelection, type Timeline, type Track, type TrackEffectsFunction, type TrackSpectrogramOverrides, type WaveformConfig, type WaveformDataObject, type ZoomLevel, appendPeaks, appendToAudioBuffer, applyFadeIn, applyFadeOut, audibleLatencySamples, buildSpectrogramCanvasId, calculateDuration, clipDurationTime, clipEndTime, clipOffsetTime, clipPixelWidth, clipStartTime, clipsOverlap, computeMusicalTicks, concatenateAudioData, createAudioBuffer, createClip, createClipFromSeconds, createClipFromTicks, createTimeline, createTrack, dBToNormalized, detectMeterChanges, exponentialCurve, findGaps, gainToDb, gainToNormalized, generateCurve, generatePeaks, getClipsAtSample, getClipsInRange, getShortcutLabel, handleKeyboardEvent, linearCurve, logarithmicCurve, normalizedToDb, parseSpectrogramCanvasId, pixelsToSamples, pixelsToSeconds, probeRangeSupport, resolveRecordingOffsetSamples, sCurveCurve, samplesToPixels, samplesToSeconds, samplesToTicks, secondsToPixels, secondsToSamples, snapTickToGrid, snapToGrid, snapToTicks, sortClipsByTime, ticksPerBar, ticksPerBeat, ticksToSamples, trackChannelCount };
1096
+ /**
1097
+ * Framework-agnostic microphone enumeration.
1098
+ *
1099
+ * Works without requesting a stream, but browsers redact device information
1100
+ * until microphone permission is granted: labels are empty everywhere, and
1101
+ * Chrome/Safari typically also redact deviceIds (often exposing a single
1102
+ * placeholder entry). The `hasLabels` flag tells consumers whether the list
1103
+ * is a usable picker or just a "microphones exist" signal — re-enumerate
1104
+ * after a successful getUserMedia to get the real list.
1105
+ *
1106
+ * Follows the probeRangeSupport pattern: the browser API is injectable for
1107
+ * tests and non-browser environments degrade gracefully.
1108
+ */
1109
+ interface MicrophoneDeviceInfo {
1110
+ deviceId: string;
1111
+ /** Real device label, or a generated fallback name when redacted. */
1112
+ label: string;
1113
+ groupId: string;
1114
+ }
1115
+ interface MicrophoneEnumeration {
1116
+ /** False when enumeration is unavailable — SSR/Node, insecure context
1117
+ * (mediaDevices requires HTTPS or localhost), or a very old browser. */
1118
+ supported: boolean;
1119
+ /** True when the browser exposed real device labels. False before
1120
+ * microphone permission is granted (labels — and often deviceIds — are
1121
+ * redacted); the `devices` labels are then generated fallbacks. */
1122
+ hasLabels: boolean;
1123
+ devices: MicrophoneDeviceInfo[];
1124
+ }
1125
+ /**
1126
+ * List audio input devices without requesting a stream.
1127
+ *
1128
+ * @param mediaDevices - Injectable for tests; defaults to `navigator.mediaDevices`.
1129
+ */
1130
+ declare function enumerateMicrophones(mediaDevices?: MediaDevices): Promise<MicrophoneEnumeration>;
1131
+ /**
1132
+ * Subscribe to the microphone device list: the listener fires once with the
1133
+ * initial enumeration and again on every `devicechange` (hot-plug, permission
1134
+ * grant in some browsers). Returns an unsubscribe function.
1135
+ *
1136
+ * Enumeration failures are logged and skipped — the subscription survives a
1137
+ * transient failure and delivers the next successful enumeration.
1138
+ *
1139
+ * @param mediaDevices - Injectable for tests; defaults to `navigator.mediaDevices`.
1140
+ */
1141
+ declare function watchMicrophoneDevices(listener: (enumeration: MicrophoneEnumeration) => void, mediaDevices?: MediaDevices): () => void;
1142
+
1143
+ /** Edges within this distance (seconds) are considered "linked". */
1144
+ declare const LINK_THRESHOLD = 0.01;
1145
+ /** Minimum annotation duration (seconds) enforced by boundary edits. */
1146
+ declare const MIN_ANNOTATION_DURATION = 0.1;
1147
+ /** Link threshold for INTEGER TICK positions — closer than half a tick means equal. */
1148
+ declare const ANNOTATION_LINK_THRESHOLD_TICKS = 0.5;
1149
+ /** Minimum tick-annotation duration: a 128th note, floored at 1 tick. */
1150
+ declare function annotationMinDurationTicks(ppqn: number): number;
1151
+ /** Unit constants for a boundary-math run. Defaults are the seconds-domain values. */
1152
+ interface AnnotationBoundaryOptions {
1153
+ linkThreshold?: number;
1154
+ minDuration?: number;
1155
+ }
1156
+ interface AnnotationBoundaryUpdate {
1157
+ annotationIndex: number;
1158
+ newTime: number;
1159
+ isDraggingStart: boolean;
1160
+ annotations: AnnotationData[];
1161
+ duration: number;
1162
+ linkEndpoints: boolean;
1163
+ }
1164
+ /**
1165
+ * Pure boundary-update logic shared by the React annotations package and the
1166
+ * dawcore annotation web components. Handles linked endpoints (moving one edge
1167
+ * drags the linked neighbor edge, with cascade) and collision push-back.
1168
+ * Returns a NEW array; never mutates inputs.
1169
+ */
1170
+ declare function updateAnnotationBoundaries(params: AnnotationBoundaryUpdate, options?: AnnotationBoundaryOptions): AnnotationData[];
1171
+
1172
+ type AnnotationShortcutAction = 'selectPrevious' | 'selectNext' | 'selectFirst' | 'selectLast' | 'clearSelection' | 'moveStartEarlier' | 'moveStartLater' | 'moveEndEarlier' | 'moveEndLater' | 'playActive';
1173
+ /** Key remapping map — null on the consumer means "use defaults". */
1174
+ interface AnnotationShortcutMap {
1175
+ selectPrevious?: KeyBinding;
1176
+ selectNext?: KeyBinding;
1177
+ selectFirst?: KeyBinding;
1178
+ selectLast?: KeyBinding;
1179
+ clearSelection?: KeyBinding;
1180
+ moveStartEarlier?: KeyBinding;
1181
+ moveStartLater?: KeyBinding;
1182
+ moveEndEarlier?: KeyBinding;
1183
+ moveEndLater?: KeyBinding;
1184
+ playActive?: KeyBinding;
1185
+ }
1186
+ declare const DEFAULT_ANNOTATION_SHORTCUTS: Record<AnnotationShortcutAction, KeyBinding[]>;
1187
+ /**
1188
+ * Flatten defaults + remap into a matchable list. A remapped action replaces
1189
+ * ALL of its default bindings with the single provided binding.
1190
+ */
1191
+ declare function resolveAnnotationShortcuts(remap: AnnotationShortcutMap | null): Array<{
1192
+ action: AnnotationShortcutAction;
1193
+ binding: KeyBinding;
1194
+ }>;
1195
+
1196
+ export { ANNOTATION_LINK_THRESHOLD_TICKS, type AnnotationAction, type AnnotationActionOptions, type AnnotationBoundaryOptions, type AnnotationBoundaryUpdate, type AnnotationData, type AnnotationEventMap, type AnnotationFormat, type AnnotationListOptions, type AnnotationShortcutAction, type AnnotationShortcutMap, type AudioBuffer$1 as AudioBuffer, type AudioClip, type Bits, type ClipTrack, type ColorMapEntry, type ColorMapName, type ColorMapValue, type CreateClipOptions, type CreateClipOptionsSeconds, type CreateClipOptionsTicks, type CreateTrackOptions, DEFAULT_ANNOTATION_SHORTCUTS, DEFAULT_SPECTROGRAM_COLOR_MAP, type FFTSize, type Fade, type FadeConfig, type FadeType, type Gap, InteractionState, type KeyBinding, type KeyboardShortcut, LINK_THRESHOLD, MAX_CANVAS_WIDTH, MIN_ANNOTATION_DURATION, MIN_PIXELS_PER_UNIT, type MeterEntry, type MicrophoneDeviceInfo, type MicrophoneEnumeration, type MidiNoteData, type MusicalTick, type MusicalTickData, type MusicalTickParams, PPQN, type PeakData, type Peaks, type PlaylistConfig, type PlayoutState, type RangeSupport, type RenderAnnotationItemProps, type RenderMode, SPECTROGRAM_DEFAULTS, type SnapTo, type SpectrogramCanvasIdParts, type SpectrogramComputeConfig, type SpectrogramConfig, type SpectrogramData, type SpectrogramDisplayConfig, type TickType, type TimeSelection, type Timeline, type Track, type TrackEffectsFunction, type TrackSpectrogramOverrides, type WaveformConfig, type WaveformDataObject, type ZoomLevel, annotationMinDurationTicks, appendPeaks, appendToAudioBuffer, applyFadeIn, applyFadeOut, audibleLatencySamples, buildSpectrogramCanvasId, calculateDuration, carveClipRange, clipDurationTime, clipEndTime, clipOffsetTime, clipPixelWidth, clipStartTime, clipsOverlap, computeMusicalTicks, concatenateAudioData, createAudioBuffer, createClip, createClipFromSeconds, createClipFromTicks, createTimeline, createTrack, dBToNormalized, detectMeterChanges, enumerateMicrophones, exponentialCurve, findGaps, gainToDb, gainToNormalized, generateCurve, generatePeaks, getClipsAtSample, getClipsInRange, getShortcutLabel, handleKeyboardEvent, linearCurve, logarithmicCurve, matchesKeyBinding, normalizedToDb, parseSpectrogramCanvasId, pixelsToSamples, pixelsToSeconds, probeRangeSupport, resolveAnnotationShortcuts, resolveRecordingOffsetSamples, sCurveCurve, samplesToPixels, samplesToSeconds, samplesToTicks, secondsToPixels, secondsToSamples, snapTickToGrid, snapToGrid, snapToTicks, sortClipsByTime, ticksPerBar, ticksPerBeat, ticksToBarBeat, ticksToSamples, trackChannelCount, updateAnnotationBoundaries, watchMicrophoneDevices };
package/dist/index.d.ts CHANGED
@@ -476,6 +476,10 @@ interface AnnotationData {
476
476
  end: number;
477
477
  lines: string[];
478
478
  language?: string;
479
+ /** Musical position (ticks). Authoritative when BOTH tick fields are set —
480
+ * start/end seconds are then a derived cache (clip startTick pattern). */
481
+ startTick?: number;
482
+ endTick?: number;
479
483
  }
480
484
  /**
481
485
  * Annotation format definition for parsing/serializing
@@ -775,6 +779,21 @@ declare const MIN_PIXELS_PER_UNIT = 8;
775
779
  * correct across meter changes.
776
780
  */
777
781
  declare function computeMusicalTicks(params: MusicalTickParams): MusicalTickData;
782
+ /**
783
+ * Convert an absolute tick to a 1-based bar.beat position, honoring meter
784
+ * changes. Bars/beats are counted per meter segment (the same walk
785
+ * `computeMusicalTicks` uses to build its `barOffset` accumulator). Beat is
786
+ * the integer beat the tick falls WITHIN (floor, 1-based) — a tick exactly on
787
+ * a bar line is beat 1.
788
+ *
789
+ * Guards: empty `meterEntries` → treat as 4/4 from tick 0. `ppqn <= 0` →
790
+ * returns `{ bar: 1, beat: 1 }` (division-by-zero guard, matches
791
+ * `computeMusicalTicks`'s ppqn guard).
792
+ */
793
+ declare function ticksToBarBeat(tick: number, meterEntries: MeterEntry[], ppqn: number): {
794
+ bar: number;
795
+ beat: number;
796
+ };
778
797
  /**
779
798
  * Snaps a tick position to the nearest grid boundary defined by `snapTo`.
780
799
  *
@@ -880,6 +899,37 @@ declare const SPECTROGRAM_DEFAULTS: Required<Omit<SpectrogramConfig, 'maxFrequen
880
899
  /** Default color map when none is specified. */
881
900
  declare const DEFAULT_SPECTROGRAM_COLOR_MAP: ColorMapValue;
882
901
 
902
+ /**
903
+ * Punch-in replace: carve a sample range out of a clip list.
904
+ *
905
+ * Used when a newly recorded clip lands at the playhead and must REPLACE any
906
+ * existing clip content between its start and end (issue #579): partial
907
+ * overlaps are trimmed, fully-covered clips are removed, and a clip that
908
+ * spans the whole range is split into two.
909
+ */
910
+
911
+ /**
912
+ * Return a new clip list with the sample range [rangeStart, rangeEnd)
913
+ * removed from every clip that overlaps it.
914
+ *
915
+ * - Clips outside the range are returned by reference (untouched).
916
+ * - Clips fully inside the range are dropped.
917
+ * - A clip overlapping only the range's start keeps its head (right-trim).
918
+ * - A clip overlapping only the range's end keeps its tail: its start moves
919
+ * to rangeEnd and `offsetSamples` advances by the carved amount.
920
+ * - A clip containing the whole range splits into head + tail; the tail is a
921
+ * new clip (deterministic id `<original>-carve-<rangeEnd>`) sharing the
922
+ * same audioBuffer.
923
+ *
924
+ * Clips whose `startSample` changes lose their `startTick` — a sample-space
925
+ * carve cannot recompute ticks, and the engine re-enriches clips without
926
+ * `startTick` on ingestion (see AudioClip.startTick).
927
+ *
928
+ * Pure and immutable: input clips are never mutated. An empty or inverted
929
+ * range returns the input list unchanged.
930
+ */
931
+ declare function carveClipRange(clips: readonly AudioClip[], rangeStart: number, rangeEnd: number): AudioClip[];
932
+
883
933
  /** Clip start position in seconds */
884
934
  declare function clipStartTime(clip: AudioClip): number;
885
935
  /** Clip end position in seconds (start + duration) */
@@ -994,6 +1044,13 @@ interface KeyboardShortcut {
994
1044
  description?: string;
995
1045
  preventDefault?: boolean;
996
1046
  }
1047
+ /** A key + modifier combination, without an action. Used for remapping maps. */
1048
+ type KeyBinding = Pick<KeyboardShortcut, 'key' | 'ctrlKey' | 'shiftKey' | 'metaKey' | 'altKey'>;
1049
+ /**
1050
+ * Does a keyboard event match a key binding?
1051
+ * `undefined` modifier = match any state; `false` = must NOT be pressed.
1052
+ */
1053
+ declare function matchesKeyBinding(event: KeyboardEvent, binding: KeyBinding): boolean;
997
1054
  /**
998
1055
  * Handle a keyboard event against a list of shortcuts.
999
1056
  * Pure function, no framework dependency.
@@ -1036,4 +1093,104 @@ type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
1036
1093
  */
1037
1094
  declare function probeRangeSupport(url: string, fetchImpl?: FetchLike): Promise<RangeSupport>;
1038
1095
 
1039
- export { type AnnotationAction, type AnnotationActionOptions, type AnnotationData, type AnnotationEventMap, type AnnotationFormat, type AnnotationListOptions, type AudioBuffer$1 as AudioBuffer, type AudioClip, type Bits, type ClipTrack, type ColorMapEntry, type ColorMapName, type ColorMapValue, type CreateClipOptions, type CreateClipOptionsSeconds, type CreateClipOptionsTicks, type CreateTrackOptions, DEFAULT_SPECTROGRAM_COLOR_MAP, type FFTSize, type Fade, type FadeConfig, type FadeType, type Gap, InteractionState, type KeyboardShortcut, MAX_CANVAS_WIDTH, MIN_PIXELS_PER_UNIT, type MeterEntry, type MidiNoteData, type MusicalTick, type MusicalTickData, type MusicalTickParams, PPQN, type PeakData, type Peaks, type PlaylistConfig, type PlayoutState, type RangeSupport, type RenderAnnotationItemProps, type RenderMode, SPECTROGRAM_DEFAULTS, type SnapTo, type SpectrogramCanvasIdParts, type SpectrogramComputeConfig, type SpectrogramConfig, type SpectrogramData, type SpectrogramDisplayConfig, type TickType, type TimeSelection, type Timeline, type Track, type TrackEffectsFunction, type TrackSpectrogramOverrides, type WaveformConfig, type WaveformDataObject, type ZoomLevel, appendPeaks, appendToAudioBuffer, applyFadeIn, applyFadeOut, audibleLatencySamples, buildSpectrogramCanvasId, calculateDuration, clipDurationTime, clipEndTime, clipOffsetTime, clipPixelWidth, clipStartTime, clipsOverlap, computeMusicalTicks, concatenateAudioData, createAudioBuffer, createClip, createClipFromSeconds, createClipFromTicks, createTimeline, createTrack, dBToNormalized, detectMeterChanges, exponentialCurve, findGaps, gainToDb, gainToNormalized, generateCurve, generatePeaks, getClipsAtSample, getClipsInRange, getShortcutLabel, handleKeyboardEvent, linearCurve, logarithmicCurve, normalizedToDb, parseSpectrogramCanvasId, pixelsToSamples, pixelsToSeconds, probeRangeSupport, resolveRecordingOffsetSamples, sCurveCurve, samplesToPixels, samplesToSeconds, samplesToTicks, secondsToPixels, secondsToSamples, snapTickToGrid, snapToGrid, snapToTicks, sortClipsByTime, ticksPerBar, ticksPerBeat, ticksToSamples, trackChannelCount };
1096
+ /**
1097
+ * Framework-agnostic microphone enumeration.
1098
+ *
1099
+ * Works without requesting a stream, but browsers redact device information
1100
+ * until microphone permission is granted: labels are empty everywhere, and
1101
+ * Chrome/Safari typically also redact deviceIds (often exposing a single
1102
+ * placeholder entry). The `hasLabels` flag tells consumers whether the list
1103
+ * is a usable picker or just a "microphones exist" signal — re-enumerate
1104
+ * after a successful getUserMedia to get the real list.
1105
+ *
1106
+ * Follows the probeRangeSupport pattern: the browser API is injectable for
1107
+ * tests and non-browser environments degrade gracefully.
1108
+ */
1109
+ interface MicrophoneDeviceInfo {
1110
+ deviceId: string;
1111
+ /** Real device label, or a generated fallback name when redacted. */
1112
+ label: string;
1113
+ groupId: string;
1114
+ }
1115
+ interface MicrophoneEnumeration {
1116
+ /** False when enumeration is unavailable — SSR/Node, insecure context
1117
+ * (mediaDevices requires HTTPS or localhost), or a very old browser. */
1118
+ supported: boolean;
1119
+ /** True when the browser exposed real device labels. False before
1120
+ * microphone permission is granted (labels — and often deviceIds — are
1121
+ * redacted); the `devices` labels are then generated fallbacks. */
1122
+ hasLabels: boolean;
1123
+ devices: MicrophoneDeviceInfo[];
1124
+ }
1125
+ /**
1126
+ * List audio input devices without requesting a stream.
1127
+ *
1128
+ * @param mediaDevices - Injectable for tests; defaults to `navigator.mediaDevices`.
1129
+ */
1130
+ declare function enumerateMicrophones(mediaDevices?: MediaDevices): Promise<MicrophoneEnumeration>;
1131
+ /**
1132
+ * Subscribe to the microphone device list: the listener fires once with the
1133
+ * initial enumeration and again on every `devicechange` (hot-plug, permission
1134
+ * grant in some browsers). Returns an unsubscribe function.
1135
+ *
1136
+ * Enumeration failures are logged and skipped — the subscription survives a
1137
+ * transient failure and delivers the next successful enumeration.
1138
+ *
1139
+ * @param mediaDevices - Injectable for tests; defaults to `navigator.mediaDevices`.
1140
+ */
1141
+ declare function watchMicrophoneDevices(listener: (enumeration: MicrophoneEnumeration) => void, mediaDevices?: MediaDevices): () => void;
1142
+
1143
+ /** Edges within this distance (seconds) are considered "linked". */
1144
+ declare const LINK_THRESHOLD = 0.01;
1145
+ /** Minimum annotation duration (seconds) enforced by boundary edits. */
1146
+ declare const MIN_ANNOTATION_DURATION = 0.1;
1147
+ /** Link threshold for INTEGER TICK positions — closer than half a tick means equal. */
1148
+ declare const ANNOTATION_LINK_THRESHOLD_TICKS = 0.5;
1149
+ /** Minimum tick-annotation duration: a 128th note, floored at 1 tick. */
1150
+ declare function annotationMinDurationTicks(ppqn: number): number;
1151
+ /** Unit constants for a boundary-math run. Defaults are the seconds-domain values. */
1152
+ interface AnnotationBoundaryOptions {
1153
+ linkThreshold?: number;
1154
+ minDuration?: number;
1155
+ }
1156
+ interface AnnotationBoundaryUpdate {
1157
+ annotationIndex: number;
1158
+ newTime: number;
1159
+ isDraggingStart: boolean;
1160
+ annotations: AnnotationData[];
1161
+ duration: number;
1162
+ linkEndpoints: boolean;
1163
+ }
1164
+ /**
1165
+ * Pure boundary-update logic shared by the React annotations package and the
1166
+ * dawcore annotation web components. Handles linked endpoints (moving one edge
1167
+ * drags the linked neighbor edge, with cascade) and collision push-back.
1168
+ * Returns a NEW array; never mutates inputs.
1169
+ */
1170
+ declare function updateAnnotationBoundaries(params: AnnotationBoundaryUpdate, options?: AnnotationBoundaryOptions): AnnotationData[];
1171
+
1172
+ type AnnotationShortcutAction = 'selectPrevious' | 'selectNext' | 'selectFirst' | 'selectLast' | 'clearSelection' | 'moveStartEarlier' | 'moveStartLater' | 'moveEndEarlier' | 'moveEndLater' | 'playActive';
1173
+ /** Key remapping map — null on the consumer means "use defaults". */
1174
+ interface AnnotationShortcutMap {
1175
+ selectPrevious?: KeyBinding;
1176
+ selectNext?: KeyBinding;
1177
+ selectFirst?: KeyBinding;
1178
+ selectLast?: KeyBinding;
1179
+ clearSelection?: KeyBinding;
1180
+ moveStartEarlier?: KeyBinding;
1181
+ moveStartLater?: KeyBinding;
1182
+ moveEndEarlier?: KeyBinding;
1183
+ moveEndLater?: KeyBinding;
1184
+ playActive?: KeyBinding;
1185
+ }
1186
+ declare const DEFAULT_ANNOTATION_SHORTCUTS: Record<AnnotationShortcutAction, KeyBinding[]>;
1187
+ /**
1188
+ * Flatten defaults + remap into a matchable list. A remapped action replaces
1189
+ * ALL of its default bindings with the single provided binding.
1190
+ */
1191
+ declare function resolveAnnotationShortcuts(remap: AnnotationShortcutMap | null): Array<{
1192
+ action: AnnotationShortcutAction;
1193
+ binding: KeyBinding;
1194
+ }>;
1195
+
1196
+ export { ANNOTATION_LINK_THRESHOLD_TICKS, type AnnotationAction, type AnnotationActionOptions, type AnnotationBoundaryOptions, type AnnotationBoundaryUpdate, type AnnotationData, type AnnotationEventMap, type AnnotationFormat, type AnnotationListOptions, type AnnotationShortcutAction, type AnnotationShortcutMap, type AudioBuffer$1 as AudioBuffer, type AudioClip, type Bits, type ClipTrack, type ColorMapEntry, type ColorMapName, type ColorMapValue, type CreateClipOptions, type CreateClipOptionsSeconds, type CreateClipOptionsTicks, type CreateTrackOptions, DEFAULT_ANNOTATION_SHORTCUTS, DEFAULT_SPECTROGRAM_COLOR_MAP, type FFTSize, type Fade, type FadeConfig, type FadeType, type Gap, InteractionState, type KeyBinding, type KeyboardShortcut, LINK_THRESHOLD, MAX_CANVAS_WIDTH, MIN_ANNOTATION_DURATION, MIN_PIXELS_PER_UNIT, type MeterEntry, type MicrophoneDeviceInfo, type MicrophoneEnumeration, type MidiNoteData, type MusicalTick, type MusicalTickData, type MusicalTickParams, PPQN, type PeakData, type Peaks, type PlaylistConfig, type PlayoutState, type RangeSupport, type RenderAnnotationItemProps, type RenderMode, SPECTROGRAM_DEFAULTS, type SnapTo, type SpectrogramCanvasIdParts, type SpectrogramComputeConfig, type SpectrogramConfig, type SpectrogramData, type SpectrogramDisplayConfig, type TickType, type TimeSelection, type Timeline, type Track, type TrackEffectsFunction, type TrackSpectrogramOverrides, type WaveformConfig, type WaveformDataObject, type ZoomLevel, annotationMinDurationTicks, appendPeaks, appendToAudioBuffer, applyFadeIn, applyFadeOut, audibleLatencySamples, buildSpectrogramCanvasId, calculateDuration, carveClipRange, clipDurationTime, clipEndTime, clipOffsetTime, clipPixelWidth, clipStartTime, clipsOverlap, computeMusicalTicks, concatenateAudioData, createAudioBuffer, createClip, createClipFromSeconds, createClipFromTicks, createTimeline, createTrack, dBToNormalized, detectMeterChanges, enumerateMicrophones, exponentialCurve, findGaps, gainToDb, gainToNormalized, generateCurve, generatePeaks, getClipsAtSample, getClipsInRange, getShortcutLabel, handleKeyboardEvent, linearCurve, logarithmicCurve, matchesKeyBinding, normalizedToDb, parseSpectrogramCanvasId, pixelsToSamples, pixelsToSeconds, probeRangeSupport, resolveAnnotationShortcuts, resolveRecordingOffsetSamples, sCurveCurve, samplesToPixels, samplesToSeconds, samplesToTicks, secondsToPixels, secondsToSamples, snapTickToGrid, snapToGrid, snapToTicks, sortClipsByTime, ticksPerBar, ticksPerBeat, ticksToBarBeat, ticksToSamples, trackChannelCount, updateAnnotationBoundaries, watchMicrophoneDevices };