@waveform-playlist/browser 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.mjs CHANGED
@@ -2675,6 +2675,12 @@ function useTrackDynamicEffects() {
2675
2675
 
2676
2676
  // src/hooks/useExportWav.ts
2677
2677
  import { useState as useState11, useCallback as useCallback14 } from "react";
2678
+ import {
2679
+ gainToDb,
2680
+ trackChannelCount,
2681
+ applyFadeIn,
2682
+ applyFadeOut
2683
+ } from "@waveform-playlist/core";
2678
2684
  import {
2679
2685
  getUnderlyingAudioParam,
2680
2686
  getGlobalAudioContext as getGlobalAudioContext2
@@ -2788,47 +2794,24 @@ function useExportWav() {
2788
2794
  totalDurationSamples += Math.round(sampleRate * 0.1);
2789
2795
  const duration = totalDurationSamples / sampleRate;
2790
2796
  const tracksToRender = mode === "individual" ? [{ track: tracks[trackIndex], state: trackStates[trackIndex], index: trackIndex }] : tracks.map((track, index) => ({ track, state: trackStates[index], index }));
2791
- const hasSolo = trackStates.some((state) => state.soloed);
2792
- const hasOfflineTrackEffects = !!createOfflineTrackEffects;
2793
- let renderedBuffer;
2794
- if ((effectsFunction || hasOfflineTrackEffects) && applyEffects) {
2795
- renderedBuffer = yield renderWithToneEffects(
2796
- tracksToRender,
2797
- trackStates,
2798
- hasSolo,
2799
- duration,
2800
- sampleRate,
2801
- effectsFunction,
2802
- createOfflineTrackEffects,
2803
- (p) => {
2804
- setProgress(p);
2805
- onProgress == null ? void 0 : onProgress(p);
2806
- }
2807
- );
2808
- } else {
2809
- const offlineCtx = new OfflineAudioContext(2, totalDurationSamples, sampleRate);
2810
- let scheduledClips = 0;
2811
- const totalClips = tracksToRender.reduce((sum, { track }) => sum + track.clips.length, 0);
2812
- for (const { track, state } of tracksToRender) {
2813
- if (state.muted && !state.soloed) continue;
2814
- if (hasSolo && !state.soloed) continue;
2815
- for (const clip of track.clips) {
2816
- yield scheduleClip(offlineCtx, clip, state, sampleRate, applyEffects);
2817
- scheduledClips++;
2818
- const currentProgress = scheduledClips / totalClips * 0.5;
2819
- setProgress(currentProgress);
2820
- onProgress == null ? void 0 : onProgress(currentProgress);
2821
- }
2822
- }
2823
- setProgress(0.5);
2824
- onProgress == null ? void 0 : onProgress(0.5);
2825
- renderedBuffer = yield offlineCtx.startRendering();
2826
- }
2827
- setProgress(0.9);
2828
- onProgress == null ? void 0 : onProgress(0.9);
2797
+ const hasSolo = mode === "master" && trackStates.some((state) => state.soloed);
2798
+ const reportProgress = (p) => {
2799
+ setProgress(p);
2800
+ onProgress == null ? void 0 : onProgress(p);
2801
+ };
2802
+ const renderedBuffer = yield renderOffline(
2803
+ tracksToRender,
2804
+ hasSolo,
2805
+ duration,
2806
+ sampleRate,
2807
+ applyEffects,
2808
+ effectsFunction,
2809
+ createOfflineTrackEffects,
2810
+ reportProgress
2811
+ );
2812
+ reportProgress(0.9);
2829
2813
  const blob = encodeWav(renderedBuffer, { bitDepth });
2830
- setProgress(1);
2831
- onProgress == null ? void 0 : onProgress(1);
2814
+ reportProgress(1);
2832
2815
  if (autoDownload) {
2833
2816
  const exportFilename = mode === "individual" ? `${filename}_${tracks[trackIndex].name}` : filename;
2834
2817
  downloadBlob(blob, `${exportFilename}.wav`);
@@ -2855,29 +2838,35 @@ function useExportWav() {
2855
2838
  error
2856
2839
  };
2857
2840
  }
2858
- function renderWithToneEffects(tracksToRender, _trackStates, hasSolo, duration, sampleRate, effectsFunction, createOfflineTrackEffects, onProgress) {
2841
+ function renderOffline(tracksToRender, hasSolo, duration, sampleRate, applyEffects, effectsFunction, createOfflineTrackEffects, onProgress) {
2859
2842
  return __async(this, null, function* () {
2860
2843
  const { Offline, Volume: Volume2, Gain, Panner, Player, ToneAudioBuffer } = yield import("tone");
2861
2844
  onProgress(0.1);
2845
+ const audibleTracks = tracksToRender.filter(({ state }) => {
2846
+ if (state.muted && !state.soloed) return false;
2847
+ if (hasSolo && !state.soloed) return false;
2848
+ return true;
2849
+ });
2850
+ const outputChannels = audibleTracks.reduce(
2851
+ (max, { track }) => Math.max(max, trackChannelCount(track)),
2852
+ 1
2853
+ );
2862
2854
  let buffer;
2863
2855
  try {
2864
2856
  buffer = yield Offline(
2865
2857
  (_0) => __async(null, [_0], function* ({ transport, destination }) {
2866
2858
  const masterVolume = new Volume2(0);
2867
- let cleanup = void 0;
2868
- if (effectsFunction) {
2869
- cleanup = effectsFunction(masterVolume, destination, true);
2859
+ if (effectsFunction && applyEffects) {
2860
+ effectsFunction(masterVolume, destination, true);
2870
2861
  } else {
2871
2862
  masterVolume.connect(destination);
2872
2863
  }
2873
- for (const { track, state } of tracksToRender) {
2874
- if (state.muted && !state.soloed) continue;
2875
- if (hasSolo && !state.soloed) continue;
2864
+ for (const { track, state } of audibleTracks) {
2876
2865
  const trackVolume = new Volume2(gainToDb(state.volume));
2877
- const trackPan = new Panner(state.pan);
2866
+ const trackPan = new Panner({ pan: state.pan, channelCount: trackChannelCount(track) });
2878
2867
  const trackMute = new Gain(state.muted ? 0 : 1);
2879
2868
  const trackEffects = createOfflineTrackEffects == null ? void 0 : createOfflineTrackEffects(track.id);
2880
- if (trackEffects) {
2869
+ if (trackEffects && applyEffects) {
2881
2870
  trackEffects(trackMute, masterVolume, true);
2882
2871
  } else {
2883
2872
  trackMute.connect(masterVolume);
@@ -2894,6 +2883,12 @@ function renderWithToneEffects(tracksToRender, _trackStates, hasSolo, duration,
2894
2883
  fadeIn,
2895
2884
  fadeOut
2896
2885
  } = clip;
2886
+ if (!audioBuffer) {
2887
+ console.warn(
2888
+ '[waveform-playlist] Skipping clip "' + (clip.name || clip.id) + '" - no audioBuffer for export'
2889
+ );
2890
+ continue;
2891
+ }
2897
2892
  const startTime = startSample / sampleRate;
2898
2893
  const clipDuration = durationSamples / sampleRate;
2899
2894
  const offset = offsetSamples / sampleRate;
@@ -2902,167 +2897,56 @@ function renderWithToneEffects(tracksToRender, _trackStates, hasSolo, duration,
2902
2897
  const fadeGain = new Gain(clipGain);
2903
2898
  player.connect(fadeGain);
2904
2899
  fadeGain.connect(trackVolume);
2905
- if (fadeIn) {
2906
- const fadeInStart = startTime;
2907
- const fadeInEnd = startTime + fadeIn.duration;
2908
- const audioParam = getUnderlyingAudioParam(fadeGain.gain);
2909
- if (audioParam) {
2910
- audioParam.setValueAtTime(0, fadeInStart);
2911
- audioParam.linearRampToValueAtTime(clipGain, fadeInEnd);
2912
- }
2913
- }
2914
- if (fadeOut) {
2915
- const fadeOutStart = startTime + clipDuration - fadeOut.duration;
2916
- const fadeOutEnd = startTime + clipDuration;
2900
+ if (applyEffects) {
2917
2901
  const audioParam = getUnderlyingAudioParam(fadeGain.gain);
2918
2902
  if (audioParam) {
2919
- audioParam.setValueAtTime(clipGain, fadeOutStart);
2920
- audioParam.linearRampToValueAtTime(0, fadeOutEnd);
2903
+ applyClipFades(audioParam, clipGain, startTime, clipDuration, fadeIn, fadeOut);
2904
+ } else if (fadeIn || fadeOut) {
2905
+ console.warn(
2906
+ '[waveform-playlist] Cannot apply fades for clip "' + (clip.name || clip.id) + '" - AudioParam not accessible'
2907
+ );
2921
2908
  }
2922
2909
  }
2923
2910
  player.start(startTime, offset, clipDuration);
2924
2911
  }
2925
2912
  }
2926
2913
  transport.start(0);
2927
- if (cleanup) {
2928
- }
2929
2914
  }),
2930
2915
  duration,
2931
- 2,
2932
- // stereo
2916
+ outputChannels,
2933
2917
  sampleRate
2934
2918
  );
2935
2919
  } catch (err) {
2936
2920
  if (err instanceof Error) {
2937
2921
  throw err;
2938
2922
  } else {
2939
- throw new Error(`Tone.Offline rendering failed: ${String(err)}`);
2923
+ throw new Error("Tone.Offline rendering failed: " + String(err));
2940
2924
  }
2941
2925
  }
2942
2926
  onProgress(0.9);
2943
- return buffer.get();
2944
- });
2945
- }
2946
- function gainToDb(gain) {
2947
- return 20 * Math.log10(Math.max(gain, 1e-4));
2948
- }
2949
- function scheduleClip(ctx, clip, trackState, sampleRate, applyEffects) {
2950
- return __async(this, null, function* () {
2951
- const {
2952
- audioBuffer,
2953
- startSample,
2954
- durationSamples,
2955
- offsetSamples,
2956
- gain: clipGain,
2957
- fadeIn,
2958
- fadeOut
2959
- } = clip;
2960
- if (!audioBuffer) {
2961
- console.warn(`Skipping clip "${clip.name || clip.id}" - no audioBuffer for export`);
2962
- return;
2927
+ const result = buffer.get();
2928
+ if (!result) {
2929
+ throw new Error("Offline rendering produced no audio buffer");
2963
2930
  }
2964
- const startTime = startSample / sampleRate;
2965
- const duration = durationSamples / sampleRate;
2966
- const offset = offsetSamples / sampleRate;
2967
- const source = ctx.createBufferSource();
2968
- source.buffer = audioBuffer;
2969
- const gainNode = ctx.createGain();
2970
- const baseGain = clipGain * trackState.volume;
2971
- const pannerNode = ctx.createStereoPanner();
2972
- pannerNode.pan.value = trackState.pan;
2973
- source.connect(gainNode);
2974
- gainNode.connect(pannerNode);
2975
- pannerNode.connect(ctx.destination);
2976
- if (applyEffects) {
2977
- if (fadeIn) {
2978
- gainNode.gain.setValueAtTime(0, startTime);
2979
- } else {
2980
- gainNode.gain.setValueAtTime(baseGain, startTime);
2981
- }
2982
- if (fadeIn) {
2983
- const fadeInStart = startTime;
2984
- const fadeInEnd = startTime + fadeIn.duration;
2985
- applyFadeEnvelope(
2986
- gainNode.gain,
2987
- fadeInStart,
2988
- fadeInEnd,
2989
- 0,
2990
- baseGain,
2991
- fadeIn.type || "linear"
2992
- );
2993
- }
2994
- if (fadeOut) {
2995
- const fadeOutStart = startTime + duration - fadeOut.duration;
2996
- const fadeOutEnd = startTime + duration;
2997
- if (!fadeIn || fadeIn.duration < duration - fadeOut.duration) {
2998
- gainNode.gain.setValueAtTime(baseGain, fadeOutStart);
2999
- }
3000
- applyFadeEnvelope(
3001
- gainNode.gain,
3002
- fadeOutStart,
3003
- fadeOutEnd,
3004
- baseGain,
3005
- 0,
3006
- fadeOut.type || "linear"
3007
- );
3008
- }
3009
- } else {
3010
- gainNode.gain.setValueAtTime(baseGain, startTime);
3011
- }
3012
- source.start(startTime, offset, duration);
2931
+ return result;
3013
2932
  });
3014
2933
  }
3015
- function applyFadeEnvelope(gainParam, startTime, endTime, startValue, endValue, fadeType) {
3016
- const duration = endTime - startTime;
3017
- if (duration <= 0) return;
3018
- switch (fadeType) {
3019
- case "linear":
3020
- gainParam.setValueAtTime(startValue, startTime);
3021
- gainParam.linearRampToValueAtTime(endValue, endTime);
3022
- break;
3023
- case "exponential": {
3024
- const expStart = Math.max(startValue, 1e-4);
3025
- const expEnd = Math.max(endValue, 1e-4);
3026
- gainParam.setValueAtTime(expStart, startTime);
3027
- gainParam.exponentialRampToValueAtTime(expEnd, endTime);
3028
- if (endValue === 0) {
3029
- gainParam.setValueAtTime(0, endTime);
3030
- }
3031
- break;
3032
- }
3033
- case "logarithmic": {
3034
- const logCurve = generateFadeCurve(startValue, endValue, 256, "logarithmic");
3035
- gainParam.setValueCurveAtTime(logCurve, startTime, duration);
3036
- break;
3037
- }
3038
- case "sCurve": {
3039
- const sCurve = generateFadeCurve(startValue, endValue, 256, "sCurve");
3040
- gainParam.setValueCurveAtTime(sCurve, startTime, duration);
3041
- break;
3042
- }
3043
- default:
3044
- gainParam.setValueAtTime(startValue, startTime);
3045
- gainParam.linearRampToValueAtTime(endValue, endTime);
2934
+ function applyClipFades(gainParam, clipGain, startTime, clipDuration, fadeIn, fadeOut) {
2935
+ if (fadeIn) {
2936
+ gainParam.setValueAtTime(0, startTime);
2937
+ } else {
2938
+ gainParam.setValueAtTime(clipGain, startTime);
3046
2939
  }
3047
- }
3048
- function generateFadeCurve(startValue, endValue, numPoints, curveType) {
3049
- const curve = new Float32Array(numPoints);
3050
- const range = endValue - startValue;
3051
- for (let i = 0; i < numPoints; i++) {
3052
- const t = i / (numPoints - 1);
3053
- let curveValue;
3054
- if (curveType === "logarithmic") {
3055
- if (range > 0) {
3056
- curveValue = Math.log10(1 + t * 9) / Math.log10(10);
3057
- } else {
3058
- curveValue = 1 - Math.log10(1 + (1 - t) * 9) / Math.log10(10);
3059
- }
3060
- } else {
3061
- curveValue = t * t * (3 - 2 * t);
2940
+ if (fadeIn) {
2941
+ applyFadeIn(gainParam, startTime, fadeIn.duration, fadeIn.type || "linear", 0, clipGain);
2942
+ }
2943
+ if (fadeOut) {
2944
+ const fadeOutStart = startTime + clipDuration - fadeOut.duration;
2945
+ if (!fadeIn || fadeIn.duration < clipDuration - fadeOut.duration) {
2946
+ gainParam.setValueAtTime(clipGain, fadeOutStart);
3062
2947
  }
3063
- curve[i] = startValue + range * curveValue;
2948
+ applyFadeOut(gainParam, fadeOutStart, fadeOut.duration, fadeOut.type || "linear", clipGain, 0);
3064
2949
  }
3065
- return curve;
3066
2950
  }
3067
2951
 
3068
2952
  // src/hooks/useAnimationFrameLoop.ts
@@ -3759,6 +3643,7 @@ var WaveformPlaylistProvider = ({
3759
3643
  const playbackEndTimeRef = useRef15(null);
3760
3644
  const scrollContainerRef = useRef15(null);
3761
3645
  const isAutomaticScrollRef = useRef15(false);
3646
+ const frameCallbacksRef = useRef15(/* @__PURE__ */ new Map());
3762
3647
  const continuousPlayRef = useRef15((_d = annotationList == null ? void 0 : annotationList.isContinuousPlay) != null ? _d : false);
3763
3648
  const activeAnnotationIdRef = useRef15(null);
3764
3649
  const engineTracksRef = useRef15(null);
@@ -4219,10 +4104,30 @@ var WaveformPlaylistProvider = ({
4219
4104
  const elapsed = getContext2().currentTime - ((_a2 = playbackStartTimeRef.current) != null ? _a2 : 0);
4220
4105
  return ((_b2 = audioStartPositionRef.current) != null ? _b2 : 0) + elapsed;
4221
4106
  }, []);
4107
+ const registerFrameCallback = useCallback19((id, cb) => {
4108
+ frameCallbacksRef.current.set(id, cb);
4109
+ }, []);
4110
+ const unregisterFrameCallback = useCallback19((id) => {
4111
+ frameCallbacksRef.current.delete(id);
4112
+ }, []);
4222
4113
  const startAnimationLoop = useCallback19(() => {
4114
+ const audioCtx = getGlobalAudioContext4();
4223
4115
  const updateTime = () => {
4224
4116
  const time = getPlaybackTime();
4225
4117
  currentTimeRef.current = time;
4118
+ const latency = "outputLatency" in audioCtx ? audioCtx.outputLatency : 0;
4119
+ const visualTime = Math.max(0, time - latency);
4120
+ const sr = sampleRateRef.current;
4121
+ const spp = samplesPerPixelRef.current;
4122
+ const frameData = {
4123
+ time,
4124
+ visualTime,
4125
+ sampleRate: sr,
4126
+ samplesPerPixel: spp
4127
+ };
4128
+ for (const cb of frameCallbacksRef.current.values()) {
4129
+ cb(frameData);
4130
+ }
4226
4131
  const currentAnnotations = annotationsRef.current;
4227
4132
  if (currentAnnotations.length > 0) {
4228
4133
  const currentAnnotation = currentAnnotations.find(
@@ -4257,10 +4162,9 @@ var WaveformPlaylistProvider = ({
4257
4162
  }
4258
4163
  if (isAutomaticScrollRef.current && scrollContainerRef.current && duration > 0) {
4259
4164
  const container = scrollContainerRef.current;
4260
- const sr = sampleRateRef.current;
4261
- const pixelPosition = time * sr / samplesPerPixelRef.current;
4165
+ const pixelPosition = visualTime * sr / spp;
4262
4166
  const containerWidth = container.clientWidth;
4263
- const targetScrollLeft = Math.max(0, pixelPosition - containerWidth / 2);
4167
+ const targetScrollLeft = Math.round(Math.max(0, pixelPosition - containerWidth / 2));
4264
4168
  container.scrollLeft = targetScrollLeft;
4265
4169
  }
4266
4170
  if (playbackEndTimeRef.current !== null && time >= playbackEndTimeRef.current) {
@@ -4496,7 +4400,9 @@ var WaveformPlaylistProvider = ({
4496
4400
  currentTimeRef,
4497
4401
  playbackStartTimeRef,
4498
4402
  audioStartPositionRef,
4499
- getPlaybackTime
4403
+ getPlaybackTime,
4404
+ registerFrameCallback,
4405
+ unregisterFrameCallback
4500
4406
  }),
4501
4407
  [
4502
4408
  isPlaying,
@@ -4504,7 +4410,9 @@ var WaveformPlaylistProvider = ({
4504
4410
  currentTimeRef,
4505
4411
  playbackStartTimeRef,
4506
4412
  audioStartPositionRef,
4507
- getPlaybackTime
4413
+ getPlaybackTime,
4414
+ registerFrameCallback,
4415
+ unregisterFrameCallback
4508
4416
  ]
4509
4417
  );
4510
4418
  const stateValue = useMemo4(
@@ -5299,32 +5207,19 @@ var PositionDisplay = styled.span`
5299
5207
  var AudioPosition = ({ className }) => {
5300
5208
  var _a;
5301
5209
  const timeRef = useRef17(null);
5302
- const animationFrameRef = useRef17(null);
5303
- const { isPlaying, currentTimeRef, getPlaybackTime } = usePlaybackAnimation();
5210
+ const { isPlaying, currentTimeRef, registerFrameCallback, unregisterFrameCallback } = usePlaybackAnimation();
5304
5211
  const { timeFormat: format } = usePlaylistData();
5305
5212
  useEffect12(() => {
5306
- const updateTime = () => {
5307
- var _a2;
5308
- if (timeRef.current) {
5309
- const time = isPlaying ? getPlaybackTime() : (_a2 = currentTimeRef.current) != null ? _a2 : 0;
5310
- timeRef.current.textContent = formatTime(time, format);
5311
- }
5312
- if (isPlaying) {
5313
- animationFrameRef.current = requestAnimationFrame(updateTime);
5314
- }
5315
- };
5213
+ const id = "audio-position";
5316
5214
  if (isPlaying) {
5317
- animationFrameRef.current = requestAnimationFrame(updateTime);
5318
- } else {
5319
- updateTime();
5215
+ registerFrameCallback(id, ({ time }) => {
5216
+ if (timeRef.current) {
5217
+ timeRef.current.textContent = formatTime(time, format);
5218
+ }
5219
+ });
5320
5220
  }
5321
- return () => {
5322
- if (animationFrameRef.current) {
5323
- cancelAnimationFrame(animationFrameRef.current);
5324
- animationFrameRef.current = null;
5325
- }
5326
- };
5327
- }, [isPlaying, format, currentTimeRef, getPlaybackTime]);
5221
+ return () => unregisterFrameCallback(id);
5222
+ }, [isPlaying, format, registerFrameCallback, unregisterFrameCallback]);
5328
5223
  useEffect12(() => {
5329
5224
  var _a2;
5330
5225
  if (!isPlaying && timeRef.current) {
@@ -5507,33 +5402,20 @@ var PlayheadLine = styled2.div.attrs((props) => ({
5507
5402
  `;
5508
5403
  var AnimatedPlayhead = ({ color = "#ff0000" }) => {
5509
5404
  const playheadRef = useRef18(null);
5510
- const animationFrameRef = useRef18(null);
5511
- const { isPlaying, currentTimeRef, getPlaybackTime } = usePlaybackAnimation();
5405
+ const { isPlaying, currentTimeRef, registerFrameCallback, unregisterFrameCallback } = usePlaybackAnimation();
5512
5406
  const { samplesPerPixel, sampleRate, progressBarWidth } = usePlaylistData();
5513
5407
  useEffect13(() => {
5514
- const updatePosition = () => {
5515
- var _a;
5516
- if (playheadRef.current) {
5517
- const time = isPlaying ? getPlaybackTime() : (_a = currentTimeRef.current) != null ? _a : 0;
5518
- const position = time * sampleRate / samplesPerPixel;
5519
- playheadRef.current.style.transform = `translate3d(${position}px, 0, 0)`;
5520
- }
5521
- if (isPlaying) {
5522
- animationFrameRef.current = requestAnimationFrame(updatePosition);
5523
- }
5524
- };
5408
+ const id = "playhead";
5525
5409
  if (isPlaying) {
5526
- animationFrameRef.current = requestAnimationFrame(updatePosition);
5527
- } else {
5528
- updatePosition();
5410
+ registerFrameCallback(id, ({ visualTime, sampleRate: sr, samplesPerPixel: spp }) => {
5411
+ if (playheadRef.current) {
5412
+ const px = visualTime * sr / spp;
5413
+ playheadRef.current.style.transform = `translate3d(${px}px, 0, 0)`;
5414
+ }
5415
+ });
5529
5416
  }
5530
- return () => {
5531
- if (animationFrameRef.current) {
5532
- cancelAnimationFrame(animationFrameRef.current);
5533
- animationFrameRef.current = null;
5534
- }
5535
- };
5536
- }, [isPlaying, sampleRate, samplesPerPixel, currentTimeRef, getPlaybackTime]);
5417
+ return () => unregisterFrameCallback(id);
5418
+ }, [isPlaying, registerFrameCallback, unregisterFrameCallback]);
5537
5419
  useEffect13(() => {
5538
5420
  var _a;
5539
5421
  if (!isPlaying && playheadRef.current) {
@@ -5546,7 +5428,7 @@ var AnimatedPlayhead = ({ color = "#ff0000" }) => {
5546
5428
  };
5547
5429
 
5548
5430
  // src/components/ChannelWithProgress.tsx
5549
- import { useRef as useRef19, useEffect as useEffect14 } from "react";
5431
+ import { useId, useRef as useRef19, useEffect as useEffect14 } from "react";
5550
5432
  import styled3 from "styled-components";
5551
5433
  import {
5552
5434
  clipPixelWidth as computeClipPixelWidth
@@ -5612,10 +5494,10 @@ var ChannelWithProgress = (_a) => {
5612
5494
  "clipOffsetSeconds"
5613
5495
  ]);
5614
5496
  const progressRef = useRef19(null);
5615
- const animationFrameRef = useRef19(null);
5497
+ const callbackId = useId();
5616
5498
  const theme = useTheme();
5617
5499
  const { waveHeight } = usePlaylistInfo();
5618
- const { isPlaying, currentTimeRef, getPlaybackTime } = usePlaybackAnimation();
5500
+ const { isPlaying, currentTimeRef, registerFrameCallback, unregisterFrameCallback } = usePlaybackAnimation();
5619
5501
  const { samplesPerPixel, sampleRate } = usePlaylistData();
5620
5502
  const progressColor = (theme == null ? void 0 : theme.waveProgressColor) || "rgba(0, 0, 0, 0.1)";
5621
5503
  const clipPixelWidth = computeClipPixelWidth(
@@ -5624,46 +5506,32 @@ var ChannelWithProgress = (_a) => {
5624
5506
  samplesPerPixel
5625
5507
  );
5626
5508
  useEffect14(() => {
5627
- const updateProgress = () => {
5628
- var _a2;
5629
- if (progressRef.current) {
5630
- const currentTime = isPlaying ? getPlaybackTime() : (_a2 = currentTimeRef.current) != null ? _a2 : 0;
5631
- const currentSample = currentTime * sampleRate;
5632
- const clipEndSample = clipStartSample + clipDurationSamples;
5633
- let ratio = 0;
5634
- if (currentSample <= clipStartSample) {
5635
- ratio = 0;
5636
- } else if (currentSample >= clipEndSample) {
5637
- ratio = 1;
5638
- } else {
5639
- const playedSamples = currentSample - clipStartSample;
5640
- ratio = playedSamples / clipDurationSamples;
5641
- }
5642
- progressRef.current.style.transform = `scaleX(${ratio})`;
5643
- }
5644
- if (isPlaying) {
5645
- animationFrameRef.current = requestAnimationFrame(updateProgress);
5646
- }
5647
- };
5648
5509
  if (isPlaying) {
5649
- animationFrameRef.current = requestAnimationFrame(updateProgress);
5650
- } else {
5651
- updateProgress();
5510
+ registerFrameCallback(callbackId, ({ visualTime, sampleRate: sr }) => {
5511
+ if (progressRef.current) {
5512
+ const currentSample = visualTime * sr;
5513
+ const clipEndSample = clipStartSample + clipDurationSamples;
5514
+ let ratio = 0;
5515
+ if (currentSample <= clipStartSample) {
5516
+ ratio = 0;
5517
+ } else if (currentSample >= clipEndSample) {
5518
+ ratio = 1;
5519
+ } else {
5520
+ const playedSamples = currentSample - clipStartSample;
5521
+ ratio = playedSamples / clipDurationSamples;
5522
+ }
5523
+ progressRef.current.style.transform = `scaleX(${ratio})`;
5524
+ }
5525
+ });
5652
5526
  }
5653
- return () => {
5654
- if (animationFrameRef.current) {
5655
- cancelAnimationFrame(animationFrameRef.current);
5656
- animationFrameRef.current = null;
5657
- }
5658
- };
5527
+ return () => unregisterFrameCallback(callbackId);
5659
5528
  }, [
5660
5529
  isPlaying,
5661
- sampleRate,
5662
5530
  clipStartSample,
5663
5531
  clipDurationSamples,
5664
- clipPixelWidth,
5665
- currentTimeRef,
5666
- getPlaybackTime
5532
+ callbackId,
5533
+ registerFrameCallback,
5534
+ unregisterFrameCallback
5667
5535
  ]);
5668
5536
  useEffect14(() => {
5669
5537
  var _a2;