import FFT from "fft.js" ; import { ceilPow2 } from "./util" ; /**
 * One of the supported buffer types. Other numeric array types may not work
 * correctly.
 */ export type Buffer = Float32Array | Float64Array | number [ ] ; typeParam export class Autocorrelator < T extends Buffer > { private readonly _inputLength : number ; private _fft : FFT ; private _bufferSupplier : ( size : number ) => T ; private _paddedInputBuffer : T ; private _transformBuffer : T ; private _inverseBuffer : T ; /**
     * A helper method to create an  Autocorrelator using  Float32Array buffers.
     *
     *  param inputLength static forFloat32Array ( this , inputLength ) { return new Autocorrelator ( inputLength , ( length ) => new Float32Array ( length ) ) ; } /**
     * A helper method to create an  Autocorrelator using  Float64Array buffers.
     *
     *  param inputLength static forFloat64Array ( this , inputLength ) { return new Autocorrelator ( inputLength , ( length ) => new Float64Array ( length ) ) ; } /**
     * A helper method to create an  Autocorrelator using `number[]` buffers.
     *
     *  param inputLength static forNumberArray ( this , inputLength ) { return new Autocorrelator ( inputLength , ( length ) => Array < number > ( length ) ) ; } /**
     * Constructs a new  Autocorrelator able to handle input arrays of the
     * given length.
     *
     *  param inputLength param bufferSupplier constructor ( inputLength , bufferSupplier ) { if ( inputLength < 1 ) { throw new Error ( `Input length must be at least one` ) ; } this . _inputLength = inputLength ; this . _fft = new FFT ( ceilPow2 ( 2 * inputLength ) ) ; this . _bufferSupplier = bufferSupplier ; this . _paddedInputBuffer = this . _bufferSupplier ( this . _fft . size ) ; this . _transformBuffer = this . _bufferSupplier ( 2 * this . _fft . size ) ; this . _inverseBuffer = this . _bufferSupplier ( 2 * this . _fft . size ) ; } returns get inputLength ( ) : number { return this . _inputLength ; } param input param output returns autocorrelate ( input , output = this . _bufferSupplier ( input . length ) ) { if ( input . length !== this . _inputLength ) { throw new Error ( `Input must have length ${ this . _inputLength } but had length ${ input . length }` ) ; } for ( let i = 0 ; i < input . length ; i ++ ) { this . _paddedInputBuffer [ i ] = input [ i ] ; } for ( let i = input . length ; i < this . _paddedInputBuffer . length ; i ++ ) { this . _paddedInputBuffer [ i ] = 0 ; } this . _fft . realTransform ( this . _transformBuffer , this . _paddedInputBuffer ) ; this . _fft . completeSpectrum ( this . _transformBuffer ) ; const tb = this . _transformBuffer ; for ( let i = 0 ; i < tb . length ; i += 2 ) { tb [ i ] = tb [ i ] * tb [ i ] + tb [ i + 1 ] * tb [ i + 1 ] ; tb [ i + 1 ] = 0 ; } this . _fft . inverseTransform ( this . _inverseBuffer , this . _transformBuffer ) ; for ( let i = 0 ; i < input . length ; i ++ ) { output [ i ] = this . _inverseBuffer [ 2 * i ] ; } return output ; } } /**
 * Returns an array of all the key maximum positions in the given input array.
 *
 * In McLeod's paper, a key maximum is the highest maximum between a positively
 * sloped zero crossing and a negatively sloped one.
 *
 * TODO: it may be more efficient not to construct a new output array each time,
 * but that would also make the code more complicated (more so than the changes
 * that were needed to remove the other allocations).
 */ function getKeyMaximumIndices ( input ) { const keyIndices = [ ] ; let lookingForMaximum = false ; let max = - Infinity ; let maxIndex = - 1 ; for ( let i = 1 ; i < input . length - 1 ; i ++ ) { if ( input [ i - 1 ] <= 0 && input [ i ] > 0 ) { lookingForMaximum = true ; maxIndex = i ; max = input [ i ] ; } else if ( input [ i - 1 ] > 0 && input [ i ] <= 0 ) { lookingForMaximum = false ; if ( maxIndex !== - 1 ) { keyIndices . push ( maxIndex ) ; } } else if ( lookingForMaximum && input [ i ] > max ) { max = input [ i ] ; maxIndex = i ; } } return keyIndices ; } param index param data returns function refineResultIndex ( index , data ) { const [ x0 , x1 , x2 ] = [ index - 1 , index , index + 1 ] ; const [ y0 , y1 , y2 ] = [ data [ x0 ] , data [ x1 ] , data [ x2 ] ] ; const a = y0 / 2 - y1 + y2 / 2 ; const b = - ( y0 / 2 ) * ( x1 + x2 ) + y1 * ( x0 + x2 ) - ( y2 / 2 ) * ( x0 + x1 ) ; const c = ( y0 * x1 * x2 ) / 2 - y1 * x0 * x2 + ( y2 * x0 * x1 ) / 2 ; const xMax = - b / ( 2 * a ) ; const yMax = a * xMax * xMax + b * xMax + c ; return [ xMax , yMax ] ; } typeParam export class PitchDetector < T extends Buffer > { private _autocorrelator : Autocorrelator < T > ; private _nsdfBuffer : T ; private readonly _clarityThreshold = 0.9 ; /**
     * A helper method to create an  PitchDetector using  Float32Array buffers.
     *
     *  param inputLength static forFloat32Array ( this , inputLength ) { return new PitchDetector ( inputLength , ( length ) => new Float32Array ( length ) ) ; } /**
     * A helper method to create an  PitchDetector using  Float64Array buffers.
     *
     *  param inputLength static forFloat64Array ( this , inputLength ) { return new PitchDetector ( inputLength , ( length ) => new Float64Array ( length ) ) ; } /**
     * A helper method to create an  PitchDetector using `number[]` buffers.
     *
     *  param inputLength static forNumberArray ( this , inputLength ) { return new PitchDetector ( inputLength , ( length ) => Array < number > ( length ) ) ; } /**
     * Constructs a new  PitchDetector able to handle input arrays of the
     * given length.
     *
     *  param inputLength param bufferSupplier constructor ( inputLength , bufferSupplier ) { this . _autocorrelator = new Autocorrelator ( inputLength , bufferSupplier ) ; this . _nsdfBuffer = bufferSupplier ( inputLength ) ; } returns get inputLength ( ) : number { return this . _autocorrelator . inputLength ; } param input param sampleRate returns findPitch ( input , sampleRate ) { this . _nsdf ( input ) ; const keyMaximumIndices = getKeyMaximumIndices ( this . _nsdfBuffer ) ; if ( keyMaximumIndices . length === 0 ) { return [ 0 , 0 ] ; } const nMax = Math . max ( ... keyMaximumIndices . map ( ( i ) => this . _nsdfBuffer [ i ] ) ) ; const resultIndex = keyMaximumIndices . find ( ( i ) => this . _nsdfBuffer [ i ] >= this . _clarityThreshold * nMax ) ! ; const [ refinedResultIndex , clarity ] = refineResultIndex ( resultIndex , this . _nsdfBuffer ) ; return [ sampleRate / refinedResultIndex , Math . min ( clarity , 1.0 ) ] ; } /**
     * Computes the NSDF of the input and stores it in the internal buffer. This
     * is equation (9) in the McLeod pitch method paper.
     */ private _nsdf ( input ) { this . _autocorrelator . autocorrelate ( input , this . _nsdfBuffer ) ; let m = 2 * this . _nsdfBuffer [ 0 ] ; let i ; for ( i = 0 ; i < this . _nsdfBuffer . length && m > 0 ; i ++ ) { this . _nsdfBuffer [ i ] = ( 2 * this . _nsdfBuffer [ i ] ) / m ; m -= input [ i ] ** 2 + input [ input . length - i - 1 ] ** 2 ; } for ( ; i < this . _nsdfBuffer . length ; i ++ ) { this . _nsdfBuffer [ i ] = 0 ; } } }