Using *SCRATCH* to process a selection of several tracks

I’m trying to write a plugin which convolves all selected tracks (assuming mono).
My approach is to store the audio so far as a property of SCRATCH, then retrieve this audio when Nyquist invokes the plugin again on the next track.
I can’t tell why this isn’t working. I’m not sure, but I think nil audio is being added to the property list.

;nyquist plug-in
;version 4
;type process
;preview linear
;name "Convolve Tracks"
;action "Convolving..."
;author "dm"

(defun get-storage ()

(defun clear-storage ()

(defun store (signal)
	(putprop '*SCRATCH* signal 'DM-CONVOLVE-STORAGE))

(defun conv-stored (signal)
	(convolve signal (get-storage)))

(setq index (get '*TRACK* 'INDEX))
(setq tracks (length (get '*SELECTION* 'TRACKS)))
;(print (symbol-plist '*SCRATCH*))
(if (= index 1)
	(if 	(< tracks 2) (print "Select two or more tracks")
		(store *TRACK*))
	(if	(= index tracks) (prog1 (conv-stored *TRACK*) (clear-storage))
		(store (conv-stored *TRACK*))))

Storing the track sound in scratch is a little bit tricky.
Internally, Nyquist uses pointers, so when attaching the track sound to scratch, Nyquist is actually just adding a pointer, which points to the sound.
If the return value from processing a track does not reference the sound, then the sound is deleted, so the pointer on scratch now points to NIL.

Here’s a simple example:

(if (= (get '*TRACK* 'INDEX) 1)
    (putprop '*SCRATCH* *TRACK* 'sig)
    (get '*SCRATCH* 'sig))

With two mono tracks selected:

When processing the first track we add a pointer to TRACK to SCRATCH, and return the sound from TRACK, but we have not referenced the sound in the return value, so the sound gets deleted.

When processing the second track, we retrieve the pointer from SCRATCH, but the previous TRACK sound has been deleted, so the pointer now points to NIL.

Now consider this version where we explicitly reference the sound in TRACK in our return value and save it in memory when processing the first track:

(if (= (get '*TRACK* 'INDEX) 1)
      (putprop '*SCRATCH* *track* 'sig)
      (format nil "stored ~a samples" (snd-length *track* ny:all)))
    (get '*SCRATCH* 'sig))

Thank you, Steve!

I think I am misunderstanding your explanation. I replaced store in my code with this macro:

(defmacro store (signal)
	(putprop '*SCRATCH* ,signal 'DM-CONVOLVE-STORAGE)
	(snd-flatten ,signal ,ny:all)))

As I understand it, the return value calls store every time, unless we’re on the last track, and store calls snd-flatten, which forces samples to be computed and stored in memory. putprop creates a pointer and points it to this sound (or if DM-CONVOLVE-STORAGE is already a property of SCRATCH, it just changes where it’s pointing).
I can tell from how fast the plugin runs that it’s still not convolving anything, but there’s no ‘table size must be greater than 0’ error in the debug window.

I’m also wondering about my clear-storage function now. Is remprop just deleting the pointer? Will Nyquist delete the stored samples on its own now that no properties in SCRATCH point to them?

Yes, that should work. You just need to ensure that the sound is retained in memory, which that should do.

It’s working for me.

Here’s my “before and after”:

It works when pasted into the nyquist prompt, but not when installed as a plugin. Why is that?
convolve.ny (774 Bytes)

Try changing the store macro to:

(defmacro store (signal)
    (putprop '*SCRATCH* ,signal 'DM-CONVOLVE-STORAGE)
    (format t "~a" (snd-flatten ,signal ,ny:all))))

That works, thank you!
Why is format necessary in this case?

For process effects, if a non-audio result is returned, processing stops after the first returned value.
In most cases this is what you would want. For example, if the user selects 16 stereo tracks and applies an effect that requires mono tracks, you wouldn’t want the error message popping up 32 times before it finally quits.

An exception is made for the Nyquist prompt as this is really intended as a plug-in developer’s tool, and it’s useful for developers to see what is going wrong (or not) for each selected track.

In this case, snd-flatten returns an integer, which would cause the process effect to stop after the first track, but by redirecting that to the debug window, the effect can continue to the next track.

Strictly speaking, this creates an error, because “store” now returns NIL, which is an error for process type effects, but it is non-fatal so the effect continues to process the other tracks.

Actually that is not quite true. It is NOT necessary to the retain the sound in memory. All that is required is that the script returns an evaluation of the sound. For example, this will work:

(defun store (signal)
  (putprop '*SCRATCH* signal 'DM-CONVOLVE-STORAGE)
  (snd-play signal))

Again, this is, strictly speaking, an error because snd-play returns NIL, but it computes all of the samples without retaining them in memory.

One way to get this to work without any errors, would be to return an evaluation that is identical to signal, for example:

(defun store (signal)
  (putprop '*SCRATCH* signal 'DM-CONVOLVE-STORAGE)
  (sum (s-rest 0) signal))

You may notice that when more than 2 tracks are selected, unlike using snd-play, this modifies all of the intermediate tracks as well as the final track, which may or may not be a good thing, depending on what you want the behaviour to be.

By the way, it is recommended to use spaces rather than tabs when indenting Nyquist / LISP code (and most other code). Tabs will often mess up the appearance when others view your code as it is unlikely that they will be using the exact same tab size as you.
There’s a good, short guide to LISP indentation here:

Thank you again, very informative!
So if I understand your first reply, what you’re saying is that process plug-ins will stop processing tracks after the plug-in returns a string or number, but will continue if the plug-in returns audio, nil (non-fatal error), a label track, or an empty string.
Storing a sound in SCRATCH requires a return value which calls a function which computes that sound, but this function does not need to store it. In particular, snd-play just computes its argument’s samples and throws them out, returning nil. Format composed with snd-flatten also works because it returns nil.

Generally speaking:

;type process
Iterate through selected audio tracks, modify the audio and return audio to the track(s)

;type analyze
Iterate through selected audio tracks, perform some sort of analysis, return a non-audio result (usually text or labels)

;type generate
Generate audio into the selected audio track(s), or into a new mono track if no audio tracks selected.

Occasionally you will encounter a plug-in that does not fit neatly into any of these three categories. One notable exception is “Regular Interval Labels”, which is classed as an “Analyze” plug-in, although it does not actually analyze anything (other than using the duration of the selection).

The next version of Audacity will have an additional new type:
;type tool
These plug-ins appear in a new “Tools” menu.
Roughly speaking, you can think of the “tool” type as a catch-all menu for effects that do not fit nicely in the other effect types, but there are some special features, abilities and limitations to “tool” types. The documentation has not yet fully caught up, so more about this later.

No. Process effects will stop processing tracks after the plug-in returns anything other than audio or NIL.

Calling “NIL” an error, is really being quite pedantic. It will print to Audacity’s log “Nyquist returned nyx_error.” but that’s really only because it is not returning a data type recognized by Audacity.

SCRATCH was a relatively late addition to Nyquist. It is specific to Audacity and is not part of the core Nyquist language. It was added as a kind of “scratchpad” to allow values to be retained beyond the lifespan of the code execution. It wasn’t actually intended for storing sounds, but of course, once SCRATCH was created, someone was sure to try storing a sound in it. That was when it was discovered that storing a sound in scratch didn’t work, because after the code is executed, Nyquist tries to clean up after itself.

To fully understand what happens with sounds in SCRATCH you would need to study Nyquist’s source code (mostly written in C). C is not one of my languages, so really I only understand this as far as observing the behaviors and effects.

By the way, the easy way to store short sounds in SCRATCH is to copy the sample values into an array, then store the array in scratch.
(see and )

Another “by the way”…

In very recent versions of Audacity, if an empty string is returned, it is treated as a no-op (a valid result that does nothing).
Previous versions would pop up an empty message box, but now a message box is popped up if a non-empty string is returned.
For process effects, an empty string will stop processing after the first track, because it is not audio and not NIL.