@waveform-playlist/browser 11.1.0 → 11.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -41,6 +41,15 @@ interface TrackState$1 {
41
41
  volume: number;
42
42
  pan: number;
43
43
  }
44
+ /** Per-frame data passed to registered animation callbacks. */
45
+ interface FrameData {
46
+ /** Raw engine time (for state/logic — NOT for visual positioning). */
47
+ readonly time: number;
48
+ /** time - outputLatency (for DOM positioning — matches speaker output). */
49
+ readonly visualTime: number;
50
+ readonly sampleRate: number;
51
+ readonly samplesPerPixel: number;
52
+ }
44
53
  interface PlaybackAnimationContextValue {
45
54
  isPlaying: boolean;
46
55
  currentTime: number;
@@ -49,6 +58,10 @@ interface PlaybackAnimationContextValue {
49
58
  audioStartPositionRef: React__default.RefObject<number>;
50
59
  /** Returns current playback time from engine (auto-wraps at loop boundaries). */
51
60
  getPlaybackTime: () => number;
61
+ /** Register a per-frame callback driven by the single animation loop. */
62
+ registerFrameCallback: (id: string, cb: (data: FrameData) => void) => void;
63
+ /** Unregister a per-frame callback. */
64
+ unregisterFrameCallback: (id: string) => void;
52
65
  }
53
66
  interface PlaylistStateContextValue {
54
67
  continuousPlay: boolean;
@@ -186,6 +199,10 @@ interface WaveformPlaylistProviderProps {
186
199
  /** Disable automatic stop when the cursor reaches the end of the longest
187
200
  * track. Useful for DAW-style recording beyond existing audio. */
188
201
  indefinitePlayback?: boolean;
202
+ /** Desired AudioContext sample rate. Creates a cross-browser AudioContext at
203
+ * this rate via standardized-audio-context. Pre-computed peaks (.dat files)
204
+ * render instantly when they match. On mismatch, falls back to worker. */
205
+ sampleRate?: number;
189
206
  children: ReactNode;
190
207
  }
191
208
  declare const WaveformPlaylistProvider: React__default.FC<WaveformPlaylistProviderProps>;
@@ -1170,7 +1187,7 @@ declare const TimeFormatSelect: React__default.FC<{
1170
1187
  }>;
1171
1188
  /**
1172
1189
  * Audio position display that uses the playlist context.
1173
- * Uses requestAnimationFrame for smooth 60fps updates during playback.
1190
+ * Updates via the shared animation frame registry — no own rAF loop.
1174
1191
  * Direct DOM manipulation avoids React re-renders.
1175
1192
  */
1176
1193
  declare const AudioPosition: React__default.FC<{
@@ -1807,4 +1824,4 @@ declare function getWaveformDataMetadata(src: string): Promise<{
1807
1824
  bits: 8 | 16;
1808
1825
  }>;
1809
1826
 
1810
- export { type ActiveEffect, type AnnotationIntegration, AnnotationIntegrationProvider, AudioPosition, type AudioTrackConfig, AutomaticScrollCheckbox, ClearAllButton, type ClearAllButtonProps, ClipCollisionModifier, ClipInteractionProvider, type ClipInteractionProviderProps, ContinuousPlayCheckbox, DownloadAnnotationsButton, EditableCheckbox, type EffectDefinition, type EffectInstance, type EffectParameter, type ExportOptions, type ExportResult, ExportWavButton, type ExportWavButtonProps, FastForwardButton, type GetAnnotationBoxLabelFn, KeyboardShortcuts, type KeyboardShortcutsProps, LinkEndpointsCheckbox, LoopButton, MasterVolumeControl, type MasterVolumeControls, type MediaElementAnimationContextValue, MediaElementAnnotationList, type MediaElementAnnotationListProps, type MediaElementControlsContextValue, type MediaElementDataContextValue, MediaElementPlaylist, type MediaElementPlaylistProps, MediaElementPlaylistProvider, type MediaElementStateContextValue, type MediaElementTrackConfig, MediaElementWaveform, type MediaElementWaveformProps, type OnAnnotationUpdateFn, type ParameterType, PauseButton, PlayButton, PlaylistAnnotationList, type PlaylistAnnotationListProps, PlaylistVisualization, type PlaylistVisualizationProps, RewindButton, SelectionTimeInputs, SetLoopRegionButton, SkipBackwardButton, SkipForwardButton, SnapToGridModifier, type SpectrogramIntegration, SpectrogramIntegrationProvider, StopButton, type TimeFormatControls, TimeFormatSelect, type TrackActiveEffect, type TrackEffectsState, type TrackLoadError, type TrackSource, type TrackState$1 as TrackState, type UseDynamicEffectsReturn, type UseDynamicTracksReturn, type UseExportWavReturn, type UseOutputMeterOptions, type UseOutputMeterReturn, type UsePlaybackShortcutsOptions, type UsePlaybackShortcutsReturn, type UseTrackDynamicEffectsReturn, Waveform, WaveformPlaylistProvider, type WaveformProps, type WaveformTrack, type ZoomControls, ZoomInButton, ZoomOutButton, createEffectChain, createEffectInstance, effectCategories, effectDefinitions, getEffectDefinition, getEffectsByCategory, getWaveformDataMetadata, loadPeaksFromWaveformData, loadWaveformData, noDropAnimationPlugins, useAnnotationDragHandlers, useAnnotationIntegration, useAnnotationKeyboardControls, useAudioTracks, useClipDragHandlers, useClipInteractionEnabled, useClipSplitting, useDragSensors, useDynamicEffects, useDynamicTracks, useExportWav, useKeyboardShortcuts, useMasterAnalyser, useMasterVolume, useMediaElementAnimation, useMediaElementControls, useMediaElementData, useMediaElementState, useOutputMeter, usePlaybackAnimation, usePlaybackShortcuts, usePlaylistControls, usePlaylistData, usePlaylistState, useSpectrogramIntegration, useTimeFormat, useTrackDynamicEffects, useZoomControls, waveformDataToPeaks };
1827
+ export { type ActiveEffect, type AnnotationIntegration, AnnotationIntegrationProvider, AudioPosition, type AudioTrackConfig, AutomaticScrollCheckbox, ClearAllButton, type ClearAllButtonProps, ClipCollisionModifier, ClipInteractionProvider, type ClipInteractionProviderProps, ContinuousPlayCheckbox, DownloadAnnotationsButton, EditableCheckbox, type EffectDefinition, type EffectInstance, type EffectParameter, type ExportOptions, type ExportResult, ExportWavButton, type ExportWavButtonProps, FastForwardButton, type FrameData, type GetAnnotationBoxLabelFn, KeyboardShortcuts, type KeyboardShortcutsProps, LinkEndpointsCheckbox, LoopButton, MasterVolumeControl, type MasterVolumeControls, type MediaElementAnimationContextValue, MediaElementAnnotationList, type MediaElementAnnotationListProps, type MediaElementControlsContextValue, type MediaElementDataContextValue, MediaElementPlaylist, type MediaElementPlaylistProps, MediaElementPlaylistProvider, type MediaElementStateContextValue, type MediaElementTrackConfig, MediaElementWaveform, type MediaElementWaveformProps, type OnAnnotationUpdateFn, type ParameterType, PauseButton, PlayButton, PlaylistAnnotationList, type PlaylistAnnotationListProps, PlaylistVisualization, type PlaylistVisualizationProps, RewindButton, SelectionTimeInputs, SetLoopRegionButton, SkipBackwardButton, SkipForwardButton, SnapToGridModifier, type SpectrogramIntegration, SpectrogramIntegrationProvider, StopButton, type TimeFormatControls, TimeFormatSelect, type TrackActiveEffect, type TrackEffectsState, type TrackLoadError, type TrackSource, type TrackState$1 as TrackState, type UseDynamicEffectsReturn, type UseDynamicTracksReturn, type UseExportWavReturn, type UseOutputMeterOptions, type UseOutputMeterReturn, type UsePlaybackShortcutsOptions, type UsePlaybackShortcutsReturn, type UseTrackDynamicEffectsReturn, Waveform, WaveformPlaylistProvider, type WaveformProps, type WaveformTrack, type ZoomControls, ZoomInButton, ZoomOutButton, createEffectChain, createEffectInstance, effectCategories, effectDefinitions, getEffectDefinition, getEffectsByCategory, getWaveformDataMetadata, loadPeaksFromWaveformData, loadWaveformData, noDropAnimationPlugins, useAnnotationDragHandlers, useAnnotationIntegration, useAnnotationKeyboardControls, useAudioTracks, useClipDragHandlers, useClipInteractionEnabled, useClipSplitting, useDragSensors, useDynamicEffects, useDynamicTracks, useExportWav, useKeyboardShortcuts, useMasterAnalyser, useMasterVolume, useMediaElementAnimation, useMediaElementControls, useMediaElementData, useMediaElementState, useOutputMeter, usePlaybackAnimation, usePlaybackShortcuts, usePlaylistControls, usePlaylistData, usePlaylistState, useSpectrogramIntegration, useTimeFormat, useTrackDynamicEffects, useZoomControls, waveformDataToPeaks };
package/dist/index.d.ts CHANGED
@@ -41,6 +41,15 @@ interface TrackState$1 {
41
41
  volume: number;
42
42
  pan: number;
43
43
  }
44
+ /** Per-frame data passed to registered animation callbacks. */
45
+ interface FrameData {
46
+ /** Raw engine time (for state/logic — NOT for visual positioning). */
47
+ readonly time: number;
48
+ /** time - outputLatency (for DOM positioning — matches speaker output). */
49
+ readonly visualTime: number;
50
+ readonly sampleRate: number;
51
+ readonly samplesPerPixel: number;
52
+ }
44
53
  interface PlaybackAnimationContextValue {
45
54
  isPlaying: boolean;
46
55
  currentTime: number;
@@ -49,6 +58,10 @@ interface PlaybackAnimationContextValue {
49
58
  audioStartPositionRef: React__default.RefObject<number>;
50
59
  /** Returns current playback time from engine (auto-wraps at loop boundaries). */
51
60
  getPlaybackTime: () => number;
61
+ /** Register a per-frame callback driven by the single animation loop. */
62
+ registerFrameCallback: (id: string, cb: (data: FrameData) => void) => void;
63
+ /** Unregister a per-frame callback. */
64
+ unregisterFrameCallback: (id: string) => void;
52
65
  }
53
66
  interface PlaylistStateContextValue {
54
67
  continuousPlay: boolean;
@@ -186,6 +199,10 @@ interface WaveformPlaylistProviderProps {
186
199
  /** Disable automatic stop when the cursor reaches the end of the longest
187
200
  * track. Useful for DAW-style recording beyond existing audio. */
188
201
  indefinitePlayback?: boolean;
202
+ /** Desired AudioContext sample rate. Creates a cross-browser AudioContext at
203
+ * this rate via standardized-audio-context. Pre-computed peaks (.dat files)
204
+ * render instantly when they match. On mismatch, falls back to worker. */
205
+ sampleRate?: number;
189
206
  children: ReactNode;
190
207
  }
191
208
  declare const WaveformPlaylistProvider: React__default.FC<WaveformPlaylistProviderProps>;
@@ -1170,7 +1187,7 @@ declare const TimeFormatSelect: React__default.FC<{
1170
1187
  }>;
1171
1188
  /**
1172
1189
  * Audio position display that uses the playlist context.
1173
- * Uses requestAnimationFrame for smooth 60fps updates during playback.
1190
+ * Updates via the shared animation frame registry — no own rAF loop.
1174
1191
  * Direct DOM manipulation avoids React re-renders.
1175
1192
  */
1176
1193
  declare const AudioPosition: React__default.FC<{
@@ -1807,4 +1824,4 @@ declare function getWaveformDataMetadata(src: string): Promise<{
1807
1824
  bits: 8 | 16;
1808
1825
  }>;
1809
1826
 
1810
- export { type ActiveEffect, type AnnotationIntegration, AnnotationIntegrationProvider, AudioPosition, type AudioTrackConfig, AutomaticScrollCheckbox, ClearAllButton, type ClearAllButtonProps, ClipCollisionModifier, ClipInteractionProvider, type ClipInteractionProviderProps, ContinuousPlayCheckbox, DownloadAnnotationsButton, EditableCheckbox, type EffectDefinition, type EffectInstance, type EffectParameter, type ExportOptions, type ExportResult, ExportWavButton, type ExportWavButtonProps, FastForwardButton, type GetAnnotationBoxLabelFn, KeyboardShortcuts, type KeyboardShortcutsProps, LinkEndpointsCheckbox, LoopButton, MasterVolumeControl, type MasterVolumeControls, type MediaElementAnimationContextValue, MediaElementAnnotationList, type MediaElementAnnotationListProps, type MediaElementControlsContextValue, type MediaElementDataContextValue, MediaElementPlaylist, type MediaElementPlaylistProps, MediaElementPlaylistProvider, type MediaElementStateContextValue, type MediaElementTrackConfig, MediaElementWaveform, type MediaElementWaveformProps, type OnAnnotationUpdateFn, type ParameterType, PauseButton, PlayButton, PlaylistAnnotationList, type PlaylistAnnotationListProps, PlaylistVisualization, type PlaylistVisualizationProps, RewindButton, SelectionTimeInputs, SetLoopRegionButton, SkipBackwardButton, SkipForwardButton, SnapToGridModifier, type SpectrogramIntegration, SpectrogramIntegrationProvider, StopButton, type TimeFormatControls, TimeFormatSelect, type TrackActiveEffect, type TrackEffectsState, type TrackLoadError, type TrackSource, type TrackState$1 as TrackState, type UseDynamicEffectsReturn, type UseDynamicTracksReturn, type UseExportWavReturn, type UseOutputMeterOptions, type UseOutputMeterReturn, type UsePlaybackShortcutsOptions, type UsePlaybackShortcutsReturn, type UseTrackDynamicEffectsReturn, Waveform, WaveformPlaylistProvider, type WaveformProps, type WaveformTrack, type ZoomControls, ZoomInButton, ZoomOutButton, createEffectChain, createEffectInstance, effectCategories, effectDefinitions, getEffectDefinition, getEffectsByCategory, getWaveformDataMetadata, loadPeaksFromWaveformData, loadWaveformData, noDropAnimationPlugins, useAnnotationDragHandlers, useAnnotationIntegration, useAnnotationKeyboardControls, useAudioTracks, useClipDragHandlers, useClipInteractionEnabled, useClipSplitting, useDragSensors, useDynamicEffects, useDynamicTracks, useExportWav, useKeyboardShortcuts, useMasterAnalyser, useMasterVolume, useMediaElementAnimation, useMediaElementControls, useMediaElementData, useMediaElementState, useOutputMeter, usePlaybackAnimation, usePlaybackShortcuts, usePlaylistControls, usePlaylistData, usePlaylistState, useSpectrogramIntegration, useTimeFormat, useTrackDynamicEffects, useZoomControls, waveformDataToPeaks };
1827
+ export { type ActiveEffect, type AnnotationIntegration, AnnotationIntegrationProvider, AudioPosition, type AudioTrackConfig, AutomaticScrollCheckbox, ClearAllButton, type ClearAllButtonProps, ClipCollisionModifier, ClipInteractionProvider, type ClipInteractionProviderProps, ContinuousPlayCheckbox, DownloadAnnotationsButton, EditableCheckbox, type EffectDefinition, type EffectInstance, type EffectParameter, type ExportOptions, type ExportResult, ExportWavButton, type ExportWavButtonProps, FastForwardButton, type FrameData, type GetAnnotationBoxLabelFn, KeyboardShortcuts, type KeyboardShortcutsProps, LinkEndpointsCheckbox, LoopButton, MasterVolumeControl, type MasterVolumeControls, type MediaElementAnimationContextValue, MediaElementAnnotationList, type MediaElementAnnotationListProps, type MediaElementControlsContextValue, type MediaElementDataContextValue, MediaElementPlaylist, type MediaElementPlaylistProps, MediaElementPlaylistProvider, type MediaElementStateContextValue, type MediaElementTrackConfig, MediaElementWaveform, type MediaElementWaveformProps, type OnAnnotationUpdateFn, type ParameterType, PauseButton, PlayButton, PlaylistAnnotationList, type PlaylistAnnotationListProps, PlaylistVisualization, type PlaylistVisualizationProps, RewindButton, SelectionTimeInputs, SetLoopRegionButton, SkipBackwardButton, SkipForwardButton, SnapToGridModifier, type SpectrogramIntegration, SpectrogramIntegrationProvider, StopButton, type TimeFormatControls, TimeFormatSelect, type TrackActiveEffect, type TrackEffectsState, type TrackLoadError, type TrackSource, type TrackState$1 as TrackState, type UseDynamicEffectsReturn, type UseDynamicTracksReturn, type UseExportWavReturn, type UseOutputMeterOptions, type UseOutputMeterReturn, type UsePlaybackShortcutsOptions, type UsePlaybackShortcutsReturn, type UseTrackDynamicEffectsReturn, Waveform, WaveformPlaylistProvider, type WaveformProps, type WaveformTrack, type ZoomControls, ZoomInButton, ZoomOutButton, createEffectChain, createEffectInstance, effectCategories, effectDefinitions, getEffectDefinition, getEffectsByCategory, getWaveformDataMetadata, loadPeaksFromWaveformData, loadWaveformData, noDropAnimationPlugins, useAnnotationDragHandlers, useAnnotationIntegration, useAnnotationKeyboardControls, useAudioTracks, useClipDragHandlers, useClipInteractionEnabled, useClipSplitting, useDragSensors, useDynamicEffects, useDynamicTracks, useExportWav, useKeyboardShortcuts, useMasterAnalyser, useMasterVolume, useMediaElementAnimation, useMediaElementControls, useMediaElementData, useMediaElementState, useOutputMeter, usePlaybackAnimation, usePlaybackShortcuts, usePlaylistControls, usePlaylistData, usePlaylistState, useSpectrogramIntegration, useTimeFormat, useTrackDynamicEffects, useZoomControls, waveformDataToPeaks };
package/dist/index.js CHANGED
@@ -172,11 +172,13 @@ var import_tone4 = require("tone");
172
172
  var import_waveform_data = __toESM(require("waveform-data"));
173
173
  function loadWaveformData(src) {
174
174
  return __async(this, null, function* () {
175
+ var _a, _b;
175
176
  const response = yield fetch(src);
176
177
  if (!response.ok) {
177
178
  throw new Error(`Failed to fetch waveform data: ${response.statusText}`);
178
179
  }
179
- const isBinary = src.endsWith(".dat");
180
+ const { pathname } = new URL(src, (_b = (_a = globalThis.location) == null ? void 0 : _a.href) != null ? _b : "http://localhost");
181
+ const isBinary = pathname.toLowerCase().endsWith(".dat");
180
182
  if (isBinary) {
181
183
  const arrayBuffer = yield response.arrayBuffer();
182
184
  return import_waveform_data.default.create(arrayBuffer);
@@ -3764,6 +3766,7 @@ var WaveformPlaylistProvider = ({
3764
3766
  soundFontCache,
3765
3767
  deferEngineRebuild = false,
3766
3768
  indefinitePlayback = false,
3769
+ sampleRate: sampleRateProp,
3767
3770
  children
3768
3771
  }) => {
3769
3772
  var _a, _b, _c, _d;
@@ -3819,6 +3822,7 @@ var WaveformPlaylistProvider = ({
3819
3822
  const playbackEndTimeRef = (0, import_react24.useRef)(null);
3820
3823
  const scrollContainerRef = (0, import_react24.useRef)(null);
3821
3824
  const isAutomaticScrollRef = (0, import_react24.useRef)(false);
3825
+ const frameCallbacksRef = (0, import_react24.useRef)(/* @__PURE__ */ new Map());
3822
3826
  const continuousPlayRef = (0, import_react24.useRef)((_d = annotationList == null ? void 0 : annotationList.isContinuousPlay) != null ? _d : false);
3823
3827
  const activeAnnotationIdRef = (0, import_react24.useRef)(null);
3824
3828
  const engineTracksRef = (0, import_react24.useRef)(null);
@@ -3827,9 +3831,21 @@ var WaveformPlaylistProvider = ({
3827
3831
  const isDraggingRef = (0, import_react24.useRef)(false);
3828
3832
  const prevTracksRef = (0, import_react24.useRef)([]);
3829
3833
  const samplesPerPixelRef = (0, import_react24.useRef)(initialSamplesPerPixel);
3830
- const sampleRateRef = (0, import_react24.useRef)(
3831
- typeof AudioContext !== "undefined" ? (0, import_playout5.getGlobalAudioContext)().sampleRate : 48e3
3832
- );
3834
+ const [initialSampleRate] = (0, import_react24.useState)(() => {
3835
+ if (typeof AudioContext === "undefined") return sampleRateProp != null ? sampleRateProp : 48e3;
3836
+ try {
3837
+ if (sampleRateProp !== void 0) {
3838
+ return (0, import_playout5.configureGlobalContext)({ sampleRate: sampleRateProp });
3839
+ }
3840
+ return (0, import_playout5.getGlobalAudioContext)().sampleRate;
3841
+ } catch (err) {
3842
+ console.warn(
3843
+ "[waveform-playlist] Failed to configure AudioContext: " + String(err) + " \u2014 falling back to " + (sampleRateProp != null ? sampleRateProp : 48e3) + " Hz"
3844
+ );
3845
+ return sampleRateProp != null ? sampleRateProp : 48e3;
3846
+ }
3847
+ });
3848
+ const sampleRateRef = (0, import_react24.useRef)(initialSampleRate);
3833
3849
  const { timeFormat, setTimeFormat, formatTime: formatTime2 } = useTimeFormat();
3834
3850
  const zoom = useZoomControls({ engineRef, initialSamplesPerPixel });
3835
3851
  const { samplesPerPixel, onEngineState: onZoomEngineState } = zoom;
@@ -4179,15 +4195,28 @@ var WaveformPlaylistProvider = ({
4179
4195
  let peaks;
4180
4196
  if (clip.waveformData) {
4181
4197
  try {
4198
+ const wdRate = clip.waveformData.sample_rate;
4199
+ const clipRate = clip.sampleRate;
4200
+ let peakOffset = clip.offsetSamples;
4201
+ let peakDuration = clip.durationSamples;
4202
+ let peakSpp = samplesPerPixel;
4203
+ if (wdRate !== clipRate && clipRate > 0 && wdRate > 0) {
4204
+ const ratio = wdRate / clipRate;
4205
+ peakOffset = Math.round(clip.offsetSamples * ratio);
4206
+ peakDuration = Math.round(clip.durationSamples * ratio);
4207
+ peakSpp = Math.max(1, Math.round(samplesPerPixel * ratio));
4208
+ }
4182
4209
  peaks = extractPeaksFromWaveformDataFull(
4183
4210
  clip.waveformData,
4184
- samplesPerPixel,
4211
+ peakSpp,
4185
4212
  mono,
4186
- clip.offsetSamples,
4187
- clip.durationSamples
4213
+ peakOffset,
4214
+ peakDuration
4188
4215
  );
4189
4216
  } catch (err) {
4190
- console.warn("[waveform-playlist] Failed to extract peaks from waveformData:", err);
4217
+ console.warn(
4218
+ "[waveform-playlist] Failed to extract peaks from waveformData: " + String(err)
4219
+ );
4191
4220
  }
4192
4221
  }
4193
4222
  if (!peaks) {
@@ -4202,7 +4231,9 @@ var WaveformPlaylistProvider = ({
4202
4231
  clip.durationSamples
4203
4232
  );
4204
4233
  } catch (err) {
4205
- console.warn("[waveform-playlist] Failed to extract peaks from cache:", err);
4234
+ console.warn(
4235
+ "[waveform-playlist] Failed to extract peaks from cache: " + String(err)
4236
+ );
4206
4237
  }
4207
4238
  }
4208
4239
  }
@@ -4252,10 +4283,30 @@ var WaveformPlaylistProvider = ({
4252
4283
  const elapsed = (0, import_tone4.getContext)().currentTime - ((_a2 = playbackStartTimeRef.current) != null ? _a2 : 0);
4253
4284
  return ((_b2 = audioStartPositionRef.current) != null ? _b2 : 0) + elapsed;
4254
4285
  }, []);
4286
+ const registerFrameCallback = (0, import_react24.useCallback)((id, cb) => {
4287
+ frameCallbacksRef.current.set(id, cb);
4288
+ }, []);
4289
+ const unregisterFrameCallback = (0, import_react24.useCallback)((id) => {
4290
+ frameCallbacksRef.current.delete(id);
4291
+ }, []);
4255
4292
  const startAnimationLoop = (0, import_react24.useCallback)(() => {
4293
+ const audioCtx = (0, import_playout5.getGlobalAudioContext)();
4256
4294
  const updateTime = () => {
4257
4295
  const time = getPlaybackTime();
4258
4296
  currentTimeRef.current = time;
4297
+ const latency = "outputLatency" in audioCtx ? audioCtx.outputLatency : 0;
4298
+ const visualTime = Math.max(0, time - latency);
4299
+ const sr = sampleRateRef.current;
4300
+ const spp = samplesPerPixelRef.current;
4301
+ const frameData = {
4302
+ time,
4303
+ visualTime,
4304
+ sampleRate: sr,
4305
+ samplesPerPixel: spp
4306
+ };
4307
+ for (const cb of frameCallbacksRef.current.values()) {
4308
+ cb(frameData);
4309
+ }
4259
4310
  const currentAnnotations = annotationsRef.current;
4260
4311
  if (currentAnnotations.length > 0) {
4261
4312
  const currentAnnotation = currentAnnotations.find(
@@ -4290,10 +4341,9 @@ var WaveformPlaylistProvider = ({
4290
4341
  }
4291
4342
  if (isAutomaticScrollRef.current && scrollContainerRef.current && duration > 0) {
4292
4343
  const container = scrollContainerRef.current;
4293
- const sr = sampleRateRef.current;
4294
- const pixelPosition = time * sr / samplesPerPixelRef.current;
4344
+ const pixelPosition = visualTime * sr / spp;
4295
4345
  const containerWidth = container.clientWidth;
4296
- const targetScrollLeft = Math.max(0, pixelPosition - containerWidth / 2);
4346
+ const targetScrollLeft = Math.round(Math.max(0, pixelPosition - containerWidth / 2));
4297
4347
  container.scrollLeft = targetScrollLeft;
4298
4348
  }
4299
4349
  if (playbackEndTimeRef.current !== null && time >= playbackEndTimeRef.current) {
@@ -4529,7 +4579,9 @@ var WaveformPlaylistProvider = ({
4529
4579
  currentTimeRef,
4530
4580
  playbackStartTimeRef,
4531
4581
  audioStartPositionRef,
4532
- getPlaybackTime
4582
+ getPlaybackTime,
4583
+ registerFrameCallback,
4584
+ unregisterFrameCallback
4533
4585
  }),
4534
4586
  [
4535
4587
  isPlaying,
@@ -4537,7 +4589,9 @@ var WaveformPlaylistProvider = ({
4537
4589
  currentTimeRef,
4538
4590
  playbackStartTimeRef,
4539
4591
  audioStartPositionRef,
4540
- getPlaybackTime
4592
+ getPlaybackTime,
4593
+ registerFrameCallback,
4594
+ unregisterFrameCallback
4541
4595
  ]
4542
4596
  );
4543
4597
  const stateValue = (0, import_react24.useMemo)(
@@ -5318,32 +5372,19 @@ var PositionDisplay = import_styled_components3.default.span`
5318
5372
  var AudioPosition = ({ className }) => {
5319
5373
  var _a;
5320
5374
  const timeRef = (0, import_react27.useRef)(null);
5321
- const animationFrameRef = (0, import_react27.useRef)(null);
5322
- const { isPlaying, currentTimeRef, getPlaybackTime } = usePlaybackAnimation();
5375
+ const { isPlaying, currentTimeRef, registerFrameCallback, unregisterFrameCallback } = usePlaybackAnimation();
5323
5376
  const { timeFormat: format } = usePlaylistData();
5324
5377
  (0, import_react27.useEffect)(() => {
5325
- const updateTime = () => {
5326
- var _a2;
5327
- if (timeRef.current) {
5328
- const time = isPlaying ? getPlaybackTime() : (_a2 = currentTimeRef.current) != null ? _a2 : 0;
5329
- timeRef.current.textContent = (0, import_ui_components6.formatTime)(time, format);
5330
- }
5331
- if (isPlaying) {
5332
- animationFrameRef.current = requestAnimationFrame(updateTime);
5333
- }
5334
- };
5378
+ const id = "audio-position";
5335
5379
  if (isPlaying) {
5336
- animationFrameRef.current = requestAnimationFrame(updateTime);
5337
- } else {
5338
- updateTime();
5380
+ registerFrameCallback(id, ({ time }) => {
5381
+ if (timeRef.current) {
5382
+ timeRef.current.textContent = (0, import_ui_components6.formatTime)(time, format);
5383
+ }
5384
+ });
5339
5385
  }
5340
- return () => {
5341
- if (animationFrameRef.current) {
5342
- cancelAnimationFrame(animationFrameRef.current);
5343
- animationFrameRef.current = null;
5344
- }
5345
- };
5346
- }, [isPlaying, format, currentTimeRef, getPlaybackTime]);
5386
+ return () => unregisterFrameCallback(id);
5387
+ }, [isPlaying, format, registerFrameCallback, unregisterFrameCallback]);
5347
5388
  (0, import_react27.useEffect)(() => {
5348
5389
  var _a2;
5349
5390
  if (!isPlaying && timeRef.current) {
@@ -5503,33 +5544,20 @@ var PlayheadLine = import_styled_components4.default.div.attrs((props) => ({
5503
5544
  `;
5504
5545
  var AnimatedPlayhead = ({ color = "#ff0000" }) => {
5505
5546
  const playheadRef = (0, import_react30.useRef)(null);
5506
- const animationFrameRef = (0, import_react30.useRef)(null);
5507
- const { isPlaying, currentTimeRef, getPlaybackTime } = usePlaybackAnimation();
5547
+ const { isPlaying, currentTimeRef, registerFrameCallback, unregisterFrameCallback } = usePlaybackAnimation();
5508
5548
  const { samplesPerPixel, sampleRate, progressBarWidth } = usePlaylistData();
5509
5549
  (0, import_react30.useEffect)(() => {
5510
- const updatePosition = () => {
5511
- var _a;
5512
- if (playheadRef.current) {
5513
- const time = isPlaying ? getPlaybackTime() : (_a = currentTimeRef.current) != null ? _a : 0;
5514
- const position = time * sampleRate / samplesPerPixel;
5515
- playheadRef.current.style.transform = `translate3d(${position}px, 0, 0)`;
5516
- }
5517
- if (isPlaying) {
5518
- animationFrameRef.current = requestAnimationFrame(updatePosition);
5519
- }
5520
- };
5550
+ const id = "playhead";
5521
5551
  if (isPlaying) {
5522
- animationFrameRef.current = requestAnimationFrame(updatePosition);
5523
- } else {
5524
- updatePosition();
5552
+ registerFrameCallback(id, ({ visualTime, sampleRate: sr, samplesPerPixel: spp }) => {
5553
+ if (playheadRef.current) {
5554
+ const px = visualTime * sr / spp;
5555
+ playheadRef.current.style.transform = `translate3d(${px}px, 0, 0)`;
5556
+ }
5557
+ });
5525
5558
  }
5526
- return () => {
5527
- if (animationFrameRef.current) {
5528
- cancelAnimationFrame(animationFrameRef.current);
5529
- animationFrameRef.current = null;
5530
- }
5531
- };
5532
- }, [isPlaying, sampleRate, samplesPerPixel, currentTimeRef, getPlaybackTime]);
5559
+ return () => unregisterFrameCallback(id);
5560
+ }, [isPlaying, registerFrameCallback, unregisterFrameCallback]);
5533
5561
  (0, import_react30.useEffect)(() => {
5534
5562
  var _a;
5535
5563
  if (!isPlaying && playheadRef.current) {
@@ -5601,10 +5629,10 @@ var ChannelWithProgress = (_a) => {
5601
5629
  "clipOffsetSeconds"
5602
5630
  ]);
5603
5631
  const progressRef = (0, import_react31.useRef)(null);
5604
- const animationFrameRef = (0, import_react31.useRef)(null);
5632
+ const callbackId = (0, import_react31.useId)();
5605
5633
  const theme = (0, import_ui_components8.useTheme)();
5606
5634
  const { waveHeight } = (0, import_ui_components8.usePlaylistInfo)();
5607
- const { isPlaying, currentTimeRef, getPlaybackTime } = usePlaybackAnimation();
5635
+ const { isPlaying, currentTimeRef, registerFrameCallback, unregisterFrameCallback } = usePlaybackAnimation();
5608
5636
  const { samplesPerPixel, sampleRate } = usePlaylistData();
5609
5637
  const progressColor = (theme == null ? void 0 : theme.waveProgressColor) || "rgba(0, 0, 0, 0.1)";
5610
5638
  const clipPixelWidth = (0, import_core6.clipPixelWidth)(
@@ -5613,46 +5641,32 @@ var ChannelWithProgress = (_a) => {
5613
5641
  samplesPerPixel
5614
5642
  );
5615
5643
  (0, import_react31.useEffect)(() => {
5616
- const updateProgress = () => {
5617
- var _a2;
5618
- if (progressRef.current) {
5619
- const currentTime = isPlaying ? getPlaybackTime() : (_a2 = currentTimeRef.current) != null ? _a2 : 0;
5620
- const currentSample = currentTime * sampleRate;
5621
- const clipEndSample = clipStartSample + clipDurationSamples;
5622
- let ratio = 0;
5623
- if (currentSample <= clipStartSample) {
5624
- ratio = 0;
5625
- } else if (currentSample >= clipEndSample) {
5626
- ratio = 1;
5627
- } else {
5628
- const playedSamples = currentSample - clipStartSample;
5629
- ratio = playedSamples / clipDurationSamples;
5630
- }
5631
- progressRef.current.style.transform = `scaleX(${ratio})`;
5632
- }
5633
- if (isPlaying) {
5634
- animationFrameRef.current = requestAnimationFrame(updateProgress);
5635
- }
5636
- };
5637
5644
  if (isPlaying) {
5638
- animationFrameRef.current = requestAnimationFrame(updateProgress);
5639
- } else {
5640
- updateProgress();
5645
+ registerFrameCallback(callbackId, ({ visualTime, sampleRate: sr }) => {
5646
+ if (progressRef.current) {
5647
+ const currentSample = visualTime * sr;
5648
+ const clipEndSample = clipStartSample + clipDurationSamples;
5649
+ let ratio = 0;
5650
+ if (currentSample <= clipStartSample) {
5651
+ ratio = 0;
5652
+ } else if (currentSample >= clipEndSample) {
5653
+ ratio = 1;
5654
+ } else {
5655
+ const playedSamples = currentSample - clipStartSample;
5656
+ ratio = playedSamples / clipDurationSamples;
5657
+ }
5658
+ progressRef.current.style.transform = `scaleX(${ratio})`;
5659
+ }
5660
+ });
5641
5661
  }
5642
- return () => {
5643
- if (animationFrameRef.current) {
5644
- cancelAnimationFrame(animationFrameRef.current);
5645
- animationFrameRef.current = null;
5646
- }
5647
- };
5662
+ return () => unregisterFrameCallback(callbackId);
5648
5663
  }, [
5649
5664
  isPlaying,
5650
- sampleRate,
5651
5665
  clipStartSample,
5652
5666
  clipDurationSamples,
5653
- clipPixelWidth,
5654
- currentTimeRef,
5655
- getPlaybackTime
5667
+ callbackId,
5668
+ registerFrameCallback,
5669
+ unregisterFrameCallback
5656
5670
  ]);
5657
5671
  (0, import_react31.useEffect)(() => {
5658
5672
  var _a2;