Help Needed: Generate Audio Track : Automatically Increment Amplitude of Tone:

Using: Audacity 3.0.2

Hello,
Please could I get some help on the following:

I would like to generate an audio track:

  • With a specified tone Frequency, Amplitude & Duration (say 2 to 3 seconds)


  • Followed by silence (say 1.5 to 2 seconds)


  • Automatically repeat the above combination but amplified by set level (say 2 to 5dbfs) - up to a set max dbfs


  • It would be a bonus if I could also generate labels for each segment at the same time. Not critical as I could manually import labels once the the track has been generated.

I would like to use the above track to test my hearing periodically - as I have a fluctuating hearing loss and need to monitor it.

Also, can someone suggest a good way to learn coding for Nyquist prompt.

Thank You.
Jasu



  • Only Audacity can create an audio track, but when Audacity runs a “generate” type plug-in, it will add an empty mono track to receive the generated
  • audio unless an audio track is already selected.
    If an audio track is already selected, Audacity will use the selected track to receive the generated audio.

A “generate” type plug-in has a “magic comment” at the beginning:

;type generate

The semicolon tells Nyquist to ignore everything on the line after the semicolon (it’s a “code comment”)
As with all comments, this comment is ignored by Nyquist, but “magic comments” are special in that they are understood by Audacity and pass information to Audacity.
In this case, the magic comment “;type” tells Audacity what kind of plug-in it is. The keyword “generate” specifies that it is a generator kind of plug-in.


The examples below may be run in the Nyquist Prompt effect (Nyquist Prompt - Audacity Manual)
Tip. We can see if the code returns any error messages by running the Nyquist Prompt with the Debug button instead of the OK button.


The easiest way to generate a tone of a specified length is with the OSC function (Nyquist Functions)
Example: Generate 1 second tone at note “C4”

;type generate

(osc c4)

Nyquist is clever enough to know that “C4” is MIDI note 60. In fact we can demonstrate that:

(print C4)  ;returns 60

An alternative way to generate C4 (MIDI note 60):

;type generate

(osc 60)

But what if we want to generate a tone specified by frequency (Hz) rather than note name or MIDI number?
The OSC function only understands note numbers, which it calls “steps” (Note names like “A4”, “D5” etc represent note numbers / “step” numbers), so we have to convert from Hz to steps. Fortunately there is a function to do that, called: HZ-TO-STEP.
We can use it like this to generate note 72 (A4) which has a frequency of 440 Hz.

;type generate

(osc (hz-to-step 440))

The second (optional) argument for the OSC function is the duration of the sound. For generate type plug-ins, times / durations are in seconds, which makes this very easy :wink:

Example: Create a tone at 440 Hz that is 2.5 seconds duration:

;type generate

(osc (hz-to-step 440) 2.5)

Note that all the tones that we have generated so far have been “full scale” (amplitude = 1).
To scale the amplitude, we can multiply the tone by a number.
Example, to generate a 440 Hz that is 2.5 seconds duration and amplitude 0.8:

;type generate

(scale 0.8
       (osc (hz-to-step 440) 2.5))

or written another way (this is identical to the previous example, but uses the MULT function instead of the SCALE function:

;type generate

(mult 0.8
      (osc (hz-to-step 440) 2.5))

Look these up:
MULT: Nyquist Functions
SCALE: Nyquist Functions

The main difference between using SCALE or MULT, is that SCALE requires at least one of the arguments to be a sound or multi-channel sound, whereas MULT works with sounds, multi-channel sounds or numbers. I generally prefer to use MULT, but that’s just my preference.


But what if we want to scale the sound by a varying amount?
For that, we will need to create some kind of control signal that varies over time.
Traditionally, Nyquist uses low sample rate sounds as control signals, where the control signal sample rate is 1/20th of the track sample rate.

A good way to create simple control signals is with “piece-wise approximations”.
Nyquist provides an entire family of piece-wise approximations (Nyquist Functions)
The one that we shall be using is a “linear” piece-wise approximation with values for each time position. The envelope interpolates linearly between each time / value point.

Example:
Create a control signal that rises from zero to full scale (“1”) over a period of 5 seconds, then falls back to zero over the next 10 seconds.
For this, we need to specify control points at:

  • time= 0, value=0
  • time=5, value=1
  • time=15, value=0

If we look in the Nyquist manual (Nyquist Functions), we can see that the command is in the form:

(pwlv l1 t2 l2 t3 l3 ... tn ln)

(“SAL” is an alternative way of writing Nyquist code, but I only know the original “LISP” syntax. Most of Audacity’s documentation is written using LISP syntax, and most Nyquist plug-ins use LISP syntax).

Note that in the PWLV command, the initial time value is omitted - it is assumed to be zero.
So the command that we need is:

;type generate

(pwlv 0 5 1 15 0)

Were you surprised by the result? Can you explain why it is only 0.75 seconds long? (hint: the answer has already been described earlier in this post)

Now lets try amplifying a 15 second tone by this envelope:

;type generate

(mult (pwlv 0 5 1 15 0)
      (osc A4 15))

What about silence?
The easiest way to think of silence in this case is as sound that has zero amplitude.

Example:
Create 2 seconds of silence, followed by 1 second tone that starts at amplitude 1 and goes down to zero at time=4

To do this, our control points need to be:

  • time=0 (omitted), value=0
  • time=2, value=0 (keep the first 2 seconds at zero amplitude)
  • time=2, value= 1
  • time=4, value=0

and here is our code:

;type generate

(mult (pwlv 0 2 0 2 1 4 0)
      (osc A4 4))

Have a play with this and ensure that you understand what we have done so far.
Feel free to ask questions.


There’s a list of reference material here: Manuals and reference material
and an increasing amount of stuff in the Audacity wiki: Missing features - Audacity Support
and of course the Audacity manual: Nyquist - Audacity Manual

Mostly you will learn by looking at examples, trying out commands from the Nyquist manual, trying out examples from the wiki, and generally playing with it.

Steve was very kind with his time, and recently taught me about Nyquist and Lisp.
So here are a few points that will make your life a lot easier.
(Sorry if you are already familiar with coding).

In Lisp, and Xlisp (the variant that Nyquist uses), there has to be matching brackets/parentheses.
So looking at Steve’s previous example:
ny-1.png
Note the matching sets of parentheses.

There is also no difference between these two below, however, the second one is neater and easier to read:
(You should get into the habit of using the 2nd option from early on).
NY-3.png
NY-2.png
Then to code Xlisp, get yourself a good text editor, especially one that highlights matching brackets.
Notepad++ for Windows, Sublime Text or Editra for MacOS are good choices.
As regards Linux, Steve will be able to advise you.

Lastly, keep in mind that Nyquist can only return one thing, either text/labels or sound.
For example, you can’t do this:

(mult 0.8 (osc (hz-to-step 440) 2.5))  ; generate a 440 Hz tone scaled to 0.8 max val for 2.5 sec.
(sum *track*) ; now add it to the existing audio on the track.

It’s wrong in two ways:

  1. You can’t have Nyquist returning two things, i.e. the tone then mix (add) it.
  2. The “sum” command needs two variables.

BTW, the variable track is special and it means the existing audio on a track (if any).

What you can do is:

(sum *track* (mult 0.8 (osc (hz-to-step 440) 2.5)))

Or more readable:

(sum *track* 
            (mult 0.8 
                  (osc (hz-to-step 440) 2.5)
            )
)

Hope that helps.

I would disagree with your “more readable” version on two counts:

  1. In LISP languages you should avoid dangling parenthese
  2. excessive spacing on the second line (perhaps you used tabs instead of spaces)

Personally I would probably write it like this:

(sum *track* 
     (mult 0.8 (osc (hz-to-step 440) 2.5)))

so that it is clear that we are adding track to an expression, where the expression is treated as one thing.

Others may prefer to break down the second line a little more like this (though I don’t think it is necessary in this case):

(sum *track* 
     (mult 0.8 
           (osc (hz-to-step 440) 2.5)))

or to be a little more compact, like this:

(sum *track* 
    (mult 0.8 
        (osc (hz-to-step 440) 2.5)))

“Advanced” tip:
Another alternative is to assign the initial “osc” tone to a local variable using “LET”.
We can then easily apply additional operations without resorting to long lines, and without littering the namespace with global variables:

(let ((tone (osc (hz-to-step 440) 2.5)))
  (setf tone (mult 0.8 tone))
  (sum *track* tone))

(@Paul2, if you want more information about local variables vs global variables, feel free to start a new topic - it’s a really useful thing as scripts get bigger.)

I’ve started writing a page about indentation and code formatting for Nyquist. It isn’t complete, but you may find it helpful: https://github.com/SteveDaulton/AudioNyq_Coding_Standards/blob/main/Spacing%20and%20Indentation.md

OK point taken about the dangling brackets, old habit from using javascript.

As regards your github link:

Ah, it was set to private while I was setting it up. I’ve now made it public.

Steve, Thank You a lot for not only helping me but for the detailed and step wise development of the solution. Your thought, effort and time to this is sincerely appreciated.

In regards to creating the control signal:

;type generate

(mult (pwlv 0 2 0 2 1 4 0)
(osc A4 4))

I tried extrapolating to generate a few steps of the tone + silence:

;type generate

(mult (pwlv 0 2 0 2 1 4 1 4 0 6 0 6 1 8 1 8 0 10 0)
      (osc A4 4))

However, it generates only 1 combination of 2 sec silence followed by 2 sec. of tone. Am I doing something wrong here.

Furthermore

  • Is there way to automate the generation of the control signal, if not I could use excel to generate it.

Thank You.

Very close.

You have correctly extrapolated the control signal, but the “A4” tone is still only 4 seconds long. When you multiply the two the output terminates when the shorter (tone) ends.

In your example above, the control signal is 10 seconds long, so you need the generated tone to also be 10 seconds long.

;type generate

(mult (pwlv 0 2 0 2 1 4 1 4 0 6 0 6 1 8 1 8 0 10 0)
      (osc A4 10))



No need to use Excel. Nyquist is a full featured programming language and can easily do this without breaking a sweat.
IF you wanted to do it this way (hint: there’s a better way), you could generate the PWLV breakpoints using loops:

;type generate

(setf target-duration 20)

(setf cycle (list 0 0 2 0 2 1 4 1 4 0))
(setf cycle-len 4)

(setf breakpoints (reverse cycle))

(do ((start cycle-len (+ start cycle-len)))
    ((> start target-duration))
  (dotimes (i (length cycle))
    ; time values are even numbered
    (when (evenp i)
      (setf (nth i cycle) (+ (nth i cycle) cycle-len)))
    (push (nth i cycle) breakpoints)))

(setf breakpoints (cdr (reverse breakpoints)))

(mult (pwlv-list breakpoints)
      (osc A4 target-duration))

Here is the same code with loads of comments to explain what it is doing:

;type generate

(setf target-duration 20)

; Control points for one cycle.
(setf cycle (list 0 0 2 0 2 1 4 1 4 0))
; Length of one cycle
(setf cycle-len 4)

;; 'breakpoints' will be the list of control points.
;; It is easier to add points in reverse order,
;; then reverse back at the end.
(setf breakpoints (reverse cycle))

;; Main loop
(do ((start cycle-len (+ start cycle-len)))
    ; Stop when we have at least as much control signal
    ; as the target length.
    ((> start target-duration))
  ;; Inner loop
  ;; Add each point of the cycle to the breakpoint list.
  (dotimes (i (length cycle))
    ; time values are even numbered
    (when (evenp i)
      (setf (nth i cycle) (+ (nth i cycle) cycle-len)))
    (push (nth i cycle) breakpoints)))

;; Reverse the breakpoints back to the correct order
;; and remove the first time point.
(setf breakpoints (cdr (reverse breakpoints)))

(mult (pwlv-list breakpoints)
      (osc A4 target-duration))

But as I mentioned above, there’s a simpler (and better) method:

;type generate

(setf tone (mult (pwlv 0 2 0 2 1 4 0)
                 (osc A4 4)))

(seqrep (i 10) (cue tone))

Here we just create one “cycle” of the shaped tone, then repeat it multiple times (10 times) as a sequence, using SEQREP.

Once you have the series of tones, you can apply an envelope to it in the same way as we did when shaping the amplitude of (osc duration). If it’s a long series of tones, then a simple ramp should be sufficient:

(mult (pwlv 0 ))

Steve,

So the code I have come up with is as below - is this what you were hinting? It does generate a ramped tone pattern.

;type generate
(setf tone (mult (pwlv 0 2 0 2 1 4 1)
                 (osc A4 4)))
(mult (seqrep (i 10) (cue tone)) (pwlv 0 20 0.8)))

The output is
https://drive.google.com/open?id=1d18i5rS_RQ5xozV3-2z0qRvg8B91u4uM

However a few things:

  • The ramp is linearly increasing, similar to the unscaled waveform can the scaling also be in steps - such that each segment is of the same amplitude. That is stepped ramping


  • Instead of ramping the amplitude can the ramp be in steps of db or dbfs?

Note: Is there a way to insert images into the post from the desktop? I could only find it by using web url. I have tried using a link from my Google Drive - though unsucessfully

Thank You

That image is not publicly accessible. See here for how to upload an image to the forum: https://forum.audacityteam.org/t/how-to-attach-files-to-forum-posts/24026/1

I was (incorrectly) assuming that you would have more tones than that.
(With a long series of tones, the slope across the top of each individual tone becomes insignificant.)

You could do something like this:
The code below increase each tone by +3 dB

;type generate
(setf cycle (mult (pwlv 0 2 0 2 1 4 1)
                 (osc A4 4)))

(setf initial-db -15)
(setf increment 3.0)


(defun tone-gen (sig idb incdb)
  ;;; Scale sig by idb + incdb.
  (let* ((total (+ idb incdb))
         (factor (db-to-linear total)))
    (mult sig factor)))
    
(seqrep (i 10)
  (cue (tone-gen cycle
                 initial-db
                 (* i increment))))

Hello Steve,

Thanks a lot. I have what I was looking for. Just for your info, the screenshot of all the tracks is below. I plan to use it to test my hearing by playing a single track at a time and find out the lowest amplitude I can each hear, in each ear - simply by having an earbud in one ear at a time.
Screenshot_All Tracks.jpg
Your commitment to helping out is much appreciated.

Regards.
Jasu :smiley:

Just for interest, I created 60 tones from -60 dB to -1 dB with this code:

;type generate

(setf initial-db -60)
(setf increment 1.0)
(setf numb 60)  ;number of tones

(setf cycle (mult (pwlv 0 2 0 2 1 4 1)
                 (osc A4 4)))

(defun tone-gen (sig idb incdb)
  ;;; Scale sig by idb + incdb.
  (let* ((total (+ idb incdb))
         (factor (db-to-linear total)))
    (mult sig factor)))
    
(seqrep (i numb)
  (cue (tone-gen cycle
                 initial-db
                 (* i increment))))

Notice that the logarithmic dB scale can be clearly seen:

First Track000.png