@waveform-playlist/browser 11.3.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;
2963
- }
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);
2927
+ const result = buffer.get();
2928
+ if (!result) {
2929
+ throw new Error("Offline rendering produced no audio buffer");
3011
2930
  }
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