@waveform-playlist/core 11.2.0 → 11.3.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.d.mts +77 -1
- package/dist/index.d.ts +77 -1
- package/dist/index.js +139 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +132 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -639,6 +639,13 @@ declare function dBToNormalized(dB: number, floor?: number): number;
|
|
|
639
639
|
* @returns dB value (floor at 0, 0 dB at 1, positive dB above 1)
|
|
640
640
|
*/
|
|
641
641
|
declare function normalizedToDb(normalized: number, floor?: number): number;
|
|
642
|
+
/**
|
|
643
|
+
* Convert a linear gain value to decibels.
|
|
644
|
+
*
|
|
645
|
+
* @param gain - Linear gain (0 = silence, 1 = unity)
|
|
646
|
+
* @returns Decibel value (e.g., 0.5 → ≈ -6.02 dB)
|
|
647
|
+
*/
|
|
648
|
+
declare function gainToDb(gain: number): number;
|
|
642
649
|
/**
|
|
643
650
|
* Convert a linear gain value (0-1+) to normalized 0-1 via dB.
|
|
644
651
|
*
|
|
@@ -652,6 +659,70 @@ declare function normalizedToDb(normalized: number, floor?: number): number;
|
|
|
652
659
|
*/
|
|
653
660
|
declare function gainToNormalized(gain: number, floor?: number): number;
|
|
654
661
|
|
|
662
|
+
/** All supported snap-to-grid values. */
|
|
663
|
+
type SnapTo = 'bar' | 'beat' | '1/2' | '1/4' | '1/8' | '1/16' | '1/32' | '1/2T' | '1/4T' | '1/8T' | '1/16T' | 'off';
|
|
664
|
+
/**
|
|
665
|
+
* Returns the tick interval for the given SnapTo value.
|
|
666
|
+
*
|
|
667
|
+
* Straight subdivisions (1/2, 1/4, 1/8, 1/16, 1/32) are always expressed as
|
|
668
|
+
* fractions of a quarter note (ppqn), independent of the time signature
|
|
669
|
+
* denominator. Triplet subdivisions use × 2/3 of the corresponding straight
|
|
670
|
+
* value. 'bar' and 'beat' depend on the time signature. 'off' returns 0.
|
|
671
|
+
*/
|
|
672
|
+
declare function snapToTicks(snapTo: SnapTo, timeSignature: [number, number], ppqn?: number): number;
|
|
673
|
+
/**
|
|
674
|
+
* Three-tier tick hierarchy (following Audacity's model):
|
|
675
|
+
* major — Bar boundaries. Always labeled, strongest grid lines.
|
|
676
|
+
* minor — Beat boundaries. Labeled when wide enough, medium grid lines.
|
|
677
|
+
* minorMinor — Subdivisions (eighths, sixteenths). Never labeled, ruler ticks only (no grid).
|
|
678
|
+
*/
|
|
679
|
+
type TickType = 'major' | 'minor' | 'minorMinor';
|
|
680
|
+
/** Zoom level category used to select which subdivision to iterate at. */
|
|
681
|
+
type ZoomLevel = 'coarse' | 'bar' | 'beat' | 'eighth' | 'sixteenth';
|
|
682
|
+
/** A single musical tick with rendering metadata. */
|
|
683
|
+
interface MusicalTick {
|
|
684
|
+
/** Pixel position of the tick in the timeline. */
|
|
685
|
+
pixel: number;
|
|
686
|
+
/** Three-tier type: major (bar), minor (beat), minorMinor (subdivision). */
|
|
687
|
+
type: TickType;
|
|
688
|
+
/** Human-readable label. Present for major ticks always; minor ticks when zoomed in. */
|
|
689
|
+
label?: string;
|
|
690
|
+
/** 0-based global bar index (for alternating bar-level striping). */
|
|
691
|
+
barIndex: number;
|
|
692
|
+
}
|
|
693
|
+
/** Result of computeMusicalTicks(). */
|
|
694
|
+
interface MusicalTickData {
|
|
695
|
+
ticks: MusicalTick[];
|
|
696
|
+
pixelsPerBar: number;
|
|
697
|
+
pixelsPerBeat: number;
|
|
698
|
+
zoomLevel: ZoomLevel;
|
|
699
|
+
/** At 'coarse' zoom: how many bars between rendered tick lines. */
|
|
700
|
+
coarseBarStep?: number;
|
|
701
|
+
}
|
|
702
|
+
/** Parameters for computeMusicalTicks(). */
|
|
703
|
+
interface MusicalTickParams {
|
|
704
|
+
timeSignature: [number, number];
|
|
705
|
+
/** Ticks per pixel (zoom level — lower value = more zoomed in). */
|
|
706
|
+
ticksPerPixel: number;
|
|
707
|
+
startPixel: number;
|
|
708
|
+
endPixel: number;
|
|
709
|
+
/** Pulses per quarter note. Defaults to 960. */
|
|
710
|
+
ppqn?: number;
|
|
711
|
+
}
|
|
712
|
+
/** Minimum pixels per musical unit before switching to a coarser zoom level. */
|
|
713
|
+
declare const MIN_PIXELS_PER_UNIT = 8;
|
|
714
|
+
/**
|
|
715
|
+
* Determines the zoom level and computes which tick lines to render for a
|
|
716
|
+
* given viewport. Pure tick arithmetic — no BPM or sample rate required.
|
|
717
|
+
*/
|
|
718
|
+
declare function computeMusicalTicks(params: MusicalTickParams): MusicalTickData;
|
|
719
|
+
/**
|
|
720
|
+
* Snaps a tick position to the nearest grid boundary defined by `snapTo`.
|
|
721
|
+
*
|
|
722
|
+
* Returns the original tick unchanged when `snapTo` is 'off'.
|
|
723
|
+
*/
|
|
724
|
+
declare function snapTickToGrid(tick: number, snapTo: SnapTo, timeSignature: [number, number], ppqn?: number): number;
|
|
725
|
+
|
|
655
726
|
/** Clip start position in seconds */
|
|
656
727
|
declare function clipStartTime(clip: AudioClip): number;
|
|
657
728
|
/** Clip end position in seconds (start + duration) */
|
|
@@ -660,6 +731,11 @@ declare function clipEndTime(clip: AudioClip): number;
|
|
|
660
731
|
declare function clipOffsetTime(clip: AudioClip): number;
|
|
661
732
|
/** Clip duration in seconds */
|
|
662
733
|
declare function clipDurationTime(clip: AudioClip): number;
|
|
734
|
+
/**
|
|
735
|
+
* Max audio channel count across a track's clips.
|
|
736
|
+
* Used to set Panner channelCount and offline render output channels.
|
|
737
|
+
*/
|
|
738
|
+
declare function trackChannelCount(track: ClipTrack): number;
|
|
663
739
|
/**
|
|
664
740
|
* Clip width in pixels at a given samplesPerPixel.
|
|
665
741
|
* Shared by Clip.tsx (container sizing) and ChannelWithProgress.tsx (progress overlay)
|
|
@@ -745,4 +821,4 @@ declare function handleKeyboardEvent(event: KeyboardEvent, shortcuts: KeyboardSh
|
|
|
745
821
|
*/
|
|
746
822
|
declare const getShortcutLabel: (shortcut: KeyboardShortcut) => string;
|
|
747
823
|
|
|
748
|
-
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 CreateTrackOptions, type FFTSize, type Fade, type FadeConfig, type FadeType, type Gap, InteractionState, type KeyboardShortcut, MAX_CANVAS_WIDTH, type MidiNoteData, PPQN, type PeakData, type Peaks, type PlaylistConfig, type PlayoutState, type RenderAnnotationItemProps, type RenderMode, type SpectrogramComputeConfig, type SpectrogramConfig, type SpectrogramData, type SpectrogramDisplayConfig, type TimeSelection, type Timeline, type Track, type TrackEffectsFunction, type TrackSpectrogramOverrides, type WaveformConfig, type WaveformDataObject, applyFadeIn, applyFadeOut, clipDurationTime, clipEndTime, clipOffsetTime, clipPixelWidth, clipStartTime, clipsOverlap, createClip, createClipFromSeconds, createTimeline, createTrack, dBToNormalized, exponentialCurve, findGaps, gainToNormalized, generateCurve, getClipsAtSample, getClipsInRange, getShortcutLabel, handleKeyboardEvent, linearCurve, logarithmicCurve, normalizedToDb, pixelsToSamples, pixelsToSeconds, sCurveCurve, samplesToPixels, samplesToSeconds, samplesToTicks, secondsToPixels, secondsToSamples, snapToGrid, sortClipsByTime, ticksPerBar, ticksPerBeat, ticksToBarBeatLabel, ticksToSamples };
|
|
824
|
+
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 CreateTrackOptions, type FFTSize, type Fade, type FadeConfig, type FadeType, type Gap, InteractionState, type KeyboardShortcut, MAX_CANVAS_WIDTH, MIN_PIXELS_PER_UNIT, type MidiNoteData, type MusicalTick, type MusicalTickData, type MusicalTickParams, PPQN, type PeakData, type Peaks, type PlaylistConfig, type PlayoutState, type RenderAnnotationItemProps, type RenderMode, type SnapTo, 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, applyFadeIn, applyFadeOut, clipDurationTime, clipEndTime, clipOffsetTime, clipPixelWidth, clipStartTime, clipsOverlap, computeMusicalTicks, createClip, createClipFromSeconds, createTimeline, createTrack, dBToNormalized, exponentialCurve, findGaps, gainToDb, gainToNormalized, generateCurve, getClipsAtSample, getClipsInRange, getShortcutLabel, handleKeyboardEvent, linearCurve, logarithmicCurve, normalizedToDb, pixelsToSamples, pixelsToSeconds, sCurveCurve, samplesToPixels, samplesToSeconds, samplesToTicks, secondsToPixels, secondsToSamples, snapTickToGrid, snapToGrid, snapToTicks, sortClipsByTime, ticksPerBar, ticksPerBeat, ticksToBarBeatLabel, ticksToSamples, trackChannelCount };
|
package/dist/index.d.ts
CHANGED
|
@@ -639,6 +639,13 @@ declare function dBToNormalized(dB: number, floor?: number): number;
|
|
|
639
639
|
* @returns dB value (floor at 0, 0 dB at 1, positive dB above 1)
|
|
640
640
|
*/
|
|
641
641
|
declare function normalizedToDb(normalized: number, floor?: number): number;
|
|
642
|
+
/**
|
|
643
|
+
* Convert a linear gain value to decibels.
|
|
644
|
+
*
|
|
645
|
+
* @param gain - Linear gain (0 = silence, 1 = unity)
|
|
646
|
+
* @returns Decibel value (e.g., 0.5 → ≈ -6.02 dB)
|
|
647
|
+
*/
|
|
648
|
+
declare function gainToDb(gain: number): number;
|
|
642
649
|
/**
|
|
643
650
|
* Convert a linear gain value (0-1+) to normalized 0-1 via dB.
|
|
644
651
|
*
|
|
@@ -652,6 +659,70 @@ declare function normalizedToDb(normalized: number, floor?: number): number;
|
|
|
652
659
|
*/
|
|
653
660
|
declare function gainToNormalized(gain: number, floor?: number): number;
|
|
654
661
|
|
|
662
|
+
/** All supported snap-to-grid values. */
|
|
663
|
+
type SnapTo = 'bar' | 'beat' | '1/2' | '1/4' | '1/8' | '1/16' | '1/32' | '1/2T' | '1/4T' | '1/8T' | '1/16T' | 'off';
|
|
664
|
+
/**
|
|
665
|
+
* Returns the tick interval for the given SnapTo value.
|
|
666
|
+
*
|
|
667
|
+
* Straight subdivisions (1/2, 1/4, 1/8, 1/16, 1/32) are always expressed as
|
|
668
|
+
* fractions of a quarter note (ppqn), independent of the time signature
|
|
669
|
+
* denominator. Triplet subdivisions use × 2/3 of the corresponding straight
|
|
670
|
+
* value. 'bar' and 'beat' depend on the time signature. 'off' returns 0.
|
|
671
|
+
*/
|
|
672
|
+
declare function snapToTicks(snapTo: SnapTo, timeSignature: [number, number], ppqn?: number): number;
|
|
673
|
+
/**
|
|
674
|
+
* Three-tier tick hierarchy (following Audacity's model):
|
|
675
|
+
* major — Bar boundaries. Always labeled, strongest grid lines.
|
|
676
|
+
* minor — Beat boundaries. Labeled when wide enough, medium grid lines.
|
|
677
|
+
* minorMinor — Subdivisions (eighths, sixteenths). Never labeled, ruler ticks only (no grid).
|
|
678
|
+
*/
|
|
679
|
+
type TickType = 'major' | 'minor' | 'minorMinor';
|
|
680
|
+
/** Zoom level category used to select which subdivision to iterate at. */
|
|
681
|
+
type ZoomLevel = 'coarse' | 'bar' | 'beat' | 'eighth' | 'sixteenth';
|
|
682
|
+
/** A single musical tick with rendering metadata. */
|
|
683
|
+
interface MusicalTick {
|
|
684
|
+
/** Pixel position of the tick in the timeline. */
|
|
685
|
+
pixel: number;
|
|
686
|
+
/** Three-tier type: major (bar), minor (beat), minorMinor (subdivision). */
|
|
687
|
+
type: TickType;
|
|
688
|
+
/** Human-readable label. Present for major ticks always; minor ticks when zoomed in. */
|
|
689
|
+
label?: string;
|
|
690
|
+
/** 0-based global bar index (for alternating bar-level striping). */
|
|
691
|
+
barIndex: number;
|
|
692
|
+
}
|
|
693
|
+
/** Result of computeMusicalTicks(). */
|
|
694
|
+
interface MusicalTickData {
|
|
695
|
+
ticks: MusicalTick[];
|
|
696
|
+
pixelsPerBar: number;
|
|
697
|
+
pixelsPerBeat: number;
|
|
698
|
+
zoomLevel: ZoomLevel;
|
|
699
|
+
/** At 'coarse' zoom: how many bars between rendered tick lines. */
|
|
700
|
+
coarseBarStep?: number;
|
|
701
|
+
}
|
|
702
|
+
/** Parameters for computeMusicalTicks(). */
|
|
703
|
+
interface MusicalTickParams {
|
|
704
|
+
timeSignature: [number, number];
|
|
705
|
+
/** Ticks per pixel (zoom level — lower value = more zoomed in). */
|
|
706
|
+
ticksPerPixel: number;
|
|
707
|
+
startPixel: number;
|
|
708
|
+
endPixel: number;
|
|
709
|
+
/** Pulses per quarter note. Defaults to 960. */
|
|
710
|
+
ppqn?: number;
|
|
711
|
+
}
|
|
712
|
+
/** Minimum pixels per musical unit before switching to a coarser zoom level. */
|
|
713
|
+
declare const MIN_PIXELS_PER_UNIT = 8;
|
|
714
|
+
/**
|
|
715
|
+
* Determines the zoom level and computes which tick lines to render for a
|
|
716
|
+
* given viewport. Pure tick arithmetic — no BPM or sample rate required.
|
|
717
|
+
*/
|
|
718
|
+
declare function computeMusicalTicks(params: MusicalTickParams): MusicalTickData;
|
|
719
|
+
/**
|
|
720
|
+
* Snaps a tick position to the nearest grid boundary defined by `snapTo`.
|
|
721
|
+
*
|
|
722
|
+
* Returns the original tick unchanged when `snapTo` is 'off'.
|
|
723
|
+
*/
|
|
724
|
+
declare function snapTickToGrid(tick: number, snapTo: SnapTo, timeSignature: [number, number], ppqn?: number): number;
|
|
725
|
+
|
|
655
726
|
/** Clip start position in seconds */
|
|
656
727
|
declare function clipStartTime(clip: AudioClip): number;
|
|
657
728
|
/** Clip end position in seconds (start + duration) */
|
|
@@ -660,6 +731,11 @@ declare function clipEndTime(clip: AudioClip): number;
|
|
|
660
731
|
declare function clipOffsetTime(clip: AudioClip): number;
|
|
661
732
|
/** Clip duration in seconds */
|
|
662
733
|
declare function clipDurationTime(clip: AudioClip): number;
|
|
734
|
+
/**
|
|
735
|
+
* Max audio channel count across a track's clips.
|
|
736
|
+
* Used to set Panner channelCount and offline render output channels.
|
|
737
|
+
*/
|
|
738
|
+
declare function trackChannelCount(track: ClipTrack): number;
|
|
663
739
|
/**
|
|
664
740
|
* Clip width in pixels at a given samplesPerPixel.
|
|
665
741
|
* Shared by Clip.tsx (container sizing) and ChannelWithProgress.tsx (progress overlay)
|
|
@@ -745,4 +821,4 @@ declare function handleKeyboardEvent(event: KeyboardEvent, shortcuts: KeyboardSh
|
|
|
745
821
|
*/
|
|
746
822
|
declare const getShortcutLabel: (shortcut: KeyboardShortcut) => string;
|
|
747
823
|
|
|
748
|
-
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 CreateTrackOptions, type FFTSize, type Fade, type FadeConfig, type FadeType, type Gap, InteractionState, type KeyboardShortcut, MAX_CANVAS_WIDTH, type MidiNoteData, PPQN, type PeakData, type Peaks, type PlaylistConfig, type PlayoutState, type RenderAnnotationItemProps, type RenderMode, type SpectrogramComputeConfig, type SpectrogramConfig, type SpectrogramData, type SpectrogramDisplayConfig, type TimeSelection, type Timeline, type Track, type TrackEffectsFunction, type TrackSpectrogramOverrides, type WaveformConfig, type WaveformDataObject, applyFadeIn, applyFadeOut, clipDurationTime, clipEndTime, clipOffsetTime, clipPixelWidth, clipStartTime, clipsOverlap, createClip, createClipFromSeconds, createTimeline, createTrack, dBToNormalized, exponentialCurve, findGaps, gainToNormalized, generateCurve, getClipsAtSample, getClipsInRange, getShortcutLabel, handleKeyboardEvent, linearCurve, logarithmicCurve, normalizedToDb, pixelsToSamples, pixelsToSeconds, sCurveCurve, samplesToPixels, samplesToSeconds, samplesToTicks, secondsToPixels, secondsToSamples, snapToGrid, sortClipsByTime, ticksPerBar, ticksPerBeat, ticksToBarBeatLabel, ticksToSamples };
|
|
824
|
+
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 CreateTrackOptions, type FFTSize, type Fade, type FadeConfig, type FadeType, type Gap, InteractionState, type KeyboardShortcut, MAX_CANVAS_WIDTH, MIN_PIXELS_PER_UNIT, type MidiNoteData, type MusicalTick, type MusicalTickData, type MusicalTickParams, PPQN, type PeakData, type Peaks, type PlaylistConfig, type PlayoutState, type RenderAnnotationItemProps, type RenderMode, type SnapTo, 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, applyFadeIn, applyFadeOut, clipDurationTime, clipEndTime, clipOffsetTime, clipPixelWidth, clipStartTime, clipsOverlap, computeMusicalTicks, createClip, createClipFromSeconds, createTimeline, createTrack, dBToNormalized, exponentialCurve, findGaps, gainToDb, gainToNormalized, generateCurve, getClipsAtSample, getClipsInRange, getShortcutLabel, handleKeyboardEvent, linearCurve, logarithmicCurve, normalizedToDb, pixelsToSamples, pixelsToSeconds, sCurveCurve, samplesToPixels, samplesToSeconds, samplesToTicks, secondsToPixels, secondsToSamples, snapTickToGrid, snapToGrid, snapToTicks, sortClipsByTime, ticksPerBar, ticksPerBeat, ticksToBarBeatLabel, ticksToSamples, trackChannelCount };
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
InteractionState: () => InteractionState,
|
|
24
24
|
MAX_CANVAS_WIDTH: () => MAX_CANVAS_WIDTH,
|
|
25
|
+
MIN_PIXELS_PER_UNIT: () => MIN_PIXELS_PER_UNIT,
|
|
25
26
|
PPQN: () => PPQN,
|
|
26
27
|
applyFadeIn: () => applyFadeIn,
|
|
27
28
|
applyFadeOut: () => applyFadeOut,
|
|
@@ -31,6 +32,7 @@ __export(index_exports, {
|
|
|
31
32
|
clipPixelWidth: () => clipPixelWidth,
|
|
32
33
|
clipStartTime: () => clipStartTime,
|
|
33
34
|
clipsOverlap: () => clipsOverlap,
|
|
35
|
+
computeMusicalTicks: () => computeMusicalTicks,
|
|
34
36
|
createClip: () => createClip,
|
|
35
37
|
createClipFromSeconds: () => createClipFromSeconds,
|
|
36
38
|
createTimeline: () => createTimeline,
|
|
@@ -38,6 +40,7 @@ __export(index_exports, {
|
|
|
38
40
|
dBToNormalized: () => dBToNormalized,
|
|
39
41
|
exponentialCurve: () => exponentialCurve,
|
|
40
42
|
findGaps: () => findGaps,
|
|
43
|
+
gainToDb: () => gainToDb,
|
|
41
44
|
gainToNormalized: () => gainToNormalized,
|
|
42
45
|
generateCurve: () => generateCurve,
|
|
43
46
|
getClipsAtSample: () => getClipsAtSample,
|
|
@@ -55,12 +58,15 @@ __export(index_exports, {
|
|
|
55
58
|
samplesToTicks: () => samplesToTicks,
|
|
56
59
|
secondsToPixels: () => secondsToPixels,
|
|
57
60
|
secondsToSamples: () => secondsToSamples,
|
|
61
|
+
snapTickToGrid: () => snapTickToGrid,
|
|
58
62
|
snapToGrid: () => snapToGrid,
|
|
63
|
+
snapToTicks: () => snapToTicks,
|
|
59
64
|
sortClipsByTime: () => sortClipsByTime,
|
|
60
65
|
ticksPerBar: () => ticksPerBar,
|
|
61
66
|
ticksPerBeat: () => ticksPerBeat,
|
|
62
67
|
ticksToBarBeatLabel: () => ticksToBarBeatLabel,
|
|
63
|
-
ticksToSamples: () => ticksToSamples
|
|
68
|
+
ticksToSamples: () => ticksToSamples,
|
|
69
|
+
trackChannelCount: () => trackChannelCount
|
|
64
70
|
});
|
|
65
71
|
module.exports = __toCommonJS(index_exports);
|
|
66
72
|
|
|
@@ -332,12 +338,131 @@ function normalizedToDb(normalized, floor = DEFAULT_FLOOR) {
|
|
|
332
338
|
const clamped = Math.max(0, normalized);
|
|
333
339
|
return clamped * -floor + floor;
|
|
334
340
|
}
|
|
341
|
+
function gainToDb(gain) {
|
|
342
|
+
return 20 * Math.log10(Math.max(gain, 1e-4));
|
|
343
|
+
}
|
|
335
344
|
function gainToNormalized(gain, floor = DEFAULT_FLOOR) {
|
|
336
345
|
if (gain <= 0) return 0;
|
|
337
346
|
const db = 20 * Math.log10(gain);
|
|
338
347
|
return dBToNormalized(db, floor);
|
|
339
348
|
}
|
|
340
349
|
|
|
350
|
+
// src/utils/musicalTicks.ts
|
|
351
|
+
function snapToTicks(snapTo, timeSignature, ppqn = 960) {
|
|
352
|
+
switch (snapTo) {
|
|
353
|
+
case "bar":
|
|
354
|
+
return ticksPerBar(timeSignature, ppqn);
|
|
355
|
+
case "beat":
|
|
356
|
+
return ticksPerBeat(timeSignature, ppqn);
|
|
357
|
+
case "1/2":
|
|
358
|
+
return ppqn * 2;
|
|
359
|
+
case "1/4":
|
|
360
|
+
return ppqn;
|
|
361
|
+
case "1/8":
|
|
362
|
+
return ppqn / 2;
|
|
363
|
+
case "1/16":
|
|
364
|
+
return ppqn / 4;
|
|
365
|
+
case "1/32":
|
|
366
|
+
return ppqn / 8;
|
|
367
|
+
case "1/2T":
|
|
368
|
+
return Math.round(ppqn * 2 * 2 / 3);
|
|
369
|
+
case "1/4T":
|
|
370
|
+
return Math.round(ppqn * 2 / 3);
|
|
371
|
+
case "1/8T":
|
|
372
|
+
return Math.round(ppqn * 2 / 6);
|
|
373
|
+
case "1/16T":
|
|
374
|
+
return Math.round(ppqn * 2 / 12);
|
|
375
|
+
case "off":
|
|
376
|
+
return 0;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
var MIN_PIXELS_PER_UNIT = 8;
|
|
380
|
+
var MIN_PIXELS_PER_LABEL = 60;
|
|
381
|
+
function computeMusicalTicks(params) {
|
|
382
|
+
const { timeSignature, ticksPerPixel, startPixel, endPixel, ppqn = 960 } = params;
|
|
383
|
+
if (ticksPerPixel <= 0 || ppqn <= 0 || timeSignature[1] <= 0) {
|
|
384
|
+
return { ticks: [], pixelsPerBar: 0, pixelsPerBeat: 0, zoomLevel: "coarse" };
|
|
385
|
+
}
|
|
386
|
+
const tpBeat = ticksPerBeat(timeSignature, ppqn);
|
|
387
|
+
const tpBar = ticksPerBar(timeSignature, ppqn);
|
|
388
|
+
const tpEighth = ppqn / 2;
|
|
389
|
+
const tpSixteenth = ppqn / 4;
|
|
390
|
+
const pixelsPerBar = tpBar / ticksPerPixel;
|
|
391
|
+
const pixelsPerBeat = tpBeat / ticksPerPixel;
|
|
392
|
+
const pixelsPerEighth = tpEighth / ticksPerPixel;
|
|
393
|
+
const pixelsPerSixteenth = tpSixteenth / ticksPerPixel;
|
|
394
|
+
let zoomLevel;
|
|
395
|
+
if (pixelsPerBar < MIN_PIXELS_PER_UNIT) {
|
|
396
|
+
zoomLevel = "coarse";
|
|
397
|
+
} else if (pixelsPerBeat < MIN_PIXELS_PER_UNIT) {
|
|
398
|
+
zoomLevel = "bar";
|
|
399
|
+
} else if (pixelsPerEighth < MIN_PIXELS_PER_UNIT) {
|
|
400
|
+
zoomLevel = "beat";
|
|
401
|
+
} else if (pixelsPerSixteenth < MIN_PIXELS_PER_UNIT) {
|
|
402
|
+
zoomLevel = "eighth";
|
|
403
|
+
} else {
|
|
404
|
+
zoomLevel = "sixteenth";
|
|
405
|
+
}
|
|
406
|
+
let stepTicks;
|
|
407
|
+
let coarseBarStep;
|
|
408
|
+
if (zoomLevel === "coarse") {
|
|
409
|
+
let multiplier = 2;
|
|
410
|
+
while (tpBar * multiplier / ticksPerPixel < MIN_PIXELS_PER_UNIT) {
|
|
411
|
+
multiplier *= 2;
|
|
412
|
+
}
|
|
413
|
+
stepTicks = tpBar * multiplier;
|
|
414
|
+
coarseBarStep = multiplier;
|
|
415
|
+
} else if (zoomLevel === "bar") {
|
|
416
|
+
stepTicks = tpBar;
|
|
417
|
+
} else if (zoomLevel === "beat") {
|
|
418
|
+
stepTicks = tpBeat;
|
|
419
|
+
} else if (zoomLevel === "eighth") {
|
|
420
|
+
stepTicks = tpEighth;
|
|
421
|
+
} else {
|
|
422
|
+
stepTicks = tpSixteenth;
|
|
423
|
+
}
|
|
424
|
+
const startTick = startPixel * ticksPerPixel;
|
|
425
|
+
const endTick = endPixel * ticksPerPixel;
|
|
426
|
+
const firstStep = Math.floor(startTick / stepTicks) * stepTicks;
|
|
427
|
+
const ticks = [];
|
|
428
|
+
for (let tick = firstStep; tick <= endTick; tick += stepTicks) {
|
|
429
|
+
const pixel = tick / ticksPerPixel;
|
|
430
|
+
if (pixel < startPixel || pixel > endPixel) {
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
let type;
|
|
434
|
+
if (tick % tpBar === 0) {
|
|
435
|
+
type = "major";
|
|
436
|
+
} else if (tick % tpBeat === 0) {
|
|
437
|
+
type = "minor";
|
|
438
|
+
} else {
|
|
439
|
+
type = "minorMinor";
|
|
440
|
+
}
|
|
441
|
+
const barIndex = Math.floor(tick / tpBar);
|
|
442
|
+
let label;
|
|
443
|
+
if (type === "major") {
|
|
444
|
+
label = ticksToBarBeatLabel(tick, timeSignature, ppqn);
|
|
445
|
+
} else if (type === "minor" && pixelsPerBeat >= MIN_PIXELS_PER_LABEL) {
|
|
446
|
+
label = ticksToBarBeatLabel(tick, timeSignature, ppqn);
|
|
447
|
+
}
|
|
448
|
+
ticks.push({ pixel, type, barIndex, ...label !== void 0 ? { label } : {} });
|
|
449
|
+
}
|
|
450
|
+
const result = {
|
|
451
|
+
ticks,
|
|
452
|
+
pixelsPerBar,
|
|
453
|
+
pixelsPerBeat,
|
|
454
|
+
zoomLevel,
|
|
455
|
+
...coarseBarStep !== void 0 ? { coarseBarStep } : {}
|
|
456
|
+
};
|
|
457
|
+
return result;
|
|
458
|
+
}
|
|
459
|
+
function snapTickToGrid(tick, snapTo, timeSignature, ppqn = 960) {
|
|
460
|
+
if (snapTo === "off") return tick;
|
|
461
|
+
const gridSize = snapToTicks(snapTo, timeSignature, ppqn);
|
|
462
|
+
if (gridSize <= 0) return tick;
|
|
463
|
+
return Math.round(tick / gridSize) * gridSize;
|
|
464
|
+
}
|
|
465
|
+
|
|
341
466
|
// src/clipTimeHelpers.ts
|
|
342
467
|
function clipStartTime(clip) {
|
|
343
468
|
return clip.startSample / clip.sampleRate;
|
|
@@ -351,6 +476,12 @@ function clipOffsetTime(clip) {
|
|
|
351
476
|
function clipDurationTime(clip) {
|
|
352
477
|
return clip.durationSamples / clip.sampleRate;
|
|
353
478
|
}
|
|
479
|
+
function trackChannelCount(track) {
|
|
480
|
+
return track.clips.reduce(
|
|
481
|
+
(max, clip) => Math.max(max, clip.audioBuffer?.numberOfChannels ?? 1),
|
|
482
|
+
1
|
|
483
|
+
);
|
|
484
|
+
}
|
|
354
485
|
function clipPixelWidth(startSample, durationSamples, samplesPerPixel) {
|
|
355
486
|
return Math.floor((startSample + durationSamples) / samplesPerPixel) - Math.floor(startSample / samplesPerPixel);
|
|
356
487
|
}
|
|
@@ -488,6 +619,7 @@ var getShortcutLabel = (shortcut) => {
|
|
|
488
619
|
0 && (module.exports = {
|
|
489
620
|
InteractionState,
|
|
490
621
|
MAX_CANVAS_WIDTH,
|
|
622
|
+
MIN_PIXELS_PER_UNIT,
|
|
491
623
|
PPQN,
|
|
492
624
|
applyFadeIn,
|
|
493
625
|
applyFadeOut,
|
|
@@ -497,6 +629,7 @@ var getShortcutLabel = (shortcut) => {
|
|
|
497
629
|
clipPixelWidth,
|
|
498
630
|
clipStartTime,
|
|
499
631
|
clipsOverlap,
|
|
632
|
+
computeMusicalTicks,
|
|
500
633
|
createClip,
|
|
501
634
|
createClipFromSeconds,
|
|
502
635
|
createTimeline,
|
|
@@ -504,6 +637,7 @@ var getShortcutLabel = (shortcut) => {
|
|
|
504
637
|
dBToNormalized,
|
|
505
638
|
exponentialCurve,
|
|
506
639
|
findGaps,
|
|
640
|
+
gainToDb,
|
|
507
641
|
gainToNormalized,
|
|
508
642
|
generateCurve,
|
|
509
643
|
getClipsAtSample,
|
|
@@ -521,11 +655,14 @@ var getShortcutLabel = (shortcut) => {
|
|
|
521
655
|
samplesToTicks,
|
|
522
656
|
secondsToPixels,
|
|
523
657
|
secondsToSamples,
|
|
658
|
+
snapTickToGrid,
|
|
524
659
|
snapToGrid,
|
|
660
|
+
snapToTicks,
|
|
525
661
|
sortClipsByTime,
|
|
526
662
|
ticksPerBar,
|
|
527
663
|
ticksPerBeat,
|
|
528
664
|
ticksToBarBeatLabel,
|
|
529
|
-
ticksToSamples
|
|
665
|
+
ticksToSamples,
|
|
666
|
+
trackChannelCount
|
|
530
667
|
});
|
|
531
668
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/constants.ts","../src/types/clip.ts","../src/types/index.ts","../src/utils/conversions.ts","../src/utils/beatsAndBars.ts","../src/utils/dBUtils.ts","../src/clipTimeHelpers.ts","../src/fades.ts","../src/keyboard.ts"],"sourcesContent":["export * from './constants';\nexport * from './types';\nexport * from './utils';\nexport * from './clipTimeHelpers';\nexport * from './fades';\nexport * from './keyboard';\n","/**\n * Maximum width in CSS pixels for a single canvas chunk.\n * Canvas elements are split into chunks of this width to enable\n * horizontal virtual scrolling — only visible chunks are mounted.\n */\nexport const MAX_CANVAS_WIDTH = 1000;\n","/**\n * Clip-Based Model Types\n *\n * These types support a professional multi-track editing model where:\n * - Each track can contain multiple audio clips\n * - Clips can be positioned anywhere on the timeline\n * - Clips have independent trim points (offset/duration)\n * - Gaps between clips are silent\n * - Clips can overlap (for crossfades)\n */\n\nimport { Fade } from './index';\nimport type { RenderMode, SpectrogramConfig, ColorMapValue } from './spectrogram';\n\n/**\n * WaveformData object from waveform-data.js library.\n * Supports resample() and slice() for dynamic zoom levels.\n * See: https://github.com/bbc/waveform-data.js\n */\nexport interface WaveformDataObject {\n /** Sample rate of the original audio */\n readonly sample_rate: number;\n /** Number of audio samples per pixel */\n readonly scale: number;\n /** Length of waveform data in pixels */\n readonly length: number;\n /** Bit depth (8 or 16) */\n readonly bits: number;\n /** Duration in seconds */\n readonly duration: number;\n /** Number of channels */\n readonly channels: number;\n /** Get channel data */\n channel: (index: number) => {\n min_array: () => number[];\n max_array: () => number[];\n };\n /** Resample to different scale */\n resample: (options: { scale: number } | { width: number }) => WaveformDataObject;\n /** Slice a portion of the waveform */\n slice: (\n options: { startTime: number; endTime: number } | { startIndex: number; endIndex: number }\n ) => WaveformDataObject;\n}\n\n/**\n * Generic effects function type for track-level audio processing.\n *\n * The actual implementation receives Tone.js audio nodes. Using generic types\n * here to avoid circular dependencies with the playout package.\n *\n * @param graphEnd - The end of the track's audio graph (Tone.js Gain node)\n * @param destination - Where to connect the effects output (Tone.js ToneAudioNode)\n * @param isOffline - Whether rendering offline (for export)\n * @returns Optional cleanup function called when track is disposed\n *\n * @example\n * ```typescript\n * const trackEffects: TrackEffectsFunction = (graphEnd, destination, isOffline) => {\n * const reverb = new Tone.Reverb({ decay: 1.5 });\n * graphEnd.connect(reverb);\n * reverb.connect(destination);\n *\n * return () => {\n * reverb.dispose();\n * };\n * };\n * ```\n */\nexport type TrackEffectsFunction = (\n graphEnd: unknown,\n destination: unknown,\n isOffline: boolean\n) => void | (() => void);\n\n/**\n * Represents a single audio clip on the timeline\n *\n * IMPORTANT: All positions/durations are stored as SAMPLE COUNTS (integers)\n * to avoid floating-point precision errors. Convert to seconds only when\n * needed for playback using: seconds = samples / sampleRate\n *\n * Clips can be created with just waveformData (for instant visual rendering)\n * and have audioBuffer added later when audio finishes loading.\n */\nexport interface AudioClip {\n /** Unique identifier for this clip */\n id: string;\n\n /**\n * The audio buffer containing the audio data.\n * Optional for peaks-first rendering - can be added later.\n * Required for playback and editing operations.\n */\n audioBuffer?: AudioBuffer;\n\n /** Position on timeline where this clip starts (in samples at timeline sampleRate) */\n startSample: number;\n\n /** Duration of this clip (in samples) - how much of the audio buffer to play */\n durationSamples: number;\n\n /** Offset into the audio buffer where playback starts (in samples) - the \"trim start\" point */\n offsetSamples: number;\n\n /**\n * Sample rate for this clip's audio.\n * Required when audioBuffer is not provided (for peaks-first rendering).\n * When audioBuffer is present, this should match audioBuffer.sampleRate.\n */\n sampleRate: number;\n\n /**\n * Total duration of the source audio in samples.\n * Required when audioBuffer is not provided (for trim bounds calculation).\n * When audioBuffer is present, this should equal audioBuffer.length.\n */\n sourceDurationSamples: number;\n\n /** Optional fade in effect */\n fadeIn?: Fade;\n\n /** Optional fade out effect */\n fadeOut?: Fade;\n\n /** Clip-specific gain/volume multiplier (0.0 to 1.0+) */\n gain: number;\n\n /** Optional label/name for this clip */\n name?: string;\n\n /** Optional color for visual distinction */\n color?: string;\n\n /**\n * Pre-computed waveform data from waveform-data.js library.\n * When provided, the library will use this instead of computing peaks from the audioBuffer.\n * Supports resampling to different zoom levels and slicing for clip trimming.\n * Load with: `const waveformData = await loadWaveformData('/path/to/peaks.dat')`\n */\n waveformData?: WaveformDataObject;\n\n /**\n * MIDI note data — when present, this clip plays MIDI instead of audio.\n * The playout adapter uses this field to detect MIDI clips and route them\n * to MidiToneTrack (PolySynth) instead of ToneTrack (AudioBufferSourceNode).\n */\n midiNotes?: MidiNoteData[];\n\n /** MIDI channel (0-indexed). Channel 9 = GM percussion. */\n midiChannel?: number;\n\n /** MIDI program number (0-127). GM instrument number for SoundFont playback. */\n midiProgram?: number;\n}\n\n/**\n * Represents a track containing multiple audio clips\n */\nexport interface ClipTrack {\n /** Unique identifier for this track */\n id: string;\n\n /** Display name for this track */\n name: string;\n\n /** Array of audio clips on this track */\n clips: AudioClip[];\n\n /** Whether this track is muted */\n muted: boolean;\n\n /** Whether this track is soloed */\n soloed: boolean;\n\n /** Track volume (0.0 to 1.0+) */\n volume: number;\n\n /** Stereo pan (-1.0 = left, 0 = center, 1.0 = right) */\n pan: number;\n\n /** Optional track color for visual distinction */\n color?: string;\n\n /** Track height in pixels (for UI) */\n height?: number;\n\n /** Optional effects function for this track */\n effects?: TrackEffectsFunction;\n\n /** Visualization render mode. Default: 'waveform' */\n renderMode?: RenderMode;\n\n /** Per-track spectrogram configuration (FFT size, window, frequency scale, etc.) */\n spectrogramConfig?: SpectrogramConfig;\n\n /** Per-track spectrogram color map name or custom color array */\n spectrogramColorMap?: ColorMapValue;\n}\n\n/**\n * Represents the entire timeline/project\n */\nexport interface Timeline {\n /** All tracks in the timeline */\n tracks: ClipTrack[];\n\n /** Total timeline duration in seconds */\n duration: number;\n\n /** Sample rate for all audio (typically 44100 or 48000) */\n sampleRate: number;\n\n /** Optional project name */\n name?: string;\n\n /** Optional tempo (BPM) for grid snapping */\n tempo?: number;\n\n /** Optional time signature for grid snapping */\n timeSignature?: {\n numerator: number;\n denominator: number;\n };\n}\n\n/**\n * Options for creating a new audio clip (using sample counts)\n *\n * Either audioBuffer OR (sampleRate + sourceDurationSamples + waveformData) must be provided.\n * Providing waveformData without audioBuffer enables peaks-first rendering.\n */\nexport interface CreateClipOptions {\n /** Audio buffer - optional for peaks-first rendering */\n audioBuffer?: AudioBuffer;\n startSample: number; // Position on timeline (in samples)\n durationSamples?: number; // Defaults to full buffer/source duration (in samples)\n offsetSamples?: number; // Defaults to 0\n gain?: number; // Defaults to 1.0\n name?: string;\n color?: string;\n fadeIn?: Fade;\n fadeOut?: Fade;\n /** Pre-computed waveform data from waveform-data.js (e.g., from BBC audiowaveform) */\n waveformData?: WaveformDataObject;\n /** Sample rate - required if audioBuffer not provided */\n sampleRate?: number;\n /** Total source audio duration in samples - required if audioBuffer not provided */\n sourceDurationSamples?: number;\n /** MIDI note data — passed through to the created AudioClip */\n midiNotes?: MidiNoteData[];\n /** MIDI channel (0-indexed). Channel 9 = GM percussion. */\n midiChannel?: number;\n /** MIDI program number (0-127). GM instrument for SoundFont playback. */\n midiProgram?: number;\n}\n\n/**\n * Options for creating a new audio clip (using seconds for convenience)\n *\n * Either audioBuffer OR (sampleRate + sourceDuration + waveformData) must be provided.\n * Providing waveformData without audioBuffer enables peaks-first rendering.\n */\nexport interface CreateClipOptionsSeconds {\n /** Audio buffer - optional for peaks-first rendering */\n audioBuffer?: AudioBuffer;\n startTime: number; // Position on timeline (in seconds)\n duration?: number; // Defaults to full buffer/source duration (in seconds)\n offset?: number; // Defaults to 0 (in seconds)\n gain?: number; // Defaults to 1.0\n name?: string;\n color?: string;\n fadeIn?: Fade;\n fadeOut?: Fade;\n /** Pre-computed waveform data from waveform-data.js (e.g., from BBC audiowaveform) */\n waveformData?: WaveformDataObject;\n /** Sample rate - required if audioBuffer not provided */\n sampleRate?: number;\n /** Total source audio duration in seconds - required if audioBuffer not provided */\n sourceDuration?: number;\n /** MIDI note data — passed through to the created AudioClip */\n midiNotes?: MidiNoteData[];\n /** MIDI channel (0-indexed). Channel 9 = GM percussion. */\n midiChannel?: number;\n /** MIDI program number (0-127). GM instrument for SoundFont playback. */\n midiProgram?: number;\n}\n\n/**\n * Options for creating a new track\n */\nexport interface CreateTrackOptions {\n name: string;\n clips?: AudioClip[];\n muted?: boolean;\n soloed?: boolean;\n volume?: number;\n pan?: number;\n color?: string;\n height?: number;\n spectrogramConfig?: SpectrogramConfig;\n spectrogramColorMap?: ColorMapValue;\n}\n\n/**\n * Creates a new AudioClip with sensible defaults (using sample counts)\n *\n * For peaks-first rendering (no audioBuffer), sampleRate and sourceDurationSamples can be:\n * - Provided explicitly via options\n * - Derived from waveformData (sample_rate and duration properties)\n */\nexport function createClip(options: CreateClipOptions): AudioClip {\n const {\n audioBuffer,\n startSample,\n offsetSamples = 0,\n gain = 1.0,\n name,\n color,\n fadeIn,\n fadeOut,\n waveformData,\n midiNotes,\n midiChannel,\n midiProgram,\n } = options;\n\n // Determine sample rate: audioBuffer > explicit option > waveformData\n const sampleRate = audioBuffer?.sampleRate ?? options.sampleRate ?? waveformData?.sample_rate;\n\n // Determine source duration: audioBuffer > explicit option > waveformData (converted to samples)\n const sourceDurationSamples =\n audioBuffer?.length ??\n options.sourceDurationSamples ??\n (waveformData && sampleRate ? Math.ceil(waveformData.duration * sampleRate) : undefined);\n\n if (sampleRate === undefined) {\n throw new Error(\n 'createClip: sampleRate is required when audioBuffer is not provided (can use waveformData.sample_rate)'\n );\n }\n if (sourceDurationSamples === undefined) {\n throw new Error(\n 'createClip: sourceDurationSamples is required when audioBuffer is not provided (can use waveformData.duration)'\n );\n }\n\n if (audioBuffer && waveformData && audioBuffer.sampleRate !== waveformData.sample_rate) {\n console.warn(\n '[waveform-playlist] \"' +\n (name ?? 'unnamed') +\n '\": pre-computed peaks at ' +\n waveformData.sample_rate +\n ' Hz do not match decoded audio at ' +\n audioBuffer.sampleRate +\n ' Hz — peaks may be recomputed from decoded audio'\n );\n }\n\n // Default duration to full source duration\n const durationSamples = options.durationSamples ?? sourceDurationSamples;\n\n return {\n id: generateId(),\n audioBuffer,\n startSample,\n durationSamples,\n offsetSamples,\n sampleRate,\n sourceDurationSamples,\n gain,\n name,\n color,\n fadeIn,\n fadeOut,\n waveformData,\n midiNotes,\n midiChannel,\n midiProgram,\n };\n}\n\n/**\n * Creates a new AudioClip from time-based values (convenience function)\n * Converts seconds to samples using the audioBuffer's sampleRate or explicit sampleRate\n *\n * For peaks-first rendering (no audioBuffer), sampleRate and sourceDuration can be:\n * - Provided explicitly via options\n * - Derived from waveformData (sample_rate and duration properties)\n */\nexport function createClipFromSeconds(options: CreateClipOptionsSeconds): AudioClip {\n const {\n audioBuffer,\n startTime,\n offset = 0,\n gain = 1.0,\n name,\n color,\n fadeIn,\n fadeOut,\n waveformData,\n midiNotes,\n midiChannel,\n midiProgram,\n } = options;\n\n // Determine sample rate: audioBuffer > explicit option > waveformData\n const sampleRate = audioBuffer?.sampleRate ?? options.sampleRate ?? waveformData?.sample_rate;\n if (sampleRate === undefined) {\n throw new Error(\n 'createClipFromSeconds: sampleRate is required when audioBuffer is not provided (can use waveformData.sample_rate)'\n );\n }\n\n // Determine source duration: audioBuffer > explicit option > waveformData\n const sourceDuration = audioBuffer?.duration ?? options.sourceDuration ?? waveformData?.duration;\n if (sourceDuration === undefined) {\n throw new Error(\n 'createClipFromSeconds: sourceDuration is required when audioBuffer is not provided (can use waveformData.duration)'\n );\n }\n\n // Note: audioBuffer and waveformData may have different sample rates (e.g., Opus at 48000\n // decoded on 44100 hardware). This is expected — callers fall back to worker to recompute peaks.\n\n // Default clip duration to full source duration\n const duration = options.duration ?? sourceDuration;\n\n return createClip({\n audioBuffer,\n startSample: Math.round(startTime * sampleRate),\n durationSamples: Math.round(duration * sampleRate),\n offsetSamples: Math.round(offset * sampleRate),\n sampleRate,\n sourceDurationSamples: Math.ceil(sourceDuration * sampleRate),\n gain,\n name,\n color,\n fadeIn,\n fadeOut,\n waveformData,\n midiNotes,\n midiChannel,\n midiProgram,\n });\n}\n\n/**\n * Creates a new ClipTrack with sensible defaults\n */\nexport function createTrack(options: CreateTrackOptions): ClipTrack {\n const {\n name,\n clips = [],\n muted = false,\n soloed = false,\n volume = 1.0,\n pan = 0,\n color,\n height,\n spectrogramConfig,\n spectrogramColorMap,\n } = options;\n\n return {\n id: generateId(),\n name,\n clips,\n muted,\n soloed,\n volume,\n pan,\n color,\n height,\n spectrogramConfig,\n spectrogramColorMap,\n };\n}\n\n/**\n * Creates a new Timeline with sensible defaults\n */\nexport function createTimeline(\n tracks: ClipTrack[],\n sampleRate: number = 44100,\n options?: {\n name?: string;\n tempo?: number;\n timeSignature?: { numerator: number; denominator: number };\n }\n): Timeline {\n // Calculate total duration from all clips across all tracks (in seconds)\n const durationSamples = tracks.reduce((maxSamples, track) => {\n const trackSamples = track.clips.reduce((max, clip) => {\n return Math.max(max, clip.startSample + clip.durationSamples);\n }, 0);\n return Math.max(maxSamples, trackSamples);\n }, 0);\n\n const duration = durationSamples / sampleRate;\n\n return {\n tracks,\n duration,\n sampleRate,\n name: options?.name,\n tempo: options?.tempo,\n timeSignature: options?.timeSignature,\n };\n}\n\n/**\n * Generates a unique ID for clips and tracks\n */\nfunction generateId(): string {\n return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n}\n\n/**\n * MIDI note data for clips that play MIDI instead of audio.\n * When present on an AudioClip, the clip is treated as a MIDI clip\n * by the playout adapter.\n */\nexport interface MidiNoteData {\n /** MIDI note number (0-127) */\n midi: number;\n /** Note name in scientific pitch notation (\"C4\", \"G#3\") */\n name: string;\n /** Start time in seconds, relative to clip start */\n time: number;\n /** Duration in seconds */\n duration: number;\n /** Velocity (0-1 normalized) */\n velocity: number;\n /** MIDI channel (0-indexed). Channel 9 = GM percussion. Enables per-note routing in flattened tracks. */\n channel?: number;\n}\n\n/**\n * Utility: Get all clips within a sample range\n */\nexport function getClipsInRange(\n track: ClipTrack,\n startSample: number,\n endSample: number\n): AudioClip[] {\n return track.clips.filter((clip) => {\n const clipEnd = clip.startSample + clip.durationSamples;\n // Clip overlaps with range if:\n // - Clip starts before range ends AND\n // - Clip ends after range starts\n return clip.startSample < endSample && clipEnd > startSample;\n });\n}\n\n/**\n * Utility: Get all clips at a specific sample position\n */\nexport function getClipsAtSample(track: ClipTrack, sample: number): AudioClip[] {\n return track.clips.filter((clip) => {\n const clipEnd = clip.startSample + clip.durationSamples;\n return sample >= clip.startSample && sample < clipEnd;\n });\n}\n\n/**\n * Utility: Check if two clips overlap\n */\nexport function clipsOverlap(clip1: AudioClip, clip2: AudioClip): boolean {\n const clip1End = clip1.startSample + clip1.durationSamples;\n const clip2End = clip2.startSample + clip2.durationSamples;\n\n return clip1.startSample < clip2End && clip1End > clip2.startSample;\n}\n\n/**\n * Utility: Sort clips by startSample\n */\nexport function sortClipsByTime(clips: AudioClip[]): AudioClip[] {\n return [...clips].sort((a, b) => a.startSample - b.startSample);\n}\n\n/**\n * Utility: Find gaps between clips (silent regions)\n */\nexport interface Gap {\n startSample: number;\n endSample: number;\n durationSamples: number;\n}\n\nexport function findGaps(track: ClipTrack): Gap[] {\n if (track.clips.length === 0) return [];\n\n const sorted = sortClipsByTime(track.clips);\n const gaps: Gap[] = [];\n\n for (let i = 0; i < sorted.length - 1; i++) {\n const currentClipEnd = sorted[i].startSample + sorted[i].durationSamples;\n const nextClipStart = sorted[i + 1].startSample;\n\n if (nextClipStart > currentClipEnd) {\n gaps.push({\n startSample: currentClipEnd,\n endSample: nextClipStart,\n durationSamples: nextClipStart - currentClipEnd,\n });\n }\n }\n\n return gaps;\n}\n","/**\n * Peaks type - represents a typed array of interleaved min/max peak data\n */\nexport type Peaks = Int8Array | Int16Array;\n\n/**\n * Bits type - number of bits for peak data\n */\nexport type Bits = 8 | 16;\n\n/**\n * PeakData - result of peak extraction\n */\nexport interface PeakData {\n /** Number of peak pairs extracted */\n length: number;\n /** Array of peak data for each channel (interleaved min/max) */\n data: Peaks[];\n /** Bit depth of peak data */\n bits: Bits;\n}\n\nexport interface WaveformConfig {\n sampleRate: number;\n samplesPerPixel: number;\n waveHeight?: number;\n waveOutlineColor?: string;\n waveFillColor?: string;\n waveProgressColor?: string;\n}\n\nexport interface AudioBuffer {\n length: number;\n duration: number;\n numberOfChannels: number;\n sampleRate: number;\n getChannelData(channel: number): Float32Array;\n}\n\nexport interface Track {\n id: string;\n name: string;\n src?: string | AudioBuffer; // Support both URL strings and AudioBuffer objects\n gain: number;\n muted: boolean;\n soloed: boolean;\n stereoPan: number;\n startTime: number;\n endTime?: number;\n fadeIn?: Fade;\n fadeOut?: Fade;\n cueIn?: number;\n cueOut?: number;\n}\n\n/**\n * Simple fade configuration\n */\nexport interface Fade {\n /** Duration of the fade in seconds */\n duration: number;\n /** Type of fade curve (default: 'linear') */\n type?: FadeType;\n}\n\nexport type FadeType = 'logarithmic' | 'linear' | 'sCurve' | 'exponential';\n\n/**\n * Alias for Fade — used by media-element-playout and playout packages\n */\nexport type FadeConfig = Fade;\n\nexport interface PlaylistConfig {\n samplesPerPixel?: number;\n waveHeight?: number;\n container?: HTMLElement;\n isAutomaticScroll?: boolean;\n timescale?: boolean;\n colors?: {\n waveOutlineColor?: string;\n waveFillColor?: string;\n waveProgressColor?: string;\n };\n controls?: {\n show?: boolean;\n width?: number;\n };\n zoomLevels?: number[];\n}\n\nexport interface PlayoutState {\n isPlaying: boolean;\n isPaused: boolean;\n cursor: number;\n duration: number;\n}\n\nexport interface TimeSelection {\n start: number;\n end: number;\n}\n\nexport enum InteractionState {\n Cursor = 'cursor',\n Select = 'select',\n Shift = 'shift',\n FadeIn = 'fadein',\n FadeOut = 'fadeout',\n}\n\n// Export clip-based model types\nexport * from './clip';\n\n// Export spectrogram types\nexport * from './spectrogram';\n\n// Export annotation types\nexport * from './annotations';\n","export function samplesToSeconds(samples: number, sampleRate: number): number {\n return samples / sampleRate;\n}\n\nexport function secondsToSamples(seconds: number, sampleRate: number): number {\n return Math.ceil(seconds * sampleRate);\n}\n\nexport function samplesToPixels(samples: number, samplesPerPixel: number): number {\n return Math.floor(samples / samplesPerPixel);\n}\n\nexport function pixelsToSamples(pixels: number, samplesPerPixel: number): number {\n return Math.floor(pixels * samplesPerPixel);\n}\n\nexport function pixelsToSeconds(\n pixels: number,\n samplesPerPixel: number,\n sampleRate: number\n): number {\n return (pixels * samplesPerPixel) / sampleRate;\n}\n\nexport function secondsToPixels(\n seconds: number,\n samplesPerPixel: number,\n sampleRate: number\n): number {\n return Math.ceil((seconds * sampleRate) / samplesPerPixel);\n}\n","/** Default PPQN matching Tone.js Transport (192 ticks per quarter note) */\nexport const PPQN = 192;\n\n/** Number of PPQN ticks per beat for the given time signature. */\nexport function ticksPerBeat(timeSignature: [number, number], ppqn = PPQN): number {\n const [, denominator] = timeSignature;\n return ppqn * (4 / denominator);\n}\n\n/** Number of PPQN ticks per bar for the given time signature. */\nexport function ticksPerBar(timeSignature: [number, number], ppqn = PPQN): number {\n const [numerator] = timeSignature;\n return numerator * ticksPerBeat(timeSignature, ppqn);\n}\n\n/** Convert PPQN ticks to sample count. Uses Math.round for integer sample alignment. */\nexport function ticksToSamples(\n ticks: number,\n bpm: number,\n sampleRate: number,\n ppqn = PPQN\n): number {\n return Math.round((ticks * 60 * sampleRate) / (bpm * ppqn));\n}\n\n/** Convert sample count to PPQN ticks. Inverse of ticksToSamples. */\nexport function samplesToTicks(\n samples: number,\n bpm: number,\n sampleRate: number,\n ppqn = PPQN\n): number {\n return Math.round((samples * ppqn * bpm) / (60 * sampleRate));\n}\n\n/** Snap a tick position to the nearest grid line (rounds to nearest). */\nexport function snapToGrid(ticks: number, gridSizeTicks: number): number {\n return Math.round(ticks / gridSizeTicks) * gridSizeTicks;\n}\n\n/** Format ticks as a 1-indexed bar.beat label. Beat 1 shows bar number only (e.g., \"3\" not \"3.1\"). */\nexport function ticksToBarBeatLabel(\n ticks: number,\n timeSignature: [number, number],\n ppqn = PPQN\n): string {\n const barTicks = ticksPerBar(timeSignature, ppqn);\n const beatTicks = ticksPerBeat(timeSignature, ppqn);\n const bar = Math.floor(ticks / barTicks) + 1;\n const beatInBar = Math.floor((ticks % barTicks) / beatTicks) + 1;\n if (beatInBar === 1) return `${bar}`;\n return `${bar}.${beatInBar}`;\n}\n","const DEFAULT_FLOOR = -100;\n\n/**\n * Convert a dB value to a normalized range.\n *\n * Maps dB values linearly: floor → 0, 0 dB → 1.\n * Values above 0 dB map to > 1 (e.g., +5 dB → 1.05 with default floor).\n *\n * @param dB - Decibel value (typically -Infinity to +5)\n * @param floor - Minimum dB value mapped to 0. Default: -100 (Firefox compat)\n * @returns Normalized value (0 at floor, 1 at 0 dB, >1 above 0 dB)\n */\nexport function dBToNormalized(dB: number, floor: number = DEFAULT_FLOOR): number {\n if (Number.isNaN(dB)) {\n console.warn('[waveform-playlist] dBToNormalized received NaN');\n return 0;\n }\n if (floor >= 0) {\n console.warn('[waveform-playlist] dBToNormalized floor must be negative, got:', floor);\n return 0;\n }\n if (!isFinite(dB) || dB <= floor) return 0;\n return (dB - floor) / -floor;\n}\n\n/**\n * Convert a normalized value back to dB.\n *\n * Maps linearly: 0 → floor, 1 → 0 dB.\n * Values above 1 map to positive dB (e.g., 1.05 → +5 dB with default floor).\n *\n * @param normalized - Normalized value (0 = floor, 1 = 0 dB)\n * @param floor - Minimum dB value (maps from 0). Must be negative. Default: -100\n * @returns dB value (floor at 0, 0 dB at 1, positive dB above 1)\n */\nexport function normalizedToDb(normalized: number, floor: number = DEFAULT_FLOOR): number {\n if (!isFinite(normalized)) return floor;\n if (floor >= 0) {\n console.warn('[waveform-playlist] normalizedToDb floor must be negative, got:', floor);\n return DEFAULT_FLOOR;\n }\n const clamped = Math.max(0, normalized);\n return clamped * -floor + floor;\n}\n\n/**\n * Convert a linear gain value (0-1+) to normalized 0-1 via dB.\n *\n * Combines gain-to-dB (20 * log10) with dBToNormalized for a consistent\n * mapping from raw AudioWorklet peak/RMS values to the 0-1 range used\n * by UI meter components.\n *\n * @param gain - Linear gain value (typically 0 to 1, can exceed 1)\n * @param floor - Minimum dB value mapped to 0. Default: -100\n * @returns Normalized value (0 at silence/floor, 1 at 0 dB, >1 above 0 dB)\n */\nexport function gainToNormalized(gain: number, floor: number = DEFAULT_FLOOR): number {\n if (gain <= 0) return 0;\n const db = 20 * Math.log10(gain);\n return dBToNormalized(db, floor);\n}\n","import type { AudioClip } from './types';\n\n/** Clip start position in seconds */\nexport function clipStartTime(clip: AudioClip): number {\n return clip.startSample / clip.sampleRate;\n}\n\n/** Clip end position in seconds (start + duration) */\nexport function clipEndTime(clip: AudioClip): number {\n return (clip.startSample + clip.durationSamples) / clip.sampleRate;\n}\n\n/** Clip offset into source audio in seconds */\nexport function clipOffsetTime(clip: AudioClip): number {\n return clip.offsetSamples / clip.sampleRate;\n}\n\n/** Clip duration in seconds */\nexport function clipDurationTime(clip: AudioClip): number {\n return clip.durationSamples / clip.sampleRate;\n}\n\n/**\n * Clip width in pixels at a given samplesPerPixel.\n * Shared by Clip.tsx (container sizing) and ChannelWithProgress.tsx (progress overlay)\n * to ensure pixel-perfect alignment. Floor-based endpoint subtraction guarantees\n * adjacent clips have no pixel gaps.\n */\nexport function clipPixelWidth(\n startSample: number,\n durationSamples: number,\n samplesPerPixel: number\n): number {\n return (\n Math.floor((startSample + durationSamples) / samplesPerPixel) -\n Math.floor(startSample / samplesPerPixel)\n );\n}\n","/**\n * Fade curve utilities for Web Audio API\n *\n * Pure functions that generate fade curves and apply them to AudioParam.\n * No Tone.js dependency — works with native Web Audio nodes.\n */\n\nimport type { FadeType } from './types';\n\n/**\n * Generate a linear fade curve\n */\nexport function linearCurve(length: number, fadeIn: boolean): Float32Array {\n const curve = new Float32Array(length);\n const scale = length - 1;\n\n for (let i = 0; i < length; i++) {\n const x = i / scale;\n curve[i] = fadeIn ? x : 1 - x;\n }\n\n return curve;\n}\n\n/**\n * Generate an exponential fade curve\n */\nexport function exponentialCurve(length: number, fadeIn: boolean): Float32Array {\n const curve = new Float32Array(length);\n const scale = length - 1;\n\n for (let i = 0; i < length; i++) {\n const x = i / scale;\n const index = fadeIn ? i : length - 1 - i;\n curve[index] = Math.exp(2 * x - 1) / Math.E;\n }\n\n return curve;\n}\n\n/**\n * Generate an S-curve (sine-based smooth curve)\n */\nexport function sCurveCurve(length: number, fadeIn: boolean): Float32Array {\n const curve = new Float32Array(length);\n const phase = fadeIn ? Math.PI / 2 : -Math.PI / 2;\n\n for (let i = 0; i < length; i++) {\n curve[i] = Math.sin((Math.PI * i) / length - phase) / 2 + 0.5;\n }\n\n return curve;\n}\n\n/**\n * Generate a logarithmic fade curve\n */\nexport function logarithmicCurve(length: number, fadeIn: boolean, base: number = 10): Float32Array {\n const curve = new Float32Array(length);\n\n for (let i = 0; i < length; i++) {\n const index = fadeIn ? i : length - 1 - i;\n const x = i / length;\n curve[index] = Math.log(1 + base * x) / Math.log(1 + base);\n }\n\n return curve;\n}\n\n/**\n * Generate a fade curve of the specified type\n */\nexport function generateCurve(type: FadeType, length: number, fadeIn: boolean): Float32Array {\n switch (type) {\n case 'linear':\n return linearCurve(length, fadeIn);\n case 'exponential':\n return exponentialCurve(length, fadeIn);\n case 'sCurve':\n return sCurveCurve(length, fadeIn);\n case 'logarithmic':\n return logarithmicCurve(length, fadeIn);\n default:\n return linearCurve(length, fadeIn);\n }\n}\n\n/**\n * Apply a fade in to an AudioParam\n *\n * @param param - The AudioParam to apply the fade to (usually gain)\n * @param startTime - When the fade starts (in seconds, AudioContext time)\n * @param duration - Duration of the fade in seconds\n * @param type - Type of fade curve\n * @param startValue - Starting value (default: 0)\n * @param endValue - Ending value (default: 1)\n */\nexport function applyFadeIn(\n param: AudioParam,\n startTime: number,\n duration: number,\n type: FadeType = 'linear',\n startValue: number = 0,\n endValue: number = 1\n): void {\n if (duration <= 0) return;\n\n if (type === 'linear') {\n param.setValueAtTime(startValue, startTime);\n param.linearRampToValueAtTime(endValue, startTime + duration);\n } else if (type === 'exponential') {\n param.setValueAtTime(Math.max(startValue, 0.001), startTime);\n param.exponentialRampToValueAtTime(Math.max(endValue, 0.001), startTime + duration);\n } else {\n const curve = generateCurve(type, 10000, true);\n const scaledCurve = new Float32Array(curve.length);\n const range = endValue - startValue;\n for (let i = 0; i < curve.length; i++) {\n scaledCurve[i] = startValue + curve[i] * range;\n }\n param.setValueCurveAtTime(scaledCurve, startTime, duration);\n }\n}\n\n/**\n * Apply a fade out to an AudioParam\n *\n * @param param - The AudioParam to apply the fade to (usually gain)\n * @param startTime - When the fade starts (in seconds, AudioContext time)\n * @param duration - Duration of the fade in seconds\n * @param type - Type of fade curve\n * @param startValue - Starting value (default: 1)\n * @param endValue - Ending value (default: 0)\n */\nexport function applyFadeOut(\n param: AudioParam,\n startTime: number,\n duration: number,\n type: FadeType = 'linear',\n startValue: number = 1,\n endValue: number = 0\n): void {\n if (duration <= 0) return;\n\n if (type === 'linear') {\n param.setValueAtTime(startValue, startTime);\n param.linearRampToValueAtTime(endValue, startTime + duration);\n } else if (type === 'exponential') {\n param.setValueAtTime(Math.max(startValue, 0.001), startTime);\n param.exponentialRampToValueAtTime(Math.max(endValue, 0.001), startTime + duration);\n } else {\n const curve = generateCurve(type, 10000, false);\n const scaledCurve = new Float32Array(curve.length);\n const range = startValue - endValue;\n for (let i = 0; i < curve.length; i++) {\n scaledCurve[i] = endValue + curve[i] * range;\n }\n param.setValueCurveAtTime(scaledCurve, startTime, duration);\n }\n}\n","/**\n * Framework-agnostic keyboard shortcut handling.\n * Used by both React (useKeyboardShortcuts) and Web Components (daw-editor).\n */\n\nexport interface KeyboardShortcut {\n key: string;\n ctrlKey?: boolean;\n shiftKey?: boolean;\n metaKey?: boolean;\n altKey?: boolean;\n action: () => void;\n description?: string;\n preventDefault?: boolean;\n}\n\n/**\n * Handle a keyboard event against a list of shortcuts.\n * Pure function, no framework dependency.\n */\nexport function handleKeyboardEvent(\n event: KeyboardEvent,\n shortcuts: KeyboardShortcut[],\n enabled: boolean\n): void {\n if (!enabled) return;\n\n // Ignore key repeat events — holding a key fires keydown repeatedly.\n // Without this guard, holding Space rapidly toggles play/pause.\n if (event.repeat) return;\n\n const target = event.target as HTMLElement;\n if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {\n return;\n }\n\n const matchingShortcut = shortcuts.find((shortcut) => {\n const keyMatch =\n event.key.toLowerCase() === shortcut.key.toLowerCase() || event.key === shortcut.key;\n\n const ctrlMatch = shortcut.ctrlKey === undefined || event.ctrlKey === shortcut.ctrlKey;\n const shiftMatch = shortcut.shiftKey === undefined || event.shiftKey === shortcut.shiftKey;\n const metaMatch = shortcut.metaKey === undefined || event.metaKey === shortcut.metaKey;\n const altMatch = shortcut.altKey === undefined || event.altKey === shortcut.altKey;\n\n return keyMatch && ctrlMatch && shiftMatch && metaMatch && altMatch;\n });\n\n if (matchingShortcut) {\n if (matchingShortcut.preventDefault !== false) {\n event.preventDefault();\n }\n matchingShortcut.action();\n }\n}\n\n/**\n * Get a human-readable string representation of a keyboard shortcut.\n *\n * @param shortcut - The keyboard shortcut\n * @returns Human-readable string (e.g., \"Cmd+Shift+S\")\n */\nexport const getShortcutLabel = (shortcut: KeyboardShortcut): string => {\n const parts: string[] = [];\n\n // Use Cmd on Mac, Ctrl on other platforms\n const isMac = typeof navigator !== 'undefined' && navigator.platform.includes('Mac');\n\n if (shortcut.metaKey) {\n parts.push(isMac ? 'Cmd' : 'Ctrl');\n }\n\n if (shortcut.ctrlKey && !shortcut.metaKey) {\n parts.push('Ctrl');\n }\n\n if (shortcut.altKey) {\n parts.push(isMac ? 'Option' : 'Alt');\n }\n\n if (shortcut.shiftKey) {\n parts.push('Shift');\n }\n\n parts.push(shortcut.key.toUpperCase());\n\n return parts.join('+');\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKO,IAAM,mBAAmB;;;ACkTzB,SAAS,WAAW,SAAuC;AAChE,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA,gBAAgB;AAAA,IAChB,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAGJ,QAAM,aAAa,aAAa,cAAc,QAAQ,cAAc,cAAc;AAGlF,QAAM,wBACJ,aAAa,UACb,QAAQ,0BACP,gBAAgB,aAAa,KAAK,KAAK,aAAa,WAAW,UAAU,IAAI;AAEhF,MAAI,eAAe,QAAW;AAC5B,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,MAAI,0BAA0B,QAAW;AACvC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,MAAI,eAAe,gBAAgB,YAAY,eAAe,aAAa,aAAa;AACtF,YAAQ;AAAA,MACN,2BACG,QAAQ,aACT,8BACA,aAAa,cACb,uCACA,YAAY,aACZ;AAAA,IACJ;AAAA,EACF;AAGA,QAAM,kBAAkB,QAAQ,mBAAmB;AAEnD,SAAO;AAAA,IACL,IAAI,WAAW;AAAA,IACf;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAUO,SAAS,sBAAsB,SAA8C;AAClF,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA,SAAS;AAAA,IACT,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAGJ,QAAM,aAAa,aAAa,cAAc,QAAQ,cAAc,cAAc;AAClF,MAAI,eAAe,QAAW;AAC5B,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAGA,QAAM,iBAAiB,aAAa,YAAY,QAAQ,kBAAkB,cAAc;AACxF,MAAI,mBAAmB,QAAW;AAChC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAMA,QAAM,WAAW,QAAQ,YAAY;AAErC,SAAO,WAAW;AAAA,IAChB;AAAA,IACA,aAAa,KAAK,MAAM,YAAY,UAAU;AAAA,IAC9C,iBAAiB,KAAK,MAAM,WAAW,UAAU;AAAA,IACjD,eAAe,KAAK,MAAM,SAAS,UAAU;AAAA,IAC7C;AAAA,IACA,uBAAuB,KAAK,KAAK,iBAAiB,UAAU;AAAA,IAC5D;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACH;AAKO,SAAS,YAAY,SAAwC;AAClE,QAAM;AAAA,IACJ;AAAA,IACA,QAAQ,CAAC;AAAA,IACT,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,SAAS;AAAA,IACT,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI;AAEJ,SAAO;AAAA,IACL,IAAI,WAAW;AAAA,IACf;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAKO,SAAS,eACd,QACA,aAAqB,OACrB,SAKU;AAEV,QAAM,kBAAkB,OAAO,OAAO,CAAC,YAAY,UAAU;AAC3D,UAAM,eAAe,MAAM,MAAM,OAAO,CAAC,KAAK,SAAS;AACrD,aAAO,KAAK,IAAI,KAAK,KAAK,cAAc,KAAK,eAAe;AAAA,IAC9D,GAAG,CAAC;AACJ,WAAO,KAAK,IAAI,YAAY,YAAY;AAAA,EAC1C,GAAG,CAAC;AAEJ,QAAM,WAAW,kBAAkB;AAEnC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,MAAM,SAAS;AAAA,IACf,OAAO,SAAS;AAAA,IAChB,eAAe,SAAS;AAAA,EAC1B;AACF;AAKA,SAAS,aAAqB;AAC5B,SAAO,GAAG,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,OAAO,GAAG,CAAC,CAAC;AACjE;AAyBO,SAAS,gBACd,OACA,aACA,WACa;AACb,SAAO,MAAM,MAAM,OAAO,CAAC,SAAS;AAClC,UAAM,UAAU,KAAK,cAAc,KAAK;AAIxC,WAAO,KAAK,cAAc,aAAa,UAAU;AAAA,EACnD,CAAC;AACH;AAKO,SAAS,iBAAiB,OAAkB,QAA6B;AAC9E,SAAO,MAAM,MAAM,OAAO,CAAC,SAAS;AAClC,UAAM,UAAU,KAAK,cAAc,KAAK;AACxC,WAAO,UAAU,KAAK,eAAe,SAAS;AAAA,EAChD,CAAC;AACH;AAKO,SAAS,aAAa,OAAkB,OAA2B;AACxE,QAAM,WAAW,MAAM,cAAc,MAAM;AAC3C,QAAM,WAAW,MAAM,cAAc,MAAM;AAE3C,SAAO,MAAM,cAAc,YAAY,WAAW,MAAM;AAC1D;AAKO,SAAS,gBAAgB,OAAiC;AAC/D,SAAO,CAAC,GAAG,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,EAAE,WAAW;AAChE;AAWO,SAAS,SAAS,OAAyB;AAChD,MAAI,MAAM,MAAM,WAAW,EAAG,QAAO,CAAC;AAEtC,QAAM,SAAS,gBAAgB,MAAM,KAAK;AAC1C,QAAM,OAAc,CAAC;AAErB,WAAS,IAAI,GAAG,IAAI,OAAO,SAAS,GAAG,KAAK;AAC1C,UAAM,iBAAiB,OAAO,CAAC,EAAE,cAAc,OAAO,CAAC,EAAE;AACzD,UAAM,gBAAgB,OAAO,IAAI,CAAC,EAAE;AAEpC,QAAI,gBAAgB,gBAAgB;AAClC,WAAK,KAAK;AAAA,QACR,aAAa;AAAA,QACb,WAAW;AAAA,QACX,iBAAiB,gBAAgB;AAAA,MACnC,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;;;AC7fO,IAAK,mBAAL,kBAAKA,sBAAL;AACL,EAAAA,kBAAA,YAAS;AACT,EAAAA,kBAAA,YAAS;AACT,EAAAA,kBAAA,WAAQ;AACR,EAAAA,kBAAA,YAAS;AACT,EAAAA,kBAAA,aAAU;AALA,SAAAA;AAAA,GAAA;;;ACtGL,SAAS,iBAAiB,SAAiB,YAA4B;AAC5E,SAAO,UAAU;AACnB;AAEO,SAAS,iBAAiB,SAAiB,YAA4B;AAC5E,SAAO,KAAK,KAAK,UAAU,UAAU;AACvC;AAEO,SAAS,gBAAgB,SAAiB,iBAAiC;AAChF,SAAO,KAAK,MAAM,UAAU,eAAe;AAC7C;AAEO,SAAS,gBAAgB,QAAgB,iBAAiC;AAC/E,SAAO,KAAK,MAAM,SAAS,eAAe;AAC5C;AAEO,SAAS,gBACd,QACA,iBACA,YACQ;AACR,SAAQ,SAAS,kBAAmB;AACtC;AAEO,SAAS,gBACd,SACA,iBACA,YACQ;AACR,SAAO,KAAK,KAAM,UAAU,aAAc,eAAe;AAC3D;;;AC7BO,IAAM,OAAO;AAGb,SAAS,aAAa,eAAiC,OAAO,MAAc;AACjF,QAAM,CAAC,EAAE,WAAW,IAAI;AACxB,SAAO,QAAQ,IAAI;AACrB;AAGO,SAAS,YAAY,eAAiC,OAAO,MAAc;AAChF,QAAM,CAAC,SAAS,IAAI;AACpB,SAAO,YAAY,aAAa,eAAe,IAAI;AACrD;AAGO,SAAS,eACd,OACA,KACA,YACA,OAAO,MACC;AACR,SAAO,KAAK,MAAO,QAAQ,KAAK,cAAe,MAAM,KAAK;AAC5D;AAGO,SAAS,eACd,SACA,KACA,YACA,OAAO,MACC;AACR,SAAO,KAAK,MAAO,UAAU,OAAO,OAAQ,KAAK,WAAW;AAC9D;AAGO,SAAS,WAAW,OAAe,eAA+B;AACvE,SAAO,KAAK,MAAM,QAAQ,aAAa,IAAI;AAC7C;AAGO,SAAS,oBACd,OACA,eACA,OAAO,MACC;AACR,QAAM,WAAW,YAAY,eAAe,IAAI;AAChD,QAAM,YAAY,aAAa,eAAe,IAAI;AAClD,QAAM,MAAM,KAAK,MAAM,QAAQ,QAAQ,IAAI;AAC3C,QAAM,YAAY,KAAK,MAAO,QAAQ,WAAY,SAAS,IAAI;AAC/D,MAAI,cAAc,EAAG,QAAO,GAAG,GAAG;AAClC,SAAO,GAAG,GAAG,IAAI,SAAS;AAC5B;;;ACpDA,IAAM,gBAAgB;AAYf,SAAS,eAAe,IAAY,QAAgB,eAAuB;AAChF,MAAI,OAAO,MAAM,EAAE,GAAG;AACpB,YAAQ,KAAK,iDAAiD;AAC9D,WAAO;AAAA,EACT;AACA,MAAI,SAAS,GAAG;AACd,YAAQ,KAAK,mEAAmE,KAAK;AACrF,WAAO;AAAA,EACT;AACA,MAAI,CAAC,SAAS,EAAE,KAAK,MAAM,MAAO,QAAO;AACzC,UAAQ,KAAK,SAAS,CAAC;AACzB;AAYO,SAAS,eAAe,YAAoB,QAAgB,eAAuB;AACxF,MAAI,CAAC,SAAS,UAAU,EAAG,QAAO;AAClC,MAAI,SAAS,GAAG;AACd,YAAQ,KAAK,mEAAmE,KAAK;AACrF,WAAO;AAAA,EACT;AACA,QAAM,UAAU,KAAK,IAAI,GAAG,UAAU;AACtC,SAAO,UAAU,CAAC,QAAQ;AAC5B;AAaO,SAAS,iBAAiB,MAAc,QAAgB,eAAuB;AACpF,MAAI,QAAQ,EAAG,QAAO;AACtB,QAAM,KAAK,KAAK,KAAK,MAAM,IAAI;AAC/B,SAAO,eAAe,IAAI,KAAK;AACjC;;;ACzDO,SAAS,cAAc,MAAyB;AACrD,SAAO,KAAK,cAAc,KAAK;AACjC;AAGO,SAAS,YAAY,MAAyB;AACnD,UAAQ,KAAK,cAAc,KAAK,mBAAmB,KAAK;AAC1D;AAGO,SAAS,eAAe,MAAyB;AACtD,SAAO,KAAK,gBAAgB,KAAK;AACnC;AAGO,SAAS,iBAAiB,MAAyB;AACxD,SAAO,KAAK,kBAAkB,KAAK;AACrC;AAQO,SAAS,eACd,aACA,iBACA,iBACQ;AACR,SACE,KAAK,OAAO,cAAc,mBAAmB,eAAe,IAC5D,KAAK,MAAM,cAAc,eAAe;AAE5C;;;ACzBO,SAAS,YAAY,QAAgB,QAA+B;AACzE,QAAM,QAAQ,IAAI,aAAa,MAAM;AACrC,QAAM,QAAQ,SAAS;AAEvB,WAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC/B,UAAM,IAAI,IAAI;AACd,UAAM,CAAC,IAAI,SAAS,IAAI,IAAI;AAAA,EAC9B;AAEA,SAAO;AACT;AAKO,SAAS,iBAAiB,QAAgB,QAA+B;AAC9E,QAAM,QAAQ,IAAI,aAAa,MAAM;AACrC,QAAM,QAAQ,SAAS;AAEvB,WAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC/B,UAAM,IAAI,IAAI;AACd,UAAM,QAAQ,SAAS,IAAI,SAAS,IAAI;AACxC,UAAM,KAAK,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK;AAAA,EAC5C;AAEA,SAAO;AACT;AAKO,SAAS,YAAY,QAAgB,QAA+B;AACzE,QAAM,QAAQ,IAAI,aAAa,MAAM;AACrC,QAAM,QAAQ,SAAS,KAAK,KAAK,IAAI,CAAC,KAAK,KAAK;AAEhD,WAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC/B,UAAM,CAAC,IAAI,KAAK,IAAK,KAAK,KAAK,IAAK,SAAS,KAAK,IAAI,IAAI;AAAA,EAC5D;AAEA,SAAO;AACT;AAKO,SAAS,iBAAiB,QAAgB,QAAiB,OAAe,IAAkB;AACjG,QAAM,QAAQ,IAAI,aAAa,MAAM;AAErC,WAAS,IAAI,GAAG,IAAI,QAAQ,KAAK;AAC/B,UAAM,QAAQ,SAAS,IAAI,SAAS,IAAI;AACxC,UAAM,IAAI,IAAI;AACd,UAAM,KAAK,IAAI,KAAK,IAAI,IAAI,OAAO,CAAC,IAAI,KAAK,IAAI,IAAI,IAAI;AAAA,EAC3D;AAEA,SAAO;AACT;AAKO,SAAS,cAAc,MAAgB,QAAgB,QAA+B;AAC3F,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO,YAAY,QAAQ,MAAM;AAAA,IACnC,KAAK;AACH,aAAO,iBAAiB,QAAQ,MAAM;AAAA,IACxC,KAAK;AACH,aAAO,YAAY,QAAQ,MAAM;AAAA,IACnC,KAAK;AACH,aAAO,iBAAiB,QAAQ,MAAM;AAAA,IACxC;AACE,aAAO,YAAY,QAAQ,MAAM;AAAA,EACrC;AACF;AAYO,SAAS,YACd,OACA,WACA,UACA,OAAiB,UACjB,aAAqB,GACrB,WAAmB,GACb;AACN,MAAI,YAAY,EAAG;AAEnB,MAAI,SAAS,UAAU;AACrB,UAAM,eAAe,YAAY,SAAS;AAC1C,UAAM,wBAAwB,UAAU,YAAY,QAAQ;AAAA,EAC9D,WAAW,SAAS,eAAe;AACjC,UAAM,eAAe,KAAK,IAAI,YAAY,IAAK,GAAG,SAAS;AAC3D,UAAM,6BAA6B,KAAK,IAAI,UAAU,IAAK,GAAG,YAAY,QAAQ;AAAA,EACpF,OAAO;AACL,UAAM,QAAQ,cAAc,MAAM,KAAO,IAAI;AAC7C,UAAM,cAAc,IAAI,aAAa,MAAM,MAAM;AACjD,UAAM,QAAQ,WAAW;AACzB,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,kBAAY,CAAC,IAAI,aAAa,MAAM,CAAC,IAAI;AAAA,IAC3C;AACA,UAAM,oBAAoB,aAAa,WAAW,QAAQ;AAAA,EAC5D;AACF;AAYO,SAAS,aACd,OACA,WACA,UACA,OAAiB,UACjB,aAAqB,GACrB,WAAmB,GACb;AACN,MAAI,YAAY,EAAG;AAEnB,MAAI,SAAS,UAAU;AACrB,UAAM,eAAe,YAAY,SAAS;AAC1C,UAAM,wBAAwB,UAAU,YAAY,QAAQ;AAAA,EAC9D,WAAW,SAAS,eAAe;AACjC,UAAM,eAAe,KAAK,IAAI,YAAY,IAAK,GAAG,SAAS;AAC3D,UAAM,6BAA6B,KAAK,IAAI,UAAU,IAAK,GAAG,YAAY,QAAQ;AAAA,EACpF,OAAO;AACL,UAAM,QAAQ,cAAc,MAAM,KAAO,KAAK;AAC9C,UAAM,cAAc,IAAI,aAAa,MAAM,MAAM;AACjD,UAAM,QAAQ,aAAa;AAC3B,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,kBAAY,CAAC,IAAI,WAAW,MAAM,CAAC,IAAI;AAAA,IACzC;AACA,UAAM,oBAAoB,aAAa,WAAW,QAAQ;AAAA,EAC5D;AACF;;;AC3IO,SAAS,oBACd,OACA,WACA,SACM;AACN,MAAI,CAAC,QAAS;AAId,MAAI,MAAM,OAAQ;AAElB,QAAM,SAAS,MAAM;AACrB,MAAI,OAAO,YAAY,WAAW,OAAO,YAAY,cAAc,OAAO,mBAAmB;AAC3F;AAAA,EACF;AAEA,QAAM,mBAAmB,UAAU,KAAK,CAAC,aAAa;AACpD,UAAM,WACJ,MAAM,IAAI,YAAY,MAAM,SAAS,IAAI,YAAY,KAAK,MAAM,QAAQ,SAAS;AAEnF,UAAM,YAAY,SAAS,YAAY,UAAa,MAAM,YAAY,SAAS;AAC/E,UAAM,aAAa,SAAS,aAAa,UAAa,MAAM,aAAa,SAAS;AAClF,UAAM,YAAY,SAAS,YAAY,UAAa,MAAM,YAAY,SAAS;AAC/E,UAAM,WAAW,SAAS,WAAW,UAAa,MAAM,WAAW,SAAS;AAE5E,WAAO,YAAY,aAAa,cAAc,aAAa;AAAA,EAC7D,CAAC;AAED,MAAI,kBAAkB;AACpB,QAAI,iBAAiB,mBAAmB,OAAO;AAC7C,YAAM,eAAe;AAAA,IACvB;AACA,qBAAiB,OAAO;AAAA,EAC1B;AACF;AAQO,IAAM,mBAAmB,CAAC,aAAuC;AACtE,QAAM,QAAkB,CAAC;AAGzB,QAAM,QAAQ,OAAO,cAAc,eAAe,UAAU,SAAS,SAAS,KAAK;AAEnF,MAAI,SAAS,SAAS;AACpB,UAAM,KAAK,QAAQ,QAAQ,MAAM;AAAA,EACnC;AAEA,MAAI,SAAS,WAAW,CAAC,SAAS,SAAS;AACzC,UAAM,KAAK,MAAM;AAAA,EACnB;AAEA,MAAI,SAAS,QAAQ;AACnB,UAAM,KAAK,QAAQ,WAAW,KAAK;AAAA,EACrC;AAEA,MAAI,SAAS,UAAU;AACrB,UAAM,KAAK,OAAO;AAAA,EACpB;AAEA,QAAM,KAAK,SAAS,IAAI,YAAY,CAAC;AAErC,SAAO,MAAM,KAAK,GAAG;AACvB;","names":["InteractionState"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/constants.ts","../src/types/clip.ts","../src/types/index.ts","../src/utils/conversions.ts","../src/utils/beatsAndBars.ts","../src/utils/dBUtils.ts","../src/utils/musicalTicks.ts","../src/clipTimeHelpers.ts","../src/fades.ts","../src/keyboard.ts"],"sourcesContent":["export * from './constants';\nexport * from './types';\nexport * from './utils';\nexport * from './clipTimeHelpers';\nexport * from './fades';\nexport * from './keyboard';\n","/**\n * Maximum width in CSS pixels for a single canvas chunk.\n * Canvas elements are split into chunks of this width to enable\n * horizontal virtual scrolling — only visible chunks are mounted.\n */\nexport const MAX_CANVAS_WIDTH = 1000;\n","/**\n * Clip-Based Model Types\n *\n * These types support a professional multi-track editing model where:\n * - Each track can contain multiple audio clips\n * - Clips can be positioned anywhere on the timeline\n * - Clips have independent trim points (offset/duration)\n * - Gaps between clips are silent\n * - Clips can overlap (for crossfades)\n */\n\nimport { Fade } from './index';\nimport type { RenderMode, SpectrogramConfig, ColorMapValue } from './spectrogram';\n\n/**\n * WaveformData object from waveform-data.js library.\n * Supports resample() and slice() for dynamic zoom levels.\n * See: https://github.com/bbc/waveform-data.js\n */\nexport interface WaveformDataObject {\n /** Sample rate of the original audio */\n readonly sample_rate: number;\n /** Number of audio samples per pixel */\n readonly scale: number;\n /** Length of waveform data in pixels */\n readonly length: number;\n /** Bit depth (8 or 16) */\n readonly bits: number;\n /** Duration in seconds */\n readonly duration: number;\n /** Number of channels */\n readonly channels: number;\n /** Get channel data */\n channel: (index: number) => {\n min_array: () => number[];\n max_array: () => number[];\n };\n /** Resample to different scale */\n resample: (options: { scale: number } | { width: number }) => WaveformDataObject;\n /** Slice a portion of the waveform */\n slice: (\n options: { startTime: number; endTime: number } | { startIndex: number; endIndex: number }\n ) => WaveformDataObject;\n}\n\n/**\n * Generic effects function type for track-level audio processing.\n *\n * The actual implementation receives Tone.js audio nodes. Using generic types\n * here to avoid circular dependencies with the playout package.\n *\n * @param graphEnd - The end of the track's audio graph (Tone.js Gain node)\n * @param destination - Where to connect the effects output (Tone.js ToneAudioNode)\n * @param isOffline - Whether rendering offline (for export)\n * @returns Optional cleanup function called when track is disposed\n *\n * @example\n * ```typescript\n * const trackEffects: TrackEffectsFunction = (graphEnd, destination, isOffline) => {\n * const reverb = new Tone.Reverb({ decay: 1.5 });\n * graphEnd.connect(reverb);\n * reverb.connect(destination);\n *\n * return () => {\n * reverb.dispose();\n * };\n * };\n * ```\n */\nexport type TrackEffectsFunction = (\n graphEnd: unknown,\n destination: unknown,\n isOffline: boolean\n) => void | (() => void);\n\n/**\n * Represents a single audio clip on the timeline\n *\n * IMPORTANT: All positions/durations are stored as SAMPLE COUNTS (integers)\n * to avoid floating-point precision errors. Convert to seconds only when\n * needed for playback using: seconds = samples / sampleRate\n *\n * Clips can be created with just waveformData (for instant visual rendering)\n * and have audioBuffer added later when audio finishes loading.\n */\nexport interface AudioClip {\n /** Unique identifier for this clip */\n id: string;\n\n /**\n * The audio buffer containing the audio data.\n * Optional for peaks-first rendering - can be added later.\n * Required for playback and editing operations.\n */\n audioBuffer?: AudioBuffer;\n\n /** Position on timeline where this clip starts (in samples at timeline sampleRate) */\n startSample: number;\n\n /** Duration of this clip (in samples) - how much of the audio buffer to play */\n durationSamples: number;\n\n /** Offset into the audio buffer where playback starts (in samples) - the \"trim start\" point */\n offsetSamples: number;\n\n /**\n * Sample rate for this clip's audio.\n * Required when audioBuffer is not provided (for peaks-first rendering).\n * When audioBuffer is present, this should match audioBuffer.sampleRate.\n */\n sampleRate: number;\n\n /**\n * Total duration of the source audio in samples.\n * Required when audioBuffer is not provided (for trim bounds calculation).\n * When audioBuffer is present, this should equal audioBuffer.length.\n */\n sourceDurationSamples: number;\n\n /** Optional fade in effect */\n fadeIn?: Fade;\n\n /** Optional fade out effect */\n fadeOut?: Fade;\n\n /** Clip-specific gain/volume multiplier (0.0 to 1.0+) */\n gain: number;\n\n /** Optional label/name for this clip */\n name?: string;\n\n /** Optional color for visual distinction */\n color?: string;\n\n /**\n * Pre-computed waveform data from waveform-data.js library.\n * When provided, the library will use this instead of computing peaks from the audioBuffer.\n * Supports resampling to different zoom levels and slicing for clip trimming.\n * Load with: `const waveformData = await loadWaveformData('/path/to/peaks.dat')`\n */\n waveformData?: WaveformDataObject;\n\n /**\n * MIDI note data — when present, this clip plays MIDI instead of audio.\n * The playout adapter uses this field to detect MIDI clips and route them\n * to MidiToneTrack (PolySynth) instead of ToneTrack (AudioBufferSourceNode).\n */\n midiNotes?: MidiNoteData[];\n\n /** MIDI channel (0-indexed). Channel 9 = GM percussion. */\n midiChannel?: number;\n\n /** MIDI program number (0-127). GM instrument number for SoundFont playback. */\n midiProgram?: number;\n}\n\n/**\n * Represents a track containing multiple audio clips\n */\nexport interface ClipTrack {\n /** Unique identifier for this track */\n id: string;\n\n /** Display name for this track */\n name: string;\n\n /** Array of audio clips on this track */\n clips: AudioClip[];\n\n /** Whether this track is muted */\n muted: boolean;\n\n /** Whether this track is soloed */\n soloed: boolean;\n\n /** Track volume (0.0 to 1.0+) */\n volume: number;\n\n /** Stereo pan (-1.0 = left, 0 = center, 1.0 = right) */\n pan: number;\n\n /** Optional track color for visual distinction */\n color?: string;\n\n /** Track height in pixels (for UI) */\n height?: number;\n\n /** Optional effects function for this track */\n effects?: TrackEffectsFunction;\n\n /** Visualization render mode. Default: 'waveform' */\n renderMode?: RenderMode;\n\n /** Per-track spectrogram configuration (FFT size, window, frequency scale, etc.) */\n spectrogramConfig?: SpectrogramConfig;\n\n /** Per-track spectrogram color map name or custom color array */\n spectrogramColorMap?: ColorMapValue;\n}\n\n/**\n * Represents the entire timeline/project\n */\nexport interface Timeline {\n /** All tracks in the timeline */\n tracks: ClipTrack[];\n\n /** Total timeline duration in seconds */\n duration: number;\n\n /** Sample rate for all audio (typically 44100 or 48000) */\n sampleRate: number;\n\n /** Optional project name */\n name?: string;\n\n /** Optional tempo (BPM) for grid snapping */\n tempo?: number;\n\n /** Optional time signature for grid snapping */\n timeSignature?: {\n numerator: number;\n denominator: number;\n };\n}\n\n/**\n * Options for creating a new audio clip (using sample counts)\n *\n * Either audioBuffer OR (sampleRate + sourceDurationSamples + waveformData) must be provided.\n * Providing waveformData without audioBuffer enables peaks-first rendering.\n */\nexport interface CreateClipOptions {\n /** Audio buffer - optional for peaks-first rendering */\n audioBuffer?: AudioBuffer;\n startSample: number; // Position on timeline (in samples)\n durationSamples?: number; // Defaults to full buffer/source duration (in samples)\n offsetSamples?: number; // Defaults to 0\n gain?: number; // Defaults to 1.0\n name?: string;\n color?: string;\n fadeIn?: Fade;\n fadeOut?: Fade;\n /** Pre-computed waveform data from waveform-data.js (e.g., from BBC audiowaveform) */\n waveformData?: WaveformDataObject;\n /** Sample rate - required if audioBuffer not provided */\n sampleRate?: number;\n /** Total source audio duration in samples - required if audioBuffer not provided */\n sourceDurationSamples?: number;\n /** MIDI note data — passed through to the created AudioClip */\n midiNotes?: MidiNoteData[];\n /** MIDI channel (0-indexed). Channel 9 = GM percussion. */\n midiChannel?: number;\n /** MIDI program number (0-127). GM instrument for SoundFont playback. */\n midiProgram?: number;\n}\n\n/**\n * Options for creating a new audio clip (using seconds for convenience)\n *\n * Either audioBuffer OR (sampleRate + sourceDuration + waveformData) must be provided.\n * Providing waveformData without audioBuffer enables peaks-first rendering.\n */\nexport interface CreateClipOptionsSeconds {\n /** Audio buffer - optional for peaks-first rendering */\n audioBuffer?: AudioBuffer;\n startTime: number; // Position on timeline (in seconds)\n duration?: number; // Defaults to full buffer/source duration (in seconds)\n offset?: number; // Defaults to 0 (in seconds)\n gain?: number; // Defaults to 1.0\n name?: string;\n color?: string;\n fadeIn?: Fade;\n fadeOut?: Fade;\n /** Pre-computed waveform data from waveform-data.js (e.g., from BBC audiowaveform) */\n waveformData?: WaveformDataObject;\n /** Sample rate - required if audioBuffer not provided */\n sampleRate?: number;\n /** Total source audio duration in seconds - required if audioBuffer not provided */\n sourceDuration?: number;\n /** MIDI note data — passed through to the created AudioClip */\n midiNotes?: MidiNoteData[];\n /** MIDI channel (0-indexed). Channel 9 = GM percussion. */\n midiChannel?: number;\n /** MIDI program number (0-127). GM instrument for SoundFont playback. */\n midiProgram?: number;\n}\n\n/**\n * Options for creating a new track\n */\nexport interface CreateTrackOptions {\n name: string;\n clips?: AudioClip[];\n muted?: boolean;\n soloed?: boolean;\n volume?: number;\n pan?: number;\n color?: string;\n height?: number;\n spectrogramConfig?: SpectrogramConfig;\n spectrogramColorMap?: ColorMapValue;\n}\n\n/**\n * Creates a new AudioClip with sensible defaults (using sample counts)\n *\n * For peaks-first rendering (no audioBuffer), sampleRate and sourceDurationSamples can be:\n * - Provided explicitly via options\n * - Derived from waveformData (sample_rate and duration properties)\n */\nexport function createClip(options: CreateClipOptions): AudioClip {\n const {\n audioBuffer,\n startSample,\n offsetSamples = 0,\n gain = 1.0,\n name,\n color,\n fadeIn,\n fadeOut,\n waveformData,\n midiNotes,\n midiChannel,\n midiProgram,\n } = options;\n\n // Determine sample rate: audioBuffer > explicit option > waveformData\n const sampleRate = audioBuffer?.sampleRate ?? options.sampleRate ?? waveformData?.sample_rate;\n\n // Determine source duration: audioBuffer > explicit option > waveformData (converted to samples)\n const sourceDurationSamples =\n audioBuffer?.length ??\n options.sourceDurationSamples ??\n (waveformData && sampleRate ? Math.ceil(waveformData.duration * sampleRate) : undefined);\n\n if (sampleRate === undefined) {\n throw new Error(\n 'createClip: sampleRate is required when audioBuffer is not provided (can use waveformData.sample_rate)'\n );\n }\n if (sourceDurationSamples === undefined) {\n throw new Error(\n 'createClip: sourceDurationSamples is required when audioBuffer is not provided (can use waveformData.duration)'\n );\n }\n\n if (audioBuffer && waveformData && audioBuffer.sampleRate !== waveformData.sample_rate) {\n console.warn(\n '[waveform-playlist] \"' +\n (name ?? 'unnamed') +\n '\": pre-computed peaks at ' +\n waveformData.sample_rate +\n ' Hz do not match decoded audio at ' +\n audioBuffer.sampleRate +\n ' Hz — peaks may be recomputed from decoded audio'\n );\n }\n\n // Default duration to full source duration\n const durationSamples = options.durationSamples ?? sourceDurationSamples;\n\n return {\n id: generateId(),\n audioBuffer,\n startSample,\n durationSamples,\n offsetSamples,\n sampleRate,\n sourceDurationSamples,\n gain,\n name,\n color,\n fadeIn,\n fadeOut,\n waveformData,\n midiNotes,\n midiChannel,\n midiProgram,\n };\n}\n\n/**\n * Creates a new AudioClip from time-based values (convenience function)\n * Converts seconds to samples using the audioBuffer's sampleRate or explicit sampleRate\n *\n * For peaks-first rendering (no audioBuffer), sampleRate and sourceDuration can be:\n * - Provided explicitly via options\n * - Derived from waveformData (sample_rate and duration properties)\n */\nexport function createClipFromSeconds(options: CreateClipOptionsSeconds): AudioClip {\n const {\n audioBuffer,\n startTime,\n offset = 0,\n gain = 1.0,\n name,\n color,\n fadeIn,\n fadeOut,\n waveformData,\n midiNotes,\n midiChannel,\n midiProgram,\n } = options;\n\n // Determine sample rate: audioBuffer > explicit option > waveformData\n const sampleRate = audioBuffer?.sampleRate ?? options.sampleRate ?? waveformData?.sample_rate;\n if (sampleRate === undefined) {\n throw new Error(\n 'createClipFromSeconds: sampleRate is required when audioBuffer is not provided (can use waveformData.sample_rate)'\n );\n }\n\n // Determine source duration: audioBuffer > explicit option > waveformData\n const sourceDuration = audioBuffer?.duration ?? options.sourceDuration ?? waveformData?.duration;\n if (sourceDuration === undefined) {\n throw new Error(\n 'createClipFromSeconds: sourceDuration is required when audioBuffer is not provided (can use waveformData.duration)'\n );\n }\n\n // Note: audioBuffer and waveformData may have different sample rates (e.g., Opus at 48000\n // decoded on 44100 hardware). This is expected — callers fall back to worker to recompute peaks.\n\n // Default clip duration to full source duration\n const duration = options.duration ?? sourceDuration;\n\n return createClip({\n audioBuffer,\n startSample: Math.round(startTime * sampleRate),\n durationSamples: Math.round(duration * sampleRate),\n offsetSamples: Math.round(offset * sampleRate),\n sampleRate,\n sourceDurationSamples: Math.ceil(sourceDuration * sampleRate),\n gain,\n name,\n color,\n fadeIn,\n fadeOut,\n waveformData,\n midiNotes,\n midiChannel,\n midiProgram,\n });\n}\n\n/**\n * Creates a new ClipTrack with sensible defaults\n */\nexport function createTrack(options: CreateTrackOptions): ClipTrack {\n const {\n name,\n clips = [],\n muted = false,\n soloed = false,\n volume = 1.0,\n pan = 0,\n color,\n height,\n spectrogramConfig,\n spectrogramColorMap,\n } = options;\n\n return {\n id: generateId(),\n name,\n clips,\n muted,\n soloed,\n volume,\n pan,\n color,\n height,\n spectrogramConfig,\n spectrogramColorMap,\n };\n}\n\n/**\n * Creates a new Timeline with sensible defaults\n */\nexport function createTimeline(\n tracks: ClipTrack[],\n sampleRate: number = 44100,\n options?: {\n name?: string;\n tempo?: number;\n timeSignature?: { numerator: number; denominator: number };\n }\n): Timeline {\n // Calculate total duration from all clips across all tracks (in seconds)\n const durationSamples = tracks.reduce((maxSamples, track) => {\n const trackSamples = track.clips.reduce((max, clip) => {\n return Math.max(max, clip.startSample + clip.durationSamples);\n }, 0);\n return Math.max(maxSamples, trackSamples);\n }, 0);\n\n const duration = durationSamples / sampleRate;\n\n return {\n tracks,\n duration,\n sampleRate,\n name: options?.name,\n tempo: options?.tempo,\n timeSignature: options?.timeSignature,\n };\n}\n\n/**\n * Generates a unique ID for clips and tracks\n */\nfunction generateId(): string {\n return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n}\n\n/**\n * MIDI note data for clips that play MIDI instead of audio.\n * When present on an AudioClip, the clip is treated as a MIDI clip\n * by the playout adapter.\n */\nexport interface MidiNoteData {\n /** MIDI note number (0-127) */\n midi: number;\n /** Note name in scientific pitch notation (\"C4\", \"G#3\") */\n name: string;\n /** Start time in seconds, relative to clip start */\n time: number;\n /** Duration in seconds */\n duration: number;\n /** Velocity (0-1 normalized) */\n velocity: number;\n /** MIDI channel (0-indexed). Channel 9 = GM percussion. Enables per-note routing in flattened tracks. */\n channel?: number;\n}\n\n/**\n * Utility: Get all clips within a sample range\n */\nexport function getClipsInRange(\n track: ClipTrack,\n startSample: number,\n endSample: number\n): AudioClip[] {\n return track.clips.filter((clip) => {\n const clipEnd = clip.startSample + clip.durationSamples;\n // Clip overlaps with range if:\n // - Clip starts before range ends AND\n // - Clip ends after range starts\n return clip.startSample < endSample && clipEnd > startSample;\n });\n}\n\n/**\n * Utility: Get all clips at a specific sample position\n */\nexport function getClipsAtSample(track: ClipTrack, sample: number): AudioClip[] {\n return track.clips.filter((clip) => {\n const clipEnd = clip.startSample + clip.durationSamples;\n return sample >= clip.startSample && sample < clipEnd;\n });\n}\n\n/**\n * Utility: Check if two clips overlap\n */\nexport function clipsOverlap(clip1: AudioClip, clip2: AudioClip): boolean {\n const clip1End = clip1.startSample + clip1.durationSamples;\n const clip2End = clip2.startSample + clip2.durationSamples;\n\n return clip1.startSample < clip2End && clip1End > clip2.startSample;\n}\n\n/**\n * Utility: Sort clips by startSample\n */\nexport function sortClipsByTime(clips: AudioClip[]): AudioClip[] {\n return [...clips].sort((a, b) => a.startSample - b.startSample);\n}\n\n/**\n * Utility: Find gaps between clips (silent regions)\n */\nexport interface Gap {\n startSample: number;\n endSample: number;\n durationSamples: number;\n}\n\nexport function findGaps(track: ClipTrack): Gap[] {\n if (track.clips.length === 0) return [];\n\n const sorted = sortClipsByTime(track.clips);\n const gaps: Gap[] = [];\n\n for (let i = 0; i < sorted.length - 1; i++) {\n const currentClipEnd = sorted[i].startSample + sorted[i].durationSamples;\n const nextClipStart = sorted[i + 1].startSample;\n\n if (nextClipStart > currentClipEnd) {\n gaps.push({\n startSample: currentClipEnd,\n endSample: nextClipStart,\n durationSamples: nextClipStart - currentClipEnd,\n });\n }\n }\n\n return gaps;\n}\n","/**\n * Peaks type - represents a typed array of interleaved min/max peak data\n */\nexport type Peaks = Int8Array | Int16Array;\n\n/**\n * Bits type - number of bits for peak data\n */\nexport type Bits = 8 | 16;\n\n/**\n * PeakData - result of peak extraction\n */\nexport interface PeakData {\n /** Number of peak pairs extracted */\n length: number;\n /** Array of peak data for each channel (interleaved min/max) */\n data: Peaks[];\n /** Bit depth of peak data */\n bits: Bits;\n}\n\nexport interface WaveformConfig {\n sampleRate: number;\n samplesPerPixel: number;\n waveHeight?: number;\n waveOutlineColor?: string;\n waveFillColor?: string;\n waveProgressColor?: string;\n}\n\nexport interface AudioBuffer {\n length: number;\n duration: number;\n numberOfChannels: number;\n sampleRate: number;\n getChannelData(channel: number): Float32Array;\n}\n\nexport interface Track {\n id: string;\n name: string;\n src?: string | AudioBuffer; // Support both URL strings and AudioBuffer objects\n gain: number;\n muted: boolean;\n soloed: boolean;\n stereoPan: number;\n startTime: number;\n endTime?: number;\n fadeIn?: Fade;\n fadeOut?: Fade;\n cueIn?: number;\n cueOut?: number;\n}\n\n/**\n * Simple fade configuration\n */\nexport interface Fade {\n /** Duration of the fade in seconds */\n duration: number;\n /** Type of fade curve (default: 'linear') */\n type?: FadeType;\n}\n\nexport type FadeType = 'logarithmic' | 'linear' | 'sCurve' | 'exponential';\n\n/**\n * Alias for Fade — used by media-element-playout and playout packages\n */\nexport type FadeConfig = Fade;\n\nexport interface PlaylistConfig {\n samplesPerPixel?: number;\n waveHeight?: number;\n container?: HTMLElement;\n isAutomaticScroll?: boolean;\n timescale?: boolean;\n colors?: {\n waveOutlineColor?: string;\n waveFillColor?: string;\n waveProgressColor?: string;\n };\n controls?: {\n show?: boolean;\n width?: number;\n };\n zoomLevels?: number[];\n}\n\nexport interface PlayoutState {\n isPlaying: boolean;\n isPaused: boolean;\n cursor: number;\n duration: number;\n}\n\nexport interface TimeSelection {\n start: number;\n end: number;\n}\n\nexport enum InteractionState {\n Cursor = 'cursor',\n Select = 'select',\n Shift = 'shift',\n FadeIn = 'fadein',\n FadeOut = 'fadeout',\n}\n\n// Export clip-based model types\nexport * from './clip';\n\n// Export spectrogram types\nexport * from './spectrogram';\n\n// Export annotation types\nexport * from './annotations';\n","export function samplesToSeconds(samples: number, sampleRate: number): number {\n return samples / sampleRate;\n}\n\nexport function secondsToSamples(seconds: number, sampleRate: number): number {\n return Math.ceil(seconds * sampleRate);\n}\n\nexport function samplesToPixels(samples: number, samplesPerPixel: number): number {\n return Math.floor(samples / samplesPerPixel);\n}\n\nexport function pixelsToSamples(pixels: number, samplesPerPixel: number): number {\n return Math.floor(pixels * samplesPerPixel);\n}\n\nexport function pixelsToSeconds(\n pixels: number,\n samplesPerPixel: number,\n sampleRate: number\n): number {\n return (pixels * samplesPerPixel) / sampleRate;\n}\n\nexport function secondsToPixels(\n seconds: number,\n samplesPerPixel: number,\n sampleRate: number\n): number {\n return Math.ceil((seconds * sampleRate) / samplesPerPixel);\n}\n","/** Default PPQN matching Tone.js Transport (192 ticks per quarter note) */\nexport const PPQN = 192;\n\n/** Number of PPQN ticks per beat for the given time signature. */\nexport function ticksPerBeat(timeSignature: [number, number], ppqn = PPQN): number {\n const [, denominator] = timeSignature;\n return ppqn * (4 / denominator);\n}\n\n/** Number of PPQN ticks per bar for the given time signature. */\nexport function ticksPerBar(timeSignature: [number, number], ppqn = PPQN): number {\n const [numerator] = timeSignature;\n return numerator * ticksPerBeat(timeSignature, ppqn);\n}\n\n/** Convert PPQN ticks to sample count. Uses Math.round for integer sample alignment. */\nexport function ticksToSamples(\n ticks: number,\n bpm: number,\n sampleRate: number,\n ppqn = PPQN\n): number {\n return Math.round((ticks * 60 * sampleRate) / (bpm * ppqn));\n}\n\n/** Convert sample count to PPQN ticks. Inverse of ticksToSamples. */\nexport function samplesToTicks(\n samples: number,\n bpm: number,\n sampleRate: number,\n ppqn = PPQN\n): number {\n return Math.round((samples * ppqn * bpm) / (60 * sampleRate));\n}\n\n/** Snap a tick position to the nearest grid line (rounds to nearest). */\nexport function snapToGrid(ticks: number, gridSizeTicks: number): number {\n return Math.round(ticks / gridSizeTicks) * gridSizeTicks;\n}\n\n/** Format ticks as a 1-indexed bar.beat label. Beat 1 shows bar number only (e.g., \"3\" not \"3.1\"). */\nexport function ticksToBarBeatLabel(\n ticks: number,\n timeSignature: [number, number],\n ppqn = PPQN\n): string {\n const barTicks = ticksPerBar(timeSignature, ppqn);\n const beatTicks = ticksPerBeat(timeSignature, ppqn);\n const bar = Math.floor(ticks / barTicks) + 1;\n const beatInBar = Math.floor((ticks % barTicks) / beatTicks) + 1;\n if (beatInBar === 1) return `${bar}`;\n return `${bar}.${beatInBar}`;\n}\n","const DEFAULT_FLOOR = -100;\n\n/**\n * Convert a dB value to a normalized range.\n *\n * Maps dB values linearly: floor → 0, 0 dB → 1.\n * Values above 0 dB map to > 1 (e.g., +5 dB → 1.05 with default floor).\n *\n * @param dB - Decibel value (typically -Infinity to +5)\n * @param floor - Minimum dB value mapped to 0. Default: -100 (Firefox compat)\n * @returns Normalized value (0 at floor, 1 at 0 dB, >1 above 0 dB)\n */\nexport function dBToNormalized(dB: number, floor: number = DEFAULT_FLOOR): number {\n if (Number.isNaN(dB)) {\n console.warn('[waveform-playlist] dBToNormalized received NaN');\n return 0;\n }\n if (floor >= 0) {\n console.warn('[waveform-playlist] dBToNormalized floor must be negative, got:', floor);\n return 0;\n }\n if (!isFinite(dB) || dB <= floor) return 0;\n return (dB - floor) / -floor;\n}\n\n/**\n * Convert a normalized value back to dB.\n *\n * Maps linearly: 0 → floor, 1 → 0 dB.\n * Values above 1 map to positive dB (e.g., 1.05 → +5 dB with default floor).\n *\n * @param normalized - Normalized value (0 = floor, 1 = 0 dB)\n * @param floor - Minimum dB value (maps from 0). Must be negative. Default: -100\n * @returns dB value (floor at 0, 0 dB at 1, positive dB above 1)\n */\nexport function normalizedToDb(normalized: number, floor: number = DEFAULT_FLOOR): number {\n if (!isFinite(normalized)) return floor;\n if (floor >= 0) {\n console.warn('[waveform-playlist] normalizedToDb floor must be negative, got:', floor);\n return DEFAULT_FLOOR;\n }\n const clamped = Math.max(0, normalized);\n return clamped * -floor + floor;\n}\n\n/**\n * Convert a linear gain value to decibels.\n *\n * @param gain - Linear gain (0 = silence, 1 = unity)\n * @returns Decibel value (e.g., 0.5 → ≈ -6.02 dB)\n */\nexport function gainToDb(gain: number): number {\n return 20 * Math.log10(Math.max(gain, 0.0001));\n}\n\n/**\n * Convert a linear gain value (0-1+) to normalized 0-1 via dB.\n *\n * Combines gain-to-dB (20 * log10) with dBToNormalized for a consistent\n * mapping from raw AudioWorklet peak/RMS values to the 0-1 range used\n * by UI meter components.\n *\n * @param gain - Linear gain value (typically 0 to 1, can exceed 1)\n * @param floor - Minimum dB value mapped to 0. Default: -100\n * @returns Normalized value (0 at silence/floor, 1 at 0 dB, >1 above 0 dB)\n */\nexport function gainToNormalized(gain: number, floor: number = DEFAULT_FLOOR): number {\n if (gain <= 0) return 0;\n // Use raw log10 (no clamp) — gain > 0 is guaranteed above,\n // and dBToNormalized handles -Infinity via its floor check.\n const db = 20 * Math.log10(gain);\n return dBToNormalized(db, floor);\n}\n","import { ticksPerBeat, ticksPerBar, ticksToBarBeatLabel } from './beatsAndBars';\n\n/** All supported snap-to-grid values. */\nexport type SnapTo =\n | 'bar'\n | 'beat'\n | '1/2'\n | '1/4'\n | '1/8'\n | '1/16'\n | '1/32'\n | '1/2T'\n | '1/4T'\n | '1/8T'\n | '1/16T'\n | 'off';\n\n/**\n * Returns the tick interval for the given SnapTo value.\n *\n * Straight subdivisions (1/2, 1/4, 1/8, 1/16, 1/32) are always expressed as\n * fractions of a quarter note (ppqn), independent of the time signature\n * denominator. Triplet subdivisions use × 2/3 of the corresponding straight\n * value. 'bar' and 'beat' depend on the time signature. 'off' returns 0.\n */\nexport function snapToTicks(snapTo: SnapTo, timeSignature: [number, number], ppqn = 960): number {\n switch (snapTo) {\n case 'bar':\n return ticksPerBar(timeSignature, ppqn);\n case 'beat':\n return ticksPerBeat(timeSignature, ppqn);\n case '1/2':\n return ppqn * 2;\n case '1/4':\n return ppqn;\n case '1/8':\n return ppqn / 2;\n case '1/16':\n return ppqn / 4;\n case '1/32':\n return ppqn / 8;\n case '1/2T':\n return Math.round((ppqn * 2 * 2) / 3);\n case '1/4T':\n return Math.round((ppqn * 2) / 3);\n case '1/8T':\n return Math.round((ppqn * 2) / 6);\n case '1/16T':\n return Math.round((ppqn * 2) / 12);\n case 'off':\n return 0;\n }\n}\n\n/**\n * Three-tier tick hierarchy (following Audacity's model):\n * major — Bar boundaries. Always labeled, strongest grid lines.\n * minor — Beat boundaries. Labeled when wide enough, medium grid lines.\n * minorMinor — Subdivisions (eighths, sixteenths). Never labeled, ruler ticks only (no grid).\n */\nexport type TickType = 'major' | 'minor' | 'minorMinor';\n\n/** Zoom level category used to select which subdivision to iterate at. */\nexport type ZoomLevel = 'coarse' | 'bar' | 'beat' | 'eighth' | 'sixteenth';\n\n/** A single musical tick with rendering metadata. */\nexport interface MusicalTick {\n /** Pixel position of the tick in the timeline. */\n pixel: number;\n /** Three-tier type: major (bar), minor (beat), minorMinor (subdivision). */\n type: TickType;\n /** Human-readable label. Present for major ticks always; minor ticks when zoomed in. */\n label?: string;\n /** 0-based global bar index (for alternating bar-level striping). */\n barIndex: number;\n}\n\n/** Result of computeMusicalTicks(). */\nexport interface MusicalTickData {\n ticks: MusicalTick[];\n pixelsPerBar: number;\n pixelsPerBeat: number;\n zoomLevel: ZoomLevel;\n /** At 'coarse' zoom: how many bars between rendered tick lines. */\n coarseBarStep?: number;\n}\n\n/** Parameters for computeMusicalTicks(). */\nexport interface MusicalTickParams {\n timeSignature: [number, number];\n /** Ticks per pixel (zoom level — lower value = more zoomed in). */\n ticksPerPixel: number;\n startPixel: number;\n endPixel: number;\n /** Pulses per quarter note. Defaults to 960. */\n ppqn?: number;\n}\n\n/** Minimum pixels per musical unit before switching to a coarser zoom level. */\nexport const MIN_PIXELS_PER_UNIT = 8;\n\n/** Minimum pixels between beat labels for readable text. */\nconst MIN_PIXELS_PER_LABEL = 60;\n\n/**\n * Determines the zoom level and computes which tick lines to render for a\n * given viewport. Pure tick arithmetic — no BPM or sample rate required.\n */\nexport function computeMusicalTicks(params: MusicalTickParams): MusicalTickData {\n const { timeSignature, ticksPerPixel, startPixel, endPixel, ppqn = 960 } = params;\n\n // Guard against invalid inputs that would cause division by zero or infinite loops\n if (ticksPerPixel <= 0 || ppqn <= 0 || timeSignature[1] <= 0) {\n return { ticks: [], pixelsPerBar: 0, pixelsPerBeat: 0, zoomLevel: 'coarse' };\n }\n\n const tpBeat = ticksPerBeat(timeSignature, ppqn);\n const tpBar = ticksPerBar(timeSignature, ppqn);\n const tpEighth = ppqn / 2;\n const tpSixteenth = ppqn / 4;\n\n const pixelsPerBar = tpBar / ticksPerPixel;\n const pixelsPerBeat = tpBeat / ticksPerPixel;\n const pixelsPerEighth = tpEighth / ticksPerPixel;\n const pixelsPerSixteenth = tpSixteenth / ticksPerPixel;\n\n // Determine zoom level based on pixel density thresholds.\n let zoomLevel: ZoomLevel;\n if (pixelsPerBar < MIN_PIXELS_PER_UNIT) {\n zoomLevel = 'coarse';\n } else if (pixelsPerBeat < MIN_PIXELS_PER_UNIT) {\n zoomLevel = 'bar';\n } else if (pixelsPerEighth < MIN_PIXELS_PER_UNIT) {\n zoomLevel = 'beat';\n } else if (pixelsPerSixteenth < MIN_PIXELS_PER_UNIT) {\n zoomLevel = 'eighth';\n } else {\n zoomLevel = 'sixteenth';\n }\n\n // Determine step size in ticks and coarse bar step when zoomed far out.\n let stepTicks: number;\n let coarseBarStep: number | undefined;\n\n if (zoomLevel === 'coarse') {\n // Choose the smallest power-of-2 multiple of tpBar that gives ≥8px.\n let multiplier = 2;\n while ((tpBar * multiplier) / ticksPerPixel < MIN_PIXELS_PER_UNIT) {\n multiplier *= 2;\n }\n stepTicks = tpBar * multiplier;\n coarseBarStep = multiplier;\n } else if (zoomLevel === 'bar') {\n stepTicks = tpBar;\n } else if (zoomLevel === 'beat') {\n stepTicks = tpBeat;\n } else if (zoomLevel === 'eighth') {\n stepTicks = tpEighth;\n } else {\n stepTicks = tpSixteenth;\n }\n\n // Convert pixel viewport to tick range, align start to step boundary.\n const startTick = startPixel * ticksPerPixel;\n const endTick = endPixel * ticksPerPixel;\n const firstStep = Math.floor(startTick / stepTicks) * stepTicks;\n\n const ticks: MusicalTick[] = [];\n\n for (let tick = firstStep; tick <= endTick; tick += stepTicks) {\n const pixel = tick / ticksPerPixel;\n\n if (pixel < startPixel || pixel > endPixel) {\n continue;\n }\n\n // Classify into three-tier hierarchy (Audacity model):\n // major = bar boundary\n // minor = beat boundary\n // minorMinor = subdivision (eighth, sixteenth)\n let type: TickType;\n if (tick % tpBar === 0) {\n type = 'major';\n } else if (tick % tpBeat === 0) {\n type = 'minor';\n } else {\n type = 'minorMinor';\n }\n\n // Bar index for alternating bar-level zebra stripes\n const barIndex = Math.floor(tick / tpBar);\n\n // Labels: major always, minor only when wide enough, minorMinor never\n let label: string | undefined;\n if (type === 'major') {\n label = ticksToBarBeatLabel(tick, timeSignature, ppqn);\n } else if (type === 'minor' && pixelsPerBeat >= MIN_PIXELS_PER_LABEL) {\n label = ticksToBarBeatLabel(tick, timeSignature, ppqn);\n }\n\n ticks.push({ pixel, type, barIndex, ...(label !== undefined ? { label } : {}) });\n }\n\n const result: MusicalTickData = {\n ticks,\n pixelsPerBar,\n pixelsPerBeat,\n zoomLevel,\n ...(coarseBarStep !== undefined ? { coarseBarStep } : {}),\n };\n\n return result;\n}\n\n/**\n * Snaps a tick position to the nearest grid boundary defined by `snapTo`.\n *\n * Returns the original tick unchanged when `snapTo` is 'off'.\n */\nexport function snapTickToGrid(\n tick: number,\n snapTo: SnapTo,\n timeSignature: [number, number],\n ppqn = 960\n): number {\n if (snapTo === 'off') return tick;\n const gridSize = snapToTicks(snapTo, timeSignature, ppqn);\n if (gridSize <= 0) return tick;\n return Math.round(tick / gridSize) * gridSize;\n}\n","import type { AudioClip, ClipTrack } from './types';\n\n/** Clip start position in seconds */\nexport function clipStartTime(clip: AudioClip): number {\n return clip.startSample / clip.sampleRate;\n}\n\n/** Clip end position in seconds (start + duration) */\nexport function clipEndTime(clip: AudioClip): number {\n return (clip.startSample + clip.durationSamples) / clip.sampleRate;\n}\n\n/** Clip offset into source audio in seconds */\nexport function clipOffsetTime(clip: AudioClip): number {\n return clip.offsetSamples / clip.sampleRate;\n}\n\n/** Clip duration in seconds */\nexport function clipDurationTime(clip: AudioClip): number {\n return clip.durationSamples / clip.sampleRate;\n}\n\n/**\n * Max audio channel count across a track's clips.\n * Used to set Panner channelCount and offline render output channels.\n */\nexport function trackChannelCount(track: ClipTrack): number {\n return track.clips.reduce(\n (max, clip) => Math.max(max, clip.audioBuffer?.numberOfChannels ?? 1),\n 1\n );\n}\n\n/**\n * Clip width in pixels at a given samplesPerPixel.\n * Shared by Clip.tsx (container sizing) and ChannelWithProgress.tsx (progress overlay)\n * to ensure pixel-perfect alignment. Floor-based endpoint subtraction guarantees\n * adjacent clips have no pixel gaps.\n */\nexport function clipPixelWidth(\n startSample: number,\n durationSamples: number,\n samplesPerPixel: number\n): number {\n return (\n Math.floor((startSample + durationSamples) / samplesPerPixel) -\n Math.floor(startSample / samplesPerPixel)\n );\n}\n","/**\n * Fade curve utilities for Web Audio API\n *\n * Pure functions that generate fade curves and apply them to AudioParam.\n * No Tone.js dependency — works with native Web Audio nodes.\n */\n\nimport type { FadeType } from './types';\n\n/**\n * Generate a linear fade curve\n */\nexport function linearCurve(length: number, fadeIn: boolean): Float32Array {\n const curve = new Float32Array(length);\n const scale = length - 1;\n\n for (let i = 0; i < length; i++) {\n const x = i / scale;\n curve[i] = fadeIn ? x : 1 - x;\n }\n\n return curve;\n}\n\n/**\n * Generate an exponential fade curve\n */\nexport function exponentialCurve(length: number, fadeIn: boolean): Float32Array {\n const curve = new Float32Array(length);\n const scale = length - 1;\n\n for (let i = 0; i < length; i++) {\n const x = i / scale;\n const index = fadeIn ? i : length - 1 - i;\n curve[index] = Math.exp(2 * x - 1) / Math.E;\n }\n\n return curve;\n}\n\n/**\n * Generate an S-curve (sine-based smooth curve)\n */\nexport function sCurveCurve(length: number, fadeIn: boolean): Float32Array {\n const curve = new Float32Array(length);\n const phase = fadeIn ? Math.PI / 2 : -Math.PI / 2;\n\n for (let i = 0; i < length; i++) {\n curve[i] = Math.sin((Math.PI * i) / length - phase) / 2 + 0.5;\n }\n\n return curve;\n}\n\n/**\n * Generate a logarithmic fade curve\n */\nexport function logarithmicCurve(length: number, fadeIn: boolean, base: number = 10): Float32Array {\n const curve = new Float32Array(length);\n\n for (let i = 0; i < length; i++) {\n const index = fadeIn ? i : length - 1 - i;\n const x = i / length;\n curve[index] = Math.log(1 + base * x) / Math.log(1 + base);\n }\n\n return curve;\n}\n\n/**\n * Generate a fade curve of the specified type\n */\nexport function generateCurve(type: FadeType, length: number, fadeIn: boolean): Float32Array {\n switch (type) {\n case 'linear':\n return linearCurve(length, fadeIn);\n case 'exponential':\n return exponentialCurve(length, fadeIn);\n case 'sCurve':\n return sCurveCurve(length, fadeIn);\n case 'logarithmic':\n return logarithmicCurve(length, fadeIn);\n default:\n return linearCurve(length, fadeIn);\n }\n}\n\n/**\n * Apply a fade in to an AudioParam\n *\n * @param param - The AudioParam to apply the fade to (usually gain)\n * @param startTime - When the fade starts (in seconds, AudioContext time)\n * @param duration - Duration of the fade in seconds\n * @param type - Type of fade curve\n * @param startValue - Starting value (default: 0)\n * @param endValue - Ending value (default: 1)\n */\nexport function applyFadeIn(\n param: AudioParam,\n startTime: number,\n duration: number,\n type: FadeType = 'linear',\n startValue: number = 0,\n endValue: number = 1\n): void {\n if (duration <= 0) return;\n\n if (type === 'linear') {\n param.setValueAtTime(startValue, startTime);\n param.linearRampToValueAtTime(endValue, startTime + duration);\n } else if (type === 'exponential') {\n param.setValueAtTime(Math.max(startValue, 0.001), startTime);\n param.exponentialRampToValueAtTime(Math.max(endValue, 0.001), startTime + duration);\n } else {\n const curve = generateCurve(type, 10000, true);\n const scaledCurve = new Float32Array(curve.length);\n const range = endValue - startValue;\n for (let i = 0; i < curve.length; i++) {\n scaledCurve[i] = startValue + curve[i] * range;\n }\n param.setValueCurveAtTime(scaledCurve, startTime, duration);\n }\n}\n\n/**\n * Apply a fade out to an AudioParam\n *\n * @param param - The AudioParam to apply the fade to (usually gain)\n * @param startTime - When the fade starts (in seconds, AudioContext time)\n * @param duration - Duration of the fade in seconds\n * @param type - Type of fade curve\n * @param startValue - Starting value (default: 1)\n * @param endValue - Ending value (default: 0)\n */\nexport function applyFadeOut(\n param: AudioParam,\n startTime: number,\n duration: number,\n type: FadeType = 'linear',\n startValue: number = 1,\n endValue: number = 0\n): void {\n if (duration <= 0) return;\n\n if (type === 'linear') {\n param.setValueAtTime(startValue, startTime);\n param.linearRampToValueAtTime(endValue, startTime + duration);\n } else if (type === 'exponential') {\n param.setValueAtTime(Math.max(startValue, 0.001), startTime);\n param.exponentialRampToValueAtTime(Math.max(endValue, 0.001), startTime + duration);\n } else {\n const curve = generateCurve(type, 10000, false);\n const scaledCurve = new Float32Array(curve.length);\n const range = startValue - endValue;\n for (let i = 0; i < curve.length; i++) {\n scaledCurve[i] = endValue + curve[i] * range;\n }\n param.setValueCurveAtTime(scaledCurve, startTime, duration);\n }\n}\n","/**\n * Framework-agnostic keyboard shortcut handling.\n * Used by both React (useKeyboardShortcuts) and Web Components (daw-editor).\n */\n\nexport interface KeyboardShortcut {\n key: string;\n ctrlKey?: boolean;\n shiftKey?: boolean;\n metaKey?: boolean;\n altKey?: boolean;\n action: () => void;\n description?: string;\n preventDefault?: boolean;\n}\n\n/**\n * Handle a keyboard event against a list of shortcuts.\n * Pure function, no framework dependency.\n */\nexport function handleKeyboardEvent(\n event: KeyboardEvent,\n shortcuts: KeyboardShortcut[],\n enabled: boolean\n): void {\n if (!enabled) return;\n\n // Ignore key repeat events — holding a key fires keydown repeatedly.\n // Without this guard, holding Space rapidly toggles play/pause.\n if (event.repeat) return;\n\n const target = event.target as HTMLElement;\n if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {\n return;\n }\n\n const matchingShortcut = shortcuts.find((shortcut) => {\n const keyMatch =\n event.key.toLowerCase() === shortcut.key.toLowerCase() || event.key === shortcut.key;\n\n const ctrlMatch = shortcut.ctrlKey === undefined || event.ctrlKey === shortcut.ctrlKey;\n const shiftMatch = shortcut.shiftKey === undefined || event.shiftKey === shortcut.shiftKey;\n const metaMatch = shortcut.metaKey === undefined || event.metaKey === shortcut.metaKey;\n const altMatch = shortcut.altKey === undefined || event.altKey === shortcut.altKey;\n\n return keyMatch && ctrlMatch && shiftMatch && metaMatch && altMatch;\n });\n\n if (matchingShortcut) {\n if (matchingShortcut.preventDefault !== false) {\n event.preventDefault();\n }\n matchingShortcut.action();\n }\n}\n\n/**\n * Get a human-readable string representation of a keyboard shortcut.\n *\n * @param shortcut - The keyboard shortcut\n * @returns Human-readable string (e.g., \"Cmd+Shift+S\")\n */\nexport const getShortcutLabel = (shortcut: KeyboardShortcut): string => {\n const parts: string[] = [];\n\n // Use Cmd on Mac, Ctrl on other platforms\n const isMac = typeof navigator !== 'undefined' && navigator.platform.includes('Mac');\n\n if (shortcut.metaKey) {\n parts.push(isMac ? 'Cmd' : 'Ctrl');\n }\n\n if (shortcut.ctrlKey && !shortcut.metaKey) {\n parts.push('Ctrl');\n }\n\n if (shortcut.altKey) {\n parts.push(isMac ? 'Option' : 'Alt');\n }\n\n if (shortcut.shiftKey) {\n parts.push('Shift');\n }\n\n parts.push(shortcut.key.toUpperCase());\n\n return parts.join('+');\n};\n"],"mappings":"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","names":["InteractionState"]}
|
package/dist/index.mjs
CHANGED
|
@@ -266,12 +266,131 @@ function normalizedToDb(normalized, floor = DEFAULT_FLOOR) {
|
|
|
266
266
|
const clamped = Math.max(0, normalized);
|
|
267
267
|
return clamped * -floor + floor;
|
|
268
268
|
}
|
|
269
|
+
function gainToDb(gain) {
|
|
270
|
+
return 20 * Math.log10(Math.max(gain, 1e-4));
|
|
271
|
+
}
|
|
269
272
|
function gainToNormalized(gain, floor = DEFAULT_FLOOR) {
|
|
270
273
|
if (gain <= 0) return 0;
|
|
271
274
|
const db = 20 * Math.log10(gain);
|
|
272
275
|
return dBToNormalized(db, floor);
|
|
273
276
|
}
|
|
274
277
|
|
|
278
|
+
// src/utils/musicalTicks.ts
|
|
279
|
+
function snapToTicks(snapTo, timeSignature, ppqn = 960) {
|
|
280
|
+
switch (snapTo) {
|
|
281
|
+
case "bar":
|
|
282
|
+
return ticksPerBar(timeSignature, ppqn);
|
|
283
|
+
case "beat":
|
|
284
|
+
return ticksPerBeat(timeSignature, ppqn);
|
|
285
|
+
case "1/2":
|
|
286
|
+
return ppqn * 2;
|
|
287
|
+
case "1/4":
|
|
288
|
+
return ppqn;
|
|
289
|
+
case "1/8":
|
|
290
|
+
return ppqn / 2;
|
|
291
|
+
case "1/16":
|
|
292
|
+
return ppqn / 4;
|
|
293
|
+
case "1/32":
|
|
294
|
+
return ppqn / 8;
|
|
295
|
+
case "1/2T":
|
|
296
|
+
return Math.round(ppqn * 2 * 2 / 3);
|
|
297
|
+
case "1/4T":
|
|
298
|
+
return Math.round(ppqn * 2 / 3);
|
|
299
|
+
case "1/8T":
|
|
300
|
+
return Math.round(ppqn * 2 / 6);
|
|
301
|
+
case "1/16T":
|
|
302
|
+
return Math.round(ppqn * 2 / 12);
|
|
303
|
+
case "off":
|
|
304
|
+
return 0;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
var MIN_PIXELS_PER_UNIT = 8;
|
|
308
|
+
var MIN_PIXELS_PER_LABEL = 60;
|
|
309
|
+
function computeMusicalTicks(params) {
|
|
310
|
+
const { timeSignature, ticksPerPixel, startPixel, endPixel, ppqn = 960 } = params;
|
|
311
|
+
if (ticksPerPixel <= 0 || ppqn <= 0 || timeSignature[1] <= 0) {
|
|
312
|
+
return { ticks: [], pixelsPerBar: 0, pixelsPerBeat: 0, zoomLevel: "coarse" };
|
|
313
|
+
}
|
|
314
|
+
const tpBeat = ticksPerBeat(timeSignature, ppqn);
|
|
315
|
+
const tpBar = ticksPerBar(timeSignature, ppqn);
|
|
316
|
+
const tpEighth = ppqn / 2;
|
|
317
|
+
const tpSixteenth = ppqn / 4;
|
|
318
|
+
const pixelsPerBar = tpBar / ticksPerPixel;
|
|
319
|
+
const pixelsPerBeat = tpBeat / ticksPerPixel;
|
|
320
|
+
const pixelsPerEighth = tpEighth / ticksPerPixel;
|
|
321
|
+
const pixelsPerSixteenth = tpSixteenth / ticksPerPixel;
|
|
322
|
+
let zoomLevel;
|
|
323
|
+
if (pixelsPerBar < MIN_PIXELS_PER_UNIT) {
|
|
324
|
+
zoomLevel = "coarse";
|
|
325
|
+
} else if (pixelsPerBeat < MIN_PIXELS_PER_UNIT) {
|
|
326
|
+
zoomLevel = "bar";
|
|
327
|
+
} else if (pixelsPerEighth < MIN_PIXELS_PER_UNIT) {
|
|
328
|
+
zoomLevel = "beat";
|
|
329
|
+
} else if (pixelsPerSixteenth < MIN_PIXELS_PER_UNIT) {
|
|
330
|
+
zoomLevel = "eighth";
|
|
331
|
+
} else {
|
|
332
|
+
zoomLevel = "sixteenth";
|
|
333
|
+
}
|
|
334
|
+
let stepTicks;
|
|
335
|
+
let coarseBarStep;
|
|
336
|
+
if (zoomLevel === "coarse") {
|
|
337
|
+
let multiplier = 2;
|
|
338
|
+
while (tpBar * multiplier / ticksPerPixel < MIN_PIXELS_PER_UNIT) {
|
|
339
|
+
multiplier *= 2;
|
|
340
|
+
}
|
|
341
|
+
stepTicks = tpBar * multiplier;
|
|
342
|
+
coarseBarStep = multiplier;
|
|
343
|
+
} else if (zoomLevel === "bar") {
|
|
344
|
+
stepTicks = tpBar;
|
|
345
|
+
} else if (zoomLevel === "beat") {
|
|
346
|
+
stepTicks = tpBeat;
|
|
347
|
+
} else if (zoomLevel === "eighth") {
|
|
348
|
+
stepTicks = tpEighth;
|
|
349
|
+
} else {
|
|
350
|
+
stepTicks = tpSixteenth;
|
|
351
|
+
}
|
|
352
|
+
const startTick = startPixel * ticksPerPixel;
|
|
353
|
+
const endTick = endPixel * ticksPerPixel;
|
|
354
|
+
const firstStep = Math.floor(startTick / stepTicks) * stepTicks;
|
|
355
|
+
const ticks = [];
|
|
356
|
+
for (let tick = firstStep; tick <= endTick; tick += stepTicks) {
|
|
357
|
+
const pixel = tick / ticksPerPixel;
|
|
358
|
+
if (pixel < startPixel || pixel > endPixel) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
let type;
|
|
362
|
+
if (tick % tpBar === 0) {
|
|
363
|
+
type = "major";
|
|
364
|
+
} else if (tick % tpBeat === 0) {
|
|
365
|
+
type = "minor";
|
|
366
|
+
} else {
|
|
367
|
+
type = "minorMinor";
|
|
368
|
+
}
|
|
369
|
+
const barIndex = Math.floor(tick / tpBar);
|
|
370
|
+
let label;
|
|
371
|
+
if (type === "major") {
|
|
372
|
+
label = ticksToBarBeatLabel(tick, timeSignature, ppqn);
|
|
373
|
+
} else if (type === "minor" && pixelsPerBeat >= MIN_PIXELS_PER_LABEL) {
|
|
374
|
+
label = ticksToBarBeatLabel(tick, timeSignature, ppqn);
|
|
375
|
+
}
|
|
376
|
+
ticks.push({ pixel, type, barIndex, ...label !== void 0 ? { label } : {} });
|
|
377
|
+
}
|
|
378
|
+
const result = {
|
|
379
|
+
ticks,
|
|
380
|
+
pixelsPerBar,
|
|
381
|
+
pixelsPerBeat,
|
|
382
|
+
zoomLevel,
|
|
383
|
+
...coarseBarStep !== void 0 ? { coarseBarStep } : {}
|
|
384
|
+
};
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
function snapTickToGrid(tick, snapTo, timeSignature, ppqn = 960) {
|
|
388
|
+
if (snapTo === "off") return tick;
|
|
389
|
+
const gridSize = snapToTicks(snapTo, timeSignature, ppqn);
|
|
390
|
+
if (gridSize <= 0) return tick;
|
|
391
|
+
return Math.round(tick / gridSize) * gridSize;
|
|
392
|
+
}
|
|
393
|
+
|
|
275
394
|
// src/clipTimeHelpers.ts
|
|
276
395
|
function clipStartTime(clip) {
|
|
277
396
|
return clip.startSample / clip.sampleRate;
|
|
@@ -285,6 +404,12 @@ function clipOffsetTime(clip) {
|
|
|
285
404
|
function clipDurationTime(clip) {
|
|
286
405
|
return clip.durationSamples / clip.sampleRate;
|
|
287
406
|
}
|
|
407
|
+
function trackChannelCount(track) {
|
|
408
|
+
return track.clips.reduce(
|
|
409
|
+
(max, clip) => Math.max(max, clip.audioBuffer?.numberOfChannels ?? 1),
|
|
410
|
+
1
|
|
411
|
+
);
|
|
412
|
+
}
|
|
288
413
|
function clipPixelWidth(startSample, durationSamples, samplesPerPixel) {
|
|
289
414
|
return Math.floor((startSample + durationSamples) / samplesPerPixel) - Math.floor(startSample / samplesPerPixel);
|
|
290
415
|
}
|
|
@@ -421,6 +546,7 @@ var getShortcutLabel = (shortcut) => {
|
|
|
421
546
|
export {
|
|
422
547
|
InteractionState,
|
|
423
548
|
MAX_CANVAS_WIDTH,
|
|
549
|
+
MIN_PIXELS_PER_UNIT,
|
|
424
550
|
PPQN,
|
|
425
551
|
applyFadeIn,
|
|
426
552
|
applyFadeOut,
|
|
@@ -430,6 +556,7 @@ export {
|
|
|
430
556
|
clipPixelWidth,
|
|
431
557
|
clipStartTime,
|
|
432
558
|
clipsOverlap,
|
|
559
|
+
computeMusicalTicks,
|
|
433
560
|
createClip,
|
|
434
561
|
createClipFromSeconds,
|
|
435
562
|
createTimeline,
|
|
@@ -437,6 +564,7 @@ export {
|
|
|
437
564
|
dBToNormalized,
|
|
438
565
|
exponentialCurve,
|
|
439
566
|
findGaps,
|
|
567
|
+
gainToDb,
|
|
440
568
|
gainToNormalized,
|
|
441
569
|
generateCurve,
|
|
442
570
|
getClipsAtSample,
|
|
@@ -454,11 +582,14 @@ export {
|
|
|
454
582
|
samplesToTicks,
|
|
455
583
|
secondsToPixels,
|
|
456
584
|
secondsToSamples,
|
|
585
|
+
snapTickToGrid,
|
|
457
586
|
snapToGrid,
|
|
587
|
+
snapToTicks,
|
|
458
588
|
sortClipsByTime,
|
|
459
589
|
ticksPerBar,
|
|
460
590
|
ticksPerBeat,
|
|
461
591
|
ticksToBarBeatLabel,
|
|
462
|
-
ticksToSamples
|
|
592
|
+
ticksToSamples,
|
|
593
|
+
trackChannelCount
|
|
463
594
|
};
|
|
464
595
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/constants.ts","../src/types/clip.ts","../src/types/index.ts","../src/utils/conversions.ts","../src/utils/beatsAndBars.ts","../src/utils/dBUtils.ts","../src/clipTimeHelpers.ts","../src/fades.ts","../src/keyboard.ts"],"sourcesContent":["/**\n * Maximum width in CSS pixels for a single canvas chunk.\n * Canvas elements are split into chunks of this width to enable\n * horizontal virtual scrolling — only visible chunks are mounted.\n */\nexport const MAX_CANVAS_WIDTH = 1000;\n","/**\n * Clip-Based Model Types\n *\n * These types support a professional multi-track editing model where:\n * - Each track can contain multiple audio clips\n * - Clips can be positioned anywhere on the timeline\n * - Clips have independent trim points (offset/duration)\n * - Gaps between clips are silent\n * - Clips can overlap (for crossfades)\n */\n\nimport { Fade } from './index';\nimport type { RenderMode, SpectrogramConfig, ColorMapValue } from './spectrogram';\n\n/**\n * WaveformData object from waveform-data.js library.\n * Supports resample() and slice() for dynamic zoom levels.\n * See: https://github.com/bbc/waveform-data.js\n */\nexport interface WaveformDataObject {\n /** Sample rate of the original audio */\n readonly sample_rate: number;\n /** Number of audio samples per pixel */\n readonly scale: number;\n /** Length of waveform data in pixels */\n readonly length: number;\n /** Bit depth (8 or 16) */\n readonly bits: number;\n /** Duration in seconds */\n readonly duration: number;\n /** Number of channels */\n readonly channels: number;\n /** Get channel data */\n channel: (index: number) => {\n min_array: () => number[];\n max_array: () => number[];\n };\n /** Resample to different scale */\n resample: (options: { scale: number } | { width: number }) => WaveformDataObject;\n /** Slice a portion of the waveform */\n slice: (\n options: { startTime: number; endTime: number } | { startIndex: number; endIndex: number }\n ) => WaveformDataObject;\n}\n\n/**\n * Generic effects function type for track-level audio processing.\n *\n * The actual implementation receives Tone.js audio nodes. Using generic types\n * here to avoid circular dependencies with the playout package.\n *\n * @param graphEnd - The end of the track's audio graph (Tone.js Gain node)\n * @param destination - Where to connect the effects output (Tone.js ToneAudioNode)\n * @param isOffline - Whether rendering offline (for export)\n * @returns Optional cleanup function called when track is disposed\n *\n * @example\n * ```typescript\n * const trackEffects: TrackEffectsFunction = (graphEnd, destination, isOffline) => {\n * const reverb = new Tone.Reverb({ decay: 1.5 });\n * graphEnd.connect(reverb);\n * reverb.connect(destination);\n *\n * return () => {\n * reverb.dispose();\n * };\n * };\n * ```\n */\nexport type TrackEffectsFunction = (\n graphEnd: unknown,\n destination: unknown,\n isOffline: boolean\n) => void | (() => void);\n\n/**\n * Represents a single audio clip on the timeline\n *\n * IMPORTANT: All positions/durations are stored as SAMPLE COUNTS (integers)\n * to avoid floating-point precision errors. Convert to seconds only when\n * needed for playback using: seconds = samples / sampleRate\n *\n * Clips can be created with just waveformData (for instant visual rendering)\n * and have audioBuffer added later when audio finishes loading.\n */\nexport interface AudioClip {\n /** Unique identifier for this clip */\n id: string;\n\n /**\n * The audio buffer containing the audio data.\n * Optional for peaks-first rendering - can be added later.\n * Required for playback and editing operations.\n */\n audioBuffer?: AudioBuffer;\n\n /** Position on timeline where this clip starts (in samples at timeline sampleRate) */\n startSample: number;\n\n /** Duration of this clip (in samples) - how much of the audio buffer to play */\n durationSamples: number;\n\n /** Offset into the audio buffer where playback starts (in samples) - the \"trim start\" point */\n offsetSamples: number;\n\n /**\n * Sample rate for this clip's audio.\n * Required when audioBuffer is not provided (for peaks-first rendering).\n * When audioBuffer is present, this should match audioBuffer.sampleRate.\n */\n sampleRate: number;\n\n /**\n * Total duration of the source audio in samples.\n * Required when audioBuffer is not provided (for trim bounds calculation).\n * When audioBuffer is present, this should equal audioBuffer.length.\n */\n sourceDurationSamples: number;\n\n /** Optional fade in effect */\n fadeIn?: Fade;\n\n /** Optional fade out effect */\n fadeOut?: Fade;\n\n /** Clip-specific gain/volume multiplier (0.0 to 1.0+) */\n gain: number;\n\n /** Optional label/name for this clip */\n name?: string;\n\n /** Optional color for visual distinction */\n color?: string;\n\n /**\n * Pre-computed waveform data from waveform-data.js library.\n * When provided, the library will use this instead of computing peaks from the audioBuffer.\n * Supports resampling to different zoom levels and slicing for clip trimming.\n * Load with: `const waveformData = await loadWaveformData('/path/to/peaks.dat')`\n */\n waveformData?: WaveformDataObject;\n\n /**\n * MIDI note data — when present, this clip plays MIDI instead of audio.\n * The playout adapter uses this field to detect MIDI clips and route them\n * to MidiToneTrack (PolySynth) instead of ToneTrack (AudioBufferSourceNode).\n */\n midiNotes?: MidiNoteData[];\n\n /** MIDI channel (0-indexed). Channel 9 = GM percussion. */\n midiChannel?: number;\n\n /** MIDI program number (0-127). GM instrument number for SoundFont playback. */\n midiProgram?: number;\n}\n\n/**\n * Represents a track containing multiple audio clips\n */\nexport interface ClipTrack {\n /** Unique identifier for this track */\n id: string;\n\n /** Display name for this track */\n name: string;\n\n /** Array of audio clips on this track */\n clips: AudioClip[];\n\n /** Whether this track is muted */\n muted: boolean;\n\n /** Whether this track is soloed */\n soloed: boolean;\n\n /** Track volume (0.0 to 1.0+) */\n volume: number;\n\n /** Stereo pan (-1.0 = left, 0 = center, 1.0 = right) */\n pan: number;\n\n /** Optional track color for visual distinction */\n color?: string;\n\n /** Track height in pixels (for UI) */\n height?: number;\n\n /** Optional effects function for this track */\n effects?: TrackEffectsFunction;\n\n /** Visualization render mode. Default: 'waveform' */\n renderMode?: RenderMode;\n\n /** Per-track spectrogram configuration (FFT size, window, frequency scale, etc.) */\n spectrogramConfig?: SpectrogramConfig;\n\n /** Per-track spectrogram color map name or custom color array */\n spectrogramColorMap?: ColorMapValue;\n}\n\n/**\n * Represents the entire timeline/project\n */\nexport interface Timeline {\n /** All tracks in the timeline */\n tracks: ClipTrack[];\n\n /** Total timeline duration in seconds */\n duration: number;\n\n /** Sample rate for all audio (typically 44100 or 48000) */\n sampleRate: number;\n\n /** Optional project name */\n name?: string;\n\n /** Optional tempo (BPM) for grid snapping */\n tempo?: number;\n\n /** Optional time signature for grid snapping */\n timeSignature?: {\n numerator: number;\n denominator: number;\n };\n}\n\n/**\n * Options for creating a new audio clip (using sample counts)\n *\n * Either audioBuffer OR (sampleRate + sourceDurationSamples + waveformData) must be provided.\n * Providing waveformData without audioBuffer enables peaks-first rendering.\n */\nexport interface CreateClipOptions {\n /** Audio buffer - optional for peaks-first rendering */\n audioBuffer?: AudioBuffer;\n startSample: number; // Position on timeline (in samples)\n durationSamples?: number; // Defaults to full buffer/source duration (in samples)\n offsetSamples?: number; // Defaults to 0\n gain?: number; // Defaults to 1.0\n name?: string;\n color?: string;\n fadeIn?: Fade;\n fadeOut?: Fade;\n /** Pre-computed waveform data from waveform-data.js (e.g., from BBC audiowaveform) */\n waveformData?: WaveformDataObject;\n /** Sample rate - required if audioBuffer not provided */\n sampleRate?: number;\n /** Total source audio duration in samples - required if audioBuffer not provided */\n sourceDurationSamples?: number;\n /** MIDI note data — passed through to the created AudioClip */\n midiNotes?: MidiNoteData[];\n /** MIDI channel (0-indexed). Channel 9 = GM percussion. */\n midiChannel?: number;\n /** MIDI program number (0-127). GM instrument for SoundFont playback. */\n midiProgram?: number;\n}\n\n/**\n * Options for creating a new audio clip (using seconds for convenience)\n *\n * Either audioBuffer OR (sampleRate + sourceDuration + waveformData) must be provided.\n * Providing waveformData without audioBuffer enables peaks-first rendering.\n */\nexport interface CreateClipOptionsSeconds {\n /** Audio buffer - optional for peaks-first rendering */\n audioBuffer?: AudioBuffer;\n startTime: number; // Position on timeline (in seconds)\n duration?: number; // Defaults to full buffer/source duration (in seconds)\n offset?: number; // Defaults to 0 (in seconds)\n gain?: number; // Defaults to 1.0\n name?: string;\n color?: string;\n fadeIn?: Fade;\n fadeOut?: Fade;\n /** Pre-computed waveform data from waveform-data.js (e.g., from BBC audiowaveform) */\n waveformData?: WaveformDataObject;\n /** Sample rate - required if audioBuffer not provided */\n sampleRate?: number;\n /** Total source audio duration in seconds - required if audioBuffer not provided */\n sourceDuration?: number;\n /** MIDI note data — passed through to the created AudioClip */\n midiNotes?: MidiNoteData[];\n /** MIDI channel (0-indexed). Channel 9 = GM percussion. */\n midiChannel?: number;\n /** MIDI program number (0-127). GM instrument for SoundFont playback. */\n midiProgram?: number;\n}\n\n/**\n * Options for creating a new track\n */\nexport interface CreateTrackOptions {\n name: string;\n clips?: AudioClip[];\n muted?: boolean;\n soloed?: boolean;\n volume?: number;\n pan?: number;\n color?: string;\n height?: number;\n spectrogramConfig?: SpectrogramConfig;\n spectrogramColorMap?: ColorMapValue;\n}\n\n/**\n * Creates a new AudioClip with sensible defaults (using sample counts)\n *\n * For peaks-first rendering (no audioBuffer), sampleRate and sourceDurationSamples can be:\n * - Provided explicitly via options\n * - Derived from waveformData (sample_rate and duration properties)\n */\nexport function createClip(options: CreateClipOptions): AudioClip {\n const {\n audioBuffer,\n startSample,\n offsetSamples = 0,\n gain = 1.0,\n name,\n color,\n fadeIn,\n fadeOut,\n waveformData,\n midiNotes,\n midiChannel,\n midiProgram,\n } = options;\n\n // Determine sample rate: audioBuffer > explicit option > waveformData\n const sampleRate = audioBuffer?.sampleRate ?? options.sampleRate ?? waveformData?.sample_rate;\n\n // Determine source duration: audioBuffer > explicit option > waveformData (converted to samples)\n const sourceDurationSamples =\n audioBuffer?.length ??\n options.sourceDurationSamples ??\n (waveformData && sampleRate ? Math.ceil(waveformData.duration * sampleRate) : undefined);\n\n if (sampleRate === undefined) {\n throw new Error(\n 'createClip: sampleRate is required when audioBuffer is not provided (can use waveformData.sample_rate)'\n );\n }\n if (sourceDurationSamples === undefined) {\n throw new Error(\n 'createClip: sourceDurationSamples is required when audioBuffer is not provided (can use waveformData.duration)'\n );\n }\n\n if (audioBuffer && waveformData && audioBuffer.sampleRate !== waveformData.sample_rate) {\n console.warn(\n '[waveform-playlist] \"' +\n (name ?? 'unnamed') +\n '\": pre-computed peaks at ' +\n waveformData.sample_rate +\n ' Hz do not match decoded audio at ' +\n audioBuffer.sampleRate +\n ' Hz — peaks may be recomputed from decoded audio'\n );\n }\n\n // Default duration to full source duration\n const durationSamples = options.durationSamples ?? sourceDurationSamples;\n\n return {\n id: generateId(),\n audioBuffer,\n startSample,\n durationSamples,\n offsetSamples,\n sampleRate,\n sourceDurationSamples,\n gain,\n name,\n color,\n fadeIn,\n fadeOut,\n waveformData,\n midiNotes,\n midiChannel,\n midiProgram,\n };\n}\n\n/**\n * Creates a new AudioClip from time-based values (convenience function)\n * Converts seconds to samples using the audioBuffer's sampleRate or explicit sampleRate\n *\n * For peaks-first rendering (no audioBuffer), sampleRate and sourceDuration can be:\n * - Provided explicitly via options\n * - Derived from waveformData (sample_rate and duration properties)\n */\nexport function createClipFromSeconds(options: CreateClipOptionsSeconds): AudioClip {\n const {\n audioBuffer,\n startTime,\n offset = 0,\n gain = 1.0,\n name,\n color,\n fadeIn,\n fadeOut,\n waveformData,\n midiNotes,\n midiChannel,\n midiProgram,\n } = options;\n\n // Determine sample rate: audioBuffer > explicit option > waveformData\n const sampleRate = audioBuffer?.sampleRate ?? options.sampleRate ?? waveformData?.sample_rate;\n if (sampleRate === undefined) {\n throw new Error(\n 'createClipFromSeconds: sampleRate is required when audioBuffer is not provided (can use waveformData.sample_rate)'\n );\n }\n\n // Determine source duration: audioBuffer > explicit option > waveformData\n const sourceDuration = audioBuffer?.duration ?? options.sourceDuration ?? waveformData?.duration;\n if (sourceDuration === undefined) {\n throw new Error(\n 'createClipFromSeconds: sourceDuration is required when audioBuffer is not provided (can use waveformData.duration)'\n );\n }\n\n // Note: audioBuffer and waveformData may have different sample rates (e.g., Opus at 48000\n // decoded on 44100 hardware). This is expected — callers fall back to worker to recompute peaks.\n\n // Default clip duration to full source duration\n const duration = options.duration ?? sourceDuration;\n\n return createClip({\n audioBuffer,\n startSample: Math.round(startTime * sampleRate),\n durationSamples: Math.round(duration * sampleRate),\n offsetSamples: Math.round(offset * sampleRate),\n sampleRate,\n sourceDurationSamples: Math.ceil(sourceDuration * sampleRate),\n gain,\n name,\n color,\n fadeIn,\n fadeOut,\n waveformData,\n midiNotes,\n midiChannel,\n midiProgram,\n });\n}\n\n/**\n * Creates a new ClipTrack with sensible defaults\n */\nexport function createTrack(options: CreateTrackOptions): ClipTrack {\n const {\n name,\n clips = [],\n muted = false,\n soloed = false,\n volume = 1.0,\n pan = 0,\n color,\n height,\n spectrogramConfig,\n spectrogramColorMap,\n } = options;\n\n return {\n id: generateId(),\n name,\n clips,\n muted,\n soloed,\n volume,\n pan,\n color,\n height,\n spectrogramConfig,\n spectrogramColorMap,\n };\n}\n\n/**\n * Creates a new Timeline with sensible defaults\n */\nexport function createTimeline(\n tracks: ClipTrack[],\n sampleRate: number = 44100,\n options?: {\n name?: string;\n tempo?: number;\n timeSignature?: { numerator: number; denominator: number };\n }\n): Timeline {\n // Calculate total duration from all clips across all tracks (in seconds)\n const durationSamples = tracks.reduce((maxSamples, track) => {\n const trackSamples = track.clips.reduce((max, clip) => {\n return Math.max(max, clip.startSample + clip.durationSamples);\n }, 0);\n return Math.max(maxSamples, trackSamples);\n }, 0);\n\n const duration = durationSamples / sampleRate;\n\n return {\n tracks,\n duration,\n sampleRate,\n name: options?.name,\n tempo: options?.tempo,\n timeSignature: options?.timeSignature,\n };\n}\n\n/**\n * Generates a unique ID for clips and tracks\n */\nfunction generateId(): string {\n return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n}\n\n/**\n * MIDI note data for clips that play MIDI instead of audio.\n * When present on an AudioClip, the clip is treated as a MIDI clip\n * by the playout adapter.\n */\nexport interface MidiNoteData {\n /** MIDI note number (0-127) */\n midi: number;\n /** Note name in scientific pitch notation (\"C4\", \"G#3\") */\n name: string;\n /** Start time in seconds, relative to clip start */\n time: number;\n /** Duration in seconds */\n duration: number;\n /** Velocity (0-1 normalized) */\n velocity: number;\n /** MIDI channel (0-indexed). Channel 9 = GM percussion. Enables per-note routing in flattened tracks. */\n channel?: number;\n}\n\n/**\n * Utility: Get all clips within a sample range\n */\nexport function getClipsInRange(\n track: ClipTrack,\n startSample: number,\n endSample: number\n): AudioClip[] {\n return track.clips.filter((clip) => {\n const clipEnd = clip.startSample + clip.durationSamples;\n // Clip overlaps with range if:\n // - Clip starts before range ends AND\n // - Clip ends after range starts\n return clip.startSample < endSample && clipEnd > startSample;\n });\n}\n\n/**\n * Utility: Get all clips at a specific sample position\n */\nexport function getClipsAtSample(track: ClipTrack, sample: number): AudioClip[] {\n return track.clips.filter((clip) => {\n const clipEnd = clip.startSample + clip.durationSamples;\n return sample >= clip.startSample && sample < clipEnd;\n });\n}\n\n/**\n * Utility: Check if two clips overlap\n */\nexport function clipsOverlap(clip1: AudioClip, clip2: AudioClip): boolean {\n const clip1End = clip1.startSample + clip1.durationSamples;\n const clip2End = clip2.startSample + clip2.durationSamples;\n\n return clip1.startSample < clip2End && clip1End > clip2.startSample;\n}\n\n/**\n * Utility: Sort clips by startSample\n */\nexport function sortClipsByTime(clips: AudioClip[]): AudioClip[] {\n return [...clips].sort((a, b) => a.startSample - b.startSample);\n}\n\n/**\n * Utility: Find gaps between clips (silent regions)\n */\nexport interface Gap {\n startSample: number;\n endSample: number;\n durationSamples: number;\n}\n\nexport function findGaps(track: ClipTrack): Gap[] {\n if (track.clips.length === 0) return [];\n\n const sorted = sortClipsByTime(track.clips);\n const gaps: Gap[] = [];\n\n for (let i = 0; i < sorted.length - 1; i++) {\n const currentClipEnd = sorted[i].startSample + sorted[i].durationSamples;\n const nextClipStart = sorted[i + 1].startSample;\n\n if (nextClipStart > currentClipEnd) {\n gaps.push({\n startSample: currentClipEnd,\n endSample: nextClipStart,\n durationSamples: nextClipStart - currentClipEnd,\n });\n }\n }\n\n return gaps;\n}\n","/**\n * Peaks type - represents a typed array of interleaved min/max peak data\n */\nexport type Peaks = Int8Array | Int16Array;\n\n/**\n * Bits type - number of bits for peak data\n */\nexport type Bits = 8 | 16;\n\n/**\n * PeakData - result of peak extraction\n */\nexport interface PeakData {\n /** Number of peak pairs extracted */\n length: number;\n /** Array of peak data for each channel (interleaved min/max) */\n data: Peaks[];\n /** Bit depth of peak data */\n bits: Bits;\n}\n\nexport interface WaveformConfig {\n sampleRate: number;\n samplesPerPixel: number;\n waveHeight?: number;\n waveOutlineColor?: string;\n waveFillColor?: string;\n waveProgressColor?: string;\n}\n\nexport interface AudioBuffer {\n length: number;\n duration: number;\n numberOfChannels: number;\n sampleRate: number;\n getChannelData(channel: number): Float32Array;\n}\n\nexport interface Track {\n id: string;\n name: string;\n src?: string | AudioBuffer; // Support both URL strings and AudioBuffer objects\n gain: number;\n muted: boolean;\n soloed: boolean;\n stereoPan: number;\n startTime: number;\n endTime?: number;\n fadeIn?: Fade;\n fadeOut?: Fade;\n cueIn?: number;\n cueOut?: number;\n}\n\n/**\n * Simple fade configuration\n */\nexport interface Fade {\n /** Duration of the fade in seconds */\n duration: number;\n /** Type of fade curve (default: 'linear') */\n type?: FadeType;\n}\n\nexport type FadeType = 'logarithmic' | 'linear' | 'sCurve' | 'exponential';\n\n/**\n * Alias for Fade — used by media-element-playout and playout packages\n */\nexport type FadeConfig = Fade;\n\nexport interface PlaylistConfig {\n samplesPerPixel?: number;\n waveHeight?: number;\n container?: HTMLElement;\n isAutomaticScroll?: boolean;\n timescale?: boolean;\n colors?: {\n waveOutlineColor?: string;\n waveFillColor?: string;\n waveProgressColor?: string;\n };\n controls?: {\n show?: boolean;\n width?: number;\n };\n zoomLevels?: number[];\n}\n\nexport interface PlayoutState {\n isPlaying: boolean;\n isPaused: boolean;\n cursor: number;\n duration: number;\n}\n\nexport interface TimeSelection {\n start: number;\n end: number;\n}\n\nexport enum InteractionState {\n Cursor = 'cursor',\n Select = 'select',\n Shift = 'shift',\n FadeIn = 'fadein',\n FadeOut = 'fadeout',\n}\n\n// Export clip-based model types\nexport * from './clip';\n\n// Export spectrogram types\nexport * from './spectrogram';\n\n// Export annotation types\nexport * from './annotations';\n","export function samplesToSeconds(samples: number, sampleRate: number): number {\n return samples / sampleRate;\n}\n\nexport function secondsToSamples(seconds: number, sampleRate: number): number {\n return Math.ceil(seconds * sampleRate);\n}\n\nexport function samplesToPixels(samples: number, samplesPerPixel: number): number {\n return Math.floor(samples / samplesPerPixel);\n}\n\nexport function pixelsToSamples(pixels: number, samplesPerPixel: number): number {\n return Math.floor(pixels * samplesPerPixel);\n}\n\nexport function pixelsToSeconds(\n pixels: number,\n samplesPerPixel: number,\n sampleRate: number\n): number {\n return (pixels * samplesPerPixel) / sampleRate;\n}\n\nexport function secondsToPixels(\n seconds: number,\n samplesPerPixel: number,\n sampleRate: number\n): number {\n return Math.ceil((seconds * sampleRate) / samplesPerPixel);\n}\n","/** Default PPQN matching Tone.js Transport (192 ticks per quarter note) */\nexport const PPQN = 192;\n\n/** Number of PPQN ticks per beat for the given time signature. */\nexport function ticksPerBeat(timeSignature: [number, number], ppqn = PPQN): number {\n const [, denominator] = timeSignature;\n return ppqn * (4 / denominator);\n}\n\n/** Number of PPQN ticks per bar for the given time signature. */\nexport function ticksPerBar(timeSignature: [number, number], ppqn = PPQN): number {\n const [numerator] = timeSignature;\n return numerator * ticksPerBeat(timeSignature, ppqn);\n}\n\n/** Convert PPQN ticks to sample count. Uses Math.round for integer sample alignment. */\nexport function ticksToSamples(\n ticks: number,\n bpm: number,\n sampleRate: number,\n ppqn = PPQN\n): number {\n return Math.round((ticks * 60 * sampleRate) / (bpm * ppqn));\n}\n\n/** Convert sample count to PPQN ticks. Inverse of ticksToSamples. */\nexport function samplesToTicks(\n samples: number,\n bpm: number,\n sampleRate: number,\n ppqn = PPQN\n): number {\n return Math.round((samples * ppqn * bpm) / (60 * sampleRate));\n}\n\n/** Snap a tick position to the nearest grid line (rounds to nearest). */\nexport function snapToGrid(ticks: number, gridSizeTicks: number): number {\n return Math.round(ticks / gridSizeTicks) * gridSizeTicks;\n}\n\n/** Format ticks as a 1-indexed bar.beat label. Beat 1 shows bar number only (e.g., \"3\" not \"3.1\"). */\nexport function ticksToBarBeatLabel(\n ticks: number,\n timeSignature: [number, number],\n ppqn = PPQN\n): string {\n const barTicks = ticksPerBar(timeSignature, ppqn);\n const beatTicks = ticksPerBeat(timeSignature, ppqn);\n const bar = Math.floor(ticks / barTicks) + 1;\n const beatInBar = Math.floor((ticks % barTicks) / beatTicks) + 1;\n if (beatInBar === 1) return `${bar}`;\n return `${bar}.${beatInBar}`;\n}\n","const DEFAULT_FLOOR = -100;\n\n/**\n * Convert a dB value to a normalized range.\n *\n * Maps dB values linearly: floor → 0, 0 dB → 1.\n * Values above 0 dB map to > 1 (e.g., +5 dB → 1.05 with default floor).\n *\n * @param dB - Decibel value (typically -Infinity to +5)\n * @param floor - Minimum dB value mapped to 0. Default: -100 (Firefox compat)\n * @returns Normalized value (0 at floor, 1 at 0 dB, >1 above 0 dB)\n */\nexport function dBToNormalized(dB: number, floor: number = DEFAULT_FLOOR): number {\n if (Number.isNaN(dB)) {\n console.warn('[waveform-playlist] dBToNormalized received NaN');\n return 0;\n }\n if (floor >= 0) {\n console.warn('[waveform-playlist] dBToNormalized floor must be negative, got:', floor);\n return 0;\n }\n if (!isFinite(dB) || dB <= floor) return 0;\n return (dB - floor) / -floor;\n}\n\n/**\n * Convert a normalized value back to dB.\n *\n * Maps linearly: 0 → floor, 1 → 0 dB.\n * Values above 1 map to positive dB (e.g., 1.05 → +5 dB with default floor).\n *\n * @param normalized - Normalized value (0 = floor, 1 = 0 dB)\n * @param floor - Minimum dB value (maps from 0). Must be negative. Default: -100\n * @returns dB value (floor at 0, 0 dB at 1, positive dB above 1)\n */\nexport function normalizedToDb(normalized: number, floor: number = DEFAULT_FLOOR): number {\n if (!isFinite(normalized)) return floor;\n if (floor >= 0) {\n console.warn('[waveform-playlist] normalizedToDb floor must be negative, got:', floor);\n return DEFAULT_FLOOR;\n }\n const clamped = Math.max(0, normalized);\n return clamped * -floor + floor;\n}\n\n/**\n * Convert a linear gain value (0-1+) to normalized 0-1 via dB.\n *\n * Combines gain-to-dB (20 * log10) with dBToNormalized for a consistent\n * mapping from raw AudioWorklet peak/RMS values to the 0-1 range used\n * by UI meter components.\n *\n * @param gain - Linear gain value (typically 0 to 1, can exceed 1)\n * @param floor - Minimum dB value mapped to 0. Default: -100\n * @returns Normalized value (0 at silence/floor, 1 at 0 dB, >1 above 0 dB)\n */\nexport function gainToNormalized(gain: number, floor: number = DEFAULT_FLOOR): number {\n if (gain <= 0) return 0;\n const db = 20 * Math.log10(gain);\n return dBToNormalized(db, floor);\n}\n","import type { AudioClip } from './types';\n\n/** Clip start position in seconds */\nexport function clipStartTime(clip: AudioClip): number {\n return clip.startSample / clip.sampleRate;\n}\n\n/** Clip end position in seconds (start + duration) */\nexport function clipEndTime(clip: AudioClip): number {\n return (clip.startSample + clip.durationSamples) / clip.sampleRate;\n}\n\n/** Clip offset into source audio in seconds */\nexport function clipOffsetTime(clip: AudioClip): number {\n return clip.offsetSamples / clip.sampleRate;\n}\n\n/** Clip duration in seconds */\nexport function clipDurationTime(clip: AudioClip): number {\n return clip.durationSamples / clip.sampleRate;\n}\n\n/**\n * Clip width in pixels at a given samplesPerPixel.\n * Shared by Clip.tsx (container sizing) and ChannelWithProgress.tsx (progress overlay)\n * to ensure pixel-perfect alignment. Floor-based endpoint subtraction guarantees\n * adjacent clips have no pixel gaps.\n */\nexport function clipPixelWidth(\n startSample: number,\n durationSamples: number,\n samplesPerPixel: number\n): number {\n return (\n Math.floor((startSample + durationSamples) / samplesPerPixel) -\n Math.floor(startSample / samplesPerPixel)\n );\n}\n","/**\n * Fade curve utilities for Web Audio API\n *\n * Pure functions that generate fade curves and apply them to AudioParam.\n * No Tone.js dependency — works with native Web Audio nodes.\n */\n\nimport type { FadeType } from './types';\n\n/**\n * Generate a linear fade curve\n */\nexport function linearCurve(length: number, fadeIn: boolean): Float32Array {\n const curve = new Float32Array(length);\n const scale = length - 1;\n\n for (let i = 0; i < length; i++) {\n const x = i / scale;\n curve[i] = fadeIn ? x : 1 - x;\n }\n\n return curve;\n}\n\n/**\n * Generate an exponential fade curve\n */\nexport function exponentialCurve(length: number, fadeIn: boolean): Float32Array {\n const curve = new Float32Array(length);\n const scale = length - 1;\n\n for (let i = 0; i < length; i++) {\n const x = i / scale;\n const index = fadeIn ? i : length - 1 - i;\n curve[index] = Math.exp(2 * x - 1) / Math.E;\n }\n\n return curve;\n}\n\n/**\n * Generate an S-curve (sine-based smooth curve)\n */\nexport function sCurveCurve(length: number, fadeIn: boolean): Float32Array {\n const curve = new Float32Array(length);\n const phase = fadeIn ? Math.PI / 2 : -Math.PI / 2;\n\n for (let i = 0; i < length; i++) {\n curve[i] = Math.sin((Math.PI * i) / length - phase) / 2 + 0.5;\n }\n\n return curve;\n}\n\n/**\n * Generate a logarithmic fade curve\n */\nexport function logarithmicCurve(length: number, fadeIn: boolean, base: number = 10): Float32Array {\n const curve = new Float32Array(length);\n\n for (let i = 0; i < length; i++) {\n const index = fadeIn ? i : length - 1 - i;\n const x = i / length;\n curve[index] = Math.log(1 + base * x) / Math.log(1 + base);\n }\n\n return curve;\n}\n\n/**\n * Generate a fade curve of the specified type\n */\nexport function generateCurve(type: FadeType, length: number, fadeIn: boolean): Float32Array {\n switch (type) {\n case 'linear':\n return linearCurve(length, fadeIn);\n case 'exponential':\n return exponentialCurve(length, fadeIn);\n case 'sCurve':\n return sCurveCurve(length, fadeIn);\n case 'logarithmic':\n return logarithmicCurve(length, fadeIn);\n default:\n return linearCurve(length, fadeIn);\n }\n}\n\n/**\n * Apply a fade in to an AudioParam\n *\n * @param param - The AudioParam to apply the fade to (usually gain)\n * @param startTime - When the fade starts (in seconds, AudioContext time)\n * @param duration - Duration of the fade in seconds\n * @param type - Type of fade curve\n * @param startValue - Starting value (default: 0)\n * @param endValue - Ending value (default: 1)\n */\nexport function applyFadeIn(\n param: AudioParam,\n startTime: number,\n duration: number,\n type: FadeType = 'linear',\n startValue: number = 0,\n endValue: number = 1\n): void {\n if (duration <= 0) return;\n\n if (type === 'linear') {\n param.setValueAtTime(startValue, startTime);\n param.linearRampToValueAtTime(endValue, startTime + duration);\n } else if (type === 'exponential') {\n param.setValueAtTime(Math.max(startValue, 0.001), startTime);\n param.exponentialRampToValueAtTime(Math.max(endValue, 0.001), startTime + duration);\n } else {\n const curve = generateCurve(type, 10000, true);\n const scaledCurve = new Float32Array(curve.length);\n const range = endValue - startValue;\n for (let i = 0; i < curve.length; i++) {\n scaledCurve[i] = startValue + curve[i] * range;\n }\n param.setValueCurveAtTime(scaledCurve, startTime, duration);\n }\n}\n\n/**\n * Apply a fade out to an AudioParam\n *\n * @param param - The AudioParam to apply the fade to (usually gain)\n * @param startTime - When the fade starts (in seconds, AudioContext time)\n * @param duration - Duration of the fade in seconds\n * @param type - Type of fade curve\n * @param startValue - Starting value (default: 1)\n * @param endValue - Ending value (default: 0)\n */\nexport function applyFadeOut(\n param: AudioParam,\n startTime: number,\n duration: number,\n type: FadeType = 'linear',\n startValue: number = 1,\n endValue: number = 0\n): void {\n if (duration <= 0) return;\n\n if (type === 'linear') {\n param.setValueAtTime(startValue, startTime);\n param.linearRampToValueAtTime(endValue, startTime + duration);\n } else if (type === 'exponential') {\n param.setValueAtTime(Math.max(startValue, 0.001), startTime);\n param.exponentialRampToValueAtTime(Math.max(endValue, 0.001), startTime + duration);\n } else {\n const curve = generateCurve(type, 10000, false);\n const scaledCurve = new Float32Array(curve.length);\n const range = startValue - endValue;\n for (let i = 0; i < curve.length; i++) {\n scaledCurve[i] = endValue + curve[i] * range;\n }\n param.setValueCurveAtTime(scaledCurve, startTime, duration);\n }\n}\n","/**\n * Framework-agnostic keyboard shortcut handling.\n * Used by both React (useKeyboardShortcuts) and Web Components (daw-editor).\n */\n\nexport interface KeyboardShortcut {\n key: string;\n ctrlKey?: boolean;\n shiftKey?: boolean;\n metaKey?: boolean;\n altKey?: boolean;\n action: () => void;\n description?: string;\n preventDefault?: boolean;\n}\n\n/**\n * Handle a keyboard event against a list of shortcuts.\n * Pure function, no framework dependency.\n */\nexport function handleKeyboardEvent(\n event: KeyboardEvent,\n shortcuts: KeyboardShortcut[],\n enabled: boolean\n): void {\n if (!enabled) return;\n\n // Ignore key repeat events — holding a key fires keydown repeatedly.\n // Without this guard, holding Space rapidly toggles play/pause.\n if (event.repeat) return;\n\n const target = event.target as HTMLElement;\n if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {\n return;\n }\n\n const matchingShortcut = shortcuts.find((shortcut) => {\n const keyMatch =\n event.key.toLowerCase() === shortcut.key.toLowerCase() || event.key === shortcut.key;\n\n const ctrlMatch = shortcut.ctrlKey === undefined || event.ctrlKey === shortcut.ctrlKey;\n const shiftMatch = shortcut.shiftKey === undefined || event.shiftKey === shortcut.shiftKey;\n const metaMatch = shortcut.metaKey === undefined || event.metaKey === shortcut.metaKey;\n const altMatch = shortcut.altKey === undefined || event.altKey === shortcut.altKey;\n\n return keyMatch && ctrlMatch && shiftMatch && metaMatch && altMatch;\n });\n\n if (matchingShortcut) {\n if (matchingShortcut.preventDefault !== false) {\n event.preventDefault();\n }\n matchingShortcut.action();\n }\n}\n\n/**\n * Get a human-readable string representation of a keyboard shortcut.\n *\n * @param shortcut - The keyboard shortcut\n * @returns Human-readable string (e.g., \"Cmd+Shift+S\")\n */\nexport const getShortcutLabel = (shortcut: KeyboardShortcut): string => {\n const parts: string[] = [];\n\n // Use Cmd on Mac, Ctrl on other platforms\n const isMac = typeof navigator !== 'undefined' && navigator.platform.includes('Mac');\n\n if (shortcut.metaKey) {\n parts.push(isMac ? 'Cmd' : 'Ctrl');\n }\n\n if (shortcut.ctrlKey && !shortcut.metaKey) {\n parts.push('Ctrl');\n }\n\n if (shortcut.altKey) {\n parts.push(isMac ? 'Option' : 'Alt');\n }\n\n if (shortcut.shiftKey) {\n parts.push('Shift');\n }\n\n parts.push(shortcut.key.toUpperCase());\n\n return parts.join('+');\n};\n"],"mappings":"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","names":["InteractionState"]}
|
|
1
|
+
{"version":3,"sources":["../src/constants.ts","../src/types/clip.ts","../src/types/index.ts","../src/utils/conversions.ts","../src/utils/beatsAndBars.ts","../src/utils/dBUtils.ts","../src/utils/musicalTicks.ts","../src/clipTimeHelpers.ts","../src/fades.ts","../src/keyboard.ts"],"sourcesContent":["/**\n * Maximum width in CSS pixels for a single canvas chunk.\n * Canvas elements are split into chunks of this width to enable\n * horizontal virtual scrolling — only visible chunks are mounted.\n */\nexport const MAX_CANVAS_WIDTH = 1000;\n","/**\n * Clip-Based Model Types\n *\n * These types support a professional multi-track editing model where:\n * - Each track can contain multiple audio clips\n * - Clips can be positioned anywhere on the timeline\n * - Clips have independent trim points (offset/duration)\n * - Gaps between clips are silent\n * - Clips can overlap (for crossfades)\n */\n\nimport { Fade } from './index';\nimport type { RenderMode, SpectrogramConfig, ColorMapValue } from './spectrogram';\n\n/**\n * WaveformData object from waveform-data.js library.\n * Supports resample() and slice() for dynamic zoom levels.\n * See: https://github.com/bbc/waveform-data.js\n */\nexport interface WaveformDataObject {\n /** Sample rate of the original audio */\n readonly sample_rate: number;\n /** Number of audio samples per pixel */\n readonly scale: number;\n /** Length of waveform data in pixels */\n readonly length: number;\n /** Bit depth (8 or 16) */\n readonly bits: number;\n /** Duration in seconds */\n readonly duration: number;\n /** Number of channels */\n readonly channels: number;\n /** Get channel data */\n channel: (index: number) => {\n min_array: () => number[];\n max_array: () => number[];\n };\n /** Resample to different scale */\n resample: (options: { scale: number } | { width: number }) => WaveformDataObject;\n /** Slice a portion of the waveform */\n slice: (\n options: { startTime: number; endTime: number } | { startIndex: number; endIndex: number }\n ) => WaveformDataObject;\n}\n\n/**\n * Generic effects function type for track-level audio processing.\n *\n * The actual implementation receives Tone.js audio nodes. Using generic types\n * here to avoid circular dependencies with the playout package.\n *\n * @param graphEnd - The end of the track's audio graph (Tone.js Gain node)\n * @param destination - Where to connect the effects output (Tone.js ToneAudioNode)\n * @param isOffline - Whether rendering offline (for export)\n * @returns Optional cleanup function called when track is disposed\n *\n * @example\n * ```typescript\n * const trackEffects: TrackEffectsFunction = (graphEnd, destination, isOffline) => {\n * const reverb = new Tone.Reverb({ decay: 1.5 });\n * graphEnd.connect(reverb);\n * reverb.connect(destination);\n *\n * return () => {\n * reverb.dispose();\n * };\n * };\n * ```\n */\nexport type TrackEffectsFunction = (\n graphEnd: unknown,\n destination: unknown,\n isOffline: boolean\n) => void | (() => void);\n\n/**\n * Represents a single audio clip on the timeline\n *\n * IMPORTANT: All positions/durations are stored as SAMPLE COUNTS (integers)\n * to avoid floating-point precision errors. Convert to seconds only when\n * needed for playback using: seconds = samples / sampleRate\n *\n * Clips can be created with just waveformData (for instant visual rendering)\n * and have audioBuffer added later when audio finishes loading.\n */\nexport interface AudioClip {\n /** Unique identifier for this clip */\n id: string;\n\n /**\n * The audio buffer containing the audio data.\n * Optional for peaks-first rendering - can be added later.\n * Required for playback and editing operations.\n */\n audioBuffer?: AudioBuffer;\n\n /** Position on timeline where this clip starts (in samples at timeline sampleRate) */\n startSample: number;\n\n /** Duration of this clip (in samples) - how much of the audio buffer to play */\n durationSamples: number;\n\n /** Offset into the audio buffer where playback starts (in samples) - the \"trim start\" point */\n offsetSamples: number;\n\n /**\n * Sample rate for this clip's audio.\n * Required when audioBuffer is not provided (for peaks-first rendering).\n * When audioBuffer is present, this should match audioBuffer.sampleRate.\n */\n sampleRate: number;\n\n /**\n * Total duration of the source audio in samples.\n * Required when audioBuffer is not provided (for trim bounds calculation).\n * When audioBuffer is present, this should equal audioBuffer.length.\n */\n sourceDurationSamples: number;\n\n /** Optional fade in effect */\n fadeIn?: Fade;\n\n /** Optional fade out effect */\n fadeOut?: Fade;\n\n /** Clip-specific gain/volume multiplier (0.0 to 1.0+) */\n gain: number;\n\n /** Optional label/name for this clip */\n name?: string;\n\n /** Optional color for visual distinction */\n color?: string;\n\n /**\n * Pre-computed waveform data from waveform-data.js library.\n * When provided, the library will use this instead of computing peaks from the audioBuffer.\n * Supports resampling to different zoom levels and slicing for clip trimming.\n * Load with: `const waveformData = await loadWaveformData('/path/to/peaks.dat')`\n */\n waveformData?: WaveformDataObject;\n\n /**\n * MIDI note data — when present, this clip plays MIDI instead of audio.\n * The playout adapter uses this field to detect MIDI clips and route them\n * to MidiToneTrack (PolySynth) instead of ToneTrack (AudioBufferSourceNode).\n */\n midiNotes?: MidiNoteData[];\n\n /** MIDI channel (0-indexed). Channel 9 = GM percussion. */\n midiChannel?: number;\n\n /** MIDI program number (0-127). GM instrument number for SoundFont playback. */\n midiProgram?: number;\n}\n\n/**\n * Represents a track containing multiple audio clips\n */\nexport interface ClipTrack {\n /** Unique identifier for this track */\n id: string;\n\n /** Display name for this track */\n name: string;\n\n /** Array of audio clips on this track */\n clips: AudioClip[];\n\n /** Whether this track is muted */\n muted: boolean;\n\n /** Whether this track is soloed */\n soloed: boolean;\n\n /** Track volume (0.0 to 1.0+) */\n volume: number;\n\n /** Stereo pan (-1.0 = left, 0 = center, 1.0 = right) */\n pan: number;\n\n /** Optional track color for visual distinction */\n color?: string;\n\n /** Track height in pixels (for UI) */\n height?: number;\n\n /** Optional effects function for this track */\n effects?: TrackEffectsFunction;\n\n /** Visualization render mode. Default: 'waveform' */\n renderMode?: RenderMode;\n\n /** Per-track spectrogram configuration (FFT size, window, frequency scale, etc.) */\n spectrogramConfig?: SpectrogramConfig;\n\n /** Per-track spectrogram color map name or custom color array */\n spectrogramColorMap?: ColorMapValue;\n}\n\n/**\n * Represents the entire timeline/project\n */\nexport interface Timeline {\n /** All tracks in the timeline */\n tracks: ClipTrack[];\n\n /** Total timeline duration in seconds */\n duration: number;\n\n /** Sample rate for all audio (typically 44100 or 48000) */\n sampleRate: number;\n\n /** Optional project name */\n name?: string;\n\n /** Optional tempo (BPM) for grid snapping */\n tempo?: number;\n\n /** Optional time signature for grid snapping */\n timeSignature?: {\n numerator: number;\n denominator: number;\n };\n}\n\n/**\n * Options for creating a new audio clip (using sample counts)\n *\n * Either audioBuffer OR (sampleRate + sourceDurationSamples + waveformData) must be provided.\n * Providing waveformData without audioBuffer enables peaks-first rendering.\n */\nexport interface CreateClipOptions {\n /** Audio buffer - optional for peaks-first rendering */\n audioBuffer?: AudioBuffer;\n startSample: number; // Position on timeline (in samples)\n durationSamples?: number; // Defaults to full buffer/source duration (in samples)\n offsetSamples?: number; // Defaults to 0\n gain?: number; // Defaults to 1.0\n name?: string;\n color?: string;\n fadeIn?: Fade;\n fadeOut?: Fade;\n /** Pre-computed waveform data from waveform-data.js (e.g., from BBC audiowaveform) */\n waveformData?: WaveformDataObject;\n /** Sample rate - required if audioBuffer not provided */\n sampleRate?: number;\n /** Total source audio duration in samples - required if audioBuffer not provided */\n sourceDurationSamples?: number;\n /** MIDI note data — passed through to the created AudioClip */\n midiNotes?: MidiNoteData[];\n /** MIDI channel (0-indexed). Channel 9 = GM percussion. */\n midiChannel?: number;\n /** MIDI program number (0-127). GM instrument for SoundFont playback. */\n midiProgram?: number;\n}\n\n/**\n * Options for creating a new audio clip (using seconds for convenience)\n *\n * Either audioBuffer OR (sampleRate + sourceDuration + waveformData) must be provided.\n * Providing waveformData without audioBuffer enables peaks-first rendering.\n */\nexport interface CreateClipOptionsSeconds {\n /** Audio buffer - optional for peaks-first rendering */\n audioBuffer?: AudioBuffer;\n startTime: number; // Position on timeline (in seconds)\n duration?: number; // Defaults to full buffer/source duration (in seconds)\n offset?: number; // Defaults to 0 (in seconds)\n gain?: number; // Defaults to 1.0\n name?: string;\n color?: string;\n fadeIn?: Fade;\n fadeOut?: Fade;\n /** Pre-computed waveform data from waveform-data.js (e.g., from BBC audiowaveform) */\n waveformData?: WaveformDataObject;\n /** Sample rate - required if audioBuffer not provided */\n sampleRate?: number;\n /** Total source audio duration in seconds - required if audioBuffer not provided */\n sourceDuration?: number;\n /** MIDI note data — passed through to the created AudioClip */\n midiNotes?: MidiNoteData[];\n /** MIDI channel (0-indexed). Channel 9 = GM percussion. */\n midiChannel?: number;\n /** MIDI program number (0-127). GM instrument for SoundFont playback. */\n midiProgram?: number;\n}\n\n/**\n * Options for creating a new track\n */\nexport interface CreateTrackOptions {\n name: string;\n clips?: AudioClip[];\n muted?: boolean;\n soloed?: boolean;\n volume?: number;\n pan?: number;\n color?: string;\n height?: number;\n spectrogramConfig?: SpectrogramConfig;\n spectrogramColorMap?: ColorMapValue;\n}\n\n/**\n * Creates a new AudioClip with sensible defaults (using sample counts)\n *\n * For peaks-first rendering (no audioBuffer), sampleRate and sourceDurationSamples can be:\n * - Provided explicitly via options\n * - Derived from waveformData (sample_rate and duration properties)\n */\nexport function createClip(options: CreateClipOptions): AudioClip {\n const {\n audioBuffer,\n startSample,\n offsetSamples = 0,\n gain = 1.0,\n name,\n color,\n fadeIn,\n fadeOut,\n waveformData,\n midiNotes,\n midiChannel,\n midiProgram,\n } = options;\n\n // Determine sample rate: audioBuffer > explicit option > waveformData\n const sampleRate = audioBuffer?.sampleRate ?? options.sampleRate ?? waveformData?.sample_rate;\n\n // Determine source duration: audioBuffer > explicit option > waveformData (converted to samples)\n const sourceDurationSamples =\n audioBuffer?.length ??\n options.sourceDurationSamples ??\n (waveformData && sampleRate ? Math.ceil(waveformData.duration * sampleRate) : undefined);\n\n if (sampleRate === undefined) {\n throw new Error(\n 'createClip: sampleRate is required when audioBuffer is not provided (can use waveformData.sample_rate)'\n );\n }\n if (sourceDurationSamples === undefined) {\n throw new Error(\n 'createClip: sourceDurationSamples is required when audioBuffer is not provided (can use waveformData.duration)'\n );\n }\n\n if (audioBuffer && waveformData && audioBuffer.sampleRate !== waveformData.sample_rate) {\n console.warn(\n '[waveform-playlist] \"' +\n (name ?? 'unnamed') +\n '\": pre-computed peaks at ' +\n waveformData.sample_rate +\n ' Hz do not match decoded audio at ' +\n audioBuffer.sampleRate +\n ' Hz — peaks may be recomputed from decoded audio'\n );\n }\n\n // Default duration to full source duration\n const durationSamples = options.durationSamples ?? sourceDurationSamples;\n\n return {\n id: generateId(),\n audioBuffer,\n startSample,\n durationSamples,\n offsetSamples,\n sampleRate,\n sourceDurationSamples,\n gain,\n name,\n color,\n fadeIn,\n fadeOut,\n waveformData,\n midiNotes,\n midiChannel,\n midiProgram,\n };\n}\n\n/**\n * Creates a new AudioClip from time-based values (convenience function)\n * Converts seconds to samples using the audioBuffer's sampleRate or explicit sampleRate\n *\n * For peaks-first rendering (no audioBuffer), sampleRate and sourceDuration can be:\n * - Provided explicitly via options\n * - Derived from waveformData (sample_rate and duration properties)\n */\nexport function createClipFromSeconds(options: CreateClipOptionsSeconds): AudioClip {\n const {\n audioBuffer,\n startTime,\n offset = 0,\n gain = 1.0,\n name,\n color,\n fadeIn,\n fadeOut,\n waveformData,\n midiNotes,\n midiChannel,\n midiProgram,\n } = options;\n\n // Determine sample rate: audioBuffer > explicit option > waveformData\n const sampleRate = audioBuffer?.sampleRate ?? options.sampleRate ?? waveformData?.sample_rate;\n if (sampleRate === undefined) {\n throw new Error(\n 'createClipFromSeconds: sampleRate is required when audioBuffer is not provided (can use waveformData.sample_rate)'\n );\n }\n\n // Determine source duration: audioBuffer > explicit option > waveformData\n const sourceDuration = audioBuffer?.duration ?? options.sourceDuration ?? waveformData?.duration;\n if (sourceDuration === undefined) {\n throw new Error(\n 'createClipFromSeconds: sourceDuration is required when audioBuffer is not provided (can use waveformData.duration)'\n );\n }\n\n // Note: audioBuffer and waveformData may have different sample rates (e.g., Opus at 48000\n // decoded on 44100 hardware). This is expected — callers fall back to worker to recompute peaks.\n\n // Default clip duration to full source duration\n const duration = options.duration ?? sourceDuration;\n\n return createClip({\n audioBuffer,\n startSample: Math.round(startTime * sampleRate),\n durationSamples: Math.round(duration * sampleRate),\n offsetSamples: Math.round(offset * sampleRate),\n sampleRate,\n sourceDurationSamples: Math.ceil(sourceDuration * sampleRate),\n gain,\n name,\n color,\n fadeIn,\n fadeOut,\n waveformData,\n midiNotes,\n midiChannel,\n midiProgram,\n });\n}\n\n/**\n * Creates a new ClipTrack with sensible defaults\n */\nexport function createTrack(options: CreateTrackOptions): ClipTrack {\n const {\n name,\n clips = [],\n muted = false,\n soloed = false,\n volume = 1.0,\n pan = 0,\n color,\n height,\n spectrogramConfig,\n spectrogramColorMap,\n } = options;\n\n return {\n id: generateId(),\n name,\n clips,\n muted,\n soloed,\n volume,\n pan,\n color,\n height,\n spectrogramConfig,\n spectrogramColorMap,\n };\n}\n\n/**\n * Creates a new Timeline with sensible defaults\n */\nexport function createTimeline(\n tracks: ClipTrack[],\n sampleRate: number = 44100,\n options?: {\n name?: string;\n tempo?: number;\n timeSignature?: { numerator: number; denominator: number };\n }\n): Timeline {\n // Calculate total duration from all clips across all tracks (in seconds)\n const durationSamples = tracks.reduce((maxSamples, track) => {\n const trackSamples = track.clips.reduce((max, clip) => {\n return Math.max(max, clip.startSample + clip.durationSamples);\n }, 0);\n return Math.max(maxSamples, trackSamples);\n }, 0);\n\n const duration = durationSamples / sampleRate;\n\n return {\n tracks,\n duration,\n sampleRate,\n name: options?.name,\n tempo: options?.tempo,\n timeSignature: options?.timeSignature,\n };\n}\n\n/**\n * Generates a unique ID for clips and tracks\n */\nfunction generateId(): string {\n return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n}\n\n/**\n * MIDI note data for clips that play MIDI instead of audio.\n * When present on an AudioClip, the clip is treated as a MIDI clip\n * by the playout adapter.\n */\nexport interface MidiNoteData {\n /** MIDI note number (0-127) */\n midi: number;\n /** Note name in scientific pitch notation (\"C4\", \"G#3\") */\n name: string;\n /** Start time in seconds, relative to clip start */\n time: number;\n /** Duration in seconds */\n duration: number;\n /** Velocity (0-1 normalized) */\n velocity: number;\n /** MIDI channel (0-indexed). Channel 9 = GM percussion. Enables per-note routing in flattened tracks. */\n channel?: number;\n}\n\n/**\n * Utility: Get all clips within a sample range\n */\nexport function getClipsInRange(\n track: ClipTrack,\n startSample: number,\n endSample: number\n): AudioClip[] {\n return track.clips.filter((clip) => {\n const clipEnd = clip.startSample + clip.durationSamples;\n // Clip overlaps with range if:\n // - Clip starts before range ends AND\n // - Clip ends after range starts\n return clip.startSample < endSample && clipEnd > startSample;\n });\n}\n\n/**\n * Utility: Get all clips at a specific sample position\n */\nexport function getClipsAtSample(track: ClipTrack, sample: number): AudioClip[] {\n return track.clips.filter((clip) => {\n const clipEnd = clip.startSample + clip.durationSamples;\n return sample >= clip.startSample && sample < clipEnd;\n });\n}\n\n/**\n * Utility: Check if two clips overlap\n */\nexport function clipsOverlap(clip1: AudioClip, clip2: AudioClip): boolean {\n const clip1End = clip1.startSample + clip1.durationSamples;\n const clip2End = clip2.startSample + clip2.durationSamples;\n\n return clip1.startSample < clip2End && clip1End > clip2.startSample;\n}\n\n/**\n * Utility: Sort clips by startSample\n */\nexport function sortClipsByTime(clips: AudioClip[]): AudioClip[] {\n return [...clips].sort((a, b) => a.startSample - b.startSample);\n}\n\n/**\n * Utility: Find gaps between clips (silent regions)\n */\nexport interface Gap {\n startSample: number;\n endSample: number;\n durationSamples: number;\n}\n\nexport function findGaps(track: ClipTrack): Gap[] {\n if (track.clips.length === 0) return [];\n\n const sorted = sortClipsByTime(track.clips);\n const gaps: Gap[] = [];\n\n for (let i = 0; i < sorted.length - 1; i++) {\n const currentClipEnd = sorted[i].startSample + sorted[i].durationSamples;\n const nextClipStart = sorted[i + 1].startSample;\n\n if (nextClipStart > currentClipEnd) {\n gaps.push({\n startSample: currentClipEnd,\n endSample: nextClipStart,\n durationSamples: nextClipStart - currentClipEnd,\n });\n }\n }\n\n return gaps;\n}\n","/**\n * Peaks type - represents a typed array of interleaved min/max peak data\n */\nexport type Peaks = Int8Array | Int16Array;\n\n/**\n * Bits type - number of bits for peak data\n */\nexport type Bits = 8 | 16;\n\n/**\n * PeakData - result of peak extraction\n */\nexport interface PeakData {\n /** Number of peak pairs extracted */\n length: number;\n /** Array of peak data for each channel (interleaved min/max) */\n data: Peaks[];\n /** Bit depth of peak data */\n bits: Bits;\n}\n\nexport interface WaveformConfig {\n sampleRate: number;\n samplesPerPixel: number;\n waveHeight?: number;\n waveOutlineColor?: string;\n waveFillColor?: string;\n waveProgressColor?: string;\n}\n\nexport interface AudioBuffer {\n length: number;\n duration: number;\n numberOfChannels: number;\n sampleRate: number;\n getChannelData(channel: number): Float32Array;\n}\n\nexport interface Track {\n id: string;\n name: string;\n src?: string | AudioBuffer; // Support both URL strings and AudioBuffer objects\n gain: number;\n muted: boolean;\n soloed: boolean;\n stereoPan: number;\n startTime: number;\n endTime?: number;\n fadeIn?: Fade;\n fadeOut?: Fade;\n cueIn?: number;\n cueOut?: number;\n}\n\n/**\n * Simple fade configuration\n */\nexport interface Fade {\n /** Duration of the fade in seconds */\n duration: number;\n /** Type of fade curve (default: 'linear') */\n type?: FadeType;\n}\n\nexport type FadeType = 'logarithmic' | 'linear' | 'sCurve' | 'exponential';\n\n/**\n * Alias for Fade — used by media-element-playout and playout packages\n */\nexport type FadeConfig = Fade;\n\nexport interface PlaylistConfig {\n samplesPerPixel?: number;\n waveHeight?: number;\n container?: HTMLElement;\n isAutomaticScroll?: boolean;\n timescale?: boolean;\n colors?: {\n waveOutlineColor?: string;\n waveFillColor?: string;\n waveProgressColor?: string;\n };\n controls?: {\n show?: boolean;\n width?: number;\n };\n zoomLevels?: number[];\n}\n\nexport interface PlayoutState {\n isPlaying: boolean;\n isPaused: boolean;\n cursor: number;\n duration: number;\n}\n\nexport interface TimeSelection {\n start: number;\n end: number;\n}\n\nexport enum InteractionState {\n Cursor = 'cursor',\n Select = 'select',\n Shift = 'shift',\n FadeIn = 'fadein',\n FadeOut = 'fadeout',\n}\n\n// Export clip-based model types\nexport * from './clip';\n\n// Export spectrogram types\nexport * from './spectrogram';\n\n// Export annotation types\nexport * from './annotations';\n","export function samplesToSeconds(samples: number, sampleRate: number): number {\n return samples / sampleRate;\n}\n\nexport function secondsToSamples(seconds: number, sampleRate: number): number {\n return Math.ceil(seconds * sampleRate);\n}\n\nexport function samplesToPixels(samples: number, samplesPerPixel: number): number {\n return Math.floor(samples / samplesPerPixel);\n}\n\nexport function pixelsToSamples(pixels: number, samplesPerPixel: number): number {\n return Math.floor(pixels * samplesPerPixel);\n}\n\nexport function pixelsToSeconds(\n pixels: number,\n samplesPerPixel: number,\n sampleRate: number\n): number {\n return (pixels * samplesPerPixel) / sampleRate;\n}\n\nexport function secondsToPixels(\n seconds: number,\n samplesPerPixel: number,\n sampleRate: number\n): number {\n return Math.ceil((seconds * sampleRate) / samplesPerPixel);\n}\n","/** Default PPQN matching Tone.js Transport (192 ticks per quarter note) */\nexport const PPQN = 192;\n\n/** Number of PPQN ticks per beat for the given time signature. */\nexport function ticksPerBeat(timeSignature: [number, number], ppqn = PPQN): number {\n const [, denominator] = timeSignature;\n return ppqn * (4 / denominator);\n}\n\n/** Number of PPQN ticks per bar for the given time signature. */\nexport function ticksPerBar(timeSignature: [number, number], ppqn = PPQN): number {\n const [numerator] = timeSignature;\n return numerator * ticksPerBeat(timeSignature, ppqn);\n}\n\n/** Convert PPQN ticks to sample count. Uses Math.round for integer sample alignment. */\nexport function ticksToSamples(\n ticks: number,\n bpm: number,\n sampleRate: number,\n ppqn = PPQN\n): number {\n return Math.round((ticks * 60 * sampleRate) / (bpm * ppqn));\n}\n\n/** Convert sample count to PPQN ticks. Inverse of ticksToSamples. */\nexport function samplesToTicks(\n samples: number,\n bpm: number,\n sampleRate: number,\n ppqn = PPQN\n): number {\n return Math.round((samples * ppqn * bpm) / (60 * sampleRate));\n}\n\n/** Snap a tick position to the nearest grid line (rounds to nearest). */\nexport function snapToGrid(ticks: number, gridSizeTicks: number): number {\n return Math.round(ticks / gridSizeTicks) * gridSizeTicks;\n}\n\n/** Format ticks as a 1-indexed bar.beat label. Beat 1 shows bar number only (e.g., \"3\" not \"3.1\"). */\nexport function ticksToBarBeatLabel(\n ticks: number,\n timeSignature: [number, number],\n ppqn = PPQN\n): string {\n const barTicks = ticksPerBar(timeSignature, ppqn);\n const beatTicks = ticksPerBeat(timeSignature, ppqn);\n const bar = Math.floor(ticks / barTicks) + 1;\n const beatInBar = Math.floor((ticks % barTicks) / beatTicks) + 1;\n if (beatInBar === 1) return `${bar}`;\n return `${bar}.${beatInBar}`;\n}\n","const DEFAULT_FLOOR = -100;\n\n/**\n * Convert a dB value to a normalized range.\n *\n * Maps dB values linearly: floor → 0, 0 dB → 1.\n * Values above 0 dB map to > 1 (e.g., +5 dB → 1.05 with default floor).\n *\n * @param dB - Decibel value (typically -Infinity to +5)\n * @param floor - Minimum dB value mapped to 0. Default: -100 (Firefox compat)\n * @returns Normalized value (0 at floor, 1 at 0 dB, >1 above 0 dB)\n */\nexport function dBToNormalized(dB: number, floor: number = DEFAULT_FLOOR): number {\n if (Number.isNaN(dB)) {\n console.warn('[waveform-playlist] dBToNormalized received NaN');\n return 0;\n }\n if (floor >= 0) {\n console.warn('[waveform-playlist] dBToNormalized floor must be negative, got:', floor);\n return 0;\n }\n if (!isFinite(dB) || dB <= floor) return 0;\n return (dB - floor) / -floor;\n}\n\n/**\n * Convert a normalized value back to dB.\n *\n * Maps linearly: 0 → floor, 1 → 0 dB.\n * Values above 1 map to positive dB (e.g., 1.05 → +5 dB with default floor).\n *\n * @param normalized - Normalized value (0 = floor, 1 = 0 dB)\n * @param floor - Minimum dB value (maps from 0). Must be negative. Default: -100\n * @returns dB value (floor at 0, 0 dB at 1, positive dB above 1)\n */\nexport function normalizedToDb(normalized: number, floor: number = DEFAULT_FLOOR): number {\n if (!isFinite(normalized)) return floor;\n if (floor >= 0) {\n console.warn('[waveform-playlist] normalizedToDb floor must be negative, got:', floor);\n return DEFAULT_FLOOR;\n }\n const clamped = Math.max(0, normalized);\n return clamped * -floor + floor;\n}\n\n/**\n * Convert a linear gain value to decibels.\n *\n * @param gain - Linear gain (0 = silence, 1 = unity)\n * @returns Decibel value (e.g., 0.5 → ≈ -6.02 dB)\n */\nexport function gainToDb(gain: number): number {\n return 20 * Math.log10(Math.max(gain, 0.0001));\n}\n\n/**\n * Convert a linear gain value (0-1+) to normalized 0-1 via dB.\n *\n * Combines gain-to-dB (20 * log10) with dBToNormalized for a consistent\n * mapping from raw AudioWorklet peak/RMS values to the 0-1 range used\n * by UI meter components.\n *\n * @param gain - Linear gain value (typically 0 to 1, can exceed 1)\n * @param floor - Minimum dB value mapped to 0. Default: -100\n * @returns Normalized value (0 at silence/floor, 1 at 0 dB, >1 above 0 dB)\n */\nexport function gainToNormalized(gain: number, floor: number = DEFAULT_FLOOR): number {\n if (gain <= 0) return 0;\n // Use raw log10 (no clamp) — gain > 0 is guaranteed above,\n // and dBToNormalized handles -Infinity via its floor check.\n const db = 20 * Math.log10(gain);\n return dBToNormalized(db, floor);\n}\n","import { ticksPerBeat, ticksPerBar, ticksToBarBeatLabel } from './beatsAndBars';\n\n/** All supported snap-to-grid values. */\nexport type SnapTo =\n | 'bar'\n | 'beat'\n | '1/2'\n | '1/4'\n | '1/8'\n | '1/16'\n | '1/32'\n | '1/2T'\n | '1/4T'\n | '1/8T'\n | '1/16T'\n | 'off';\n\n/**\n * Returns the tick interval for the given SnapTo value.\n *\n * Straight subdivisions (1/2, 1/4, 1/8, 1/16, 1/32) are always expressed as\n * fractions of a quarter note (ppqn), independent of the time signature\n * denominator. Triplet subdivisions use × 2/3 of the corresponding straight\n * value. 'bar' and 'beat' depend on the time signature. 'off' returns 0.\n */\nexport function snapToTicks(snapTo: SnapTo, timeSignature: [number, number], ppqn = 960): number {\n switch (snapTo) {\n case 'bar':\n return ticksPerBar(timeSignature, ppqn);\n case 'beat':\n return ticksPerBeat(timeSignature, ppqn);\n case '1/2':\n return ppqn * 2;\n case '1/4':\n return ppqn;\n case '1/8':\n return ppqn / 2;\n case '1/16':\n return ppqn / 4;\n case '1/32':\n return ppqn / 8;\n case '1/2T':\n return Math.round((ppqn * 2 * 2) / 3);\n case '1/4T':\n return Math.round((ppqn * 2) / 3);\n case '1/8T':\n return Math.round((ppqn * 2) / 6);\n case '1/16T':\n return Math.round((ppqn * 2) / 12);\n case 'off':\n return 0;\n }\n}\n\n/**\n * Three-tier tick hierarchy (following Audacity's model):\n * major — Bar boundaries. Always labeled, strongest grid lines.\n * minor — Beat boundaries. Labeled when wide enough, medium grid lines.\n * minorMinor — Subdivisions (eighths, sixteenths). Never labeled, ruler ticks only (no grid).\n */\nexport type TickType = 'major' | 'minor' | 'minorMinor';\n\n/** Zoom level category used to select which subdivision to iterate at. */\nexport type ZoomLevel = 'coarse' | 'bar' | 'beat' | 'eighth' | 'sixteenth';\n\n/** A single musical tick with rendering metadata. */\nexport interface MusicalTick {\n /** Pixel position of the tick in the timeline. */\n pixel: number;\n /** Three-tier type: major (bar), minor (beat), minorMinor (subdivision). */\n type: TickType;\n /** Human-readable label. Present for major ticks always; minor ticks when zoomed in. */\n label?: string;\n /** 0-based global bar index (for alternating bar-level striping). */\n barIndex: number;\n}\n\n/** Result of computeMusicalTicks(). */\nexport interface MusicalTickData {\n ticks: MusicalTick[];\n pixelsPerBar: number;\n pixelsPerBeat: number;\n zoomLevel: ZoomLevel;\n /** At 'coarse' zoom: how many bars between rendered tick lines. */\n coarseBarStep?: number;\n}\n\n/** Parameters for computeMusicalTicks(). */\nexport interface MusicalTickParams {\n timeSignature: [number, number];\n /** Ticks per pixel (zoom level — lower value = more zoomed in). */\n ticksPerPixel: number;\n startPixel: number;\n endPixel: number;\n /** Pulses per quarter note. Defaults to 960. */\n ppqn?: number;\n}\n\n/** Minimum pixels per musical unit before switching to a coarser zoom level. */\nexport const MIN_PIXELS_PER_UNIT = 8;\n\n/** Minimum pixels between beat labels for readable text. */\nconst MIN_PIXELS_PER_LABEL = 60;\n\n/**\n * Determines the zoom level and computes which tick lines to render for a\n * given viewport. Pure tick arithmetic — no BPM or sample rate required.\n */\nexport function computeMusicalTicks(params: MusicalTickParams): MusicalTickData {\n const { timeSignature, ticksPerPixel, startPixel, endPixel, ppqn = 960 } = params;\n\n // Guard against invalid inputs that would cause division by zero or infinite loops\n if (ticksPerPixel <= 0 || ppqn <= 0 || timeSignature[1] <= 0) {\n return { ticks: [], pixelsPerBar: 0, pixelsPerBeat: 0, zoomLevel: 'coarse' };\n }\n\n const tpBeat = ticksPerBeat(timeSignature, ppqn);\n const tpBar = ticksPerBar(timeSignature, ppqn);\n const tpEighth = ppqn / 2;\n const tpSixteenth = ppqn / 4;\n\n const pixelsPerBar = tpBar / ticksPerPixel;\n const pixelsPerBeat = tpBeat / ticksPerPixel;\n const pixelsPerEighth = tpEighth / ticksPerPixel;\n const pixelsPerSixteenth = tpSixteenth / ticksPerPixel;\n\n // Determine zoom level based on pixel density thresholds.\n let zoomLevel: ZoomLevel;\n if (pixelsPerBar < MIN_PIXELS_PER_UNIT) {\n zoomLevel = 'coarse';\n } else if (pixelsPerBeat < MIN_PIXELS_PER_UNIT) {\n zoomLevel = 'bar';\n } else if (pixelsPerEighth < MIN_PIXELS_PER_UNIT) {\n zoomLevel = 'beat';\n } else if (pixelsPerSixteenth < MIN_PIXELS_PER_UNIT) {\n zoomLevel = 'eighth';\n } else {\n zoomLevel = 'sixteenth';\n }\n\n // Determine step size in ticks and coarse bar step when zoomed far out.\n let stepTicks: number;\n let coarseBarStep: number | undefined;\n\n if (zoomLevel === 'coarse') {\n // Choose the smallest power-of-2 multiple of tpBar that gives ≥8px.\n let multiplier = 2;\n while ((tpBar * multiplier) / ticksPerPixel < MIN_PIXELS_PER_UNIT) {\n multiplier *= 2;\n }\n stepTicks = tpBar * multiplier;\n coarseBarStep = multiplier;\n } else if (zoomLevel === 'bar') {\n stepTicks = tpBar;\n } else if (zoomLevel === 'beat') {\n stepTicks = tpBeat;\n } else if (zoomLevel === 'eighth') {\n stepTicks = tpEighth;\n } else {\n stepTicks = tpSixteenth;\n }\n\n // Convert pixel viewport to tick range, align start to step boundary.\n const startTick = startPixel * ticksPerPixel;\n const endTick = endPixel * ticksPerPixel;\n const firstStep = Math.floor(startTick / stepTicks) * stepTicks;\n\n const ticks: MusicalTick[] = [];\n\n for (let tick = firstStep; tick <= endTick; tick += stepTicks) {\n const pixel = tick / ticksPerPixel;\n\n if (pixel < startPixel || pixel > endPixel) {\n continue;\n }\n\n // Classify into three-tier hierarchy (Audacity model):\n // major = bar boundary\n // minor = beat boundary\n // minorMinor = subdivision (eighth, sixteenth)\n let type: TickType;\n if (tick % tpBar === 0) {\n type = 'major';\n } else if (tick % tpBeat === 0) {\n type = 'minor';\n } else {\n type = 'minorMinor';\n }\n\n // Bar index for alternating bar-level zebra stripes\n const barIndex = Math.floor(tick / tpBar);\n\n // Labels: major always, minor only when wide enough, minorMinor never\n let label: string | undefined;\n if (type === 'major') {\n label = ticksToBarBeatLabel(tick, timeSignature, ppqn);\n } else if (type === 'minor' && pixelsPerBeat >= MIN_PIXELS_PER_LABEL) {\n label = ticksToBarBeatLabel(tick, timeSignature, ppqn);\n }\n\n ticks.push({ pixel, type, barIndex, ...(label !== undefined ? { label } : {}) });\n }\n\n const result: MusicalTickData = {\n ticks,\n pixelsPerBar,\n pixelsPerBeat,\n zoomLevel,\n ...(coarseBarStep !== undefined ? { coarseBarStep } : {}),\n };\n\n return result;\n}\n\n/**\n * Snaps a tick position to the nearest grid boundary defined by `snapTo`.\n *\n * Returns the original tick unchanged when `snapTo` is 'off'.\n */\nexport function snapTickToGrid(\n tick: number,\n snapTo: SnapTo,\n timeSignature: [number, number],\n ppqn = 960\n): number {\n if (snapTo === 'off') return tick;\n const gridSize = snapToTicks(snapTo, timeSignature, ppqn);\n if (gridSize <= 0) return tick;\n return Math.round(tick / gridSize) * gridSize;\n}\n","import type { AudioClip, ClipTrack } from './types';\n\n/** Clip start position in seconds */\nexport function clipStartTime(clip: AudioClip): number {\n return clip.startSample / clip.sampleRate;\n}\n\n/** Clip end position in seconds (start + duration) */\nexport function clipEndTime(clip: AudioClip): number {\n return (clip.startSample + clip.durationSamples) / clip.sampleRate;\n}\n\n/** Clip offset into source audio in seconds */\nexport function clipOffsetTime(clip: AudioClip): number {\n return clip.offsetSamples / clip.sampleRate;\n}\n\n/** Clip duration in seconds */\nexport function clipDurationTime(clip: AudioClip): number {\n return clip.durationSamples / clip.sampleRate;\n}\n\n/**\n * Max audio channel count across a track's clips.\n * Used to set Panner channelCount and offline render output channels.\n */\nexport function trackChannelCount(track: ClipTrack): number {\n return track.clips.reduce(\n (max, clip) => Math.max(max, clip.audioBuffer?.numberOfChannels ?? 1),\n 1\n );\n}\n\n/**\n * Clip width in pixels at a given samplesPerPixel.\n * Shared by Clip.tsx (container sizing) and ChannelWithProgress.tsx (progress overlay)\n * to ensure pixel-perfect alignment. Floor-based endpoint subtraction guarantees\n * adjacent clips have no pixel gaps.\n */\nexport function clipPixelWidth(\n startSample: number,\n durationSamples: number,\n samplesPerPixel: number\n): number {\n return (\n Math.floor((startSample + durationSamples) / samplesPerPixel) -\n Math.floor(startSample / samplesPerPixel)\n );\n}\n","/**\n * Fade curve utilities for Web Audio API\n *\n * Pure functions that generate fade curves and apply them to AudioParam.\n * No Tone.js dependency — works with native Web Audio nodes.\n */\n\nimport type { FadeType } from './types';\n\n/**\n * Generate a linear fade curve\n */\nexport function linearCurve(length: number, fadeIn: boolean): Float32Array {\n const curve = new Float32Array(length);\n const scale = length - 1;\n\n for (let i = 0; i < length; i++) {\n const x = i / scale;\n curve[i] = fadeIn ? x : 1 - x;\n }\n\n return curve;\n}\n\n/**\n * Generate an exponential fade curve\n */\nexport function exponentialCurve(length: number, fadeIn: boolean): Float32Array {\n const curve = new Float32Array(length);\n const scale = length - 1;\n\n for (let i = 0; i < length; i++) {\n const x = i / scale;\n const index = fadeIn ? i : length - 1 - i;\n curve[index] = Math.exp(2 * x - 1) / Math.E;\n }\n\n return curve;\n}\n\n/**\n * Generate an S-curve (sine-based smooth curve)\n */\nexport function sCurveCurve(length: number, fadeIn: boolean): Float32Array {\n const curve = new Float32Array(length);\n const phase = fadeIn ? Math.PI / 2 : -Math.PI / 2;\n\n for (let i = 0; i < length; i++) {\n curve[i] = Math.sin((Math.PI * i) / length - phase) / 2 + 0.5;\n }\n\n return curve;\n}\n\n/**\n * Generate a logarithmic fade curve\n */\nexport function logarithmicCurve(length: number, fadeIn: boolean, base: number = 10): Float32Array {\n const curve = new Float32Array(length);\n\n for (let i = 0; i < length; i++) {\n const index = fadeIn ? i : length - 1 - i;\n const x = i / length;\n curve[index] = Math.log(1 + base * x) / Math.log(1 + base);\n }\n\n return curve;\n}\n\n/**\n * Generate a fade curve of the specified type\n */\nexport function generateCurve(type: FadeType, length: number, fadeIn: boolean): Float32Array {\n switch (type) {\n case 'linear':\n return linearCurve(length, fadeIn);\n case 'exponential':\n return exponentialCurve(length, fadeIn);\n case 'sCurve':\n return sCurveCurve(length, fadeIn);\n case 'logarithmic':\n return logarithmicCurve(length, fadeIn);\n default:\n return linearCurve(length, fadeIn);\n }\n}\n\n/**\n * Apply a fade in to an AudioParam\n *\n * @param param - The AudioParam to apply the fade to (usually gain)\n * @param startTime - When the fade starts (in seconds, AudioContext time)\n * @param duration - Duration of the fade in seconds\n * @param type - Type of fade curve\n * @param startValue - Starting value (default: 0)\n * @param endValue - Ending value (default: 1)\n */\nexport function applyFadeIn(\n param: AudioParam,\n startTime: number,\n duration: number,\n type: FadeType = 'linear',\n startValue: number = 0,\n endValue: number = 1\n): void {\n if (duration <= 0) return;\n\n if (type === 'linear') {\n param.setValueAtTime(startValue, startTime);\n param.linearRampToValueAtTime(endValue, startTime + duration);\n } else if (type === 'exponential') {\n param.setValueAtTime(Math.max(startValue, 0.001), startTime);\n param.exponentialRampToValueAtTime(Math.max(endValue, 0.001), startTime + duration);\n } else {\n const curve = generateCurve(type, 10000, true);\n const scaledCurve = new Float32Array(curve.length);\n const range = endValue - startValue;\n for (let i = 0; i < curve.length; i++) {\n scaledCurve[i] = startValue + curve[i] * range;\n }\n param.setValueCurveAtTime(scaledCurve, startTime, duration);\n }\n}\n\n/**\n * Apply a fade out to an AudioParam\n *\n * @param param - The AudioParam to apply the fade to (usually gain)\n * @param startTime - When the fade starts (in seconds, AudioContext time)\n * @param duration - Duration of the fade in seconds\n * @param type - Type of fade curve\n * @param startValue - Starting value (default: 1)\n * @param endValue - Ending value (default: 0)\n */\nexport function applyFadeOut(\n param: AudioParam,\n startTime: number,\n duration: number,\n type: FadeType = 'linear',\n startValue: number = 1,\n endValue: number = 0\n): void {\n if (duration <= 0) return;\n\n if (type === 'linear') {\n param.setValueAtTime(startValue, startTime);\n param.linearRampToValueAtTime(endValue, startTime + duration);\n } else if (type === 'exponential') {\n param.setValueAtTime(Math.max(startValue, 0.001), startTime);\n param.exponentialRampToValueAtTime(Math.max(endValue, 0.001), startTime + duration);\n } else {\n const curve = generateCurve(type, 10000, false);\n const scaledCurve = new Float32Array(curve.length);\n const range = startValue - endValue;\n for (let i = 0; i < curve.length; i++) {\n scaledCurve[i] = endValue + curve[i] * range;\n }\n param.setValueCurveAtTime(scaledCurve, startTime, duration);\n }\n}\n","/**\n * Framework-agnostic keyboard shortcut handling.\n * Used by both React (useKeyboardShortcuts) and Web Components (daw-editor).\n */\n\nexport interface KeyboardShortcut {\n key: string;\n ctrlKey?: boolean;\n shiftKey?: boolean;\n metaKey?: boolean;\n altKey?: boolean;\n action: () => void;\n description?: string;\n preventDefault?: boolean;\n}\n\n/**\n * Handle a keyboard event against a list of shortcuts.\n * Pure function, no framework dependency.\n */\nexport function handleKeyboardEvent(\n event: KeyboardEvent,\n shortcuts: KeyboardShortcut[],\n enabled: boolean\n): void {\n if (!enabled) return;\n\n // Ignore key repeat events — holding a key fires keydown repeatedly.\n // Without this guard, holding Space rapidly toggles play/pause.\n if (event.repeat) return;\n\n const target = event.target as HTMLElement;\n if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) {\n return;\n }\n\n const matchingShortcut = shortcuts.find((shortcut) => {\n const keyMatch =\n event.key.toLowerCase() === shortcut.key.toLowerCase() || event.key === shortcut.key;\n\n const ctrlMatch = shortcut.ctrlKey === undefined || event.ctrlKey === shortcut.ctrlKey;\n const shiftMatch = shortcut.shiftKey === undefined || event.shiftKey === shortcut.shiftKey;\n const metaMatch = shortcut.metaKey === undefined || event.metaKey === shortcut.metaKey;\n const altMatch = shortcut.altKey === undefined || event.altKey === shortcut.altKey;\n\n return keyMatch && ctrlMatch && shiftMatch && metaMatch && altMatch;\n });\n\n if (matchingShortcut) {\n if (matchingShortcut.preventDefault !== false) {\n event.preventDefault();\n }\n matchingShortcut.action();\n }\n}\n\n/**\n * Get a human-readable string representation of a keyboard shortcut.\n *\n * @param shortcut - The keyboard shortcut\n * @returns Human-readable string (e.g., \"Cmd+Shift+S\")\n */\nexport const getShortcutLabel = (shortcut: KeyboardShortcut): string => {\n const parts: string[] = [];\n\n // Use Cmd on Mac, Ctrl on other platforms\n const isMac = typeof navigator !== 'undefined' && navigator.platform.includes('Mac');\n\n if (shortcut.metaKey) {\n parts.push(isMac ? 'Cmd' : 'Ctrl');\n }\n\n if (shortcut.ctrlKey && !shortcut.metaKey) {\n parts.push('Ctrl');\n }\n\n if (shortcut.altKey) {\n parts.push(isMac ? 'Option' : 'Alt');\n }\n\n if (shortcut.shiftKey) {\n parts.push('Shift');\n }\n\n parts.push(shortcut.key.toUpperCase());\n\n return parts.join('+');\n};\n"],"mappings":"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","names":["InteractionState"]}
|