@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.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +86 -207
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +69 -185
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -10
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
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2868
|
-
|
|
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
|
|
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 (
|
|
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
|
|
2920
|
-
|
|
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
|
-
|
|
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(
|
|
2923
|
+
throw new Error("Tone.Offline rendering failed: " + String(err));
|
|
2940
2924
|
}
|
|
2941
2925
|
}
|
|
2942
2926
|
onProgress(0.9);
|
|
2943
|
-
|
|
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
|
-
|
|
2931
|
+
return result;
|
|
3013
2932
|
});
|
|
3014
2933
|
}
|
|
3015
|
-
function
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
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
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
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
|
-
|
|
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
|