From e069ffd02f2d877afb852f5b3274058eb4c52e78 Mon Sep 17 00:00:00 2001 From: Alex Sanders Date: Fri, 25 Oct 2024 11:03:48 +0100 Subject: [PATCH] Simplify the waveform creation (#12681) --- .../AudioPlayer/components/WaveForm.tsx | 76 ++++++------------- 1 file changed, 22 insertions(+), 54 deletions(-) diff --git a/dotcom-rendering/src/components/AudioPlayer/components/WaveForm.tsx b/dotcom-rendering/src/components/AudioPlayer/components/WaveForm.tsx index e0b8b0ed91..03e386a9b9 100644 --- a/dotcom-rendering/src/components/AudioPlayer/components/WaveForm.tsx +++ b/dotcom-rendering/src/components/AudioPlayer/components/WaveForm.tsx @@ -1,25 +1,31 @@ import { useId, useMemo } from 'react'; -const sumArray = (array: number[]) => array.reduce((a, b) => a + b, 0); - /** * Pseudo random number generator generator ([linear congruential * generator](https://en.wikipedia.org/wiki/Linear_congruential_generator)). * * I'll be honest, I don't fully understand it, but it creates a pseudo random - * number generator based on a seed, in this case an array of numbers. + * number generator based on a seed, in this case a string. * * It's deterministic, so calls to the function it returns will always return - * the same results. + * the same results, given the same seed. * * Copilot helped me with it... */ -const getSeededRandomNumberGenerator = (array: number[]) => { +const getSeededRandomNumberGenerator = (seedString: string) => { const modulus = 2147483648; - const seed = sumArray(array) % modulus; const multiplier = 1103515245; const increment = 12345; + // convert string to numerical seed + let hashedSeed = 0; + for (let i = 0; i < seedString.length; i++) { + const char = seedString.charCodeAt(i); + hashedSeed = (hashedSeed << 5) - hashedSeed + char; + hashedSeed |= 0; // Convert to 32bit integer + } + + const seed = Math.abs(hashedSeed) % modulus; let state = seed; return function () { @@ -28,21 +34,6 @@ const getSeededRandomNumberGenerator = (array: number[]) => { }; }; -function shuffle(array: number[]) { - // Create a random number generator that's seeded with array. - const getSeededRandomNumber = getSeededRandomNumberGenerator(array); - - // Sort the array using the seeded random number generator. This means that - // the same array will always be sorted in the same (pseudo random) way. - return array.sort(() => getSeededRandomNumber() - getSeededRandomNumber()); -} - -// normalize the amplitude of the fake audio data -const normalizeAmplitude = (data: number[]) => { - const multiplier = Math.pow(Math.max(...data), -1); - return data.map((n) => n * multiplier * 100); -}; - /** * Compresses an of values to a range between the threshold and the existing * maximum. @@ -52,48 +43,25 @@ const compress = (array: number[], threshold: number) => { const maxValue = Math.max(...array); return array.map( - (x) => - ((x - minValue) / (maxValue - minValue)) * (maxValue - threshold) + + (value) => + ((value - minValue) / (maxValue - minValue)) * + (maxValue - threshold) + threshold, ); }; -/** Returns a string of the specified length, repeating the input string as necessary. */ -function padString(str: string, length: number) { - // Repeat the string until it is longer than the desired length - const result = str.repeat(Math.ceil(length / str.length)); - - // Return the truncated result to the specified length - return result.slice(0, length); -} - // Generate an array of fake audio peaks based on the URL function generateWaveform(url: string, bars: number) { - // convert the URL to a base64 string - const base64 = btoa(url); - - // Pad the base64 string to the number of bars we want - const stringOfBarLength = padString(base64, bars); + const getSeededRandomNumber = getSeededRandomNumberGenerator(url); - // Convert the string to an array of char codes (fake audio data) - const valuesFromString = Array.from(stringOfBarLength).map((_, i) => - stringOfBarLength.charCodeAt(i), + // Generate an array of fake peaks, pseudo random numbers seeded by the URL + const peaks = Array.from( + { length: bars }, + () => getSeededRandomNumber() * 100, ); - // Shuffle (sort) the fake audio data using a deterministic algorithm. This - // means the same URL will always produce the same waveform, but the - // waveforms of two similar URLs (e.g. guardian podcast URLs) won't _look_ - // all that similar. - const shuffled = shuffle(valuesFromString); - - // Normalize the amplitude of the fake audio data - const normalized = normalizeAmplitude(shuffled); - - // Compress the amplitude of the fake audio data, like a podcast would - const compressed = compress(normalized, 60); - - // Return the normalized the amplitude of the fake audio data - return compressed; + // Return the compressed fake audio data (like a podcast would be) + return compress(peaks, 60); } type Theme = {