@waveform-playlist/core 12.4.0 → 12.6.0
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 +108 -1
- package/dist/index.d.ts +108 -1
- package/dist/index.js +118 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +112 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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
|
@@ -880,6 +880,37 @@ declare const SPECTROGRAM_DEFAULTS: Required<Omit<SpectrogramConfig, 'maxFrequen
|
|
|
880
880
|
/** Default color map when none is specified. */
|
|
881
881
|
declare const DEFAULT_SPECTROGRAM_COLOR_MAP: ColorMapValue;
|
|
882
882
|
|
|
883
|
+
/**
|
|
884
|
+
* Punch-in replace: carve a sample range out of a clip list.
|
|
885
|
+
*
|
|
886
|
+
* Used when a newly recorded clip lands at the playhead and must REPLACE any
|
|
887
|
+
* existing clip content between its start and end (issue #579): partial
|
|
888
|
+
* overlaps are trimmed, fully-covered clips are removed, and a clip that
|
|
889
|
+
* spans the whole range is split into two.
|
|
890
|
+
*/
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Return a new clip list with the sample range [rangeStart, rangeEnd)
|
|
894
|
+
* removed from every clip that overlaps it.
|
|
895
|
+
*
|
|
896
|
+
* - Clips outside the range are returned by reference (untouched).
|
|
897
|
+
* - Clips fully inside the range are dropped.
|
|
898
|
+
* - A clip overlapping only the range's start keeps its head (right-trim).
|
|
899
|
+
* - A clip overlapping only the range's end keeps its tail: its start moves
|
|
900
|
+
* to rangeEnd and `offsetSamples` advances by the carved amount.
|
|
901
|
+
* - A clip containing the whole range splits into head + tail; the tail is a
|
|
902
|
+
* new clip (deterministic id `<original>-carve-<rangeEnd>`) sharing the
|
|
903
|
+
* same audioBuffer.
|
|
904
|
+
*
|
|
905
|
+
* Clips whose `startSample` changes lose their `startTick` — a sample-space
|
|
906
|
+
* carve cannot recompute ticks, and the engine re-enriches clips without
|
|
907
|
+
* `startTick` on ingestion (see AudioClip.startTick).
|
|
908
|
+
*
|
|
909
|
+
* Pure and immutable: input clips are never mutated. An empty or inverted
|
|
910
|
+
* range returns the input list unchanged.
|
|
911
|
+
*/
|
|
912
|
+
declare function carveClipRange(clips: readonly AudioClip[], rangeStart: number, rangeEnd: number): AudioClip[];
|
|
913
|
+
|
|
883
914
|
/** Clip start position in seconds */
|
|
884
915
|
declare function clipStartTime(clip: AudioClip): number;
|
|
885
916
|
/** Clip end position in seconds (start + duration) */
|
|
@@ -901,6 +932,35 @@ declare function trackChannelCount(track: ClipTrack): number;
|
|
|
901
932
|
*/
|
|
902
933
|
declare function clipPixelWidth(startSample: number, durationSamples: number, samplesPerPixel: number): number;
|
|
903
934
|
|
|
935
|
+
/**
|
|
936
|
+
* Canonical spectrogram canvas-ID contract.
|
|
937
|
+
*
|
|
938
|
+
* Spectrogram canvases are identified by `${clipId}-ch${channelIndex}-chunk${chunkIndex}`.
|
|
939
|
+
* This format is produced (builders) and consumed (parsers) by several packages
|
|
940
|
+
* — the React `SpectrogramProvider`/`SpectrogramChannel`, the `<daw-spectrogram>`
|
|
941
|
+
* Lit element, and the `@dawcore/spectrogram` worker pool. Single-sourcing the
|
|
942
|
+
* build + parse here prevents the drift class where a format change lands in some
|
|
943
|
+
* sites but not others (the pool's no-match fallback then routes every channel to
|
|
944
|
+
* worker 0 → channel 0's data rendered for all channels — #556).
|
|
945
|
+
*
|
|
946
|
+
* `@waveform-playlist/core` is the home because it is zero-dependency and already
|
|
947
|
+
* a dependency of every producer and consumer (unlike `@dawcore/spectrogram`,
|
|
948
|
+
* which `@waveform-playlist/ui-components` does not depend on). The format is a
|
|
949
|
+
* pure string contract with no FFT/compute dependency.
|
|
950
|
+
*/
|
|
951
|
+
interface SpectrogramCanvasIdParts {
|
|
952
|
+
clipId: string;
|
|
953
|
+
channelIndex: number;
|
|
954
|
+
chunkIndex: number;
|
|
955
|
+
}
|
|
956
|
+
/** Build a spectrogram canvas ID from its parts. */
|
|
957
|
+
declare function buildSpectrogramCanvasId(parts: SpectrogramCanvasIdParts): string;
|
|
958
|
+
/**
|
|
959
|
+
* Parse a spectrogram canvas ID into its parts, or `null` when it doesn't match
|
|
960
|
+
* the `${clipId}-ch${channelIndex}-chunk${chunkIndex}` format.
|
|
961
|
+
*/
|
|
962
|
+
declare function parseSpectrogramCanvasId(canvasId: string): SpectrogramCanvasIdParts | null;
|
|
963
|
+
|
|
904
964
|
/**
|
|
905
965
|
* Fade curve utilities for Web Audio API
|
|
906
966
|
*
|
|
@@ -1007,4 +1067,51 @@ type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
|
|
|
1007
1067
|
*/
|
|
1008
1068
|
declare function probeRangeSupport(url: string, fetchImpl?: FetchLike): Promise<RangeSupport>;
|
|
1009
1069
|
|
|
1010
|
-
|
|
1070
|
+
/**
|
|
1071
|
+
* Framework-agnostic microphone enumeration.
|
|
1072
|
+
*
|
|
1073
|
+
* Works without requesting a stream, but browsers redact device information
|
|
1074
|
+
* until microphone permission is granted: labels are empty everywhere, and
|
|
1075
|
+
* Chrome/Safari typically also redact deviceIds (often exposing a single
|
|
1076
|
+
* placeholder entry). The `hasLabels` flag tells consumers whether the list
|
|
1077
|
+
* is a usable picker or just a "microphones exist" signal — re-enumerate
|
|
1078
|
+
* after a successful getUserMedia to get the real list.
|
|
1079
|
+
*
|
|
1080
|
+
* Follows the probeRangeSupport pattern: the browser API is injectable for
|
|
1081
|
+
* tests and non-browser environments degrade gracefully.
|
|
1082
|
+
*/
|
|
1083
|
+
interface MicrophoneDeviceInfo {
|
|
1084
|
+
deviceId: string;
|
|
1085
|
+
/** Real device label, or a generated fallback name when redacted. */
|
|
1086
|
+
label: string;
|
|
1087
|
+
groupId: string;
|
|
1088
|
+
}
|
|
1089
|
+
interface MicrophoneEnumeration {
|
|
1090
|
+
/** False when enumeration is unavailable — SSR/Node, insecure context
|
|
1091
|
+
* (mediaDevices requires HTTPS or localhost), or a very old browser. */
|
|
1092
|
+
supported: boolean;
|
|
1093
|
+
/** True when the browser exposed real device labels. False before
|
|
1094
|
+
* microphone permission is granted (labels — and often deviceIds — are
|
|
1095
|
+
* redacted); the `devices` labels are then generated fallbacks. */
|
|
1096
|
+
hasLabels: boolean;
|
|
1097
|
+
devices: MicrophoneDeviceInfo[];
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* List audio input devices without requesting a stream.
|
|
1101
|
+
*
|
|
1102
|
+
* @param mediaDevices - Injectable for tests; defaults to `navigator.mediaDevices`.
|
|
1103
|
+
*/
|
|
1104
|
+
declare function enumerateMicrophones(mediaDevices?: MediaDevices): Promise<MicrophoneEnumeration>;
|
|
1105
|
+
/**
|
|
1106
|
+
* Subscribe to the microphone device list: the listener fires once with the
|
|
1107
|
+
* initial enumeration and again on every `devicechange` (hot-plug, permission
|
|
1108
|
+
* grant in some browsers). Returns an unsubscribe function.
|
|
1109
|
+
*
|
|
1110
|
+
* Enumeration failures are logged and skipped — the subscription survives a
|
|
1111
|
+
* transient failure and delivers the next successful enumeration.
|
|
1112
|
+
*
|
|
1113
|
+
* @param mediaDevices - Injectable for tests; defaults to `navigator.mediaDevices`.
|
|
1114
|
+
*/
|
|
1115
|
+
declare function watchMicrophoneDevices(listener: (enumeration: MicrophoneEnumeration) => void, mediaDevices?: MediaDevices): () => void;
|
|
1116
|
+
|
|
1117
|
+
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 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, 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, normalizedToDb, parseSpectrogramCanvasId, pixelsToSamples, pixelsToSeconds, probeRangeSupport, resolveRecordingOffsetSamples, sCurveCurve, samplesToPixels, samplesToSeconds, samplesToTicks, secondsToPixels, secondsToSamples, snapTickToGrid, snapToGrid, snapToTicks, sortClipsByTime, ticksPerBar, ticksPerBeat, ticksToSamples, trackChannelCount, watchMicrophoneDevices };
|
package/dist/index.d.ts
CHANGED
|
@@ -880,6 +880,37 @@ declare const SPECTROGRAM_DEFAULTS: Required<Omit<SpectrogramConfig, 'maxFrequen
|
|
|
880
880
|
/** Default color map when none is specified. */
|
|
881
881
|
declare const DEFAULT_SPECTROGRAM_COLOR_MAP: ColorMapValue;
|
|
882
882
|
|
|
883
|
+
/**
|
|
884
|
+
* Punch-in replace: carve a sample range out of a clip list.
|
|
885
|
+
*
|
|
886
|
+
* Used when a newly recorded clip lands at the playhead and must REPLACE any
|
|
887
|
+
* existing clip content between its start and end (issue #579): partial
|
|
888
|
+
* overlaps are trimmed, fully-covered clips are removed, and a clip that
|
|
889
|
+
* spans the whole range is split into two.
|
|
890
|
+
*/
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Return a new clip list with the sample range [rangeStart, rangeEnd)
|
|
894
|
+
* removed from every clip that overlaps it.
|
|
895
|
+
*
|
|
896
|
+
* - Clips outside the range are returned by reference (untouched).
|
|
897
|
+
* - Clips fully inside the range are dropped.
|
|
898
|
+
* - A clip overlapping only the range's start keeps its head (right-trim).
|
|
899
|
+
* - A clip overlapping only the range's end keeps its tail: its start moves
|
|
900
|
+
* to rangeEnd and `offsetSamples` advances by the carved amount.
|
|
901
|
+
* - A clip containing the whole range splits into head + tail; the tail is a
|
|
902
|
+
* new clip (deterministic id `<original>-carve-<rangeEnd>`) sharing the
|
|
903
|
+
* same audioBuffer.
|
|
904
|
+
*
|
|
905
|
+
* Clips whose `startSample` changes lose their `startTick` — a sample-space
|
|
906
|
+
* carve cannot recompute ticks, and the engine re-enriches clips without
|
|
907
|
+
* `startTick` on ingestion (see AudioClip.startTick).
|
|
908
|
+
*
|
|
909
|
+
* Pure and immutable: input clips are never mutated. An empty or inverted
|
|
910
|
+
* range returns the input list unchanged.
|
|
911
|
+
*/
|
|
912
|
+
declare function carveClipRange(clips: readonly AudioClip[], rangeStart: number, rangeEnd: number): AudioClip[];
|
|
913
|
+
|
|
883
914
|
/** Clip start position in seconds */
|
|
884
915
|
declare function clipStartTime(clip: AudioClip): number;
|
|
885
916
|
/** Clip end position in seconds (start + duration) */
|
|
@@ -901,6 +932,35 @@ declare function trackChannelCount(track: ClipTrack): number;
|
|
|
901
932
|
*/
|
|
902
933
|
declare function clipPixelWidth(startSample: number, durationSamples: number, samplesPerPixel: number): number;
|
|
903
934
|
|
|
935
|
+
/**
|
|
936
|
+
* Canonical spectrogram canvas-ID contract.
|
|
937
|
+
*
|
|
938
|
+
* Spectrogram canvases are identified by `${clipId}-ch${channelIndex}-chunk${chunkIndex}`.
|
|
939
|
+
* This format is produced (builders) and consumed (parsers) by several packages
|
|
940
|
+
* — the React `SpectrogramProvider`/`SpectrogramChannel`, the `<daw-spectrogram>`
|
|
941
|
+
* Lit element, and the `@dawcore/spectrogram` worker pool. Single-sourcing the
|
|
942
|
+
* build + parse here prevents the drift class where a format change lands in some
|
|
943
|
+
* sites but not others (the pool's no-match fallback then routes every channel to
|
|
944
|
+
* worker 0 → channel 0's data rendered for all channels — #556).
|
|
945
|
+
*
|
|
946
|
+
* `@waveform-playlist/core` is the home because it is zero-dependency and already
|
|
947
|
+
* a dependency of every producer and consumer (unlike `@dawcore/spectrogram`,
|
|
948
|
+
* which `@waveform-playlist/ui-components` does not depend on). The format is a
|
|
949
|
+
* pure string contract with no FFT/compute dependency.
|
|
950
|
+
*/
|
|
951
|
+
interface SpectrogramCanvasIdParts {
|
|
952
|
+
clipId: string;
|
|
953
|
+
channelIndex: number;
|
|
954
|
+
chunkIndex: number;
|
|
955
|
+
}
|
|
956
|
+
/** Build a spectrogram canvas ID from its parts. */
|
|
957
|
+
declare function buildSpectrogramCanvasId(parts: SpectrogramCanvasIdParts): string;
|
|
958
|
+
/**
|
|
959
|
+
* Parse a spectrogram canvas ID into its parts, or `null` when it doesn't match
|
|
960
|
+
* the `${clipId}-ch${channelIndex}-chunk${chunkIndex}` format.
|
|
961
|
+
*/
|
|
962
|
+
declare function parseSpectrogramCanvasId(canvasId: string): SpectrogramCanvasIdParts | null;
|
|
963
|
+
|
|
904
964
|
/**
|
|
905
965
|
* Fade curve utilities for Web Audio API
|
|
906
966
|
*
|
|
@@ -1007,4 +1067,51 @@ type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
|
|
|
1007
1067
|
*/
|
|
1008
1068
|
declare function probeRangeSupport(url: string, fetchImpl?: FetchLike): Promise<RangeSupport>;
|
|
1009
1069
|
|
|
1010
|
-
|
|
1070
|
+
/**
|
|
1071
|
+
* Framework-agnostic microphone enumeration.
|
|
1072
|
+
*
|
|
1073
|
+
* Works without requesting a stream, but browsers redact device information
|
|
1074
|
+
* until microphone permission is granted: labels are empty everywhere, and
|
|
1075
|
+
* Chrome/Safari typically also redact deviceIds (often exposing a single
|
|
1076
|
+
* placeholder entry). The `hasLabels` flag tells consumers whether the list
|
|
1077
|
+
* is a usable picker or just a "microphones exist" signal — re-enumerate
|
|
1078
|
+
* after a successful getUserMedia to get the real list.
|
|
1079
|
+
*
|
|
1080
|
+
* Follows the probeRangeSupport pattern: the browser API is injectable for
|
|
1081
|
+
* tests and non-browser environments degrade gracefully.
|
|
1082
|
+
*/
|
|
1083
|
+
interface MicrophoneDeviceInfo {
|
|
1084
|
+
deviceId: string;
|
|
1085
|
+
/** Real device label, or a generated fallback name when redacted. */
|
|
1086
|
+
label: string;
|
|
1087
|
+
groupId: string;
|
|
1088
|
+
}
|
|
1089
|
+
interface MicrophoneEnumeration {
|
|
1090
|
+
/** False when enumeration is unavailable — SSR/Node, insecure context
|
|
1091
|
+
* (mediaDevices requires HTTPS or localhost), or a very old browser. */
|
|
1092
|
+
supported: boolean;
|
|
1093
|
+
/** True when the browser exposed real device labels. False before
|
|
1094
|
+
* microphone permission is granted (labels — and often deviceIds — are
|
|
1095
|
+
* redacted); the `devices` labels are then generated fallbacks. */
|
|
1096
|
+
hasLabels: boolean;
|
|
1097
|
+
devices: MicrophoneDeviceInfo[];
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* List audio input devices without requesting a stream.
|
|
1101
|
+
*
|
|
1102
|
+
* @param mediaDevices - Injectable for tests; defaults to `navigator.mediaDevices`.
|
|
1103
|
+
*/
|
|
1104
|
+
declare function enumerateMicrophones(mediaDevices?: MediaDevices): Promise<MicrophoneEnumeration>;
|
|
1105
|
+
/**
|
|
1106
|
+
* Subscribe to the microphone device list: the listener fires once with the
|
|
1107
|
+
* initial enumeration and again on every `devicechange` (hot-plug, permission
|
|
1108
|
+
* grant in some browsers). Returns an unsubscribe function.
|
|
1109
|
+
*
|
|
1110
|
+
* Enumeration failures are logged and skipped — the subscription survives a
|
|
1111
|
+
* transient failure and delivers the next successful enumeration.
|
|
1112
|
+
*
|
|
1113
|
+
* @param mediaDevices - Injectable for tests; defaults to `navigator.mediaDevices`.
|
|
1114
|
+
*/
|
|
1115
|
+
declare function watchMicrophoneDevices(listener: (enumeration: MicrophoneEnumeration) => void, mediaDevices?: MediaDevices): () => void;
|
|
1116
|
+
|
|
1117
|
+
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 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, 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, normalizedToDb, parseSpectrogramCanvasId, pixelsToSamples, pixelsToSeconds, probeRangeSupport, resolveRecordingOffsetSamples, sCurveCurve, samplesToPixels, samplesToSeconds, samplesToTicks, secondsToPixels, secondsToSamples, snapTickToGrid, snapToGrid, snapToTicks, sortClipsByTime, ticksPerBar, ticksPerBeat, ticksToSamples, trackChannelCount, watchMicrophoneDevices };
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,9 @@ __export(index_exports, {
|
|
|
31
31
|
applyFadeIn: () => applyFadeIn,
|
|
32
32
|
applyFadeOut: () => applyFadeOut,
|
|
33
33
|
audibleLatencySamples: () => audibleLatencySamples,
|
|
34
|
+
buildSpectrogramCanvasId: () => buildSpectrogramCanvasId,
|
|
34
35
|
calculateDuration: () => calculateDuration,
|
|
36
|
+
carveClipRange: () => carveClipRange,
|
|
35
37
|
clipDurationTime: () => clipDurationTime,
|
|
36
38
|
clipEndTime: () => clipEndTime,
|
|
37
39
|
clipOffsetTime: () => clipOffsetTime,
|
|
@@ -48,6 +50,7 @@ __export(index_exports, {
|
|
|
48
50
|
createTrack: () => createTrack,
|
|
49
51
|
dBToNormalized: () => dBToNormalized,
|
|
50
52
|
detectMeterChanges: () => detectMeterChanges,
|
|
53
|
+
enumerateMicrophones: () => enumerateMicrophones,
|
|
51
54
|
exponentialCurve: () => exponentialCurve,
|
|
52
55
|
findGaps: () => findGaps,
|
|
53
56
|
gainToDb: () => gainToDb,
|
|
@@ -61,6 +64,7 @@ __export(index_exports, {
|
|
|
61
64
|
linearCurve: () => linearCurve,
|
|
62
65
|
logarithmicCurve: () => logarithmicCurve,
|
|
63
66
|
normalizedToDb: () => normalizedToDb,
|
|
67
|
+
parseSpectrogramCanvasId: () => parseSpectrogramCanvasId,
|
|
64
68
|
pixelsToSamples: () => pixelsToSamples,
|
|
65
69
|
pixelsToSeconds: () => pixelsToSeconds,
|
|
66
70
|
probeRangeSupport: () => probeRangeSupport,
|
|
@@ -78,7 +82,8 @@ __export(index_exports, {
|
|
|
78
82
|
ticksPerBar: () => ticksPerBar,
|
|
79
83
|
ticksPerBeat: () => ticksPerBeat,
|
|
80
84
|
ticksToSamples: () => ticksToSamples,
|
|
81
|
-
trackChannelCount: () => trackChannelCount
|
|
85
|
+
trackChannelCount: () => trackChannelCount,
|
|
86
|
+
watchMicrophoneDevices: () => watchMicrophoneDevices
|
|
82
87
|
});
|
|
83
88
|
module.exports = __toCommonJS(index_exports);
|
|
84
89
|
|
|
@@ -719,6 +724,44 @@ var SPECTROGRAM_DEFAULTS = {
|
|
|
719
724
|
};
|
|
720
725
|
var DEFAULT_SPECTROGRAM_COLOR_MAP = "viridis";
|
|
721
726
|
|
|
727
|
+
// src/utils/carveClipRange.ts
|
|
728
|
+
function carveClipRange(clips, rangeStart, rangeEnd) {
|
|
729
|
+
if (!(rangeEnd > rangeStart)) {
|
|
730
|
+
return [...clips];
|
|
731
|
+
}
|
|
732
|
+
const result = [];
|
|
733
|
+
for (const clip of clips) {
|
|
734
|
+
const clipStart = clip.startSample;
|
|
735
|
+
const clipEnd = clip.startSample + clip.durationSamples;
|
|
736
|
+
if (clipEnd <= rangeStart || clipStart >= rangeEnd) {
|
|
737
|
+
result.push(clip);
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
const keepHead = clipStart < rangeStart;
|
|
741
|
+
const keepTail = clipEnd > rangeEnd;
|
|
742
|
+
if (keepHead) {
|
|
743
|
+
result.push({
|
|
744
|
+
...clip,
|
|
745
|
+
durationSamples: rangeStart - clipStart
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
if (keepTail) {
|
|
749
|
+
const carvedFromClipStart = rangeEnd - clipStart;
|
|
750
|
+
const tail = {
|
|
751
|
+
...clip,
|
|
752
|
+
// Head + tail from one clip must not share an id
|
|
753
|
+
id: keepHead ? `${clip.id}-carve-${rangeEnd}` : clip.id,
|
|
754
|
+
startSample: rangeEnd,
|
|
755
|
+
durationSamples: clipEnd - rangeEnd,
|
|
756
|
+
offsetSamples: clip.offsetSamples + carvedFromClipStart
|
|
757
|
+
};
|
|
758
|
+
delete tail.startTick;
|
|
759
|
+
result.push(tail);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return result;
|
|
763
|
+
}
|
|
764
|
+
|
|
722
765
|
// src/clipTimeHelpers.ts
|
|
723
766
|
function clipStartTime(clip) {
|
|
724
767
|
return clip.startSample / clip.sampleRate;
|
|
@@ -742,6 +785,21 @@ function clipPixelWidth(startSample, durationSamples, samplesPerPixel) {
|
|
|
742
785
|
return Math.floor((startSample + durationSamples) / samplesPerPixel) - Math.floor(startSample / samplesPerPixel);
|
|
743
786
|
}
|
|
744
787
|
|
|
788
|
+
// src/spectrogramCanvasId.ts
|
|
789
|
+
var CANVAS_ID_RE = /^(.+)-ch(\d+)-chunk(\d+)$/;
|
|
790
|
+
function buildSpectrogramCanvasId(parts) {
|
|
791
|
+
return `${parts.clipId}-ch${parts.channelIndex}-chunk${parts.chunkIndex}`;
|
|
792
|
+
}
|
|
793
|
+
function parseSpectrogramCanvasId(canvasId) {
|
|
794
|
+
const match = canvasId.match(CANVAS_ID_RE);
|
|
795
|
+
if (!match) return null;
|
|
796
|
+
return {
|
|
797
|
+
clipId: match[1],
|
|
798
|
+
channelIndex: parseInt(match[2], 10),
|
|
799
|
+
chunkIndex: parseInt(match[3], 10)
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
|
|
745
803
|
// src/fades.ts
|
|
746
804
|
function linearCurve(length, fadeIn) {
|
|
747
805
|
const curve = new Float32Array(length);
|
|
@@ -893,6 +951,59 @@ async function probeRangeSupport(url, fetchImpl = fetch) {
|
|
|
893
951
|
controller.abort();
|
|
894
952
|
}
|
|
895
953
|
}
|
|
954
|
+
|
|
955
|
+
// src/enumerateMicrophones.ts
|
|
956
|
+
var UNSUPPORTED = Object.freeze({
|
|
957
|
+
supported: false,
|
|
958
|
+
hasLabels: false,
|
|
959
|
+
devices: []
|
|
960
|
+
});
|
|
961
|
+
function resolveMediaDevices(mediaDevices) {
|
|
962
|
+
if (mediaDevices) return mediaDevices;
|
|
963
|
+
return typeof navigator !== "undefined" ? navigator.mediaDevices : void 0;
|
|
964
|
+
}
|
|
965
|
+
async function enumerateMicrophones(mediaDevices) {
|
|
966
|
+
const md = resolveMediaDevices(mediaDevices);
|
|
967
|
+
if (!md || typeof md.enumerateDevices !== "function") {
|
|
968
|
+
return UNSUPPORTED;
|
|
969
|
+
}
|
|
970
|
+
const all = await md.enumerateDevices();
|
|
971
|
+
const inputs = all.filter((device) => device.kind === "audioinput");
|
|
972
|
+
const hasLabels = inputs.some((device) => device.label.length > 0);
|
|
973
|
+
return {
|
|
974
|
+
supported: true,
|
|
975
|
+
hasLabels,
|
|
976
|
+
devices: inputs.map((device, index) => ({
|
|
977
|
+
deviceId: device.deviceId,
|
|
978
|
+
label: device.label || // Pre-permission fallbacks: Chrome redacts deviceId to '' as well,
|
|
979
|
+
// so fall through to a positional name when there's nothing to slice.
|
|
980
|
+
(device.deviceId ? `Microphone ${device.deviceId.slice(0, 8)}` : `Microphone ${index + 1}`),
|
|
981
|
+
groupId: device.groupId
|
|
982
|
+
}))
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
function watchMicrophoneDevices(listener, mediaDevices) {
|
|
986
|
+
const md = resolveMediaDevices(mediaDevices);
|
|
987
|
+
if (!md || typeof md.enumerateDevices !== "function") {
|
|
988
|
+
queueMicrotask(() => listener(UNSUPPORTED));
|
|
989
|
+
return () => {
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
let active = true;
|
|
993
|
+
const deliver = () => {
|
|
994
|
+
enumerateMicrophones(md).then((result) => {
|
|
995
|
+
if (active) listener(result);
|
|
996
|
+
}).catch((err) => {
|
|
997
|
+
console.warn("[waveform-playlist] Microphone enumeration failed:", String(err));
|
|
998
|
+
});
|
|
999
|
+
};
|
|
1000
|
+
md.addEventListener("devicechange", deliver);
|
|
1001
|
+
deliver();
|
|
1002
|
+
return () => {
|
|
1003
|
+
active = false;
|
|
1004
|
+
md.removeEventListener("devicechange", deliver);
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
896
1007
|
// Annotate the CommonJS export names for ESM import in node:
|
|
897
1008
|
0 && (module.exports = {
|
|
898
1009
|
DEFAULT_SPECTROGRAM_COLOR_MAP,
|
|
@@ -906,7 +1017,9 @@ async function probeRangeSupport(url, fetchImpl = fetch) {
|
|
|
906
1017
|
applyFadeIn,
|
|
907
1018
|
applyFadeOut,
|
|
908
1019
|
audibleLatencySamples,
|
|
1020
|
+
buildSpectrogramCanvasId,
|
|
909
1021
|
calculateDuration,
|
|
1022
|
+
carveClipRange,
|
|
910
1023
|
clipDurationTime,
|
|
911
1024
|
clipEndTime,
|
|
912
1025
|
clipOffsetTime,
|
|
@@ -923,6 +1036,7 @@ async function probeRangeSupport(url, fetchImpl = fetch) {
|
|
|
923
1036
|
createTrack,
|
|
924
1037
|
dBToNormalized,
|
|
925
1038
|
detectMeterChanges,
|
|
1039
|
+
enumerateMicrophones,
|
|
926
1040
|
exponentialCurve,
|
|
927
1041
|
findGaps,
|
|
928
1042
|
gainToDb,
|
|
@@ -936,6 +1050,7 @@ async function probeRangeSupport(url, fetchImpl = fetch) {
|
|
|
936
1050
|
linearCurve,
|
|
937
1051
|
logarithmicCurve,
|
|
938
1052
|
normalizedToDb,
|
|
1053
|
+
parseSpectrogramCanvasId,
|
|
939
1054
|
pixelsToSamples,
|
|
940
1055
|
pixelsToSeconds,
|
|
941
1056
|
probeRangeSupport,
|
|
@@ -953,6 +1068,7 @@ async function probeRangeSupport(url, fetchImpl = fetch) {
|
|
|
953
1068
|
ticksPerBar,
|
|
954
1069
|
ticksPerBeat,
|
|
955
1070
|
ticksToSamples,
|
|
956
|
-
trackChannelCount
|
|
1071
|
+
trackChannelCount,
|
|
1072
|
+
watchMicrophoneDevices
|
|
957
1073
|
});
|
|
958
1074
|
//# sourceMappingURL=index.js.map
|