From f257ac3f7f70f4b15c3e7026fc712d41eb462306 Mon Sep 17 00:00:00 2001 From: David Lepaux Date: Fri, 9 Feb 2024 17:18:09 +0100 Subject: [PATCH] chore: flixify function parameters --- github-pages/metadata.ts | 30 +++++++++++ processor/realtime-bpm-processor.ts | 4 +- src/analyzer.ts | 80 ++++++++++++++++++----------- src/realtime-bpm-analyzer.ts | 35 ++++++++----- src/types.ts | 33 +++++++++++- src/utils.ts | 6 +++ tests/lib/analyzer.ts | 4 +- tests/lib/realtime-bpm-analyzer.ts | 16 +++--- tests/utils.ts | 2 + 9 files changed, 153 insertions(+), 57 deletions(-) diff --git a/github-pages/metadata.ts b/github-pages/metadata.ts index bb99334..66ac2e3 100644 --- a/github-pages/metadata.ts +++ b/github-pages/metadata.ts @@ -45,6 +45,26 @@ export const data: Array<{path: string; title: string; description: string}> = [ title: 'Type AnalyzeChunkEventMessage', description: 'RealTimeBpmAnalyzer AnalyzeChunkEventMessage Type', }, + { + path: 'types/AnalyzerComputeBpmOptions.html', + title: 'Type AnalyzerComputeBpmOptions', + description: 'RealTimeBpmAnalyzer AnalyzerComputeBpmOptions Type', + }, + { + path: 'types/AnalyzerFindPeaksAtTheshold.html', + title: 'Type AnalyzerFindPeaksAtTheshold', + description: 'RealTimeBpmAnalyzer AnalyzerFindPeaksAtTheshold Type', + }, + { + path: 'types/AnalyzerFindPeaksOptions.html', + title: 'Type AnalyzerFindPeaksOptions', + description: 'RealTimeBpmAnalyzer AnalyzerFindPeaksOptions Type', + }, + { + path: 'types/AnalyzerGroupByTempoOptions.html', + title: 'Type AnalyzerGroupByTempoOptions', + description: 'RealTimeBpmAnalyzer AnalyzerGroupByTempoOptions Type', + }, { path: 'types/AnalyzerResetedEvent.html', title: 'Type AnalyzerResetedEvent', @@ -140,6 +160,11 @@ export const data: Array<{path: string; title: string; description: string}> = [ title: 'Type PostMessageEvents', description: 'RealTimeBpmAnalyzer PostMessageEvents Type', }, + { + path: 'types/RealtimeAnalyzeChunkOptions.html', + title: 'Type RealtimeAnalyzeChunkOptions', + description: 'RealTimeBpmAnalyzer RealtimeAnalyzeChunkOptions Type', + }, { path: 'types/RealTimeBpmAnalyzerOptions.html', title: 'Type RealTimeBpmAnalyzerOptions', @@ -150,6 +175,11 @@ export const data: Array<{path: string; title: string; description: string}> = [ title: 'Type RealTimeBpmAnalyzerParameters', description: 'RealTimeBpmAnalyzer RealTimeBpmAnalyzerParameters Type', }, + { + path: 'types/RealtimeFindPeaksOptions.html', + title: 'Type RealtimeFindPeaksOptions', + description: 'RealTimeBpmAnalyzer RealtimeFindPeaksOptions Type', + }, { path: 'types/ResetEvent.html', title: 'Type ResetEvent', diff --git a/processor/realtime-bpm-processor.ts b/processor/realtime-bpm-processor.ts index 23a372c..b13b3e1 100644 --- a/processor/realtime-bpm-processor.ts +++ b/processor/realtime-bpm-processor.ts @@ -117,9 +117,9 @@ export class RealTimeBpmProcessor extends AudioWorkletProcessor { if (isBufferFull) { // The variable sampleRate is global ! thanks to the AudioWorkletProcessor - this.realTimeBpmAnalyzer.analyzeChunck(buffer, sampleRate, bufferSize, event => { + this.realTimeBpmAnalyzer.analyzeChunck({audioSampleRate: sampleRate, channelData: buffer, bufferSize, postMessage: event => { this.port.postMessage(event); - }).catch((error: unknown) => { + }}).catch((error: unknown) => { console.error(error); }); } diff --git a/src/analyzer.ts b/src/analyzer.ts index af51e39..842a23c 100644 --- a/src/analyzer.ts +++ b/src/analyzer.ts @@ -7,20 +7,30 @@ import type { Interval, Tempo, Threshold, + AnalyzerFindPeaksOptions, + AnalyzerGroupByTempoOptions, + AnalyzerComputeBpmOptions, BiquadFilterOptions, + AnalyzerFindPeaksAtTheshold, } from './types'; import * as consts from './consts'; import * as utils from './utils'; /** * Find peaks when the signal if greater than the threshold, then move 10_000 indexes (represents ~0.23s) to ignore the descending phase of the parabol - * @param data Buffer channel data - * @param threshold Threshold for qualifying as a peak - * @param offset Position where we start to loop - * @param skipForwardIndexes Numbers of index to skip when a peak is detected + * @param options - AnalyzerFindPeaksAtTheshold + * @param options.audioSampleRate - Sample rate + * @param options.data - Buffer channel data + * @param options.threshold - Threshold for qualifying as a peak + * @param options.offset - Position where we start to loop * @returns Peaks found that are greater than the threshold */ -export function findPeaksAtThreshold(data: Float32Array, threshold: Threshold, audioSampleRate: number, offset = 0): PeaksAndThreshold { +export function findPeaksAtThreshold({ + audioSampleRate, + data, + threshold, + offset = 0, +}: AnalyzerFindPeaksAtTheshold): PeaksAndThreshold { const peaks: Peaks = []; const skipForwardIndexes = utils.computeIndexesToSkip(0.25, audioSampleRate); @@ -48,15 +58,20 @@ export function findPeaksAtThreshold(data: Float32Array, threshold: Threshold, a /** * Find the minimum amount of peaks from top to bottom threshold, it's necessary to analyze at least 10seconds at 90bpm - * @param channelData Channel data + * @param options - AnalyzerFindPeaksOptions + * @param options.audioSampleRate - Sample rate + * @param options.channelData - Channel data * @returns Suffisent amount of peaks in order to continue further the process */ -export async function findPeaks(channelData: Float32Array, audioSampleRate: number): Promise { +export async function findPeaks({ + audioSampleRate, + channelData, +}: AnalyzerFindPeaksOptions): Promise { let validPeaks: Peaks = []; let validThreshold = 0; await descendingOverThresholds(async threshold => { - const {peaks} = findPeaksAtThreshold(channelData, threshold, audioSampleRate); + const {peaks} = findPeaksAtThreshold({audioSampleRate, data: channelData, threshold}); /** * Loop over peaks @@ -81,8 +96,8 @@ export async function findPeaks(channelData: Float32Array, audioSampleRate: numb * Helpfull function to create standard and shared lowpass and highpass filters * Important Note: The original library wasn't using properly the lowpass filter and it was not applied at all. * This method should not be used unitl more research and documented tests will be acheived. - * @param context AudioContext instance - * @param options Optionnal BiquadFilterOptions + * @param context - AudioContext instance + * @param options - Optionnal BiquadFilterOptions * @returns BiquadFilterNode */ export function getBiquadFilter(context: OfflineAudioContext | AudioContext, options?: BiquadFilterOptions): BiquadFilterNode { @@ -97,8 +112,8 @@ export function getBiquadFilter(context: OfflineAudioContext | AudioContext, opt /** * Apply to the source a biquad lowpass filter - * @param buffer Audio buffer - * @param options Optionnal BiquadFilterOptions + * @param buffer - Audio buffer + * @param options - Optionnal BiquadFilterOptions * @returns A Promise that resolves an AudioBuffer instance */ export async function getOfflineLowPassSource(buffer: AudioBuffer, options?: BiquadFilterOptions): Promise { @@ -128,11 +143,15 @@ export async function getOfflineLowPassSource(buffer: AudioBuffer, options?: Biq /** * Return the computed bpm from data - * @param data Contain valid peaks - * @param audioSampleRate Audio sample rate + * @param options - AnalyzerComputeBpmOptions + * @param options.data - Contain valid peaks + * @param options.audioSampleRate - Audio sample rate * @returns A Promise that resolves BPM Candidates */ -export async function computeBpm(data: ValidPeaks, audioSampleRate: number): Promise { +export async function computeBpm({ + audioSampleRate, + data, +}: AnalyzerComputeBpmOptions): Promise { const minPeaks = consts.minPeaks; /** @@ -156,7 +175,7 @@ export async function computeBpm(data: ValidPeaks, audioSampleRate: number): Pro if (hasPeaks && foundThreshold) { const intervals = identifyIntervals(data[foundThreshold]); - const tempos = groupByTempo(audioSampleRate, intervals); + const tempos = groupByTempo({audioSampleRate, intervalCounts: intervals}); const candidates = getTopCandidates(tempos); const bpmCandidates: BpmCandidates = { @@ -175,8 +194,8 @@ export async function computeBpm(data: ValidPeaks, audioSampleRate: number): Pro /** * Sort results by count and return top candidate - * @param candidates BPMs with count - * @param length Amount of returned candidates (default: 5) + * @param candidates - BPMs with count + * @param length - Amount of returned candidates (default: 5) * @returns Returns the 5 top candidates with highest counts */ export function getTopCandidates(candidates: Tempo[], length = 5): Tempo[] { @@ -185,7 +204,7 @@ export function getTopCandidates(candidates: Tempo[], length = 5): Tempo[] { /** * Gets the top candidate from the array - * @param candidates BPMs with counts. + * @param candidates - BPMs with counts. * @returns Returns the top candidate with the highest count. */ export function getTopCandidate(candidates: Tempo[]): number { @@ -200,7 +219,7 @@ export function getTopCandidate(candidates: Tempo[]): number { /** * Identify intervals between bass peaks - * @param peaks Array of qualified bass peaks + * @param peaks - Array of qualified bass peaks * @returns Return a collection of intervals between peaks */ export function identifyIntervals(peaks: Peaks): Interval[] { @@ -242,11 +261,15 @@ export function identifyIntervals(peaks: Peaks): Interval[] { /** * Figure out best possible tempo candidates - * @param audioSampleRate Audio sample rate - * @param intervalCounts List of identified intervals + * @param options - AnalyzerGroupByTempoOptions + * @param options.audioSampleRate - Audio sample rate + * @param options.intervalCounts - List of identified intervals * @returns Intervals grouped with similar values */ -export function groupByTempo(audioSampleRate: number, intervalCounts: Interval[]): Tempo[] { +export function groupByTempo({ + audioSampleRate, + intervalCounts, +}: AnalyzerGroupByTempoOptions): Tempo[] { const tempoCounts: Tempo[] = []; for (const intervalCount of intervalCounts) { @@ -310,18 +333,17 @@ export function groupByTempo(audioSampleRate: number, intervalCounts: Interval[] } /** - * Function to detect the BPM from an AudioBuffer (which can be a whole file) - * It is the fastest way to detect the BPM - * @param originalBuffer AudioBuffer - * @param options BiquadFilterOptions + * Fastest way to detect the BPM from an AudioBuffer + * @param originalBuffer - AudioBuffer + * @param options - BiquadFilterOptions * @returns Returns the best candidates */ export async function analyzeFullBuffer(originalBuffer: AudioBuffer, options?: BiquadFilterOptions): Promise { const buffer = await getOfflineLowPassSource(originalBuffer, options); const channelData = buffer.getChannelData(0); - const {peaks} = await findPeaks(channelData, buffer.sampleRate); + const {peaks} = await findPeaks({audioSampleRate: buffer.sampleRate, channelData}); const intervals = identifyIntervals(peaks); - const tempos = groupByTempo(buffer.sampleRate, intervals); + const tempos = groupByTempo({audioSampleRate: buffer.sampleRate, intervalCounts: intervals}); const topCandidates = getTopCandidates(tempos); return topCandidates; diff --git a/src/realtime-bpm-analyzer.ts b/src/realtime-bpm-analyzer.ts index 7826506..d021f20 100644 --- a/src/realtime-bpm-analyzer.ts +++ b/src/realtime-bpm-analyzer.ts @@ -6,8 +6,8 @@ import type { NextIndexPeaks, BpmCandidates, Threshold, - FindPeaksOptions, - PostMessageEvents, + RealtimeFindPeaksOptions, + RealtimeAnalyzeChunkOptions, } from './types'; import { generateValidPeaksModel, @@ -85,7 +85,7 @@ export class RealTimeBpmAnalyzer { /** * Remve all validPeaks between the minThreshold pass in param to optimize the weight of datas - * @param minThreshold Value between 0.9 and 0.2 + * @param minThreshold - Value between 0.9 and 0.2 */ async clearValidPeaks(minThreshold: Threshold): Promise { this.minValidThreshold = Number.parseFloat(minThreshold.toFixed(2)); @@ -102,12 +102,13 @@ export class RealTimeBpmAnalyzer { /** * Attach this function to an audioprocess event on a audio/video node to compute BPM / Tempo in realtime - * @param channelData Channel data - * @param audioSampleRate Audio sample rate (44100) - * @param bufferSize Buffer size (4096) - * @param postMessage Function to post a message to the processor node + * @param options - RealtimeAnalyzeChunkOptions + * @param options.audioSampleRate - Audio sample rate (44100) + * @param options.channelData - Channel data + * @param options.bufferSize - Buffer size (4096) + * @param options.postMessage - Function to post a message to the processor node */ - async analyzeChunck(channelData: Float32Array, audioSampleRate: number, bufferSize: number, postMessage: (message: PostMessageEvents) => void): Promise { + async analyzeChunck({audioSampleRate, channelData, bufferSize, postMessage}: RealtimeAnalyzeChunkOptions): Promise { if (this.options.debug) { postMessage({message: 'ANALYZE_CHUNK', data: channelData}); } @@ -132,9 +133,9 @@ export class RealTimeBpmAnalyzer { * Mutate nextIndexPeaks and validPeaks if possible */ await this.findPeaks({ + audioSampleRate, channelData, bufferSize, - audioSampleRate, currentMinIndex, currentMaxIndex, postMessage, @@ -145,7 +146,7 @@ export class RealTimeBpmAnalyzer { */ this.skipIndexes++; - const data: BpmCandidates = await computeBpm(this.validPeaks, audioSampleRate); + const data: BpmCandidates = await computeBpm({audioSampleRate, data: this.validPeaks}); const {threshold} = data; postMessage({message: 'BPM', data}); @@ -168,16 +169,22 @@ export class RealTimeBpmAnalyzer { /** * Find the best threshold with enought peaks - * @param options Find Peaks Options + * @param options - Options for finding peaks + * @param options.audioSampleRate - Sample rate + * @param options.channelData - Channel data + * @param options.bufferSize - Buffer size + * @param options.currentMinIndex - Current minimum index + * @param options.currentMaxIndex - Current maximum index + * @param options.postMessage - Function to post a message to the processor node */ async findPeaks({ + audioSampleRate, channelData, bufferSize, - audioSampleRate, currentMinIndex, currentMaxIndex, postMessage, - }: FindPeaksOptions): Promise { + }: RealtimeFindPeaksOptions): Promise { await descendingOverThresholds(async threshold => { if (this.nextIndexPeaks[threshold] >= currentMaxIndex) { return false; @@ -188,7 +195,7 @@ export class RealTimeBpmAnalyzer { */ const offsetForNextPeak = this.nextIndexPeaks[threshold] % bufferSize; // 0 - 4095 - const {peaks, threshold: atThreshold} = findPeaksAtThreshold(channelData, threshold, audioSampleRate, offsetForNextPeak); + const {peaks, threshold: atThreshold} = findPeaksAtThreshold({audioSampleRate, data: channelData, threshold, offset: offsetForNextPeak}); /** * Loop over peaks diff --git a/src/types.ts b/src/types.ts index 5ceaa1c..0b257ca 100644 --- a/src/types.ts +++ b/src/types.ts @@ -80,15 +80,44 @@ export type RealTimeBpmAnalyzerOptions = { debug: boolean; }; -export type FindPeaksOptions = { +export type AnalyzerGroupByTempoOptions = { + audioSampleRate: number; + intervalCounts: Interval[]; +}; + +export type AnalyzerFindPeaksOptions = { + audioSampleRate: number; channelData: Float32Array; - bufferSize: number; +}; + +export type AnalyzerComputeBpmOptions = { + audioSampleRate: number; + data: ValidPeaks; +}; + +export type AnalyzerFindPeaksAtTheshold = { + audioSampleRate: number; + data: Float32Array; + threshold: Threshold; + offset?: number; +}; + +export type RealtimeFindPeaksOptions = { audioSampleRate: number; + channelData: Float32Array; + bufferSize: number; currentMinIndex: number; currentMaxIndex: number; postMessage: (data: PostMessageEvents) => void; }; +export type RealtimeAnalyzeChunkOptions = { + audioSampleRate: number; + channelData: Float32Array; + bufferSize: number; + postMessage: (data: PostMessageEvents) => void; +}; + export type ValidPeaks = Record; export type NextIndexPeaks = Record; diff --git a/src/utils.ts b/src/utils.ts index 8b09c0b..7490b24 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -123,6 +123,12 @@ export function chunckAggregator(): (pcmData: Float32Array) => AggregateData { }; } +/** + * Computes the number of indexes we need to skip based on sampleRate + * @param durationSeconds Duration expressed in seconds + * @param sampleRate Sample rate, typically 48000, 441000, etc + * @returns The number of indexes we need to skip + */ export function computeIndexesToSkip(durationSeconds: number, sampleRate: number): number { return Math.round(durationSeconds * sampleRate); } diff --git a/tests/lib/analyzer.ts b/tests/lib/analyzer.ts index 9048d42..92b9241 100644 --- a/tests/lib/analyzer.ts +++ b/tests/lib/analyzer.ts @@ -10,7 +10,7 @@ describe('Analyzer - Unit tests', () => { it('should not find peaks from empty channel data', async () => { const audioContext = new AudioContext(); const channelData = new Float32Array([0, 0, 0, 0]); - const {peaks, threshold} = await analyzer.findPeaks(channelData, audioContext.sampleRate); + const {peaks, threshold} = await analyzer.findPeaks({audioSampleRate: audioContext.sampleRate, channelData}); expect(threshold).to.be.equal(0); expect(peaks.length).to.be.equal(0); }); @@ -29,7 +29,7 @@ describe('Analyzer - Unit tests', () => { it('should not compute BPM from empty array of peaks', async () => { const audioContext = new AudioContext(); const validPeaks = utils.generateValidPeaksModel(); - const {threshold, bpm} = await analyzer.computeBpm(validPeaks, audioContext.sampleRate); + const {threshold, bpm} = await analyzer.computeBpm({audioSampleRate: audioContext.sampleRate, data: validPeaks}); expect(threshold).to.be.equal(0.2); expect(bpm.length).to.be.equal(0); }); diff --git a/tests/lib/realtime-bpm-analyzer.ts b/tests/lib/realtime-bpm-analyzer.ts index 3cc1c0f..dbdc08b 100644 --- a/tests/lib/realtime-bpm-analyzer.ts +++ b/tests/lib/realtime-bpm-analyzer.ts @@ -16,12 +16,12 @@ describe('RealTimeBpmAnalyzer - Integration tests', () => { const audioContext = new AudioContext(); const realTimeBpmAnalyzer = new RealTimeBpmAnalyzer(); const bufferSize = 4096; - const channelData = await readChannelDataToChunk(audioContext, bufferSize); + const chunks = await readChannelDataToChunk(audioContext, bufferSize); - for (const chunk of channelData) { - await realTimeBpmAnalyzer.analyzeChunck(chunk, audioContext.sampleRate, bufferSize, (data: PostMessageEvents) => { + for (const channelData of chunks) { + await realTimeBpmAnalyzer.analyzeChunck({audioSampleRate: audioContext.sampleRate, channelData, bufferSize, postMessage(data: PostMessageEvents) { // TODO: Do something - }); + }}); } expect(realTimeBpmAnalyzer).to.be.instanceOf(RealTimeBpmAnalyzer); @@ -36,12 +36,12 @@ describe('RealTimeBpmAnalyzer - Integration tests', () => { }); const bufferSize = 4096; - const channelData = await readChannelDataToChunk(audioContext, bufferSize); + const chunks = await readChannelDataToChunk(audioContext, bufferSize); - for (const chunk of channelData) { - await realTimeBpmAnalyzer.analyzeChunck(chunk, audioContext.sampleRate, bufferSize, (data: PostMessageEvents) => { + for (const channelData of chunks) { + await realTimeBpmAnalyzer.analyzeChunck({audioSampleRate: audioContext.sampleRate, channelData, bufferSize, postMessage(data: PostMessageEvents) { // TODO: Do something - }); + }}); } expect(realTimeBpmAnalyzer).to.be.instanceOf(RealTimeBpmAnalyzer); diff --git a/tests/utils.ts b/tests/utils.ts index 753a9a7..90f62be 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,5 +1,6 @@ /** * Reads the fixtures channelDataJson + * @param audioContext AudioContext instance * @returns A Float32Array */ export async function readChannelData(audioContext: AudioContext): Promise { @@ -11,6 +12,7 @@ export async function readChannelData(audioContext: AudioContext): Promise