Search code examples
c#audiofilteringsignal-processingnaudio

How can I create narrow band noise in C#?


My problem is very spesific. I want to create narrow band noise for my small wpf application. I am using NAudio library for creating a infinite stream of noise that can be stopped and started by user. I created Tone(Simple sinus wave), Warble(Sinus wave that is modulated by another wave) and White noise.

This is the class I use in order to make any ISampleProvider that is stereo be able to give sound to left, right or both depending what user wants.

using NAudio.Wave;
using System;

namespace AppForSoundCard
{
    public class SignalStereoProvider : ISampleProvider
    {
        private readonly ISampleProvider sample;
        public WaveFormat WaveFormat => sample.WaveFormat;
        public float LeftVolume { get; set; }
        public float RightVolume { get; set; }

        public SignalStereoProvider(ISampleProvider sample)
        {
            if (sample.WaveFormat.Channels != 2)
                throw new ArgumentException("Source sample provider must be stereo");
            this.sample = sample;
        }

        public int Read(float[] buffer, int offset, int count)
        {
            int samplesRead = sample.Read(buffer, offset, count);

            for (int n = 0; n < count; n += 2)
            {
                buffer[offset + n] *= LeftVolume;
                buffer[offset + n + 1] *= RightVolume;
            }

            return samplesRead;
        }
    }
}

I use this code to generate audio stream that I mentioned. You will ask what is NarrowBand32 is. It is the class that supposed to generate narrowband noise from white noise. I will write it's code after next paragraph.

signalGenerator = new SignalGenerator();
signalGenerator.Type = SignalGeneratorType.White;
signalGenerator.Gain = 1.0;
signalGenerator.Frequency = Frequency;
narrowBand = new NarrowBandProvider32(signalGenerator, Frequency, 96000, 100, dB);
stereoProvider = new SignalStereoProvider(narrowBand)
{
    RightVolume = !((ComboBoxItem)RoutingCombobox.SelectedItem).Tag.ToString().Equals("Left") ? (float)Math.Pow(10, (dB - 80) / 20.0) * (float)Settings.Default["ReferenceAmplitudeFor" + Frequency.ToString()] : 0.0f,
    LeftVolume = !((ComboBoxItem)RoutingCombobox.SelectedItem).Tag.ToString().Equals("Right") ? (float)Math.Pow(10, (dB - 80) / 20.0) * (float)Settings.Default["ReferenceAmplitudeFor" + Frequency.ToString()] : 0.0f
};
Output.Init(stereoProvider);

I first generate an white noise using NAudio's SignalGenerator class. Then give it to the NarrowbandProvider32 that I wrote. Which suppose to make white noise narrowband. After all that I make the sound either go left or right or both. Left and right volume is amplitute value for desibel value that is given by user. There is a combobox about routng in which you can choose, left, right, bilateral. Depending on your choise leftvolume is the apmlitute, right volume is the amplitute or both of them is amplitute.

I have visited several sites about how a narrowband noise can be generated. They were all suggesting bandpass filtering a white noise to generate narrow band noise. I tried that. It sort of did what I wanted but it was narrower than I wanted. You can find frequency response of the noise that I generated for 500 hz.

Here is the NarrowBand32 class code for that noise

using NAudio.Dsp;
using NAudio.Wave;
using System;

namespace AppForSoundCard
{
    class NarrowBandProvider32 : ISampleProvider
    {
        ISampleProvider sample;
        float lowFreq;
        float highFreq;
        BiQuadFilter biQuad;
        public WaveFormat WaveFormat => sample.WaveFormat;

        public NarrowBandProvider32(ISampleProvider sample, float frequency, float sampleRate, float q, float dB)
        {
            if (sample.WaveFormat.Channels != 2)
                throw new ArgumentException("Source sample provider must be stereo");

            this.sample = sample;
            //Low and High frequency variables are defined like this in audiometry.
            //these variables are the boundaries for narrowband noise
            lowFreq = (float)Math.Round(frequency / Math.Pow(2, 1.0 / 4.0)); 
            highFreq = (float)Math.Round(frequency * Math.Pow(2, 1.0 / 4.0));
            biQuad = BiQuadFilter.BandPassFilterConstantSkirtGain(sampleRate, frequency, q);
            biQuad.SetHighPassFilter(sampleRate, lowFreq, q);
            biQuad.SetLowPassFilter(sampleRate, highFreq, q);
        }

        public int Read(float[] buffer, int offset, int count)
        {
            int samplesRead = sample.Read(buffer, offset, count);


            for (int i = 0; i < samplesRead; i++)
                buffer[offset + i] = biQuad.Transform(buffer[offset + i]);

            return samplesRead;
        }
    }
}

Those the arguments I gave: narrowBand = new NarrowBandProvider32(signalGenerator, Frequency, 96000, 100, dB);

As I said this noise is close to the narrowband noise that is defined in audiometry but it is more narrow. Narrowband noise for 500 hz in audiometry has this frequency response.

As you can see it is more wide than the noise that I generated. How can I genereate a narrowband noise that is close to narrowband noise in audiometry for any hz. I only gave examples of 500 hz for the images but in my code you can generate a noise between 150hz to 8000hz. What filter should I use to filter white noise in order to generate that type of narrowband noise. Any help is appreciated.

Edit: I find a standart which explains how a narrowband noise should be for any frequency and desibel.

Where narrow-band masking is required, the noise band shall be centred geometrically around the test frequency. The band limits for the masking noise are given in Table 4. Outside these band limits the sound pressure spectrum density level of the noise shall fall at a rate of at least 12 dB per octave for at least three octaves and outside these three octaves it shall be at least 36 dB below the level at the centre frequency. Measurements are required in the range from 31,5 kHz to 10 kHz for instruments limited to 8 kHz. For EHF instruments measurements are required up to 20 kHz. Due to limitations of transducers, ear simulators, acoustic couplers and mechanical couplers, measurements of the bandwidth at 4 kHz and above may not accurately describe the spectrum of the masking noise. Therefore at centre frequencies above 3,15 kHz measurements shall be made electrically across the transducer terminals.

With that definition, I guess just an standart bandpass filter wouldn't work and I have to define a custom filter for the noise. Is there a C# library that allows defining custom filters. If there is how should I define the custom filter in order to make noises in that standart.


Solution

  • They were all suggesting bandpass filtering a white noise to generate narrow band noise. I tried that. It sort of did what I wanted but it was narrower than I wanted.

    The approach of applying a bandpass filter to a white noise source makes sense. The problem is just that the bandpass filter design is too narrow. You can make it wider by reducing the q, moving the lowFreq and highFreq a bit outward, or switching to a different filter design method.

    I suggest that rather than coding directly in C#, it might be useful to prototype this first in Python using the scipy.signal library, which has a various tools for designing and working with filters.

    In the code below, I vary the c parameter to tweak the low and high edges of the band.

    Code:

    # Copyright 2022 Google LLC.
    # SPDX-License-Identifier: Apache-2.0
    import matplotlib.pyplot as plt
    import numpy as np
    import scipy.signal as sig
    
    fs = 96000  # Sample rate.
    f0 = 500  # Center frequency in Hz.
    
    # Generate noise with a few different bandwidths.
    for c in [1.03, 1.07, 1.15]:
      # Design a second-order Butterworth filter bandpass filter.
      sos = sig.butter(2, [f0 / c, f0 * c], 'bandpass', output='sos', fs=fs)
      # Generate white noise.
      white_noise = np.random.randn(fs)
      # Run it through the filter.
      output = sig.sosfilt(sos, white_noise)
      # Use Welch's method to estimate the PSD of the filtered noise.
      f, psd = sig.welch(output, fs, nperseg=4096)
    
      plt.semilogx(f, 10 * np.log10(psd), label=f'c = {c}')
    
    plt.axvline(x=f0, color='k')
    plt.xlim(50, fs/2)
    plt.ylim(-140, -40)
    plt.xlabel('Frequency (Hz)', fontsize=15)
    plt.ylabel('PSD (dB)', fontsize=15)
    plt.legend()
    plt.show()
    

    Output: enter image description here