A police siren

Previous: Basics

In this chapter, we are going to synthesize a simple police car siren: a sinusoidal signal with an oscillating frequency.

In the previous chapter, we have generated tones with a constant frequency, this time we will have to change the frequency over time.

Variable parameters

So far, the only buffers we’ve seen were buffers containing audio signal. But in fact, sndc has no concept of “audio” at all! Buffers can be used for anything, not just audio signals.

So it means that the osc module can be used not just for simple tones but also as oscillating control parameters.

Recall the osc specification that we have seen in the previous chapter:

$ sndc -h osc
[generator] osc - A generator for sine, saw and square waves
Inputs:
    function [STRING] [REQUIRED]
        waveform: 'sin', 'square', 'saw' or 'input'
    waveform [BUFFER] [OPTIONAL]
        buffer containing waveform, used when 'input' is specified in 'function'
    freq [BUFFER | FLOAT] [REQUIRED]
        frequency in Hz
    amplitude [BUFFER | FLOAT] [OPTIONAL]
        amplitude in unit
    p_offset [FLOAT] [OPTIONAL]
        period offset, in period (1. = full period)
    a_offset [FLOAT] [OPTIONAL]
        amplitude offset, a constant that gets added to the resulting signal
    duration [FLOAT] [REQUIRED]
        duration of resulting signal in seconds
    sampling [FLOAT] [OPTIONAL]
        sampling rate, def 44100Hz
    interp [STRING] [OPTIONAL]
        interpolation of the resulting buffer, 'step', 'linear' or 'sine'
    param0 [BUFFER | FLOAT] [OPTIONAL]
        wave parameter 0
    param1 [BUFFER | FLOAT] [OPTIONAL]
        wave parameter 1
    param2 [BUFFER | FLOAT] [OPTIONAL]
        wave parameter 2
    param3 [BUFFER | FLOAT] [OPTIONAL]
        wave parameter 3
    param4 [BUFFER | FLOAT] [OPTIONAL]
        wave parameter 4
    param5 [BUFFER | FLOAT] [OPTIONAL]
        wave parameter 5
Outputs:
    out [BUFFER] output signal

We have noted that the freq parameter can also take a buffer instead of a number. That will allow the osc node to generate a signal with varying frequency over time.

Since what we want is an oscillating frequency, we can use a different instance of the osc module to generate the frequency.

The rough idea is so:

frequency: osc {
    duration: 5;
    function: "sin";
    freq: 1;
}

siren: osc {
    duration: 5;
    function: "sin";
    freq: frequency.out;
}

We will generate a siren sound that lasts 5 seconds, and the frequency modulation will have a period of one second.

However, the frequency signal here will be useless: it is oscillating between -1 and 1 which is not at all what we want. We would like a frequency signal roughly between 700Hz and 1100Hz. We can use the amplitude and a_offset parameters of the osc module to shift the signal to the correct range:

frequency: osc {
    duration: 5;
    function: "sin";
    freq: 1;
    a_offset: 900;
    amplitude: 200;
}

siren: osc {
    duration: 5;
    function: "sin";
    freq: frequency.out;
}

This way, the frequency signal will be centered around 900Hz, and will vary from 900 - 200 = 700Hz to 900 + 200 = 1100Hz.

This gives us a somewhat decent siren sound.

Buffer interpolation

In the example above, the frequency control signal and the actual siren sound have the same length and sampling rate, so internally, one “siren” sample has exactly one associated “frequency” sample. This is quite wasteful and unnecessary: the “frequency” signal is varying very slowly compared to its sampling rate so we can improve speed and memory usage by reducing its sampling rate:

frequency: osc {
    duration: 5;
    function: "sin";
    freq: 1;
    a_offset: 900;
    amplitude: 200;
    sampling: 20;
}

siren: osc {
    duration: 5;
    function: "sin";
    freq: frequency.out;
}

Now, instead of generating 5 * 44100 = 220500 samples, we only generate 20 * 5 = 100 samples for the frequency control signal, for a final siren that sounds exactly the same. Now however, the sampling rate of the frequency signal is significantly lower than the siren sound: more than 2000 samples of audio are squeezed between two samples of the frequency control signal. The result ends up being smooth because sndc interpolates the values correctly, according to one of three methods:

The interpolating method is an attribute of each buffer. In fact, if you look at the osc specification, you will see an interp input which lets you define the interpolation method of the resulting buffer.

You can try experimenting changing it in the frequency node to see how it affects the signal.

Buffer stretching

The last consideration regarding interpolation is what happens when the control buffer doesn’t have the same duration as the output buffer. Consider the following:

frequency: osc {
    duration: 1;
    function: "sin";
    freq: 1;
    a_offset: 900;
    amplitude: 200;
    sampling: 20;
}

siren: osc {
    duration: 5;
    function: "sin";
    freq: frequency.out;
}

We have reduced the duration of the control signal to one second but left the siren signal to 5 seconds. If you play this file, you will notice that the frequency modulation period is now 5 seconds. This is because, instead of using the control signal for the first one second then using a placeholder value, sndc “stretches” the control signal to fit the 5 seconds.

The control signal was oscillating at 1 Hz for one second so contained exactly one period, but we have stretched it over a 5 second buffer so the oscillation ended up with a period of 5 seconds.

Similarly, the sampling rate of 20 samples per second got divided by 5, down to 4 samples per second.

This “stretching” behaviour is standard across all modules and avoid having to deal with gaps.

In the same way, if the control signal is longer, it will be “squeezed” to fit the generated signal with the opposite effects in terms of frequency and sampling rate.

Experimenting

You can try and experiment with the various osc parameters, for instance try changing the function of either signal to something else like square.

Next: Create a kick