Should I learn Nyquist programming?

W10, V2.3.2 I have been studying your docs about programming Nyquist plug-ins. Not trivial stuff but ok. I want to know if there are major roadblocks that I should anticipate in building a plug-in to do the following: Objective: Clean up old LP recordings. Filter out short burst of noise in the recording.

Assume the noise band is mostly above the bandwidth of the music. There is usually a discontinuity between the original signal and the filtered signal at the start and end of the selection interval. This can create a “pop” in the audio.

Process:
(1) manually select the interval containing the noise burst,
(2) extend the selected interval either side of the selection by x% of the selection width
(3) apply a zero phase shift LP filter (LP filter, reverse signal, LP filter, reverse signal) to the signal over the extended selection,
(4) feather the filtered signal back into the track with a linear ramps applied to the original and filtered signal inside the extended selection intervals either side of the original selection.

Thanks Jim

A Nyquist effect can only access the selected audio. It is not able to extend the selection. It’s possible to make a Nyquist effect ignore part of the selection, but Nyquist has know knowledge of the existence of audio outside of the selection.

This is a bit tricky with Nyquist. Although there is a Nyquist function S-REVERSE, it is not implemented in Audacity’s version of Nyquist. For short audio selections, the sound may be reversed by grabbing the sound as an array, reversing the array, and then converting it back into a sound.
Example:

; Integer length of selection
(setf ln (truncate len))

; Grab samples from mono track into an array
(setf s-array (snd-fetch-array *track* ln ln))

;; Reverse the array and convert back to sound
(do ((i 0 (1+ i))
     (j (1- ln) (1- j)))
    ((>= i j) (snd-from-array 0 *sound-srate* s-array))
  (setf temp (aref s-array i))
  (setf (aref s-array i) (aref s-array j))
  (setf (aref s-array j) temp))

Steve Thank for the explanation to my inquiry. I have been working with LISP mainly for the past couple days and testing out stuff using the Nyquist window. I have found everything to be quite frustrating. Keeping up with () pairs can make one go blind, especially using that (format … business. And, I also found that some operations work when you click the Use Legacy syntax and others work only if it is not clicked. And the debug messages are not very helpful other than the “Hit End of File” which I concluded meant that the () were not matched.

I grew up in the days when FORTRAN was a new language on the CDC 1604 and 32K of 48bit words was an amazing revelation to us who only knew assembly languages. Don’t worry, I have evolved a little out of the dark ages.

After trying various experiments, I drew the conclusion you provided --that is: Nyquist only sees the explicit selection. Too bad it is not possible to alter the selection from within the script. As an alternative to worrying about the discontinuity, I tried skipping outward to the next nearest zero crossing but that algorithm doesn’t seen to be very predictable. Maybe it is coded to require an exact zero intersection instead of an interpolated crossing point. I think it should be an interpolated crossing.

Maybe you can answer another quick question. I started using that idea for a zero phase shift filter about 2 versions of Audacity back and it took quite a while to flip a file with 45 minutes of recorded music. However, after updating to 2.3.1 it was really flew through the flips. What happened to the invert operation between those releases.

Thanks – it is helpful to communicate with an expert. Jim

Some tips:

  1. Use a text editor that has parentheses matching (such as NotePad++)

  2. Take care with your code formatting and indentation (see: http://dept-info.labri.u-bordeaux.fr/~idurand/enseignement/PFS/Common/Strandh-Tutorial/indentation.html)

  3. If you find yourself counting brackets, refer to (1) and (2) above.

The most common difference that you will come across is that old (legacy) code uses the variable S to pass the selected audio data from the Audacity track to Nyquist, whereas more modern code uses the variable TRACK

If you see track in some code, then it is almost certainly modern (not “legacy”) syntax.

For your own code, it is highly recommended to use modern syntax.
You can also add the header:

;version 4

at the top of your code, and the Nyquist Prompt will then automatically turn off the “legacy syntax” mode.


This is a weakness of Nyquist. The debug messages can be quite cryptic, though with a bit of practice they become a little more helpful. In most cases, only the first few lines are at all helpful.

The debug output can be made more helpful by adding your own debug messages to your code. For example, if you have written a function and your not sure if your code is reaching the function, then you can do something like this:

(defun myfunc (var)
  (print "In MYFUNC")
  ;; rest of function code
  )

Then when you run your code, if MYFUNC is called, your debug message will be printed to the debug window.


I don’t recall the exact version number, but a few versions back there was a minor bug in the Windows version of Audacity that caused Nyquist to run much slower than it should. That bug is now fixed.

In the current version of Audacity, rather than using the Nyquist Prompt’s debug window, an alternative is to open Audacity’s “Log” window.
(see: “Show Log” https://manual.audacityteam.org/man/help_menu_diagnostics.html)
Personally I find this quite convenient as the Log window may be kept open while you are working.

Steve All your suggestions are very helpful, especially to a virgin Nyquister. Could I impose on you to recode the flip code you provided to a stereo channel instead of a mono channel. I have been going through the .ny files in the Plug-in folder but have not found a good example showing how to access both mono and stereo channels. Thanks

Steve Forget request to modify code. Sorted it out myself. Thanks

There’s several ways that it could be done. This is how I would do it (assuming that we can be sure that track is not too long):

;version 4

(defun flip (sig ln)
  (let ((s-array (snd-fetch-array sig ln ln)))
    (do ((i 0 (1+ i))
         (j (1- ln) (1- j)))
        ((>= i j) (snd-from-array 0 *sound-srate* s-array))
      (setf temp (aref s-array i))
      (setf (aref s-array i) (aref s-array j))
      (setf (aref s-array j) temp))))


(setf ln (truncate len))
(multichan-expand #'flip *track* ln)

More information about handling stereo tracks here: https://wiki.audacityteam.org/wiki/Nyquist_Stereo_Track_Tutorial

Steve I tried your latest code and it does flip the stereo channels (provided the selection is not too long). I spent time with the first code you sent, the audacity docs and the Nyquist Reference Manual and came up with what I thought would work. After spending a couple hours, I was about ready to give up trying to understand this stuff. Mine is not an elegant solution since I simply repeated your code whereas you put the flip code in a function. Either way I think it should work but mine doesn’t and for the life of me, I can’t see why. The log file is very useful — The code runs from start to end, sending all the prints to the log file but at the end of the day, nothing has happened. Now, with regard to your latest code, I understand the function logic but I’m not able to find any docs on multichan-expand. Where does that come from and where is it described? I never would have come up with that. Hope you can tell me what is wrong with the code below.

Learning this system needs more than the few Audacity webpages and the Nyquist Reference Manual. Is there a place (in the forum or elsewhere) than I can ask questions or are you it? Thanks for all you have done. At least I can talk Nyquist baby talk. Jim


;version 4

; set integer length of selection (real to integer)
(setf ln (truncate len))

(print “got length of selection”)
(print ln)

; Grab samples from Left track into an array
(setf s-array (snd-fetch-array (aref track 0) ln ln))

(print “left channel into s-array”)

;; Reverse the array and convert back to sound
(do ((i 0 (1+ i))
(j (1- ln) (1- j)))
((>= i j) (snd-from-array 0 sound-srate s-array))
(setf temp (aref s-array i))
(setf (aref s-array i) (aref s-array j))
(setf (aref s-array j) temp))

(print “left flipped”)
(print ln)


; Grab samples from Right track into an array
(setf s-array (snd-fetch-array (aref track 1) ln ln))

(print “right channel into s-array”)

;; Reverse the array and convert back to sound
(do ((i 0 (1+ i))
(j (1- ln) (1- j)))
((>= i j) (snd-from-array 0 sound-srate s-array))
(setf temp (aref s-array i))
(setf (aref s-array i) (aref s-array j))
(setf (aref s-array j) temp))

(print “right flipped”)

Tip: When posting code to the forum, click the button above the message composing box that has “</>”.
This will create a pair of “code” tags that look like this:

You can then type or paste your code between the tags like this:

code goes here ...

When posted, it will look like this:

code goes here ...

The benefit of using code tags is that it make it easier to see, and it preserves indentation.

When a Nyquist effect runs:

  1. Audacity passes audio from the first selected track to Nyquist
  2. The Nyquist code runs
  3. Nyquist passes the end result back to Audacity. This “end result” is called “the return value”
  4. If there is more than one track selected, Audacity then passes the selected audio from the next selected track, and the Nyquist code runs again, and so on…

The important thing here is that each time the Nyquist code runs, it produces exactly ONE return value.
More about return values here: https://wiki.audacityteam.org/wiki/Nyquist_Plug-ins_Reference#Return_Values

The problem with your code is that the return value is the text that is printed in the final line:

(print "right flipped")

What you actually want to return, is stereo (2 channel) audio.
(continued in next post…)

Stereo sounds.

Nyquist handles stereo sounds as an array of two sounds.
More generally, Nyquist handles muti-channel sounds as an array, where each element of the array is a sound. Nyquist can handle any number of channels, but can Audacity currently only handle mono or stereo.

More about stereo tracks here: https://wiki.audacityteam.org/wiki/Nyquist_Stereo_Track_Tutorial

More about arrays here: https://www.audacity-forum.de/download/edgar/nyquist/nyquist-doc/xlisp/xlisp-man/xlisp-man-015.htm

The simplest way to create an array of two sounds, is to use the VECTOR function.

In your code, you would need to hold the left channel result in memory (assign it to a variable), do the same with the right channel result, then as the final line, create a vector (an array) containing the two sounds.

You could assign the left channel result to a variable like this:

(setf left-channel
  (do ((i 0 (1+ i))
       (j (1- ln) (1- j)))
      ((>= i j) (snd-from-array 0 *sound-srate* s-array))
    (setf temp (aref s-array i))
    (setf (aref s-array i) (aref s-array j))
    (setf (aref s-array j) temp)))

See here for more information about the “DO” loop construct: https://www.audacity-forum.de/download/edgar/nyquist/nyquist-doc/xlisp/xlisp-ref/xlisp-ref-093.htm
Note that in the code above, the DO loop returns (snd-from-array 0 sound-srate s-array)
In other words, it is converting the array “s-array” back into a sound, and returning the sound.
The returned sound is then assigned as the value of our variable “left-channel”.

Here’s the full code:

;version 4

; set integer length of selection (real to integer)
(setf ln (truncate len))

(print "got length of selection")
(print ln)

; Grab samples from Left track into an array
(setf s-array (snd-fetch-array (aref *track* 0) ln ln))

(print "left channel into s-array")

;; Reverse the array and convert back to sound and assign to the variable "left-channel"
(setf left-channel
  (do ((i 0 (1+ i))
       (j (1- ln) (1- j)))
      ((>= i j) (snd-from-array 0 *sound-srate* s-array))
    (setf temp (aref s-array i))
    (setf (aref s-array i) (aref s-array j))
    (setf (aref s-array j) temp)))

(print "left flipped")
(print ln)


;; Do the same for the right channel:

(setf s-array (snd-fetch-array (aref *track* 1) ln ln))

(print "right channel into s-array")

(setf right-channel
  (do ((i 0 (1+ i))
       (j (1- ln) (1- j)))
      ((>= i j) (snd-from-array 0 *sound-srate* s-array))
  (setf temp (aref s-array i))
  (setf (aref s-array i) (aref s-array j))
  (setf (aref s-array j) temp)))

(print "right flipped")

; Return an array of the two sounds:
(vector left-channel right-channel)

Here: Missing features - Audacity Support

This is exactly the correct forum board. It was set up specifically to provide a place for people to ask questions about Nyquist.

There’s a lot more information now than when I started :wink:
There’s a list of useful documentation here: Manuals and reference material
I’d recommend bookmarking these links. If you’re still interested in writing Nyquist scripts, you’ll use them a lot:
Missing features - Audacity Support
Index
XLISP Language Reference

The Audacity manual has a section about Nyquist:
https://manual.audacityteam.org/man/nyquist.html

There’s a gradually increasing amount of Nyquist documentation in the Audacity wiki. The list of pages in the “Nyquist” category is here:

Well, I am learning. The concept of a loop returning something is a bit strange to me. I saw ((>= i j) (snd-from-array 0 sound-srate s-array)) but didn’t fully understand what it was doing. Also, some time back I no remember reading the section on in the multi-track tutorial about multi-channel but forgot about it and made the comment about not finding doc after searching for it in the Nyquist Reference Manual.

One of my hobbies is restoring the audio quality of old LP’s. It is an outgrowth of another hobby – genealogy. I also write about all sorts of stuff but most recently about a soldier in the Spanish American War and I did a video about a pilot who was lost during the Battle of the Bulge. The LP hobby is what got me into Audacity. I use it to digitize the LP and prepare the stereo tracks for a program call Click Repair by an Australian named Brian Davies. It is a pretty remarkable program – uses wavelets to reconstruct corrupted audio. However, with any auto process, it has occasional hickups or flat failures if the corruption is spread over a long interval (long relative to a click or pop). That is when I start using Audacity to manually clean up the audio tracks. I have developed a number of macros to help in this regard but even they make some of the work tedious. That is why I decided to learn how to make a plug-in to expedite the work. I have done about 30 LP’s that were pressed in the 1950’s and have about 70 to go. Of the 30, I will probably go back and do some over as I have learned more about Audacity and help from the macros (and now plug-ins).

Who wrote the “Repair” effect? Are there wavelet interpolation schemes in Nyquist?

Thanks for the help. I hope my welcome is not wearing out because I’m sure I will have more problems as I try to do more with Nyquist. Like I said, I write — it is a lot easier to read someone’s writing and comprehend what they are conveying than it is to write the same thing yourself.

I know you guys must have more on your plates than time but if you ever catch up it seems that converting the current plug-ins to Version 4 and commenting the hell out of them would be as useful, if not more so, than the current tutorials.

Jim

In pseudo code, that might be written as:

if (i >= j)
  {
   return(snd-from-array 0 *sound-srate* s-array)
  }
else
  {
  ... body of the loop
  ...
  }

The XLisp manual is very helpful for XLisp functions as it provides examples.

The Nyquist Reference Manual is just a “reference” and doesn’t provide much in the way of explanations.

The Nyquist documentation in the Audacity wiki aims to fill some of the gaps, covering things that are specific to Audacity and not documented anywhere else.


The Repair effect isn’t written in Nyquist. It’s written in C++. I don’t know who wrote the original version, but it has been modified by numerous developers over the years. The repair is performed by interpolation, using Least Squares AutoRegression (LSAR) https://github.com/audacity/audacity/blob/master/src/InterpolateAudio.cpp
The Repair effect could theoretically be written in Nyquist, but LSAR is pretty heavy number crunching and would be very slow in Nyquist.
Nyquist doesn’t have any built-in wavelet transformation functions.
Nyquist does have FFT / IFFT, but this is a very “advanced” topic, as you will see in this tutorial: Nyquist FFT and Inverse FFT Tutorial

For very short waveform repairs, linear interpolation may be sufficient.
For slightly longer repairs, some form of polynomial interpolation may do the job.
For longer repairs, finding an efficient interpolation method is difficult. I wrote a plug-in some years ago that gets around this problem by “patching” the audio: EZ-Patch


Plug-ins that are shipped with Audacity can, in some cases, be instructive, but they are a mixed bag. Some are very old and badly written. Some are rather complicated. The old ones are gradually being brought up to date, but there’s a lot of other priorities.

Steve: I am still bewildered by Nyquist, maybe to the point that I expect too much. All I wanted to do was to make a plug-in that would do the following: LP-filter, flip, LP-filter, flip. You provided a function that does the flip (for short time series) and it works fine for when called the first time. But if I tried to execute the function twice, only the first one flips and I can’t understand what t he second does.
(print “1st flip”)
(multichan-expand #'flip track ln)
(print “2nd flip”)
(multichan-expand #'flip track ln)
Maybe what I want to do: LPfilter-flip-LPfilter-flip is just too much. I had hoped to couple this with a line from Mazzoni’s LP filter plugin: (funcall (nth rolloff '(lp lowpass2 lowpass4 lowpass6 lowpass8)) track frequency)) and a control from the start of his plugin to get the filter order and cutoff.

I looked at the code for the repair effect. That is a piece of work.

I have frequently used your Ez-patch with great success. It is best when a single instrument is playing as you can usually count cycles in the waveform. That is more difficult with the audio from a full orchestra but still worthwhile in many situations. Your channel mixer and repair channel are also greatly appreciated and used in my LP cleanup work. The above intended script would replace conventional LP or HP filters, both of which cause phase shifts and consequently discontinuities (clicks) where they paste back into the audio track. That is why I assembled a macro to do the filtering but with the macro you have to go through the controls twice for the filter. I thought a nice nyquist script would eliminate that wasted effort.

Jim

Let me give an example:

(setf val 10)
(* 2 val)
(+ 1 val)

Question: What is the “return value” of this code?

Let’s look at the wrong reasoning first:
Line 1: We set VAL to 10
Line 2: We doubled VAL, producing 20
Line 3: We added 1, producing 21

The reason this is wrong is that in line 2, yes we applied the multiplication function to double the value of VAL, but we didn’t assign this value to anything and we have not modified VAL. VAL is still = 10.
So in line 3, we add 1 to 10, and the return value is 11.

You can prove this in the Nyquist Prompt.

If we actually want to get the answer 21, then there’s several ways we can do that:

  1. Assigned the result of (* 2 val) to a new variable, then apply the second function (addition) to the new variable:
(setf val 10)
(setf doubleval (* 2 val))
(+ 1 doubleval)
  1. Assign the result of the first function to the variable VAL, effectively updating the value of VAL:
(setf val 10)
(setf val (* 2 val))
(+ 1 val)
  1. Assign the result of the first function to a temporary “local” variable:
(setf val 10)
(let ((temp (* 2 val)))
  (+ 1 val))
  1. Nest the functions so that the value returned from the multiplication is used directly in the second (addition) function. In this version, the innermost function “(* 2 val)” is evaluated first, and its return value is the second argument (second “parameter”) in the outer function:
(setf val 10)
(+ 1 (* 2 val))



OK, so you flipped a sound, but what did you do with that flipped sound? Did you assign it to a new variable? Did you updated the value of track? Did you assign the flipped sound to a temporary “local” variable?

The verbose way to handle this would be something like:

(setf filtered (lowpass2 *track* hz))                   ;Assign filtered audio to a new variable
(setf firstflip (multichan-expand #'flip filtered ln))  ;Flip the filtered sound
(setf filtered (lowpass2 firstflip hz))                 ;Filter flipped sound
(multichan-expand #'flip *track* ln)                    ;Return result of second flip

A more condensed way using temporary “local” variables:

;version 4
;type process

(defun flip (sig ln)
  (let ((s-array (snd-fetch-array sig ln ln)))
    (do ((i 0 (1+ i))
         (j (1- ln) (1- j)))
        ((>= i j) (snd-from-array 0 *sound-srate* s-array))
      (setf temp (aref s-array i))
      (setf (aref s-array i) (aref s-array j))
      (setf (aref s-array j) temp))))


(setf ln (truncate len))
(setf hz 4000)

(let* ((flipped (multichan-expand #'flip *track* ln))
       (filtered (lowpass2 flipped hz)))
  (lowpass2 (multichan-expand #'flip filtered ln) hz))

You can do that like this (well actually, the code below does “flip → filter → flip → filter”, but same effect):

You will probably find it an instructive exercise to look up all of the functions in this code in the Nyquist / XLisp manuals to work out what it is doing and how. Feel free to ask questions, that’s what this forum board is for :wink:

;version 4
;type process

(setf hz 4000)  ;Filter corner frequency

(setf maxsamples  1000000) ;Must be less than maximum supported by snd-samples

(defun flip (sig)
  (let ((s-array (snd-samples sig maxsamples)))
    (do ((i 0 (1+ i))
         (j (1- ln) (1- j)))
        ((>= i j) (snd-from-array 0 *sound-srate* s-array))
      (setf temp (aref s-array i))
      (setf (aref s-array i) (aref s-array j))
      (setf (aref s-array j) temp))))

(defun zerophase (type sig hz)
  (if (arrayp sig)
      (setf ln (snd-length (aref sig 0) (1+ maxsamples)))
      (setf ln (snd-length sig (1+ maxsamples))))
  (when (> ln maxsamples)
    (error "zerophase max 1 million samples exceeded."))
  (let* ((sig (multichan-expand #'flip sig))
         (sig (funcall type sig hz))
         (sig (multichan-expand #'flip sig)))
    (funcall type sig hz)))


;; Example usage:
(zerophase 'highpass2 *track* hz)

&^$#@@ — Steve, you make it all look so simple! I’ve got to finish up a video project but will get back to this soon and try out your latest. As always, surely it will work. The little tutorial you wrote about 20x2+1 was very helpful. I have missed that subtlety in Lisp from the beginning. Do you know any experts in LISP and Nyquist who live in Austin TX?

Jim

Sorry no, though you only live about 1400 miles from the man who created the Nyquist language :wink: