Skip to main content

Audio Oscilloscope

audio-oscilloscope.tsx
import {Audio} from '@remotion/media'; import { createSmoothSvgPath, getWaveformPortion, useWindowedAudioData, } from '@remotion/media-utils'; import React, {forwardRef, useImperativeHandle, useRef} from 'react'; import { Interactive, Sequence, useCurrentFrame, useVideoConfig, type InteractiveBaseProps, type InteractiveTransformProps, type InteractivitySchema, type SequenceControls, } from 'remotion'; type AudioOscilloscopeProps = InteractiveBaseProps & InteractiveTransformProps & { readonly amplitude?: number; readonly audioSrc?: string; readonly backgroundColor?: string; readonly lineColor?: string; readonly lineWidth?: number; readonly windowInSeconds?: number; }; const audioOscilloscopeSchema = { ...Interactive.baseSchema, audioSrc: { type: 'asset', default: 'https://remotion.media/elements/remotion-made-this-picture-move.mp3', description: 'Audio source', }, backgroundColor: { type: 'color', default: '#07111f', description: 'Background color', }, lineColor: { type: 'color', default: '#55e6ff', description: 'Waveform color', }, lineWidth: { type: 'number', min: 1, max: 16, step: 1, default: 6, description: 'Line width', hiddenFromList: false, }, amplitude: { type: 'number', min: 0.25, max: 4, step: 0.05, default: 2, description: 'Amplitude', hiddenFromList: false, }, windowInSeconds: { type: 'number', min: 0.05, max: 1, step: 0.05, default: 0.35, description: 'Time window in seconds', hiddenFromList: false, }, ...Interactive.transformSchema, } as const satisfies InteractivitySchema; const AudioOscilloscopeInner = forwardRef< HTMLDivElement, AudioOscilloscopeProps & {readonly controls: SequenceControls | undefined} >( ( { amplitude = 2, audioSrc = 'https://remotion.media/elements/remotion-made-this-picture-move.mp3', backgroundColor = '#07111f', controls, lineColor = '#55e6ff', lineWidth = 6, name, style, windowInSeconds = 0.35, ...sequenceProps }, ref, ) => { const frame = useCurrentFrame(); const {fps} = useVideoConfig(); const outlineRef = useRef<HTMLDivElement>(null); const {audioData, dataOffsetInSeconds} = useWindowedAudioData({ fps, frame, src: audioSrc, windowInSeconds: 10, }); const waveform = audioData ? getWaveformPortion({ audioData, channel: 0, dataOffsetInSeconds, durationInSeconds: windowInSeconds, normalize: false, numberOfSamples: 128, outputRange: 'minus-one-to-one', startTimeInSeconds: frame / fps - windowInSeconds / 2, }).map((sample) => sample.amplitude) : []; const path = createSmoothSvgPath({ points: waveform.map((value, index) => ({ x: (index / (waveform.length - 1)) * 900, y: 150 + value * 150 * amplitude, })), }); useImperativeHandle(ref, () => outlineRef.current as HTMLDivElement, []); return ( <Sequence layout="none" {...sequenceProps} controls={controls} name={name ?? 'Audio oscilloscope'} outlineRef={outlineRef} > <div ref={outlineRef} style={{ backgroundColor, backgroundImage: 'linear-gradient(rgba(255, 255, 255, 0.055) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, 0.055) 1px, transparent 1px)', backgroundSize: '45px 45px', borderRadius: 32, boxSizing: 'border-box', height: 300, overflow: 'hidden', width: 900, ...style, }} > <Audio showInTimeline={false} src={audioSrc} /> <svg height={300} viewBox="0 0 900 300" width={900}> <line x1={0} x2={900} y1={150} y2={150} stroke={lineColor} strokeOpacity={0.18} strokeWidth={1} /> {path ? ( <> <path d={path} fill="none" stroke={lineColor} strokeLinecap="round" strokeLinejoin="round" strokeOpacity={0.22} strokeWidth={lineWidth * 4} /> <path d={path} fill="none" stroke={lineColor} strokeLinecap="round" strokeLinejoin="round" strokeWidth={lineWidth} /> </> ) : null} </svg> </div> </Sequence> ); }, ); export const AudioOscilloscope = Interactive.withSchema({ Component: AudioOscilloscopeInner, componentName: '<AudioOscilloscope>', componentIdentity: null, schema: audioOscilloscopeSchema, supportsEffects: false, }) as React.FC<AudioOscilloscopeProps>;