Adding sounds, numbers and vectors

This is a mini-tutorial about how Nyquist adds sounds, numbers and vectors.

The examples may be run in the Nyquist Prompt effect, and use “version 4” LISP syntax (in Audacity 2.1.2/2.1.3 ensure that “Use legacy (version 3) syntax” is NOT enabled). Note that in the code examples, text that occurs after a semicolon is treated by Nyquist as a “comment” and is ignored. Comments are provided to help people reading the code to understand what the code means.

There are three similar functions in Nyquist for adding things: “+”, “SUM”, “SIM

The first is a straightforward arithmetic “add” function that adds a list of numbers. It can operate on integers (whole numbers) or “floats” (“floating point” numbers)

(+ 5 3)  ;returns 8



(print (+ 5 3))  ;same as the first example, but explicitly "prints" the integer number "8"



(print (+ 0.5 1 2 3))  ;prints the floating point number "6.5"

The function “SIM” is identical to the function “SUM”. For clarity, “SIM” is more often used when referring to sounds, and “SUM” used when referring to numbers, but as they are functionally identical they are interchangeable. In the following examples I shall use “SUM”, but “SIM” could be used and there would be no difference.

“SUM” (and “SIM”) may be used to add numbers, returning the same result as “+”, but is slightly slower because the underlying code is more complex.

(sum 5 3)  ;returns 8



(print (sum 5 3))  ;prints the number "8"



(print (sum 0.5 1 2 3))  ;prints the number "6.5"

Working with Sounds:

The “SUM” function can also work with “sounds”. Nyquist treats mono sounds as a distinct data type. With “SUM” we can add a numeric value to a sound, which adds the numeric value to the value (amplitude) of each individual audio sample. To demonstrate this, we can use the special word TRACK.

The word TRACK is itself a special kind of symbol called a “variable” that represents data. Nyquist treats the word TRACK to represent the audio that is selected in the Audacity track (note that the asterisks are part of this special name). The audio may be mono or stereo. If it is a mono track (one audio channel), then TRACK will be a “sound”. If the track is stereo, then TRACK will be an array containing two “sounds”.

In this example we will add the numeric value “0.3” to a mono sound from a selection in a mono audio track:


Now apply this code to the selected audio:

(sum 0.3 *track*)

and the result is:


As we can see, the selected waveform has been offset by +0.3. In other words, +0.3 has been added to each sample in the selection.

Adding Sounds:

The “SUM” function is also able to add sounds together. Adding sounds is also known as “mixing”. In the simple case of adding sounds that have the same sample rate, addition is performed by adding the value (amplitude) of each individual audio sample from one sound, with the corresponding sample of the other sound.

In this example, we will introduce another function, “MULT”, which as the name suggests provides a multiplication function that works with sounds. “Multiplying” a sound by a numeric amount is commonly known as “amplifying”. If we multiply a sound by 2, then each sample value is doubled (samples with negative values become twice as negative). Doubling the amplitude of each sample value doubles the overall level of the audio - in other words, the audio is amplified by a factor of 2, which is roughly equivalent to +6 dB.

Example:


Now apply this code to the selected audio:

(mult 2 *track*)

As we can see, the amplitude of the selected audio has doubled:


So now we can try generating some sounds and adding them together.

This first code snippet will generate a sine tone with a frequency of 440 Hz. By default the function HZOSC generates a sine tone with an amplitude of +/- 1.0 (0 dB), but we have multiplied it by 0.5, so it is “amplified” (attenuated) to a lower level of +/- 0.5. The returned audio replaces whatever was previously selected in the track

(mult 0.5 (hzosc 440))


So now let’s undo that and return to what we had immediately before:

and this time we will “add” the 440 Hz sine tone to the existing track audio:

(sum (mult 0.5 (hzosc 440))
     *track*)


Now we can see (and hear) that the sine tone has been mixed with (added to) the track audio.


Working with Stereo Sounds

Nyquist handles stereo as an array (“vector”) containing two “sounds”. The first element of the array is the left channel and the second element is the right channel. Audacity tracks are either mono or stereo, so returning audio from Nyquist must either be a mono sound, or an array containing two sounds. Unfortunately there is currently no way for Nyquist to tell Audacity to create a stereo track, so to generate stereo audio with Nyquist it is necessary to make a selection in a stereo audio track.

  • If Nyquist returns an array of 2 sounds into a selected stereo track, the first sound element is the left channel and the second sound element is the right channel.
  • If Nyquist returns a mono sound into a selected stereo track, the sound is written into both channels (the sound is duplicated)
  • If Nyquist returns an array of 2 sounds into a selected mono track, an error is shown
  • If Nyquist returns an array of one sound, or more than 2 sounds, and error is shown

To create an array of sounds, we can either declare the array first, and then add the sounds,
Example:

(setf stereo (make-array 2))
(setf (aref stereo 0) (hzosc 200))
(setf (aref stereo 1) (hzosc 440))

or more usually we will use the VECTOR command that declares the array with its contents in one step
Example:

(vector (hzosc 200)  (hzosc 440))

Explanation:
In the first of these two examples, the MAKE-ARRAY function creates an array with 2 elements, and is assigned to the variable “stereo”.
We then use the AREF command to access the first element (index 0) and set it to the sound generated by (hzosc 220), which is a 220 Hz sine tone.
The second element (index 1) is set to a 440 Hz sine tone.
Note: If we run this example as is, Nyquist returns the result of the final line, which is the 440 Hz “sound” and NOT the (stereo) array of sounds. If we wish to return the stereo array, we can do so by adding a final line with the variable “stereo”

(setf stereo (make-array 2))
(setf (aref stereo 0) (hzosc 200))
(setf (aref stereo 1) (hzosc 440))
stereo  ;return the value of this variable

In the second example, we create an initialized vector containing two sounds in one step. Nyquist returns the evaluation of this command, which is the stereo audio.


Adding Stereo Sounds

The command SIM (same as “SUM”) is described in the Nyquist manual as (abridged):

Returns a sound which is the sum of the given behaviors evaluated with the current value of warp. If behaviors return multiple channel sounds, the corresponding channels are added. If the number of channels does not match, the result has as many channels as the argument with the most channels. For example, if a two-channel sound [L, R] is added to a four-channel sound [C1, C2, C3, C4], the result is [L + C1, R + C2, C3, C4]. Arguments to sim may also be numbers. If all arguments are numbers, sim is equivalent (although slower than) the LISP + function. If a number is added to a sound, snd-offset is used to add the number to each sample of the sound…

Unfortunately the manual does not explicitly state what happens when adding a number to a stereo sound, though we can deduce the behaviour by considering the multi-channel example (also worth noting that Audacity tracks currently support a maximum of 2 channels).

In the example of adding [L, R] to [C1, C2, C3, C4], the first element of the first array (“L”) is added to the first element of the second array (“C1”), the second element of the first array (“R”) is added to the second element of the second array (“C2”), and nothing is added to C3 or C4.
Schematically:
[L, R] + [C1, C2, C3, C4] = [L+C1, R+C2, C3, C4]

A similar thing happens if we add a mono sound to a multi-channel sound:
S + [C1, C2, C3, C4] = [S+C1, C2, C3, C4]

And similar again when adding a number:
0.3 + [C1, C2, C3, C4] = [0.3+C1, C2, C3, C4]

And we can extend this to arrays of numbers:

(print (sum (vector 1 4)
            (vector 2 3 2.5)))
;returns the vector #(3 7 2.5)

Example - Add a number (offset) to a stereo track:
firsttrack010.png

(sum 0.3 *track*)

firsttrack011.png
Example - Add a (mono) sound to a stereo track:
firsttrack010.png

(sum (mult 0.5 (hzosc 440)) *track*)

firsttrack012.png
Example - Add an array of two numbers to a stereo track
firsttrack010.png

(sum (vector 0.3 -0.3) *track*)

firsttrack013.png

I’ve tried that : processing each channel separately, (below), but the result is still asymmetrical

;version 4
(setq A1 (hzosc 0.090909))
(setq A2 (hzosc 0.052631))
(setq A3 (hzosc 0.034482))
(setq A4 (hzosc 0.024390))

(setq P 0.07) ; Recommend P<0.15

(setf stereo (make-array 2))
(setf (aref stereo 0)
(clip (sim (mult A1 -0.5) (mult A2 -0.5) (mult A3 -0.5) (mult A4 -0.5)
 (clip (mult (sum 0.5 (feedback-delay *track* 0.00502512 (* 11 P))) A1).5)
 (clip (mult (sum 0.5 (feedback-delay *track* 0.01492537 (* 3 P))) A2).5)
 (clip (mult (sum 0.5 (feedback-delay *track* 0.02325581 (* 2 P))) A3).5)
 (clip (mult (sum 0.5 (feedback-delay *track* 0.03255813 (* 9 P))) A4).5)).5)))

(setf (aref stereo 1) 
(clip (sim (mult A1 -0.5) (mult A2 -0.5) (mult A3 -0.5) (mult A4 -0.5)
 (clip (mult (sum 0.5 (feedback-delay *track* 0.00502512 (* 11 P))) A1).5)
 (clip (mult (sum 0.5 (feedback-delay *track* 0.01492537 (* 3 P))) A2).5)
 (clip (mult (sum 0.5 (feedback-delay *track* 0.02325581 (* 2 P))) A3).5)
 (clip (mult (sum 0.5 (feedback-delay *track* 0.03255813 (* 9 P))) A4).5)).5)))

stereo

Looking at the code for the left channel of “stereo” (I’ve rearranged the code a bit so that I can see what it’s doing):

(setf stereo (make-array 2))

(setf (aref stereo 0)
  (clip (sim  (mult A1 -0.5)
              (mult A2 -0.5)
              (mult A3 -0.5)
              (mult A4 -0.5)
              (clip (mult A1
                          (sum 0.5 (feedback-delay *track* 0.00502512 (* 11 P))))
                    0.5)
              (clip (mult A2
                          (sum 0.5 (feedback-delay *track* 0.01492537 (* 3 P))))
                    0.5)
              (clip (mult A3
                          (sum 0.5 (feedback-delay *track* 0.02325581 (* 2 P))))
                    0.5)
              (clip (mult A4
                          (sum 0.5 (feedback-delay *track* 0.03255813 (* 9 P))))
                    0.5))
        0.5))

You are using track, which is a stereo sound, so you are still adding those mono A1, A2,… terms to the left channel only. The same goes for the right channel of “stereo”.

There are a couple of ways round this problem:

One is to replace each occurrence of track with (aref track 0) when calculating the left channel of “stereo”, and (aref track 1) when calculating the right channel.

The other, and in my opinion better way, is to move the DSP code into a function. This cuts down on a lot of duplicate code. You’re wanting to apply exactly the same process to both left and right channels, so we can write the code once and use it twice.

This is the code that you are wanting to apply to each channel - I’ve replace the variable TRACK with a new name “SIG” (abbreviation of “signal”) because we want to processes the signal (mono sound) from the left channel, and then the signal from the right channel

(clip (sim  (mult A1 -0.5)
            (mult A2 -0.5)
            (mult A3 -0.5)
            (mult A4 -0.5)
            (clip (mult A1
                        (sum 0.5 (feedback-delay sig 0.00502512 (* 11 P))))
                  0.5)
            (clip (mult A2
                        (sum 0.5 (feedback-delay sig 0.01492537 (* 3 P))))
                  0.5)
            (clip (mult A3
                        (sum 0.5 (feedback-delay sig 0.02325581 (* 2 P))))
                  0.5)
            (clip (mult A4
                        (sum 0.5 (feedback-delay sig 0.03255813 (* 9 P))))
                  0.5))
      0.5))

Schematically, if we represent the above code as [mycode], then we want to pass a sound “SIG” into a function, where [mycode] acts on SIG.
If we call the function DSP, then schematically we have:

(defun dsp (sig)
  [mycode])

We can then pass the left channel of track to the function to obtain the left channel of “stereo”, and the right channel of track to the function to get the right channel of “stereo”.

(vector (dsp (aref *track* 0))
        (dsp (aref *track* 1)))

So this is the complete code:

;version 4
(setq A1 (hzosc 0.090909))
(setq A2 (hzosc 0.052631))
(setq A3 (hzosc 0.034482))
(setq A4 (hzosc 0.024390))

(setq P 0.07) ; Recommend P<0.15

(setf stereo (make-array 2))

(defun dsp (sig)
  (clip (sim  (mult A1 -0.5)
              (mult A2 -0.5)
              (mult A3 -0.5)
              (mult A4 -0.5)
              (clip (mult A1
                          (sum 0.5 (feedback-delay sig 0.00502512 (* 11 P))))
                    0.5)
              (clip (mult A2
                          (sum 0.5 (feedback-delay sig 0.01492537 (* 3 P))))
                    0.5)
              (clip (mult A3
                          (sum 0.5 (feedback-delay sig 0.02325581 (* 2 P))))
                    0.5)
              (clip (mult A4
                          (sum 0.5 (feedback-delay sig 0.03255813 (* 9 P))))
                    0.5))
        0.5))

(vector (dsp (aref *track* 0))
        (dsp (aref *track* 1)))

Addendum:
There’s a shorter way of writing the final “vector” statement, which is to use a pre-defined macro called “multichan-expand”. You would use it like this:

;version 4
(setq A1 (hzosc 0.090909))
(setq A2 (hzosc 0.052631))
(setq A3 (hzosc 0.034482))
(setq A4 (hzosc 0.024390))

(setq P 0.07) ; Recommend P<0.15

(setf stereo (make-array 2))

(defun dsp (sig)
  (clip (sim  (mult A1 -0.5)
              (mult A2 -0.5)
              (mult A3 -0.5)
              (mult A4 -0.5)
              (clip (mult A1
                          (sum 0.5 (feedback-delay sig 0.00502512 (* 11 P))))
                    0.5)
              (clip (mult A2
                          (sum 0.5 (feedback-delay sig 0.01492537 (* 3 P))))
                    0.5)
              (clip (mult A3
                          (sum 0.5 (feedback-delay sig 0.02325581 (* 2 P))))
                    0.5)
              (clip (mult A4
                          (sum 0.5 (feedback-delay sig 0.03255813 (* 9 P))))
                    0.5))
        0.5))

(multichan-expand #'dsp *track*)

Thanks Steve, that’s fixed the asymmetry.
( I would never have deciphered the syntax myself )

I’m getting the hang of ̶d̶i̶m̶ ̶s̶u̶m̶ ̶ sim sum.
This code provides a simulation of subtle variations in acoustic-feedback

;version 4

(setq X 0.5) ; Recommend X<1

(setq A1 (sum 0.5 (mult X (hzosc 0.090909))))
(setq A2 (sum 0.5 (mult X (hzosc 0.052631))))
(setq A3 (sum 0.5 (mult X (hzosc 0.034482))))
(setq A4 (sum 0.5 (mult X (hzosc 0.024390))))

(setq P 0.05) ; Recommend P<0.1

(setf stereo (make-array 2))

(defun dsp (sig)
 (mult 0.45
 (lowpass4 (highpass8
 (clip (sim 
              (mult A1 -0.5)
              (mult A2 -0.5)
              (mult A3 -0.5)
              (mult A4 -0.5)
              (clip (mult A1
                          (sum 0.5 (feedback-delay sig 0.00502512 (* 6 P))))
                    0.5)
              (clip (mult A2
                          (sum 0.5 (feedback-delay sig 0.01492537 (* 3 P))))
                    0.5)
              (clip (mult A3
                          (sum 0.5 (feedback-delay sig 0.02325581 (* 2 P))))
                    0.5)
              (clip (mult A4
                          (sum 0.5 (feedback-delay sig 0.003255813 (* 5 P))))
                    0.5))
 0.5) 
60) 6666)))

(vector
 (sim (aref *track* 0)(dsp (aref *track* 0)))
 (sim (aref *track* 1)(dsp (aref *track* 1))))