Can't seem to use *track* more than once in an fx, even with snd-copy

Prerequisite setup: generate a 3s mono track e.g. with Tone. Select some one second portion of it. Then try the following snippets from the Ny prompt, undoing after each one:

;version 4
;type process
(seq (mult (hzosc 333) 0.7) (mult (hzosc 999) 0.2)) ; ok

That one works as expected, i.e. it’s a generator pretending to be an fx, and is in fact a (practical) way to return longer audio than the original sample, even from an fx (“type process”). Audacity lengthens the track to accommodate the extra sound returned, e.g. the track will be 4s long after running the above.

You can make an actual effect from something like that e.g.

;version 4
;type process
(seq (mult *track* 0.7) (mult (hzosc 999) 0.2)) ; also ok

But I can’t seem to make it process the track more than once:

;version 4
;type process
(seq (mult *track* 0.7) (mult *track* 0.2)) ; error

I thought the problem might be with playback being destructive of the Lisp objects, so need a snd-copy, but alas even the following still doesn’t work:

;version 4
;type process
(seq (mult (snd-copy *track*) 0.7) (mult (snd-copy *track*) 0.2)) ; still error

The error in the debug windows is pretty lengthy, but it’s not incredibly clear what’s causing it. Probably the sound was already mutated as snd-copy is probably a shallow copy. For a simpler stack trace, just

(seq (snd-copy *track*) (snd-copy *track*))

Errors like this:

error: bad argument type - NIL
Function: #<Subr-SND-COPY: #0000022AF3B01E70>
Arguments:
  NIL
Function: #<FSubr-PROGV: #0000022AF3B05290>
Arguments:
  (QUOTE (*WARP*))
  (LET ((TIM T0)) (NY:TYPECHECK (NOT (NUMBERP TIM)) (ERROR "1st argument of AT-ABS (or 2nd argument of SAL's @@ operator) should be a number (start time)" TIM)) (IF (WARP-FUNCTION *WARP*) (LIST (LIST (SREF-INVERSE (WARP-FUNCTION *WARP*) TIM) (WARP-STRETCH *WARP*) (WARP-FUNCTION *WARP*))) (LIST (LIST TIM (WARP-STRETCH *WARP*) NIL))))
  (CHECK-T0 (FORCE-SRATES S%RATE (SND-COPY *TRACK*)) (QUOTE (FORCE-SRATES S%RATE (SND-COPY *TRACK*))))
Function: #<FSubr-PROGV: #0000022AF3B05290>
Arguments:
  (QUOTE (*WARP* *SUSTAIN* *START* *LOUD* *TRANSPOSE* *STOP* *CONTROL-SRATE* *SOUND-SRATE*))
  NYQ%ENVIRONMENT
  (AT-ABS T0 (FORCE-SRATES S%RATE (SND-COPY *TRACK*)))
Function: #<FSubr-SETF: #0000022AF3AFD028>
Arguments:
  FIRST%SOUND
  (EVAL-SEQ-BEHAVIOR (SND-COPY *TRACK*) "SEQ")
Function: #<Closure: #0000022AF28EEE78>
Arguments:
  1.0124
1>

Just something like

(seq (mult 0.2 (snd-copy *track*)))

works though, so it’s clearly the 2nd copy that’s causing problems.

So, is there a way to fix that and use the same track more than once in a Nyquist fx plugin?

Adapting Steve’s code from another thread (on s-reverse), I can make it work by creating an in-memory copy.

;version 4
;type process

(defun snd-memcpy (sig)
   (setf *MAX-MEMCPY-SAMPLES* 1000000)
   (let ((ln (snd-length sig (1+ *MAX-MEMCPY-SAMPLES*)))
         (srate (snd-srate sig)))
      (when (> ln *MAX-MEMCPY-SAMPLES*)
         (error "*MAX-MEMCPY-SAMPLES* exceeded"))
      (let ((ar (snd-fetch-array sig ln ln)))
         (snd-from-array 0 srate ar))))

(setq *track-memcpy* (snd-memcpy *track*))

(seq (mult *track-memcpy* 0.2) (mult *track-memcpy* 0.7))

That seems to do the right thing, although it still complains that

Warning: cannot go back in time to 0, sound came from (FORCE-SRATES S%RATE (MULT *TRACK-MEMCPY* 0.7))

Despite the warning, the sound is copied properly twice, I tested with an one with that has an obvious fade-in envelope pre-applied.

I’m not exactly illuminated on the difference between Nyquist’s notions of sound vs behavior, but the harmless waning can be suppressed by writing

(seq (cue (mult *track-memcpy* 0.2)) (cue (mult *track-memcpy* 0.7)))

To avoid the in-memory copy I guess I cold try writing a copy of the sound to scratch, but I’m not sure that circumvents the in-memory copy. There are some functions that write a sound to disk directly from Nyquist, but I haven’t dabbled with those insofar. I’m not sure if they’re accessible in a fx plug-in.

That’s not just suppressing the warning, it is fixing the problem that warning is telling you about. In fact, you only need the second CUE

(seq sound1 (cue sound2))

The reason for the warning is that sounds have a start time.
There’s no problem with sound1 because (in your code) the start time of sound1 is 0, and SEQ is returning it with a start time of 0.
The problem is with sound2. SEQ has moved on in time to the end of sound1 and then tries to add sound2, but the start time of sound2 is also 0. It can’t go back in time from the end of sound1 back to time=0, so it gives a warning and shifts the start time of sound2 to the current time (the end of sound1).

When you add CUE, you are telling SEQ to ignore the start time of sound2 and just use sound2 when needed (at the end of sound1).

I think the problem is that track (a global variable) is being garbage-collected after it has been used the first time by SEQ.
Probably the best solution is to make a local copy of track so that it sticks around for the duration of the local scope.

(let ((local-sound *track*))
  (seq (mult 0.2 local-sound)
       (cue (mult 0.7 local-sound))))

Note that you can modify or delete track once “local-sound” has been created.

(let ((local-sound *track*))
  (setf *track* nil)
  (seq (mult 0.2 local-sound)
       (cue (mult 0.7 local-sound))))

and we can see in the above that (global) track is now NIL

(let ((local-sound *track*))
  (setf *track* nil)
  (seq (mult 0.2 local-sound)
       (cue (mult 0.7 local-sound))))

(print *track*)  ;prints NIL

and “local-sound” does not exist outside of the LET

(let ((local-sound *track*))
  (seq (mult 0.2 local-sound)
       (cue (mult 0.7 local-sound))))

(print local-sound) ;error: unbound variable - LOCAL-SOUND

Thanks, that was simpler than I had thought. Looking a bit more of how seq is supposed to work, in “pure” Nyquist at least, it seems it wants to capture in a closure the current local vars. But in Audacity’s case that should probably also extend to the global vars like track. I posted this as a feature request. https://github.com/audacity/audacity/issues/2396

Also, I see there’s the built-in (C++ i.e. non-Nyquist) “Repeat…” plugin that does n copies of the selection.

I’m not sure if that’s possible (Roger Dannenberg will know). SEQ is tricky code and has to be quite aggressive with garbage collection so as to avoid filling up RAM with long tracks. TRACK is somewhat special as it is a pointer to external data. Ideally we are looking at throughput - read the audio data from disk, process, and return or release the data - we normally do not want to retain a copy in RAM. In this case we need to use track data multiple times, so I think it has to be retained in RAM, so we need to prevent the track data from being garbage collected.

This code does what we want:

(let ((local-sound *track*))
  (setf *track* nil)
  (seq (mult 0.2 local-sound)
       (cue (mult 0.7 local-sound))))

but observe that when we run it on a long track (say an hour or more), the RAM usage increases to accommodate the track audio. On very long tracks the code will fail when audio memory is exhausted.

If we really need the code to work on very long tracks, we may be able to do that by writing the track data as a temporary WAV file, then read it back in when needed (I’ve not tested).