diff --git a/.changeset/lemon-dancers-work.md b/.changeset/lemon-dancers-work.md new file mode 100644 index 000000000..94a238de5 --- /dev/null +++ b/.changeset/lemon-dancers-work.md @@ -0,0 +1,5 @@ +--- +"@livekit/components-react": patch +--- + +Logarithmic binning of multiband track volumes diff --git a/packages/react/src/components/participant/AudioVisualizer.tsx b/packages/react/src/components/participant/AudioVisualizer.tsx index b9201e7e7..43759fc46 100644 --- a/packages/react/src/components/participant/AudioVisualizer.tsx +++ b/packages/react/src/components/participant/AudioVisualizer.tsx @@ -30,7 +30,15 @@ export const AudioVisualizer: ( const barCount = 7; const trackReference = useEnsureTrackRef(trackRef); - const volumes = useMultibandTrackVolume(trackReference, { bands: 7, loPass: 300 }); + const volumes = useMultibandTrackVolume(trackReference, { + bands: 7, + loPass: 60, + hiPass: 1800, + analyserOptions: { + minDecibels: -60, + maxDecibels: -20, + }, + }); return ( trackOrTrackReference?.publication?.track; const [frequencyBands, setFrequencyBands] = React.useState>([]); + options.analyserOptions = { ...multibandDefaults.analyserOptions, ...options.analyserOptions }; const opts = { ...multibandDefaults, ...options }; React.useEffect(() => { if (!track || !track?.mediaStream) { return; } + const { analyser, cleanup } = createAudioAnalyser(track, opts.analyserOptions); const bufferLength = analyser.frequencyBinCount; @@ -132,16 +134,14 @@ export function useMultibandTrackVolume( frequencies = frequencies.slice(options.loPass, options.hiPass); const normalizedFrequencies = normalizeFrequencies(frequencies); - const chunkSize = Math.ceil(normalizedFrequencies.length / opts.bands); - const chunks: Array = []; - for (let i = 0; i < opts.bands; i++) { - const summedVolumes = normalizedFrequencies - .slice(i * chunkSize, (i + 1) * chunkSize) - .reduce((acc, val) => (acc += val), 0); - chunks.push(summedVolumes / chunkSize); - } - - setFrequencyBands(chunks); + const binVolumes = fftToLogBins( + normalizedFrequencies, + analyser.context.sampleRate, + opts.bands, + opts.analyserOptions.fftSize!, + ); + + setFrequencyBands(binVolumes); }; const interval = setInterval(updateVolume, opts.updateInterval); @@ -154,3 +154,51 @@ export function useMultibandTrackVolume( return frequencyBands; } + +function calculateLogBinEdges(minFreq: number, maxFreq: number, numBins: number): number[] { + const logBinEdges: number[] = []; + const logMinFreq = Math.log(minFreq); + const logMaxFreq = Math.log(maxFreq); + const binWidth = (logMaxFreq - logMinFreq) / numBins; + + for (let i = 0; i <= numBins; i++) { + logBinEdges.push(Math.exp(logMinFreq + i * binWidth)); + } + + return logBinEdges; +} + +function fftToLogBins( + rawFFTValues: Float32Array, + sampleRate: number, + numBins: number, + fftSize: number, +): number[] { + const deltaF = sampleRate / fftSize; + + // Define the min and max frequencies of interest + const minFreq = deltaF; // Starting from the first FFT bin frequency + const maxFreq = sampleRate / 2; // Nyquist frequency + + const logBinEdges = calculateLogBinEdges(minFreq, maxFreq, numBins); + const logBinValues = new Array(numBins).fill(0); + + for (let i = 0; i < numBins; i++) { + const startFreq = logBinEdges[i]; + const endFreq = logBinEdges[i + 1]; + + const startBin = Math.ceil(startFreq / deltaF); + const endBin = Math.floor(endFreq / deltaF); + + if (startBin >= rawFFTValues.length) { + break; + } + + for (let j = startBin; j <= endBin && j < rawFFTValues.length; j++) { + logBinValues[i] += rawFFTValues[j]; + } + logBinValues[i] /= endBin - startBin + 1; + } + + return logBinValues; +}