Skip to content

Signals: general notes

Tim Sharii edited this page Aug 31, 2021 · 5 revisions

Digital signals and systems are two most fundamental concepts in DSP. In NWaves signals are represented with classes DiscreteSignal and ComplexDiscreteSignal. There are at least two ways to look at the digital signal: 1) as some enumerable concept or a stream; 2) simply as array of values sampled at a certain frequency. NWaves follows the second approach focusing on easy and efficient indexing and block copying of samples.

DiscreteSignal

DiscreteSignal class is a handy wrapper around array of real-valued 32-bit samples with their sampling rate. It provides a handful of easy-to-use methods (listed below) and one can switch easily between a signal and its underlying float[] array referenced by the Samples property. Samples are mutable and indexable. By default construction of DiscreteSignal does not produce extra-copy of the sample array.

The sampling rate must be specified in the first parameter of signal constructor (sampling rate defines the number of samples per one second of an ideally sampled signal).

float[] samples = { 1, 3, 5, 7, 8, 1, 2, 3, 5, 7, 2, 1, 9, 2, 1, 4, 5, 6f };

// no copy
var signal = new DiscreteSignal(16000, samples);

// "allocateNew: true" means copying
var copied = new DiscreteSignal(16000, samples, true);


// mutability (each line will affect 'signal' and not affect 'copied'):

float[] sameSamples = signal.Samples;

samples[0] = 1.0f;
sameSamples[0] += 2.0f;
signal[0] *= 5;

// addressing the last sample:

var lastSample = signal[signal.Length - 1];

DiscreteSignals can also be constructed from any IEnumerable<float>, IEnumerable<int> and from repeating constants:

var ramp = new DiscreteSignal(8000, Enumerable.Range(1, 1000));
var sawtooth = new DiscreteSignal(8000, Enumerable.Range(1, 1000).Select(s => s % 20));
var constants = new DiscreteSignal(8000, 5, 0.75f);

DiscreteSignal can be deep copied from another signal:

// both lines are equivalent; the second line is shorter and better:

var copy = new DiscreteSignal(signal.SamplingRate, signal.Samples, allocateNew: true);
var copy = signal.Copy();

Properties:

  • s.SamplingRate
  • s.Samples
  • s.Length (in samples)
  • s.Duration (in seconds)

Methods:

non-mutating:

  • s = s1.Concatenate(s2)
  • s = s1.Superimpose(s2) (or simply: s = s1 + s2)
  • s = s1.Subtract(s2) (or simply: s = s1 - s2)
  • z = s + 10 (add constant 10 to each sample)
  • z = s - 10 (subtract constant 10 from each sample)
  • z = s * 5 (multiply each sample by constant 5)
  • z = s.SuperimposeMany(click, positions)
  • z = s.Delay(200)
  • z = s.Delay(-200)
  • z = s.Repeat(3)
  • z = s.First(100)
  • z = s.Last(100)
  • z = s[100, 612]
  • z = s.Copy()
  • z = s.ToComplex()

mutating (in-place):

  • s.Amplify(1.5f)
  • s.Attenuate(2)
  • s.Reverse()
  • s.HalfRectify()
  • s.FullRectify()
  • s.NormalizeMax()

describing:

  • s.Energy()
  • s.Energy(512, 1024)
  • s.Rms()
  • s.Rms(512, 1024)
  • s.ZeroCrossingRate()
  • s.ZeroCrossingRate(512, 1024)
  • s.Entropy()
  • s.Entropy(512, 1024)

static methods:

  • DiscreteSignal.Unit(128)
  • DiscreteSignal.Constant(value, 128)

Examples:

var s = new DiscreteSignal(16000, samples);

var echoSignal = s + s.Delay(180);

var marginSignal = s.First(1024).Concatenate(s.Last(1024));

var repeatMiddle = s[1000, 1512].Repeat(10);

var mean = s.Samples.Average();
var sigma = s.Samples.Average(x => (x - mean) * (x - mean));

var normSignal = s - mean;
normSignal.Attenuate(sigma);

Note that each intermediate non-mutating operation allocates new memory, so if you're not going to need these temporary signals, it can become too wasteful of memory if they are very large. Switch to arrays in these situations and handle them manually. NWaves offers many overloaded methods both for arrays and signals, so you can work almost always with float[] arrays and don't use DiscreteSignal at all. Anyway, the construction of a signal from an array costs nothing (by default).

Details

Most methods are self-explanatory, but some of them, perhaps, need clarification.

SuperimposeMany is the method for superimposing certain signal with another signal several times. One possible use-case is adding regular short clicks to particular signal:

var click = new DiscreteSignal(8000, clickSamples);
var positions = new int[] { 8000, 16000, 24000, 32000 }; // each second

var signal = new DiscreteSignal(8000, samples);
var clicked = signal.SuperimposeMany(click, positions);

Delay method can accept both positive and negative values:

var signal = new DiscreteSignal(1, new [] { 1, 2, 3, 4, 5f });
var delayed = signal.Delay(2);
var shifted = signal.Delay(-2);

// delayed : 0 0 1 2 3 4 5 (length 7)
// shifted : 3 4 5         (length 3)

Indexing s[100, 612] is like slicing in Python: create copy of the signal starting at index 100 and ending at index 612 exclusively.

DiscreteSignal.Unit(128) returns 128-sample unit impulse signal, i.e. { 1, 0, 0, 0, ..., 0 }.

DiscreteSignal.Constant(2, 128) returns 128-sample constant signal, i.e. {2, 2, 2, 2, ..., 2}.

s.NormalizeMax() normalizes signal by max absolute value. Signals loaded from WAVE files are normalized by default by 2^15. Sometimes there's a need to normalize by max absolute value (i.e. after calling s.NormalizeMax() the max sample will have value 1 (or -1)).

s.Energy(512, 1024) means: compute energy of the signal s from sample with start index 512 to sample with end index 1024 (exclusively). Other features (rms, zcr, entropy) also can be computed for the entire signal and only for some fragment of a signal.

Energy formula:

img

RMS (root-mean-square) formula:

img

ZeroCrossingRate formula:

img

Shannon Entropy is computed from bins (32 by default) distributed uniformly between the minimum and maximum absolute values of samples:

img

Contracts and exceptions

  1. Sampling rate must be positive
  2. Sampling rates must be the same for two signals participating in any binary operation

Otherwise, exceptions are thrown.

ComplexDiscreteSignal

This class is a wrapper around two arrays of doubles representing real and imaginary parts of complex values.

Properties:

  • c.Length
  • c.SamplingRate
  • c.Real
  • c.Imag
  • c.Magnitude
  • c.Power
  • c.Phase
  • c.PhaseUnwrapped
  • c.GroupDelay
  • c.PhaseDelay

Methods:

non-mutating:

  • c = c1.Multiply(c2)
  • c = c1.Divide(c2)
  • c = c1.Concatenate(c2)
  • c = c1.Superimpose(c2) (or simply: c = c1 + c2)
  • z = c + 10 (add constant 10 to each sample)
  • z = c - 10 (subtract constant 10 from each sample)
  • z = c * 5 (multiply each sample by constant 5)
  • z = c.Delay(200)
  • z = c.Delay(-200)
  • z = c.Repeat(3)
  • z = c.First(100)
  • z = c.Last(100)
  • z = c[100, 612]
  • z = c.Copy()
  • z = c.ZeroPadded(512)
  • z = c.Unwrap(phase)
  • m = c.Magnitude(re, im)
  • p = c.Phase(re, im)

mutating:

  • c.Amplify(1.5f)
  • c.Attenuate(2)

Details

ComplexDiscreteSignal can be constructed the same ways as DiscreteSignal, except that imaginary parts can also be specified (or zeros by default):

var c1 = new ComplexDiscreteSignal(8000, realSamples);
var c2 = new ComplexDiscreteSignal(8000, realSamples, imagSamples);
var c3 = new ComplexDiscreteSignal(8000, Enumerable.Range(0, 100));

Complex signal can be obtained from DiscreteSignal:

var s = new DiscreteSignal(8000, samples);
var c = s.ToComplex();

Magnitude and Phase properties return double arrays containing magnitudes and phases of complex values, respectively.

Multiply() and Divide() methods perform complex mutliplication and division of two signals, respectively.

As for the GroupDelay() and PhaseDelay() methods, they do calculate what their names indicate, but there are better analogs in TransferFunction class (since these characteristics are used in analysis of LTI systems).