Synthesizing musical notes

This is a short tutorial about generating sounds and shaping the sound with an envelope. Nyquist includes a number of oscillators and other simple sound generators that may be used as building blocks. In this tutorial we will be using the triangle waveform oscillator as the basis of a simple piano-like “ding” sound.
We’ll start with the full code:

 First, a bit about the structure.

I’ll call this the “ding” code (because it makes a sound that goes “ding”).

As you can see, there are lots of parentheses (brackets).
All Nyquist statements are in the form;
(function-name arguments)
where “arguments” is 0 or more parameters for the function to chew on.

Functions can be, and often are, nested one inside the other.
For example:
(function-a (function-b))
In this case, function-b is a function with no arguments (no parameters), and function-a is a function with one argument (one parameter). In other words, function-b is used as an argument of function-a

In the “ding” code, there are several layers of nesting.

Absolute environment

The outermost function is “abs-env“.
ABS-ENV is rather complex to describe, but in brief, it allows us to specify time in seconds within an effect.

“Effects” are one way of running Nyquist code and is characterised by processing existing audio data. They are therefore referred to as “process effects”.

Another way that Nyquist code can be used is as a “generator”, characterised by generating new audio data without taking in any pre-existing audio data. Not surprisingly these are called “generator” effects.

Generator effects, when written as plug-ins, are placed in the Audacity Generate menu.
Process effects as plug-ins are placed in the Effects menu.

Generate effects are different to process effects in how they deal with time. For a generate effect, “one second” defined in the code is “one second” in real time.

For process effects, time is stretched such that the selection length in Audacity
equates to “one second” in the code. This is generally useful because for process
effects we don’t normally want to bother with how long the selection is – we usually
want to process all of the selection, regardless of it’s length, and we can do that
by treating the length as “one unit of time”.

In the case of “ding”, we are generating a sound, but we are doing it in the Nyquist
Prompt effect. The Nyquist Prompt effect is a “process” type effect, so the time
stretching feature will apply to our “ding” code unless we tell it otherwise.

The ABS-ENV function causes the code within to use “absolute time”. That is, one second in the code is one second in real time, It allows us to process the nested code with time handled like a generator effect, even though we are running it in the Nyquist Prompt which is a process effect.

Big breath. That’s the hard bit out of the way.

The ‘LET’ special form

ABS-ENV takes one argument, and in “ding” we have one function called “LET”. The rest of the code is/are the arguments (parameters) for the LET function. “LET” is a common and very useful “block structure”.

The first arguments of “LET” are enclosed in brackets and set values for variables
that will be used within the LET. These are “local variables” in that they only exist
with the “LET” block of code.

These local variables are:

In effect, and because this is at the start of a LET function, this means:

  • let eps = 0.0001,
  • let attack = 0.001,
  • let amp = 0.3,
  • let duration = 2.5,
  • let frequency = 1760

These are the local variables that will be used within the rest of the block.

The second and final part of the LET block is called the “body” code. This is  the business part of the block that actually does something. In the case of “ding” it is just two lines:

“SETF” sets its first argument to the value of the second argument.
In this case we are creating a new variable called “envelope” and giving it the value:
(diff (pwev eps attack amp duration eps) eps)

A quick overview about what we are doing in this line:

In the final line we will be creating a boring, monotonous tone, but we will make
it beautiful by shaping its dynamics. To do that we are going to create a “control signal” that can be applied to the boring tone. This second to last line creates the control signal, which we have named “envelope“.

Control Signals:

Amplification is really just “multiplication”. If we have a digital sound and we multiply every sample value by 2, the we make the sound twice as loud – we have amplified it by “2” (the same as +6 dB). If we multiply every sample value by 0.5, then we make it half as loud – we have amplified by 0.5 (same as -6 dB).

Amplification is very easy in Nyquist.

Up until, I think Audacity 2.0.6, Audacity used the symbol “S” in Nyquist process effects
to represent audio from the selection. In Audacity 2.1.0 this was updated and Audacity
now uses the symbol *TRACK* (note that the asterisks are part of the name). We will use the symbol “S” in these quick examples. If you are using Audacity  2.1.1 or later, there is a checkbox in the Nyquist Prompt to select “legacy syntax“. Enable legacy syntax to use the old “S” symbol rather than the new *TRACK* symbol.

These two examples work on mono tracks only:

The first example multiplies “S“, which is the sound from the track, by 2. It amplifies by
a factor of 2.As you no doubt have worked out, the second amplifies by a half.

It’s a little more complex to process stereo tracks, but fortunately Nyquist has a magical
incantation that allows us to process multiple channels. It is “multichan-expand” and it is used like this:

Note the two special characters “hash” (#) and “single quote” ().
Note also that it is a “single quote” character and not a “back quote“. On my keyboard the singe quote character is the lower-case key with the @ symbol.

How cool would it be if we could amplify by a varying amount?

Pretty cool I think, and quite easy to do. What we would need to do is to create a
control signal” that is the same length as the sound that we are amplifying and that each point along the control signal, (each sample), has the value that we want to multiply by.

For example, if we want to fade out from x1 amplification to zero, then we want a control signal that starts at 1 and goes gradually down to zero.

Nyquist has a whole bunch of related functions for creating these control signals, and
they are collectively called “piece-wise approximations“.

The easiest of these “piece-wise approximations” to describe is the “piece-wise linear” version which is the function “PWL”.

Piece-wise Linear Approximations:

PWL takes pairs of arguments. Each pair consists of a time value and an amplitude value. The amplitude value of the final pair is zero and does not need to be entered.

To create an envelope of duration “one time unit” that starts at amplitude 1.0 and goes down to amplitude 0.0, we need the time/amplitude pairs:

time = 0, amplitude = 1.
time = 1, amplitude = 0.

Because PWL automatically adds the final “amplitude = zero”, we should miss that out,
and the function is written (pwl 0 1 1)

Control signals rarely need to be absolutely precise, so by default, control signals are
created with a low sample rate (1/20th of the audio sample rate). The low sample rate
makes them a bit quicker to generate, process and require less memory.

If we run (pwl 0 1 1)  on its own, it will produce a very short and inaudible control signal
(you may hear a click at the start). It is short because it is at a low sample rate, but when
run on its own it is returned to the Audacity track that has the normal audio sample rate.

We don’t need to worry about that, because we are not going to use it on its own. We are
going to apply it to (multiply) a sound.

or the version that can handle stereo tracks:

Note that for a process effect, the selection length is treated as “one unit”, so the duration of “1” in PWL is the length of the audio selection.

This provides a really useful trick for manipulating the amplitude of an audio track.

Making an Amplitude Envelope

Let’s say that we want the volume of a track to go from silence, to double amplitude and then down to quarter amplitude. To do that we want a control envelope that over the course of “one unit of time” goes from zero, to two, to 0.25.

So our time/amplitude pairs are:

  • 0 , 0
  • 0.5 , 2.0
  • 1.0 , 0.25

PWL has an implicit “amplitude = 0” at the end, so strictly speaking we should have a final pair of “1 0” at the end

  • 0 , 0
  • 0.5 , 2.0
  • 1.0 , 0.25
  • 1.0 , 0

and then miss off the final zero in the PWL function:

So to amplify the selected sound:

or the stereo version:

We can also set a named variable to have the control signal as its “value”.

and then multiply the sound “S” by the variable;

As you are probably guessing by now, multiplying a sound by a control envelope provides a useful and accessible way of precisely manipulating the amplitude of sounds. This technique is the basis of the “Adjustable Fade” effect and the “Text Envelope” plug-in:

There is more information about PWL and the other “piece-wise approximations” in the Nyquist manual.

Exponential decay envelopes

One of the “piece-wise” variations is “PWEV”.

The “V” versions have explicit start and end values. The start time has to be zero,
so that is omitted and the first value is the amplitude at zero.

The “E” stands for “exponential”. Rather than producing a straight line between each
of the control points, it creates a logarithmic curve. Natural sounds tend to decay exponentially, so using an exponential decay curve is appropriate here.

One thing to be careful of when using “piece-wise exponential approximations” is that log(0) is an illegal value.

In this case, we want the envelope to rise from zero and drop back down to zero, so what
we do is to create an envelope that rises from a small value up to the maximum amplitude
and then drops back down to the same small value. We can then “lower” the entire curve
(after it has been calculated) by subtracting that “small value” from each sample value.

Looking back at the values set at the top of  the LET block:

The “small value” I’ve called “eps” and given it the value 0.0001.

The maximum amplitude I want is about 0.3, hence “amp” = 0.3.

I want the sound to rise quite quickly to make it a bit percussive, so I’ll put the high point
at 0.001 seconds. That’s “attack” = 0.001.

The overall length I want as about 2.5 seconds, so that’s “duration” = 2.5.

Note that because we’ve used ABS-ENV, the times are in seconds.

The first part of creating the envelope is the PWEV function with time/ amplitude pairs at:

  • 0 , 0.0001
  • 0.001 , 0.3
  • 2.5 , 0.0001

or using the names of the variables:

  • 0 , eps
  • attack , amp
  • duration , eps

which gives us the command:

The second step is to subtract the “eps” amount from each sample.

We can’t use the normal “minus sign” because that only works with numbers, but an equivalent that works with sounds (including control signals) is “DIFF

Diff

(diff X Y) means “X – Y” and it works with both sounds and numbers, so to subtracts “eps” from the envelope:

and assign this as the value of a variable that I called “envelope“:

The final line simply multiplies the generated sound by the envelope.

The only new thing here is  that  I use “stretch duration”, otherwise we would have only had one second of sound (because we are still inside the “abs-env” function.

That’s it.
Hope that’s useful.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.