Skip to content

Signal builders

Tim Sharii edited this page Sep 1, 2021 · 5 revisions

Signal builder is a special object whose purpose is generating signals of a certain waveform. This object has state and can generate sample after sample (online generation) as well as entire signals at once (offline generation), set constructing parameters and reset its state.

Usage

All signal builders inherit from abstract class SignalBuilder providing two main methods for signal generation:

  • builder.NextSample()
  • builder.Build()

The former method generates new sample at each call, so the signal builder acts as a real-time generator of samples. The latter method builds the entire signal of some length. In both cases sampling rate must be specified (otherwise it'll be set to 1 by default). Other parameters depend on a particular builder (waveform).

Builders offer fluent syntax for their construction and usage. Let's take a look at the example code:

DiscreteSignal sinusoid = 
    new SineBuilder()
        .SetParameter("frequency", 500.0/*Hz*/)
        .SetParameter("phase", Math.PI / 6)
        .OfLength(1000)
        .SampledAt(44100/*Hz*/)
        .Build();

DiscreteSignal noise = 
    new PinkNoiseBuilder()
        .SetParameter("min", -2.5)
        .SetParameter("max", 2.5)
        .OfLength(800)
        .SampledAt(44100/*Hz*/)
        .DelayedBy(200/*samples*/)
        .Build();

DiscreteSignal noisy = 
    new SineBuilder()
        .SetParameter("min", -10.0)
        .SetParameter("max", 10.0)
        .SetParameter("freq", 1200.0/*Hz*/)
        .OfLength(1000)
        .SampledAt(44100/*Hz*/)
        .SuperimposedWith(noise)
        .Build();

The following methods were designed only for offline generation and will have no effect in real-time processing:

  • builder.DelayedBy()
  • builder.RepeatedTimes()
  • builder.SuperimposedWith()

Builder methods can be called in any order.

One prominent use-case of online generation is working with LFOs (low frequency oscillators):

SignalBuilder lfo = 
    new TriangleWaveBuilder()
        .SetParameter("min", 100)
        .SetParameter("max", 1500)
        .SetParameter("frequency", 2.0/*Hz*/)
        .SampledAt(16000/*Hz*/);

while (...)
{
    var sample = lfo.NextSample();
    //...
}

lfo.Reset();

If the signal builder is going to be reused later and it will have to start generating samples "from scratch", then Reset() method needs to be called. Otherwise it'll continue its work from the position where it was stopped earlier.

Builders can have various parameters. Parameters are addressed by some string keyword like "freq" or "min".

GetParametersInfo() method returns the array of keywords that will be recognized by the builder.

At the moment, there are 14 ready-to-use signal builders in NWaves.

Periodic signals:

Builder Parameter Parameter Parameter Parameter
SineBuilder "min", "low", "lo" "max", "high", "hi" "frequency", "freq" "phase", "phi"
CosineBuilder "min", "low", "lo" "max", "high", "hi" "frequency", "freq" "phase", "phi"
SawtoothBuilder "min", "low", "lo" "max", "high", "hi" "frequency", "freq"
TriangleWaveBuilder "min", "low", "lo" "max", "high", "hi" "frequency", "freq"
SquareWaveBuilder "min", "low", "lo" "max", "high", "hi" "frequency", "freq"
PulseWaveBuilder "min", "low", "lo" "max", "high", "hi" "period", "t" "pulse", "width"

Noise signals:

Builder Parameter Parameter Parameter
WhiteNoiseBuilder "min", "low", "lo" "max", "high", "hi"
PinkNoiseBuilder "min", "low", "lo" "max", "high", "hi"
RedNoiseBuilder "min", "low", "lo" "max", "high", "hi"
PerlinNoiseBuilder "min", "low", "lo" "max", "high", "hi" "scale", "octave"
AwgnBuilder "mean", "mu" "sigma", "stdev"

Misc:

Builder Parameter Parameter Parameter Parameter
WaveTableBuilder
AdsrBuilder "attackAmp", "amp" "attack", "a" "decay", "d" "sustain", "s"
RampBuilder "slope", "k" "intercept", "b"
SincBuilder "min", "low", "lo" "max", "high", "hi" "frequency", "freq"
ChirpBuilder "frequency", "freq" "min", "low", "lo" "f0", "freq0", "start" "f1", "freq1", "end"
PadSynthBuilder "frequency", "freq" "fftsize", "size" "bandwidth", "bw" "scale", "bwscale"
KarplusStrongBuilder "frequency", "freq" "stretch", "s" "feedback", "a"
KarplusStrongDrumBuilder "frequency", "freq" "stretch", "s" "feedback", "a" "probability", "prob"

For example, the cell "min", "low", "lo" means that the corresponding signal builder will recognize all these three keywords as the minimum amplitude value for a signal to be generated.

Note. WaveTableBuilder doesn't have parameters, and the array of samples (i.e. wavetable) must be passed to constructor. The samples will be looped.

Note. AdsrBuilder generates exponential ADSR envelope. Optional parameters represent the exponential slope at each corresponding stage. Defaults are 0.2 (and they're OK). There is also parameter attackAmp (or amp) that represents the gain of attack part of ADSR curve (by default, it's 1.5).

Note. PadSynthBuilder also provides method SetAmplitudes(float[] amplitudes) for specifying harmonics amplitudes.

FadeInOutBuilder

FadeInOutBuilder is the special decorator for other signal builders that adds fade-in and -out behaviour.

sound = new FadeInOutBuilder(
          new PadSynthBuilder()
               .SetParameter("fftsize", _config.FftSize)
               .SetParameter("freq", freq)
               .SetAmplitudes(_config.Amplitudes)
               .OfLength(_sampleRate * _config.Seconds)
               .SampledAt(_sampleRate))
       .In(0.1)    // 100 msec
       .Out(0.5);  // 500 msec

During offline signal generation fading out will start automatically in the end of the signal. In online scenarios the method FadeOut() can be called to start fading-out. Properties FadeStarted and FadeFinished indicate the state of fading-out.

Usage example (synthesizer)

How to write your own signal builder

  1. Create a class inherited from SignalBuilder
  2. Override float NextSample() method.
  3. If you need parameters, in constructor fill the dictionary of setters ParameterSetters, like this:
            ParameterSetters = new Dictionary<string, Action<double>>
            {
                { "low, lo, min",  param => _low = param },
                { "high, hi, max", param => _high = param }
            };

Most likely, your class will maintain some state. Do it using private members of the class.

Example:

    /// <summary>
    /// Class for generator of random samples clamped in range [min, max]
    /// </summary>
    public class ClampBuilder : SignalBuilder
    {
        /// <summary>
        /// Minimum amplitude level
        /// </summary>
        private double _min;

        /// <summary>
        /// Maximum amplitude level
        /// </summary>
        private double _max;
        
        /// <summary>
        /// Constructor
        /// </summary>
        public ClampBuilder()
        {
            ParameterSetters = new Dictionary<string, Action<double>>
            {
                { "low, lo, min",  param => _min = param },
                { "high, hi, max", param => _max = param }
            };

            _min = -0.5;
            _max =  0.5;
        }

        /// <summary>
        /// Method generates random samples clamped in range [min, max]
        /// </summary>
        /// <returns>Sample</returns>
        public override float NextSample()
        {
            var val = _rand.NextDouble();
            if (val >= _max) return (float)_max;
            if (val <= _min) return (float)_min;
            return (float)val;
        }

        private Random _rand = new Random();
    }

Usage:

DiscreteSignal clamped = 
    new ClampBuilder()
        .SetParameter("min", -0.8)
        .SetParameter("max",  0.8)
        .OfLength(100)
        .SampledAt(8000/*Hz*/)
        .Build();
Clone this wiki locally