Skip to content

Commit

Permalink
Merge pull request #71 from dlepaux/feature/flexify-parameters
Browse files Browse the repository at this point in the history
chore: flixify function parameters
  • Loading branch information
dlepaux committed Feb 9, 2024
2 parents 051710d + f257ac3 commit a4b1f2a
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 57 deletions.
30 changes: 30 additions & 0 deletions github-pages/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand Down
4 changes: 2 additions & 2 deletions processor/realtime-bpm-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
}
Expand Down
80 changes: 51 additions & 29 deletions src/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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<PeaksAndThreshold> {
export async function findPeaks({
audioSampleRate,
channelData,
}: AnalyzerFindPeaksOptions): Promise<PeaksAndThreshold> {
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
Expand All @@ -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 {
Expand All @@ -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<AudioBuffer> {
Expand Down Expand Up @@ -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<BpmCandidates> {
export async function computeBpm({
audioSampleRate,
data,
}: AnalyzerComputeBpmOptions): Promise<BpmCandidates> {
const minPeaks = consts.minPeaks;

/**
Expand All @@ -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 = {
Expand All @@ -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[] {
Expand All @@ -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 {
Expand All @@ -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[] {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<Tempo[]> {
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;
Expand Down
35 changes: 21 additions & 14 deletions src/realtime-bpm-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import type {
NextIndexPeaks,
BpmCandidates,
Threshold,
FindPeaksOptions,
PostMessageEvents,
RealtimeFindPeaksOptions,
RealtimeAnalyzeChunkOptions,
} from './types';
import {
generateValidPeaksModel,
Expand Down Expand Up @@ -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<void> {
this.minValidThreshold = Number.parseFloat(minThreshold.toFixed(2));
Expand All @@ -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<void> {
async analyzeChunck({audioSampleRate, channelData, bufferSize, postMessage}: RealtimeAnalyzeChunkOptions): Promise<void> {
if (this.options.debug) {
postMessage({message: 'ANALYZE_CHUNK', data: channelData});
}
Expand All @@ -132,9 +133,9 @@ export class RealTimeBpmAnalyzer {
* Mutate nextIndexPeaks and validPeaks if possible
*/
await this.findPeaks({
audioSampleRate,
channelData,
bufferSize,
audioSampleRate,
currentMinIndex,
currentMaxIndex,
postMessage,
Expand All @@ -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});

Expand All @@ -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<void> {
}: RealtimeFindPeaksOptions): Promise<void> {
await descendingOverThresholds(async threshold => {
if (this.nextIndexPeaks[threshold] >= currentMaxIndex) {
return false;
Expand All @@ -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
Expand Down
33 changes: 31 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Peaks>;

export type NextIndexPeaks = Record<string, number>;
Expand Down
6 changes: 6 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Loading

0 comments on commit a4b1f2a

Please sign in to comment.