function mel_filter_bank(
    num_frequency_bins,
    num_mel_filters,
    min_frequency,
    max_frequency,
    sampling_rate,
    norm = null,
    mel_scale = "htk",
    triangularize_in_mel_space = false,
) {
    if (norm !== null && norm !== "slaney") {
        throw new Error('norm must be one of null or "slaney"');
    }

    if (num_frequency_bins < 2) {
        throw new Error(`Require num_frequency_bins: ${num_frequency_bins} >= 2`);
    }

    if (min_frequency > max_frequency) {
        throw new Error(`Require min_frequency: ${min_frequency} <= max_frequency: ${max_frequency}`);
    }

    const mel_min = hertz_to_mel(min_frequency, mel_scale);
    const mel_max = hertz_to_mel(max_frequency, mel_scale);
    const mel_freqs = linspace(mel_min, mel_max, num_mel_filters + 2);

    let filter_freqs = mel_to_hertz(mel_freqs, mel_scale);
    let fft_freqs; // frequencies of FFT bins in Hz

    if (triangularize_in_mel_space) {
        const fft_bin_width = sampling_rate / ((num_frequency_bins - 1) * 2);
        fft_freqs = hertz_to_mel(Float64Array.from({ length: num_frequency_bins }, (_, i) => i * fft_bin_width), mel_scale);
        filter_freqs = mel_freqs;
    } else {
        fft_freqs = linspace(0, Math.floor(sampling_rate / 2), num_frequency_bins);
    }

    const mel_filters = _create_triangular_filter_bank(fft_freqs, filter_freqs);

    if (norm !== null && norm === "slaney") {
        // Slaney-style mel is scaled to be approx constant energy per channel
        for (let i = 0; i < num_mel_filters; ++i) {
            const filter = mel_filters[i];
            const enorm = 2.0 / (filter_freqs[i + 2] - filter_freqs[i]);
            for (let j = 0; j < num_frequency_bins; ++j) {
                // Apply this enorm to all frequency bins
                filter[j] *= enorm;
            }
        }
    }

    // TODO warn if there is a zero row

    return mel_filters;

}
Creates a frequency bin conversion matrix used to obtain a mel spectrogram. This is called a *mel filter bank*, and
various implementation exist, which differ in the number of filters, the shape of the filters, the way the filters
are spaced, the bandwidth of the filters, and the manner in which the spectrum is warped. The goal of these
features is to approximate the non-linear human perception of the variation in pitch with respect to the frequency.
@param {number} num_frequency_bins Number of frequency bins (should be the same as `n_fft // 2 + 1`
where `n_fft` is the size of the Fourier Transform used to compute the spectrogram).
@param {number} num_mel_filters Number of mel filters to generate.
@param {number} min_frequency Lowest frequency of interest in Hz.
@param {number} max_frequency Highest frequency of interest in Hz. This should not exceed `sampling_rate / 2`.
@param {number} sampling_rate Sample rate of the audio waveform.
@param {string} [norm] If `"slaney"`, divide the triangular mel weights by the width of the mel band (area normalization).
@param {string} [mel_scale] The mel frequency scale to use, `"htk"` or `"slaney"`.
@param {boolean} [triangularize_in_mel_space] If this option is enabled, the triangular filter is applied in mel space rather than frequency space.
This should be set to `true` in order to get the same results as `torchaudio` when computing mel filters.
@returns {number[][]} Triangular filter bank matrix, which is a 2D array of shape (`num_frequency_bins`, `num_mel_filters`).
This is a projection matrix to go from a spectrogram to a mel spectrogram.