expo-pro-video-editor 0.2.1 → 0.2.2
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/build/ui/AudioTrimScrubber.d.ts +9 -0
- package/build/ui/AudioTrimScrubber.d.ts.map +1 -1
- package/build/ui/AudioTrimScrubber.js +3 -3
- package/build/ui/AudioTrimScrubber.js.map +1 -1
- package/build/ui/AudioWaveform.d.ts +9 -0
- package/build/ui/AudioWaveform.d.ts.map +1 -1
- package/build/ui/AudioWaveform.js +3 -3
- package/build/ui/AudioWaveform.js.map +1 -1
- package/package.json +1 -1
- package/src/ui/AudioTrimScrubber.tsx +12 -3
- package/src/ui/AudioWaveform.tsx +12 -3
|
@@ -20,5 +20,14 @@ export declare function AudioTrimScrubber(props: {
|
|
|
20
20
|
}) => void;
|
|
21
21
|
onDelete?: () => void;
|
|
22
22
|
width: number;
|
|
23
|
+
/**
|
|
24
|
+
* Passed through to normalizeWaveformPeaks — lower it for a track whose
|
|
25
|
+
* loudness is consistently close to its own peak (a synth/chiptune track
|
|
26
|
+
* normalizes to near-max-height bars throughout at the default 1.6,
|
|
27
|
+
* looking "filled" compared to a quieter, more dynamic recording). Has no
|
|
28
|
+
* effect on relative bar-to-bar proportions within a single file, only on
|
|
29
|
+
* how aggressively the loudest moment is pushed to full height.
|
|
30
|
+
*/
|
|
31
|
+
contrastExponent?: number;
|
|
23
32
|
}): import("react").JSX.Element;
|
|
24
33
|
//# sourceMappingURL=AudioTrimScrubber.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioTrimScrubber.d.ts","sourceRoot":"","sources":["../../src/ui/AudioTrimScrubber.tsx"],"names":[],"mappings":"AAkEA;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB;;;gBAGY;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACxE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"AudioTrimScrubber.d.ts","sourceRoot":"","sources":["../../src/ui/AudioTrimScrubber.tsx"],"names":[],"mappings":"AAkEA;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB;;;gBAGY;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACxE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,+BA0LA"}
|
|
@@ -67,7 +67,7 @@ const styles = {
|
|
|
67
67
|
* before placing the handles; short tracks fit without scrolling.
|
|
68
68
|
*/
|
|
69
69
|
export function AudioTrimScrubber(props) {
|
|
70
|
-
const { inputPath, durationSeconds, startSeconds, endSeconds, currentTime, onChange, onDelete, width } = props;
|
|
70
|
+
const { inputPath, durationSeconds, startSeconds, endSeconds, currentTime, onChange, onDelete, width, contrastExponent } = props;
|
|
71
71
|
const deleteButtonSpace = onDelete ? DELETE_BUTTON_SIZE + 8 : 0;
|
|
72
72
|
const scrubberWidth = width - deleteButtonSpace;
|
|
73
73
|
/**
|
|
@@ -84,7 +84,7 @@ export function AudioTrimScrubber(props) {
|
|
|
84
84
|
.then(peaks => {
|
|
85
85
|
if (cancelled)
|
|
86
86
|
return;
|
|
87
|
-
setResult({ inputPath, barCount, peaks: normalizeWaveformPeaks(peaks) });
|
|
87
|
+
setResult({ inputPath, barCount, peaks: normalizeWaveformPeaks(peaks, contrastExponent) });
|
|
88
88
|
})
|
|
89
89
|
.catch(error => {
|
|
90
90
|
console.warn(`Waveform extraction failed for ${inputPath}:`, error);
|
|
@@ -92,7 +92,7 @@ export function AudioTrimScrubber(props) {
|
|
|
92
92
|
return () => {
|
|
93
93
|
cancelled = true;
|
|
94
94
|
};
|
|
95
|
-
}, [inputPath, barCount]);
|
|
95
|
+
}, [inputPath, barCount, contrastExponent]);
|
|
96
96
|
const peaks = result?.inputPath === inputPath && result.barCount === barCount ? result.peaks : null;
|
|
97
97
|
/**
|
|
98
98
|
* Mirrored into refs (in an effect, not during render) so the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioTrimScrubber.js","sourceRoot":"","sources":["../../src/ui/AudioTrimScrubber.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport { ActivityIndicator, PanResponder, ScrollView, Text, TouchableOpacity, View } from 'react-native';\nimport { tv } from 'tailwind-variants';\n\nimport ExpoProVideoEditorModule from '../ExpoProVideoEditorModule';\nimport { normalizeWaveformPeaks } from '../waveform';\n\nconst HANDLE_WIDTH = 18;\nconst STRIP_HEIGHT = 51;\nconst MIN_TRIM_SECONDS = 0.5;\nconst BAR_COUNT_PER_SECOND = 8;\nconst MIN_BAR_COUNT = 90;\nconst MAX_BAR_COUNT = 600;\nconst BAR_GAP = 1;\nconst MIN_BAR_HEIGHT = 3;\nconst MAX_BAR_HEIGHT = STRIP_HEIGHT - 10;\nconst DELETE_BUTTON_SIZE = 24;\n// Width per second of audio, before scrolling kicks in.\nconst PIXELS_PER_SECOND = 40;\n\nconst styles = {\n row: tv({ base: 'h-[51px] flex-row items-center gap-2' }),\n scrollContainer: tv({ base: 'h-[51px]' }),\n container: tv({ base: 'h-[51px] justify-center overflow-hidden rounded-[10px]' }),\n waveform: tv({\n base: 'h-[51px] flex-row items-center justify-center gap-px overflow-hidden bg-video-editor-surface',\n }),\n bar: tv({\n base: 'rounded-full',\n variants: {\n played: {\n true: 'bg-video-editor-accent-light',\n false: 'bg-video-editor-bar-unplayed',\n },\n },\n }),\n selectionBorder: tv({\n base: 'absolute top-0 h-[51px] border-y-4 border-video-editor-accent-light',\n }),\n dimOverlay: tv({\n base: 'absolute bottom-0 top-0 bg-black/55',\n variants: {\n rounded: {\n start: 'rounded-l-[10px]',\n end: 'rounded-r-[10px]',\n none: '',\n },\n },\n }),\n loadingOverlay: tv({ base: 'absolute inset-0 items-center justify-center' }),\n handle: tv({\n base: 'absolute top-0 h-[51px] w-[18px] items-center justify-center bg-video-editor-accent-light',\n variants: {\n side: {\n start: 'rounded-l-[10px]',\n end: 'rounded-r-[10px]',\n },\n },\n }),\n handleGrip: tv({ base: 'h-[29px] w-0.5 rounded-full bg-white' }),\n deleteButton: tv({\n base: 'h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-video-editor-border bg-white',\n }),\n deleteButtonText: tv({ base: 'text-xs text-video-editor-text' }),\n};\n\n/**\n * Independent trim UI for a standalone audio file (music / voice / a picked\n * file) — a waveform with drag handles marking the selected window. Long\n * tracks scroll horizontally so the user can pan to the part they want\n * before placing the handles; short tracks fit without scrolling.\n */\nexport function AudioTrimScrubber(props: {\n inputPath: string;\n durationSeconds: number;\n startSeconds: number;\n endSeconds: number;\n /** The audio player's own current position, in this file's own time\n * base — used to color the played portion of the waveform. `null` while\n * not actively playing this track, so bars fall back to the unplayed\n * shade. */\n currentTime: number | null;\n onChange: (range: { startSeconds: number; endSeconds: number }) => void;\n onDelete?: () => void;\n width: number;\n}) {\n const { inputPath, durationSeconds, startSeconds, endSeconds, currentTime, onChange, onDelete, width } = props;\n const deleteButtonSpace = onDelete ? DELETE_BUTTON_SIZE + 8 : 0;\n const scrubberWidth = width - deleteButtonSpace;\n /**\n * Keyed by the request that produced it, so a stale result is simply\n * never rendered — no separate reset-to-loading write is needed.\n */\n const [result, setResult] = useState<{ inputPath: string; barCount: number; peaks: number[] } | null>(null);\n\n const contentWidth = Math.max(scrubberWidth, durationSeconds * PIXELS_PER_SECOND);\n const usableContentWidth = contentWidth - HANDLE_WIDTH * 2;\n const barCount = Math.min(MAX_BAR_COUNT, Math.max(MIN_BAR_COUNT, Math.round(durationSeconds * BAR_COUNT_PER_SECOND)));\n\n useEffect(() => {\n let cancelled = false;\n\n ExpoProVideoEditorModule.extractWaveform(inputPath, barCount)\n .then(peaks => {\n if (cancelled) return;\n setResult({ inputPath, barCount, peaks: normalizeWaveformPeaks(peaks) });\n })\n .catch(error => {\n console.warn(`Waveform extraction failed for ${inputPath}:`, error);\n });\n\n return () => {\n cancelled = true;\n };\n }, [inputPath, barCount]);\n\n const peaks = result?.inputPath === inputPath && result.barCount === barCount ? result.peaks : null;\n\n /**\n * Mirrored into refs (in an effect, not during render) so the\n * PanResponder instances below — created once, in a later effect — always\n * read current values instead of whatever was in scope when built.\n */\n const startXRef = useRef(startSeconds);\n const endXRef = useRef(endSeconds);\n const durationSecondsRef = useRef(durationSeconds);\n const usableContentWidthRef = useRef(usableContentWidth);\n useEffect(() => {\n startXRef.current = startSeconds;\n endXRef.current = endSeconds;\n durationSecondsRef.current = durationSeconds;\n usableContentWidthRef.current = usableContentWidth;\n });\n\n const startGestureAnchorRef = useRef(0);\n const endGestureAnchorRef = useRef(0);\n /**\n * The trim handles sit inside the horizontal ScrollView, so a drag on a\n * handle is ambiguous with the ScrollView's own pan-to-scroll recognizer —\n * without this, dragging a handle also scrolls the content underneath it.\n * Disabling scroll for the duration of a handle drag (grant → release)\n * resolves the conflict outright instead of fighting gesture priority.\n */\n const [scrollEnabled, setScrollEnabled] = useState(true);\n\n function secondsToX(seconds: number) {\n if (durationSeconds <= 0) return 0;\n return (seconds / durationSeconds) * usableContentWidth;\n }\n\n /**\n * Built once, after mount, so PanResponder.create() never runs during\n * render — its callbacks read the refs above for live values.\n */\n const [panResponders, setPanResponders] = useState<{\n start: ReturnType<typeof PanResponder.create>;\n end: ReturnType<typeof PanResponder.create>;\n } | null>(null);\n\n useEffect(() => {\n function secondsToXCurrent(seconds: number) {\n if (durationSecondsRef.current <= 0) return 0;\n return (seconds / durationSecondsRef.current) * usableContentWidthRef.current;\n }\n\n function xToSecondsCurrent(x: number) {\n if (usableContentWidthRef.current <= 0) return 0;\n return (x / usableContentWidthRef.current) * durationSecondsRef.current;\n }\n\n setPanResponders({\n start: PanResponder.create({\n onStartShouldSetPanResponder: () => true,\n onPanResponderGrant: () => {\n setScrollEnabled(false);\n startGestureAnchorRef.current = secondsToXCurrent(startXRef.current);\n },\n onPanResponderMove: (_event, gesture) => {\n const nextSeconds = xToSecondsCurrent(startGestureAnchorRef.current + gesture.dx);\n const clamped = Math.max(0, Math.min(nextSeconds, endXRef.current - MIN_TRIM_SECONDS));\n onChange({ startSeconds: clamped, endSeconds: endXRef.current });\n },\n onPanResponderRelease: () => setScrollEnabled(true),\n onPanResponderTerminate: () => setScrollEnabled(true),\n }),\n end: PanResponder.create({\n onStartShouldSetPanResponder: () => true,\n onPanResponderGrant: () => {\n setScrollEnabled(false);\n endGestureAnchorRef.current = secondsToXCurrent(endXRef.current);\n },\n onPanResponderMove: (_event, gesture) => {\n const nextSeconds = xToSecondsCurrent(endGestureAnchorRef.current + gesture.dx);\n const clamped = Math.min(durationSecondsRef.current, Math.max(nextSeconds, startXRef.current + MIN_TRIM_SECONDS));\n onChange({ startSeconds: startXRef.current, endSeconds: clamped });\n },\n onPanResponderRelease: () => setScrollEnabled(true),\n onPanResponderTerminate: () => setScrollEnabled(true),\n }),\n });\n }, []);\n\n const startX = secondsToX(startSeconds);\n const endX = secondsToX(endSeconds);\n const barWidth = peaks && peaks.length > 0 ? usableContentWidth / peaks.length - BAR_GAP : 0;\n\n const isStartAtEdge = startX <= 0;\n const isEndAtEdge = endX >= usableContentWidth;\n\n const trimDuration = Math.max(0, endSeconds - startSeconds);\n const playedFraction = currentTime !== null && trimDuration > 0 ? Math.min(1, Math.max(0, (currentTime - startSeconds) / trimDuration)) : 0;\n const playedBarCount = currentTime !== null && peaks ? Math.round(playedFraction * peaks.length) : 0;\n\n return (\n <View className={styles.row()} style={{ width }}>\n <ScrollView\n horizontal\n showsHorizontalScrollIndicator={false}\n scrollEnabled={scrollEnabled}\n className={styles.scrollContainer()}\n style={{ width: scrubberWidth }}\n contentContainerStyle={{ width: contentWidth }}>\n <View className={styles.container()} style={{ width: contentWidth }}>\n <View className={styles.waveform()} style={{ width: usableContentWidth, marginHorizontal: HANDLE_WIDTH }}>\n {peaks ? (\n peaks.map((peak, index) => (\n <View\n key={index}\n className={styles.bar({ played: index < playedBarCount })}\n style={{\n width: Math.max(1, barWidth),\n height: Math.min(MAX_BAR_HEIGHT, Math.max(MIN_BAR_HEIGHT, peak * STRIP_HEIGHT)),\n }}\n />\n ))\n ) : (\n <View className={styles.loadingOverlay()}>\n <ActivityIndicator size=\"small\" />\n </View>\n )}\n <View\n pointerEvents=\"none\"\n className={styles.dimOverlay({ rounded: isStartAtEdge ? 'none' : 'start' })}\n style={{ left: 0, width: startX }}\n />\n <View pointerEvents=\"none\" className={styles.dimOverlay({ rounded: isEndAtEdge ? 'none' : 'end' })} style={{ left: endX, right: 0 }} />\n <View pointerEvents=\"none\" className={styles.selectionBorder()} style={{ left: startX, width: endX - startX }} />\n </View>\n <View\n className={styles.handle({ side: 'start' })}\n style={{ left: startX }}\n hitSlop={{ top: 16, bottom: 16, left: 16, right: 16 }}\n {...panResponders?.start.panHandlers}>\n <View className={styles.handleGrip()} />\n </View>\n <View\n className={styles.handle({ side: 'end' })}\n style={{ left: endX + HANDLE_WIDTH }}\n hitSlop={{ top: 16, bottom: 16, left: 16, right: 16 }}\n {...panResponders?.end.panHandlers}>\n <View className={styles.handleGrip()} />\n </View>\n </View>\n </ScrollView>\n {onDelete ? (\n <TouchableOpacity className={styles.deleteButton()} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} onPress={onDelete}>\n <Text className={styles.deleteButtonText()}>✕</Text>\n </TouchableOpacity>\n ) : null}\n </View>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"AudioTrimScrubber.js","sourceRoot":"","sources":["../../src/ui/AudioTrimScrubber.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport { ActivityIndicator, PanResponder, ScrollView, Text, TouchableOpacity, View } from 'react-native';\nimport { tv } from 'tailwind-variants';\n\nimport ExpoProVideoEditorModule from '../ExpoProVideoEditorModule';\nimport { normalizeWaveformPeaks } from '../waveform';\n\nconst HANDLE_WIDTH = 18;\nconst STRIP_HEIGHT = 51;\nconst MIN_TRIM_SECONDS = 0.5;\nconst BAR_COUNT_PER_SECOND = 8;\nconst MIN_BAR_COUNT = 90;\nconst MAX_BAR_COUNT = 600;\nconst BAR_GAP = 1;\nconst MIN_BAR_HEIGHT = 3;\nconst MAX_BAR_HEIGHT = STRIP_HEIGHT - 10;\nconst DELETE_BUTTON_SIZE = 24;\n// Width per second of audio, before scrolling kicks in.\nconst PIXELS_PER_SECOND = 40;\n\nconst styles = {\n row: tv({ base: 'h-[51px] flex-row items-center gap-2' }),\n scrollContainer: tv({ base: 'h-[51px]' }),\n container: tv({ base: 'h-[51px] justify-center overflow-hidden rounded-[10px]' }),\n waveform: tv({\n base: 'h-[51px] flex-row items-center justify-center gap-px overflow-hidden bg-video-editor-surface',\n }),\n bar: tv({\n base: 'rounded-full',\n variants: {\n played: {\n true: 'bg-video-editor-accent-light',\n false: 'bg-video-editor-bar-unplayed',\n },\n },\n }),\n selectionBorder: tv({\n base: 'absolute top-0 h-[51px] border-y-4 border-video-editor-accent-light',\n }),\n dimOverlay: tv({\n base: 'absolute bottom-0 top-0 bg-black/55',\n variants: {\n rounded: {\n start: 'rounded-l-[10px]',\n end: 'rounded-r-[10px]',\n none: '',\n },\n },\n }),\n loadingOverlay: tv({ base: 'absolute inset-0 items-center justify-center' }),\n handle: tv({\n base: 'absolute top-0 h-[51px] w-[18px] items-center justify-center bg-video-editor-accent-light',\n variants: {\n side: {\n start: 'rounded-l-[10px]',\n end: 'rounded-r-[10px]',\n },\n },\n }),\n handleGrip: tv({ base: 'h-[29px] w-0.5 rounded-full bg-white' }),\n deleteButton: tv({\n base: 'h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-video-editor-border bg-white',\n }),\n deleteButtonText: tv({ base: 'text-xs text-video-editor-text' }),\n};\n\n/**\n * Independent trim UI for a standalone audio file (music / voice / a picked\n * file) — a waveform with drag handles marking the selected window. Long\n * tracks scroll horizontally so the user can pan to the part they want\n * before placing the handles; short tracks fit without scrolling.\n */\nexport function AudioTrimScrubber(props: {\n inputPath: string;\n durationSeconds: number;\n startSeconds: number;\n endSeconds: number;\n /** The audio player's own current position, in this file's own time\n * base — used to color the played portion of the waveform. `null` while\n * not actively playing this track, so bars fall back to the unplayed\n * shade. */\n currentTime: number | null;\n onChange: (range: { startSeconds: number; endSeconds: number }) => void;\n onDelete?: () => void;\n width: number;\n /**\n * Passed through to normalizeWaveformPeaks — lower it for a track whose\n * loudness is consistently close to its own peak (a synth/chiptune track\n * normalizes to near-max-height bars throughout at the default 1.6,\n * looking \"filled\" compared to a quieter, more dynamic recording). Has no\n * effect on relative bar-to-bar proportions within a single file, only on\n * how aggressively the loudest moment is pushed to full height.\n */\n contrastExponent?: number;\n}) {\n const { inputPath, durationSeconds, startSeconds, endSeconds, currentTime, onChange, onDelete, width, contrastExponent } = props;\n const deleteButtonSpace = onDelete ? DELETE_BUTTON_SIZE + 8 : 0;\n const scrubberWidth = width - deleteButtonSpace;\n /**\n * Keyed by the request that produced it, so a stale result is simply\n * never rendered — no separate reset-to-loading write is needed.\n */\n const [result, setResult] = useState<{ inputPath: string; barCount: number; peaks: number[] } | null>(null);\n\n const contentWidth = Math.max(scrubberWidth, durationSeconds * PIXELS_PER_SECOND);\n const usableContentWidth = contentWidth - HANDLE_WIDTH * 2;\n const barCount = Math.min(MAX_BAR_COUNT, Math.max(MIN_BAR_COUNT, Math.round(durationSeconds * BAR_COUNT_PER_SECOND)));\n\n useEffect(() => {\n let cancelled = false;\n\n ExpoProVideoEditorModule.extractWaveform(inputPath, barCount)\n .then(peaks => {\n if (cancelled) return;\n setResult({ inputPath, barCount, peaks: normalizeWaveformPeaks(peaks, contrastExponent) });\n })\n .catch(error => {\n console.warn(`Waveform extraction failed for ${inputPath}:`, error);\n });\n\n return () => {\n cancelled = true;\n };\n }, [inputPath, barCount, contrastExponent]);\n\n const peaks = result?.inputPath === inputPath && result.barCount === barCount ? result.peaks : null;\n\n /**\n * Mirrored into refs (in an effect, not during render) so the\n * PanResponder instances below — created once, in a later effect — always\n * read current values instead of whatever was in scope when built.\n */\n const startXRef = useRef(startSeconds);\n const endXRef = useRef(endSeconds);\n const durationSecondsRef = useRef(durationSeconds);\n const usableContentWidthRef = useRef(usableContentWidth);\n useEffect(() => {\n startXRef.current = startSeconds;\n endXRef.current = endSeconds;\n durationSecondsRef.current = durationSeconds;\n usableContentWidthRef.current = usableContentWidth;\n });\n\n const startGestureAnchorRef = useRef(0);\n const endGestureAnchorRef = useRef(0);\n /**\n * The trim handles sit inside the horizontal ScrollView, so a drag on a\n * handle is ambiguous with the ScrollView's own pan-to-scroll recognizer —\n * without this, dragging a handle also scrolls the content underneath it.\n * Disabling scroll for the duration of a handle drag (grant → release)\n * resolves the conflict outright instead of fighting gesture priority.\n */\n const [scrollEnabled, setScrollEnabled] = useState(true);\n\n function secondsToX(seconds: number) {\n if (durationSeconds <= 0) return 0;\n return (seconds / durationSeconds) * usableContentWidth;\n }\n\n /**\n * Built once, after mount, so PanResponder.create() never runs during\n * render — its callbacks read the refs above for live values.\n */\n const [panResponders, setPanResponders] = useState<{\n start: ReturnType<typeof PanResponder.create>;\n end: ReturnType<typeof PanResponder.create>;\n } | null>(null);\n\n useEffect(() => {\n function secondsToXCurrent(seconds: number) {\n if (durationSecondsRef.current <= 0) return 0;\n return (seconds / durationSecondsRef.current) * usableContentWidthRef.current;\n }\n\n function xToSecondsCurrent(x: number) {\n if (usableContentWidthRef.current <= 0) return 0;\n return (x / usableContentWidthRef.current) * durationSecondsRef.current;\n }\n\n setPanResponders({\n start: PanResponder.create({\n onStartShouldSetPanResponder: () => true,\n onPanResponderGrant: () => {\n setScrollEnabled(false);\n startGestureAnchorRef.current = secondsToXCurrent(startXRef.current);\n },\n onPanResponderMove: (_event, gesture) => {\n const nextSeconds = xToSecondsCurrent(startGestureAnchorRef.current + gesture.dx);\n const clamped = Math.max(0, Math.min(nextSeconds, endXRef.current - MIN_TRIM_SECONDS));\n onChange({ startSeconds: clamped, endSeconds: endXRef.current });\n },\n onPanResponderRelease: () => setScrollEnabled(true),\n onPanResponderTerminate: () => setScrollEnabled(true),\n }),\n end: PanResponder.create({\n onStartShouldSetPanResponder: () => true,\n onPanResponderGrant: () => {\n setScrollEnabled(false);\n endGestureAnchorRef.current = secondsToXCurrent(endXRef.current);\n },\n onPanResponderMove: (_event, gesture) => {\n const nextSeconds = xToSecondsCurrent(endGestureAnchorRef.current + gesture.dx);\n const clamped = Math.min(durationSecondsRef.current, Math.max(nextSeconds, startXRef.current + MIN_TRIM_SECONDS));\n onChange({ startSeconds: startXRef.current, endSeconds: clamped });\n },\n onPanResponderRelease: () => setScrollEnabled(true),\n onPanResponderTerminate: () => setScrollEnabled(true),\n }),\n });\n }, []);\n\n const startX = secondsToX(startSeconds);\n const endX = secondsToX(endSeconds);\n const barWidth = peaks && peaks.length > 0 ? usableContentWidth / peaks.length - BAR_GAP : 0;\n\n const isStartAtEdge = startX <= 0;\n const isEndAtEdge = endX >= usableContentWidth;\n\n const trimDuration = Math.max(0, endSeconds - startSeconds);\n const playedFraction = currentTime !== null && trimDuration > 0 ? Math.min(1, Math.max(0, (currentTime - startSeconds) / trimDuration)) : 0;\n const playedBarCount = currentTime !== null && peaks ? Math.round(playedFraction * peaks.length) : 0;\n\n return (\n <View className={styles.row()} style={{ width }}>\n <ScrollView\n horizontal\n showsHorizontalScrollIndicator={false}\n scrollEnabled={scrollEnabled}\n className={styles.scrollContainer()}\n style={{ width: scrubberWidth }}\n contentContainerStyle={{ width: contentWidth }}>\n <View className={styles.container()} style={{ width: contentWidth }}>\n <View className={styles.waveform()} style={{ width: usableContentWidth, marginHorizontal: HANDLE_WIDTH }}>\n {peaks ? (\n peaks.map((peak, index) => (\n <View\n key={index}\n className={styles.bar({ played: index < playedBarCount })}\n style={{\n width: Math.max(1, barWidth),\n height: Math.min(MAX_BAR_HEIGHT, Math.max(MIN_BAR_HEIGHT, peak * STRIP_HEIGHT)),\n }}\n />\n ))\n ) : (\n <View className={styles.loadingOverlay()}>\n <ActivityIndicator size=\"small\" />\n </View>\n )}\n <View\n pointerEvents=\"none\"\n className={styles.dimOverlay({ rounded: isStartAtEdge ? 'none' : 'start' })}\n style={{ left: 0, width: startX }}\n />\n <View pointerEvents=\"none\" className={styles.dimOverlay({ rounded: isEndAtEdge ? 'none' : 'end' })} style={{ left: endX, right: 0 }} />\n <View pointerEvents=\"none\" className={styles.selectionBorder()} style={{ left: startX, width: endX - startX }} />\n </View>\n <View\n className={styles.handle({ side: 'start' })}\n style={{ left: startX }}\n hitSlop={{ top: 16, bottom: 16, left: 16, right: 16 }}\n {...panResponders?.start.panHandlers}>\n <View className={styles.handleGrip()} />\n </View>\n <View\n className={styles.handle({ side: 'end' })}\n style={{ left: endX + HANDLE_WIDTH }}\n hitSlop={{ top: 16, bottom: 16, left: 16, right: 16 }}\n {...panResponders?.end.panHandlers}>\n <View className={styles.handleGrip()} />\n </View>\n </View>\n </ScrollView>\n {onDelete ? (\n <TouchableOpacity className={styles.deleteButton()} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} onPress={onDelete}>\n <Text className={styles.deleteButtonText()}>✕</Text>\n </TouchableOpacity>\n ) : null}\n </View>\n );\n}\n"]}
|
|
@@ -15,5 +15,14 @@ export declare function AudioWaveform(props: {
|
|
|
15
15
|
endSeconds: number;
|
|
16
16
|
onDelete?: () => void;
|
|
17
17
|
barCount?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Passed through to normalizeWaveformPeaks — lower it for a track whose
|
|
20
|
+
* loudness is consistently close to its own peak (a synth/chiptune track
|
|
21
|
+
* normalizes to near-max-height bars throughout at the default 1.6,
|
|
22
|
+
* looking "filled" compared to a quieter, more dynamic recording). Has no
|
|
23
|
+
* effect on relative bar-to-bar proportions within a single file, only on
|
|
24
|
+
* how aggressively the loudest moment is pushed to full height.
|
|
25
|
+
*/
|
|
26
|
+
contrastExponent?: number;
|
|
18
27
|
}): import("react").JSX.Element | null;
|
|
19
28
|
//# sourceMappingURL=AudioWaveform.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioWaveform.d.ts","sourceRoot":"","sources":["../../src/ui/AudioWaveform.tsx"],"names":[],"mappings":"AAuCA;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"AudioWaveform.d.ts","sourceRoot":"","sources":["../../src/ui/AudioWaveform.tsx"],"names":[],"mappings":"AAuCA;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,sCAoFA"}
|
|
@@ -43,7 +43,7 @@ const styles = {
|
|
|
43
43
|
* read-only display (e.g. a chat bubble's voice message).
|
|
44
44
|
*/
|
|
45
45
|
export function AudioWaveform(props) {
|
|
46
|
-
const { inputPath, width, currentTime, startSeconds, endSeconds, onDelete, barCount = BAR_COUNT } = props;
|
|
46
|
+
const { inputPath, width, currentTime, startSeconds, endSeconds, onDelete, barCount = BAR_COUNT, contrastExponent } = props;
|
|
47
47
|
/**
|
|
48
48
|
* Keyed by the request that produced it, so a stale result is simply
|
|
49
49
|
* never rendered — no separate reset-to-loading write is needed.
|
|
@@ -55,7 +55,7 @@ export function AudioWaveform(props) {
|
|
|
55
55
|
.then(peaks => {
|
|
56
56
|
if (cancelled)
|
|
57
57
|
return;
|
|
58
|
-
setResult({ inputPath, barCount, peaks: normalizeWaveformPeaks(peaks), failureKind: 'none' });
|
|
58
|
+
setResult({ inputPath, barCount, peaks: normalizeWaveformPeaks(peaks, contrastExponent), failureKind: 'none' });
|
|
59
59
|
})
|
|
60
60
|
.catch(error => {
|
|
61
61
|
if (cancelled)
|
|
@@ -72,7 +72,7 @@ export function AudioWaveform(props) {
|
|
|
72
72
|
return () => {
|
|
73
73
|
cancelled = true;
|
|
74
74
|
};
|
|
75
|
-
}, [inputPath, barCount]);
|
|
75
|
+
}, [inputPath, barCount, contrastExponent]);
|
|
76
76
|
const current = result?.inputPath === inputPath && result.barCount === barCount ? result : null;
|
|
77
77
|
const peaks = current?.peaks ?? null;
|
|
78
78
|
const failureKind = current?.failureKind ?? 'none';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AudioWaveform.js","sourceRoot":"","sources":["../../src/ui/AudioWaveform.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useState } from 'react';\nimport { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native';\nimport { tv } from 'tailwind-variants';\n\nimport ExpoProVideoEditorModule from '../ExpoProVideoEditorModule';\nimport { normalizeWaveformPeaks } from '../waveform';\n\nconst STRIP_HEIGHT = 51;\nconst BAR_COUNT = 90;\nconst BAR_GAP = 1;\nconst MIN_BAR_HEIGHT = 3;\n/**\n * Leaves a visible gap above/below even the tallest bar so it never touches\n * the container's top/bottom edge, regardless of peak value.\n */\nconst MAX_BAR_HEIGHT = STRIP_HEIGHT - 10;\nconst DELETE_BUTTON_SIZE = 24;\n\nconst styles = {\n row: tv({ base: 'h-[51px] flex-row items-center gap-2' }),\n waveform: tv({\n base: 'h-[51px] flex-row items-center justify-center gap-px rounded-[10px] bg-video-editor-surface px-1.5',\n }),\n bar: tv({\n base: 'rounded-full',\n variants: {\n played: {\n true: 'bg-video-editor-bar-played',\n false: 'bg-video-editor-bar-unplayed',\n },\n },\n }),\n failedText: tv({ base: 'text-[11px] text-video-editor-text-muted' }),\n deleteButton: tv({\n base: 'h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-video-editor-border bg-white',\n }),\n deleteButtonText: tv({ base: 'text-xs text-video-editor-text' }),\n};\n\n/**\n * A peak-amplitude waveform for a completed audio or video file — bars\n * mirror the standard voice-note look (Voice Memos, WhatsApp), with the\n * already-played portion turning solid while the rest stays dim.\n *\n * `onDelete` is optional: pass it to show a delete button (e.g. an editor\n * letting the user strip a clip's own audio track); omit it for a\n * read-only display (e.g. a chat bubble's voice message).\n */\nexport function AudioWaveform(props: {\n inputPath: string;\n width: number;\n currentTime: number;\n startSeconds: number;\n endSeconds: number;\n onDelete?: () => void;\n barCount?: number;\n}) {\n const { inputPath, width, currentTime, startSeconds, endSeconds, onDelete, barCount = BAR_COUNT } = props;\n /**\n * Keyed by the request that produced it, so a stale result is simply\n * never rendered — no separate reset-to-loading write is needed.\n */\n const [result, setResult] = useState<{\n inputPath: string;\n barCount: number;\n peaks: number[] | null;\n /**\n * Distinguishes \"this file genuinely has no audio track\" (hide the row\n * entirely — there's nothing to show or delete) from a real extraction\n * failure worth surfacing (network/decode error on a file that does have\n * audio).\n */\n failureKind: 'none' | 'noAudioTrack' | 'error';\n } | null>(null);\n\n useEffect(() => {\n let cancelled = false;\n\n ExpoProVideoEditorModule.extractWaveform(inputPath, barCount)\n .then(peaks => {\n if (cancelled) return;\n setResult({ inputPath, barCount, peaks: normalizeWaveformPeaks(peaks), failureKind: 'none' });\n })\n .catch(error => {\n if (cancelled) return;\n const message = error instanceof Error ? error.message : String(error);\n if (message.includes('No audio track found')) {\n setResult({ inputPath, barCount, peaks: null, failureKind: 'noAudioTrack' });\n } else {\n console.warn(`Waveform extraction failed for ${inputPath}:`, error);\n setResult({ inputPath, barCount, peaks: null, failureKind: 'error' });\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [inputPath, barCount]);\n\n const current = result?.inputPath === inputPath && result.barCount === barCount ? result : null;\n const peaks = current?.peaks ?? null;\n const failureKind = current?.failureKind ?? 'none';\n\n const deleteButtonSpace = onDelete ? DELETE_BUTTON_SIZE + 8 : 0;\n const barsWidth = width - deleteButtonSpace;\n const barWidth = Math.max(1, barsWidth / barCount - BAR_GAP);\n\n const trimDuration = Math.max(0, endSeconds - startSeconds);\n const playedFraction = trimDuration > 0 ? Math.min(1, Math.max(0, (currentTime - startSeconds) / trimDuration)) : 0;\n const playedBarCount = Math.round(playedFraction * barCount);\n\n if (failureKind === 'noAudioTrack') return null;\n\n return (\n <View className={styles.row()} style={{ width }}>\n <View className={styles.waveform()} style={{ width: barsWidth }}>\n {peaks ? (\n peaks.map((peak, index) => (\n <View\n key={index}\n className={styles.bar({ played: index < playedBarCount })}\n style={{\n width: barWidth,\n height: Math.min(MAX_BAR_HEIGHT, Math.max(MIN_BAR_HEIGHT, peak * STRIP_HEIGHT)),\n }}\n />\n ))\n ) : failureKind === 'error' ? (\n <Text className={styles.failedText()}>Couldn't load audio waveform</Text>\n ) : (\n <ActivityIndicator size=\"small\" />\n )}\n </View>\n {onDelete ? (\n <TouchableOpacity className={styles.deleteButton()} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} onPress={onDelete}>\n <Text className={styles.deleteButtonText()}>✕</Text>\n </TouchableOpacity>\n ) : null}\n </View>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"AudioWaveform.js","sourceRoot":"","sources":["../../src/ui/AudioWaveform.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useState } from 'react';\nimport { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native';\nimport { tv } from 'tailwind-variants';\n\nimport ExpoProVideoEditorModule from '../ExpoProVideoEditorModule';\nimport { normalizeWaveformPeaks } from '../waveform';\n\nconst STRIP_HEIGHT = 51;\nconst BAR_COUNT = 90;\nconst BAR_GAP = 1;\nconst MIN_BAR_HEIGHT = 3;\n/**\n * Leaves a visible gap above/below even the tallest bar so it never touches\n * the container's top/bottom edge, regardless of peak value.\n */\nconst MAX_BAR_HEIGHT = STRIP_HEIGHT - 10;\nconst DELETE_BUTTON_SIZE = 24;\n\nconst styles = {\n row: tv({ base: 'h-[51px] flex-row items-center gap-2' }),\n waveform: tv({\n base: 'h-[51px] flex-row items-center justify-center gap-px rounded-[10px] bg-video-editor-surface px-1.5',\n }),\n bar: tv({\n base: 'rounded-full',\n variants: {\n played: {\n true: 'bg-video-editor-bar-played',\n false: 'bg-video-editor-bar-unplayed',\n },\n },\n }),\n failedText: tv({ base: 'text-[11px] text-video-editor-text-muted' }),\n deleteButton: tv({\n base: 'h-6 w-6 items-center justify-center rounded-full border-[0.5px] border-video-editor-border bg-white',\n }),\n deleteButtonText: tv({ base: 'text-xs text-video-editor-text' }),\n};\n\n/**\n * A peak-amplitude waveform for a completed audio or video file — bars\n * mirror the standard voice-note look (Voice Memos, WhatsApp), with the\n * already-played portion turning solid while the rest stays dim.\n *\n * `onDelete` is optional: pass it to show a delete button (e.g. an editor\n * letting the user strip a clip's own audio track); omit it for a\n * read-only display (e.g. a chat bubble's voice message).\n */\nexport function AudioWaveform(props: {\n inputPath: string;\n width: number;\n currentTime: number;\n startSeconds: number;\n endSeconds: number;\n onDelete?: () => void;\n barCount?: number;\n /**\n * Passed through to normalizeWaveformPeaks — lower it for a track whose\n * loudness is consistently close to its own peak (a synth/chiptune track\n * normalizes to near-max-height bars throughout at the default 1.6,\n * looking \"filled\" compared to a quieter, more dynamic recording). Has no\n * effect on relative bar-to-bar proportions within a single file, only on\n * how aggressively the loudest moment is pushed to full height.\n */\n contrastExponent?: number;\n}) {\n const { inputPath, width, currentTime, startSeconds, endSeconds, onDelete, barCount = BAR_COUNT, contrastExponent } = props;\n /**\n * Keyed by the request that produced it, so a stale result is simply\n * never rendered — no separate reset-to-loading write is needed.\n */\n const [result, setResult] = useState<{\n inputPath: string;\n barCount: number;\n peaks: number[] | null;\n /**\n * Distinguishes \"this file genuinely has no audio track\" (hide the row\n * entirely — there's nothing to show or delete) from a real extraction\n * failure worth surfacing (network/decode error on a file that does have\n * audio).\n */\n failureKind: 'none' | 'noAudioTrack' | 'error';\n } | null>(null);\n\n useEffect(() => {\n let cancelled = false;\n\n ExpoProVideoEditorModule.extractWaveform(inputPath, barCount)\n .then(peaks => {\n if (cancelled) return;\n setResult({ inputPath, barCount, peaks: normalizeWaveformPeaks(peaks, contrastExponent), failureKind: 'none' });\n })\n .catch(error => {\n if (cancelled) return;\n const message = error instanceof Error ? error.message : String(error);\n if (message.includes('No audio track found')) {\n setResult({ inputPath, barCount, peaks: null, failureKind: 'noAudioTrack' });\n } else {\n console.warn(`Waveform extraction failed for ${inputPath}:`, error);\n setResult({ inputPath, barCount, peaks: null, failureKind: 'error' });\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [inputPath, barCount, contrastExponent]);\n\n const current = result?.inputPath === inputPath && result.barCount === barCount ? result : null;\n const peaks = current?.peaks ?? null;\n const failureKind = current?.failureKind ?? 'none';\n\n const deleteButtonSpace = onDelete ? DELETE_BUTTON_SIZE + 8 : 0;\n const barsWidth = width - deleteButtonSpace;\n const barWidth = Math.max(1, barsWidth / barCount - BAR_GAP);\n\n const trimDuration = Math.max(0, endSeconds - startSeconds);\n const playedFraction = trimDuration > 0 ? Math.min(1, Math.max(0, (currentTime - startSeconds) / trimDuration)) : 0;\n const playedBarCount = Math.round(playedFraction * barCount);\n\n if (failureKind === 'noAudioTrack') return null;\n\n return (\n <View className={styles.row()} style={{ width }}>\n <View className={styles.waveform()} style={{ width: barsWidth }}>\n {peaks ? (\n peaks.map((peak, index) => (\n <View\n key={index}\n className={styles.bar({ played: index < playedBarCount })}\n style={{\n width: barWidth,\n height: Math.min(MAX_BAR_HEIGHT, Math.max(MIN_BAR_HEIGHT, peak * STRIP_HEIGHT)),\n }}\n />\n ))\n ) : failureKind === 'error' ? (\n <Text className={styles.failedText()}>Couldn't load audio waveform</Text>\n ) : (\n <ActivityIndicator size=\"small\" />\n )}\n </View>\n {onDelete ? (\n <TouchableOpacity className={styles.deleteButton()} hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }} onPress={onDelete}>\n <Text className={styles.deleteButtonText()}>✕</Text>\n </TouchableOpacity>\n ) : null}\n </View>\n );\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-pro-video-editor",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Native video composition (trim, filters, text/image overlays, audio mixing) for Expo/React Native, adapted from pro_video_editor's AVFoundation and Media3 Transformer implementation.",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -83,8 +83,17 @@ export function AudioTrimScrubber(props: {
|
|
|
83
83
|
onChange: (range: { startSeconds: number; endSeconds: number }) => void;
|
|
84
84
|
onDelete?: () => void;
|
|
85
85
|
width: number;
|
|
86
|
+
/**
|
|
87
|
+
* Passed through to normalizeWaveformPeaks — lower it for a track whose
|
|
88
|
+
* loudness is consistently close to its own peak (a synth/chiptune track
|
|
89
|
+
* normalizes to near-max-height bars throughout at the default 1.6,
|
|
90
|
+
* looking "filled" compared to a quieter, more dynamic recording). Has no
|
|
91
|
+
* effect on relative bar-to-bar proportions within a single file, only on
|
|
92
|
+
* how aggressively the loudest moment is pushed to full height.
|
|
93
|
+
*/
|
|
94
|
+
contrastExponent?: number;
|
|
86
95
|
}) {
|
|
87
|
-
const { inputPath, durationSeconds, startSeconds, endSeconds, currentTime, onChange, onDelete, width } = props;
|
|
96
|
+
const { inputPath, durationSeconds, startSeconds, endSeconds, currentTime, onChange, onDelete, width, contrastExponent } = props;
|
|
88
97
|
const deleteButtonSpace = onDelete ? DELETE_BUTTON_SIZE + 8 : 0;
|
|
89
98
|
const scrubberWidth = width - deleteButtonSpace;
|
|
90
99
|
/**
|
|
@@ -103,7 +112,7 @@ export function AudioTrimScrubber(props: {
|
|
|
103
112
|
ExpoProVideoEditorModule.extractWaveform(inputPath, barCount)
|
|
104
113
|
.then(peaks => {
|
|
105
114
|
if (cancelled) return;
|
|
106
|
-
setResult({ inputPath, barCount, peaks: normalizeWaveformPeaks(peaks) });
|
|
115
|
+
setResult({ inputPath, barCount, peaks: normalizeWaveformPeaks(peaks, contrastExponent) });
|
|
107
116
|
})
|
|
108
117
|
.catch(error => {
|
|
109
118
|
console.warn(`Waveform extraction failed for ${inputPath}:`, error);
|
|
@@ -112,7 +121,7 @@ export function AudioTrimScrubber(props: {
|
|
|
112
121
|
return () => {
|
|
113
122
|
cancelled = true;
|
|
114
123
|
};
|
|
115
|
-
}, [inputPath, barCount]);
|
|
124
|
+
}, [inputPath, barCount, contrastExponent]);
|
|
116
125
|
|
|
117
126
|
const peaks = result?.inputPath === inputPath && result.barCount === barCount ? result.peaks : null;
|
|
118
127
|
|
package/src/ui/AudioWaveform.tsx
CHANGED
|
@@ -54,8 +54,17 @@ export function AudioWaveform(props: {
|
|
|
54
54
|
endSeconds: number;
|
|
55
55
|
onDelete?: () => void;
|
|
56
56
|
barCount?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Passed through to normalizeWaveformPeaks — lower it for a track whose
|
|
59
|
+
* loudness is consistently close to its own peak (a synth/chiptune track
|
|
60
|
+
* normalizes to near-max-height bars throughout at the default 1.6,
|
|
61
|
+
* looking "filled" compared to a quieter, more dynamic recording). Has no
|
|
62
|
+
* effect on relative bar-to-bar proportions within a single file, only on
|
|
63
|
+
* how aggressively the loudest moment is pushed to full height.
|
|
64
|
+
*/
|
|
65
|
+
contrastExponent?: number;
|
|
57
66
|
}) {
|
|
58
|
-
const { inputPath, width, currentTime, startSeconds, endSeconds, onDelete, barCount = BAR_COUNT } = props;
|
|
67
|
+
const { inputPath, width, currentTime, startSeconds, endSeconds, onDelete, barCount = BAR_COUNT, contrastExponent } = props;
|
|
59
68
|
/**
|
|
60
69
|
* Keyed by the request that produced it, so a stale result is simply
|
|
61
70
|
* never rendered — no separate reset-to-loading write is needed.
|
|
@@ -79,7 +88,7 @@ export function AudioWaveform(props: {
|
|
|
79
88
|
ExpoProVideoEditorModule.extractWaveform(inputPath, barCount)
|
|
80
89
|
.then(peaks => {
|
|
81
90
|
if (cancelled) return;
|
|
82
|
-
setResult({ inputPath, barCount, peaks: normalizeWaveformPeaks(peaks), failureKind: 'none' });
|
|
91
|
+
setResult({ inputPath, barCount, peaks: normalizeWaveformPeaks(peaks, contrastExponent), failureKind: 'none' });
|
|
83
92
|
})
|
|
84
93
|
.catch(error => {
|
|
85
94
|
if (cancelled) return;
|
|
@@ -95,7 +104,7 @@ export function AudioWaveform(props: {
|
|
|
95
104
|
return () => {
|
|
96
105
|
cancelled = true;
|
|
97
106
|
};
|
|
98
|
-
}, [inputPath, barCount]);
|
|
107
|
+
}, [inputPath, barCount, contrastExponent]);
|
|
99
108
|
|
|
100
109
|
const current = result?.inputPath === inputPath && result.barCount === barCount ? result : null;
|
|
101
110
|
const peaks = current?.peaks ?? null;
|