From 8c5a4ff3b800d8e13336fdc6edc7bf6d069c8507 Mon Sep 17 00:00:00 2001 From: Max Duval Date: Fri, 25 Oct 2024 11:22:43 +0100 Subject: [PATCH 1/2] refactor(WaveForm): use an actual Generator Instead of generating a multitude of arrays, we only go over the values once and rely on the platform for creating a series of values in sequence: Generators! https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator --- .../AudioPlayer/components/WaveForm.tsx | 120 +++++------------- 1 file changed, 34 insertions(+), 86 deletions(-) diff --git a/dotcom-rendering/src/components/AudioPlayer/components/WaveForm.tsx b/dotcom-rendering/src/components/AudioPlayer/components/WaveForm.tsx index e0b8b0ed91..9f0491bbe9 100644 --- a/dotcom-rendering/src/components/AudioPlayer/components/WaveForm.tsx +++ b/dotcom-rendering/src/components/AudioPlayer/components/WaveForm.tsx @@ -1,99 +1,44 @@ 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. - * - * It's deterministic, so calls to the function it returns will always return - * the same results. + * Create a [`Generator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator) for deterministic series of numbers + * which are guaranteed to be contained inside a specific range. * - * Copilot helped me with it... + * Based on a pseudo random number generator generator ([linear congruential + * generator](https://en.wikipedia.org/wiki/Linear_congruential_generator)). + * Seeding with an audio file URL ensures that a specific file always + * returns the same (fake) series of bars. */ -const getSeededRandomNumberGenerator = (array: number[]) => { +function* waveformGenerator( + /** the URL of the audio source file */ + url: string, + /** the number of bars to generate, i.e. the length of the array */ + bars: number, + /** a destination range for values */ + [min, max]: [number, number], +) { + /** a sort of hash of the URL */ + const urlToNumber = url + .split('') + .reduce((sum, character) => sum + character.charCodeAt(0), 0); const modulus = 2147483648; - const seed = sumArray(array) % modulus; + const seed = urlToNumber % modulus; const multiplier = 1103515245; const increment = 12345; let state = seed; - - return function () { + let count = 0; + while (count++ < bars) { state = (multiplier * state + increment) % modulus; - return state / modulus; - }; -}; - -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. - */ -const compress = (array: number[], threshold: number) => { - const minValue = Math.min(...array); - const maxValue = Math.max(...array); - - return array.map( - (x) => - ((x - 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); - - // Convert the string to an array of char codes (fake audio data) - const valuesFromString = Array.from(stringOfBarLength).map((_, i) => - stringOfBarLength.charCodeAt(i), - ); - - // 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; + /** Get a number in the range [0, 1] */ + const normalised = state / modulus; + /** Compress the amplitude of the fake audio data, like a podcast would */ + const compressed = min + (max - min) * normalised; + /** sub-pixel precision is pointless data to send over the wire */ + const rounded = Math.round(compressed); + + yield rounded; + } } type Theme = { @@ -129,7 +74,10 @@ export const WaveForm = ({ ...props }: Props) => { // memoise the waveform data so they aren't recalculated on every render - const barHeights = useMemo(() => generateWaveform(src, bars), [src, bars]); + const barHeights = useMemo( + () => Array.from(waveformGenerator(src, bars, [60, 100])), + [src, bars], + ); const totalWidth = useMemo( () => bars * (barWidth + gap) - gap, [bars, barWidth, gap], From 8a8baa7fb09ffc5654bbcac0df59d30a5b4a871c Mon Sep 17 00:00:00 2001 From: Max Duval Date: Fri, 25 Oct 2024 14:22:50 +0100 Subject: [PATCH 2/2] docs: keep mostly as-is --- .../AudioPlayer/components/WaveForm.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/dotcom-rendering/src/components/AudioPlayer/components/WaveForm.tsx b/dotcom-rendering/src/components/AudioPlayer/components/WaveForm.tsx index 20437d5a47..a1bf35b8e9 100644 --- a/dotcom-rendering/src/components/AudioPlayer/components/WaveForm.tsx +++ b/dotcom-rendering/src/components/AudioPlayer/components/WaveForm.tsx @@ -1,13 +1,18 @@ import { useId, useMemo } from 'react'; /** - * Create a [`Generator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator) for deterministic series of numbers - * which are guaranteed to be contained inside a specific range. - * - * Based on a pseudo random number generator generator ([linear congruential + * Pseudo random number generator generator ([linear congruential * generator](https://en.wikipedia.org/wiki/Linear_congruential_generator)). - * Seeding with an audio file URL ensures that a specific file always - * returns the same (fake) series of bars. + * + * 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 a string. + * + * It's deterministic, so calls to the function it returns will always return + * the same results, given the same seed. + * + * Copilot helped me with it... + * + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator */ function* waveformGenerator( /** the URL of the audio source file */