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.
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.
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.
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.
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