Nyquist documentation error

The documentation for Nyquist Return Values (https://wiki.audacityteam.org/wiki/Nyquist_Plug-ins_Reference) appears to be incorrect. It states that returning an empty string “” can be used to abort the plugin. But I’ve found the following:

  • Returning a non-empty string like “When in the course of human events” shows that string in a popup dialog, then the next selected track is processed.


  • Returning an empty string like “” means no dialog is shown and the next selected track is immediately processed.


  • Returning a value of nil aborts the plugin and no further tracks are processed.

I’ve confirmed this on both Mac OS X and Windows 10 with Audacity 3.1.3

Where does it say that?
It says:

An empty string may be used as a “null return”, which means that the plug-in returns nothing, and no error.

but that does not mean that it will abort the plug-in. It means that the return value from the plug-in (the empty string) is ignored by Audacity.


Correct. The string is displayed by Audacity.

Correct, the empty string is ignored by Audacity (the empty string acts as a “no-op”, which is a “do nothing” command).

A return value of NIL is an error (look in “Help > Diagnostics > Show Log” to see the error).

If you want to abort a plug-in after processing the first track, a way to do it without raising an error is:

;type process
(if (= (get '*track* 'index) 1)
    (mult *track* 0.5)  ; whatever you want to do to the first track
    "")  ;no-op to skip other tracks

Here’s my confusion. As you pointed out it states:

“An empty string may be used as a “null return”, which means that the plug-in returns nothing, and no error. An example use would be if you wish to process only the first selected track.”

Then it gives an example returning an empty string “” to process only the first track. But that’s not what happens. Take a look at this code:

;nyquist plug-in
;version 4
;type analyze
;name "Nyquist Return Value Test"
;maxlen 2143260000
;;;debugflags trace
;author "JL (Jim) Doty"
;release 1.0.0
;copyright "Released under terms of the GNU General Public License version 2"
;;;Note from Jim: I borrowed a lot of code from Steve Daulton's "ACX Check" plug-in.

(defun string-ends-with (str sub)
  (let 
    ((nstr (length str)) 
    (nsub (length sub)))
    (and (>= nstr nsub) (string-equal sub (subseq str (- nstr nsub) nstr)))))
            
(defun ismacOS()
  (let* ((docsPath (format nil "~a" (get '*system-dir*' documents)))
    (usersPart (subseq docsPath 0 6)))
    (if (string-equal usersPart "/Users") t nil)))

;;;Searches the "Recent Files" menu for an entry that ends with:
;;; (path separator) + (Project Name) + (".aup" or ".aup3")
;;;If it finds such an entry, it returns the fully qualified file name, 
;;;including path, file name, and extension.
;;;If it doesn't find such an entry, it returns the empty string "".
(defun projPathName-from-recents()
  (let ((menus (aud-get-info "menus"))
        (menuStr (if (ismacOS) "Open Recent" "Recent Files"))
        (fnameOld (format nil "~a~a~a" *file-separator* (get '*project*' name) ".aup"))
        (fnameNew (format nil "~a~a~a" *file-separator* (get '*project*' name) ".aup3"))
        isrecent
        recent)
    (dolist (row menus nil)
      (let ((str (second (assoc 'label row))))
        (when isrecent
          (when (string= str "----")
            ;end of recent files section
            (return-from projPathName-from-recents ""))
          ;;Simplified test for match: it must end with 
          ;;(path separator) + (Project Name) + (".aup" or ".aup3")
          (when (or (string-ends-with str fnameNew) (string-ends-with str fnameOld))
            (return-from projPathName-from-recents str)))
        (when (string= str menuStr)
          ;start of recent files section
          (setf isrecent t))))))

;;;Uses projPathName-from-recents to get fully qualified projPathName, then if that 
;;;failed it returns the default documents directory. If it succeeded, it strips off 
;;;the filename and returns only the fully qualified path without path separator.
(defun get-project-folder()
  ;;; Return project's path or if not found
  ;;; return the system default document dir.
  (let* ((projPathName (projPathName-from-recents))
        (nPN (length projPathName))
        (fnameOld (format nil "~a~a~a" *file-separator* (get '*project*' name) ".aup"))
        (nOld (length fnameOld))
        (fnameNew (format nil "~a~a~a" *file-separator* (get '*project*' name) ".aup3"))
        (nNew (length fnameNew))
        (default (get '*system-dir*' documents)))
    (cond 
      ((string= projPathName "")
        (return-from get-project-folder default))
      ((string-ends-with projPathName "3")
        (return-from get-project-folder (subseq projPathName 0 (- nPN nNew))))
      (t
        (return-from get-project-folder (subseq projPathName 0 (- nPN nOld)))))))

;;If the file exists, read it into memory so new data can be appended to it
(defun read-file(fname)
  ;;; If file exists, copy its contents.
  ;;; Return the data, or empty string.
  (setf data "")
  (setf fp (open fname))
  (when fp
    (do ((line (read-line fp) (read-line fp)))
      ((not line))
    (setf data (format nil "~a~a~%" data  line)))
  (close fp))
data)

(defun get-return-string()
  (if (= (get '*track* 'index) 1)
    "Non-empty string"
    ""
  )
)

(defun get-selection-details()
  (setf details (format nil "Iteration ~a of ~a. Returning \"~a\"~%" 
    (get '*track* 'index) (length (get '*selection* 'tracks)) (get-return-string)))
  details
)

(cond
  (t

    (setf filePathName (format nil "~a~a~a" 
      (get-project-folder) 
      *file-separator*
      "Nyquist Return Value Test.txt"))

    ;;If this is not the first track to be processed, read the data already written 
    ;;to the file and prepend it to this track's data.
    (setf priorData
      (if (> (get '*track* 'index) 1)
        (read-file filePathName)
        ""
      )
    )
    ;; Open the file for writing.
    (setf fp (open filePathName :direction :output))
    ;; Use the 'format' command to write to the file pointer 'fp'.
    ;; appending any new data to any prior data that was already there.
    (format fp "~a~a" priorData (get-selection-details))
    ;; Close the file.
    (close fp)
  )
)

;;Returning a non-empty string on every pass means a dialog is displayed for each selected track
;;Returning nil aborts the plugin and no further tracks are processed
;;Returning an empty string "" means no dialog is shown, but next track is still processed

(get-return-string)

It has a function that returns “Non-empty string” on the first track, and “” for subsequent tracks. But in the background it writes data to a file each time a track is processed detailing which track is processed.

Open or create a project with at least 4 wave tracks, select all four tracks and run the plugin. It should process the first track and return “Non-empty string”, then process the 2nd track and return “”, which should abort any further processing. But if you open the file “Nyquist Return Value Test.txt”, you’ll see that it processes all four tracks.

It gives an example to “Apply a function” to the first selected track only.

When Audacity applies an “Effect” (a “;type process” plug-in in the case of Nyquist), Audacity iterates over each selected track, unless there is an error.
When Nyquist returns an empty string, it does not prevent Audacity from iterating over each selected track. Audacity still iterates over the tracks, but with “no operation” on the tracks. In effect, the tracks are skipped.

In the case of your code example, Nyquist is returning a no-op (as described in the docs), but your code also has “side effects”.
This runs for every track:

(setf filePathName (format nil "~a~a~a"
                           (get-project-folder)
                           *file-separator*
                           "Nyquist Return Value Test.txt"))

;;If this is not the first track to be processed, read the data already written
;;to the file and prepend it to this track's data.
(setf priorData (if (> (get '*track* 'index) 1)
                    (read-file filePathName)
                    ""))
;; Open the file for writing.
(setf fp (open filePathName :direction :output))
;; Use the 'format' command to write to the file pointer 'fp'.
;; appending any new data to any prior data that was already there.
(format fp "~a~a" priorData (get-selection-details))
;; Close the file.
(close fp)

When a ;type process plug-in is installed, if the plug-in returns a (non-empty) string after the first track, then Audacity stops iterating over the tracks and displays the string. The reason for this is that it would be really annoying to see an error message for every track if you have a lot of tracks selected.

(The behaviour is different for ;type analyze effects. If an analyze effect returns a string, it’s likely that the string is the desired result, and likely that you will want to see it for each track.)

Try running this code with the Debug button:

;nyquist plug-in
;version 4
;type process
;name "AAA test"

;control returnval "Return value" string ""

(format t "Track Number ~a~%" (get '*track* 'index))

; Return a string
returnval

Notice that if the effect returns a non-empty string, Audacity displays the string for the first track and then stops. However, if the effect returns a “no-op” (an empty string), then Audacity does nothing and continues to the next track.

I tried what you proposed and am still confused, so I set up a project with 4 identical tracks and ran 4 variations of the following code on it:

;nyquist plug-in
;version 4
;type process
(defun get-return-something()
  (if (= (get '*track* 'index) 2)
    "Non-empty"
    (scale 0.1 *track*)
  )
)
(get-return-something)

I ran it with line 3 as (;type process) and (;type analyze) and line 6 as (“Non-empty”) and (“”). In all four cases it did the same thing: the selected portion of tracks 1, 3 and 4 were attenuated, while track 2 remained unchanged.
Nyquist Return Value Test 2_9_2022 6_45_05 AM.png

In LISP languages, avoid trailing parentheses.
Rather than

;nyquist plug-in
;version 4
;type process
(defun get-return-something()
  (if (= (get '*track* 'index) 2)
    "Non-empty"
    (scale 0.1 *track*)
  )
)
(get-return-something)

write:

;nyquist plug-in
;version 4
;type process
(defun get-return-something()
  (if (= (get '*track* 'index) 2)
    "Non-empty"
    (scale 0.1 *track*)))
(get-return-something)

I’ll be back shortly - have to go for while…

1 Like

My guess is that you were running them in the Nyquist Prompt, rather than as a normal “installed” effect.

The Nyquist Prompt is a development tool, so it behaves slightly differently from normal installed plug-ins.

When running code in the Nyquist prompt that produces a GUI (has “;control” widgets):
Audacity reads the code from the Nyquist Prompt, and when it sees the ;control widgets, it creates a GUI and spawns a “Nyquist Worker” effect. The Nyquist Worker effect behaves like any other “normal” (installed) Nyquist plug-in.


When running code in the Nyquist Prompt that does not produce a GUI, then you may notice this difference:

Although “not showing multiple messages” is usually the best thing for Audacity users, it can make debugging difficult for Nyquist developers if their code quits early. So for running code in the Nyquist Prompt (without a “Nyquist Worker” effect), ;type process effects do not quit after the first non-empty string - Audacity will continue iterating over all selected tracks.