How to delete all samples that meet a certain condition using Nyquist per-sample processing?

I have a piece of Nyquist code where I can insert any type of math function that modifies each sample. (Huge thanks to Steve Daulton for the original code!)

;type process
;version 4
;copyright Based on example code by Steve Daulton from https://forum.audacityteam.org/t/how-to-operate-on-samples-directly-e-g-spec-f-t/31606/4

;;; DSP function that will be executed for each sample
(defun sample_function (a)
  (dotimes (i (length a) a)
    ; write your function here, with (aref a i) as the input sample
    ;               ↓
    (setf (aref a i)(awesome_function (aref a i)))))

;;; Iterate through audio selection
;;; grabbing an array full of samples at a time.
(defun iterated_function (sig)
  ; len is a global variable for number of samples in the selection
  (let* ((Samples (truncate len))
         (alen 10000)                   ; array size
         (it (truncate (/ len alen)))   ; iterations
         (remain (rem samples alen))    ; samples left over
         (adur (/ alen *sound-srate*))  ; duration of alen as sound
         (remaindur (/ remain *sound-srate*))
         (out (s-rest 0)))              ; initialise output
    (do ((i 0 (1+ i))
         tmpsnd
         (tnext 0 (+ adur tnext)))
        ((= i it))                      ; do until i=it
      ; Get the samples
      (setf sndarray (snd-fetch-array sig alen alen))
      ; Apply the DSP
      (setf sndarray (sample_function sndarray))
      ; Convert back to a ]temporary] sound
      (setf tmpsnd (snd-from-array 0 *sound-srate* sndarray))
      ;;Add 'tmpsnd' to the end of 'out'
      (setf out
        (sim out
          (at-abs tnext (cue tmpsnd)))))
    ;; Now do any remaining samples
    (if (> remain 0)
        (progn
          (setf sndarray (snd-fetch-array sig remain remain))
          (setf sndarray (sample_function sndarray))
          (sim out
            (at-abs (- dur remaindur)
              (cue (snd-from-array 0 *sound-srate* sndarray)))))
        out)))

; Duration in seconds
(setq dur (get-duration 1))

; 'expand' the iterated function for each channel of *track*.
(multichan-expand #'iterated_function *track*)

I want a version of this code that can delete (truncate) any samples that meet a specified condition (and also a certain amount of following samples if I want) – essentially a version of the Truncate Silence effect that gives you way more control over the DSP:

    (setf (aref a i)(if (condition) (delete_sample_from_array (aref a i))))))

Also, it would be interesting to do the opposite: duplicate samples that meet a condition.

I have attempted to implement this in a few ways – without luck …

  • Converting the sound to a Lisp array, conditional delete, then convert back to sound: It is really hard to loop through an array and change its length!
    • I also tried to create a new, empty array to fill it only with the samples that do or don’t pass the condition
      • But for that I (1) either have to know the resulting length or (2) create an empty array to then push values into it – however, I don’t have access to an array-push function!
  • Converting the sound to a Lisp list (made of conses), conditional delete, then convert back to sound
    • Conditional deletion in lists is really easy using the delete-if function …
    • However, NO functions exist for the sound-to-list and list-to-sound conversions!

It would be really nice to get a helpful reply soon, as others around here don’t have to wait several days to get theirs!

That is by far the easiest way so long as the selection is reasonably small and the track is mono.

It gets a lot more complicated for stereo tracks due to the need to keep both channels synchronised, and a lot more complicated for long selections that are too big to fit in a single array.

I’ll take a look at the “Short selection of mono track” version and see if I can come up with a working example. I’ll hopefully post back later today. I will not consider stereo tracks or long selections because it really does become very tricky.

Here you go. Hopefully this will help (Only part of the code is showing - scroll the code to see it all):

;maxlen 1000000

;; The maxlen line above limits the maximum number
;; of samples to 1 million, so as to avoid crashing.


(setf ln (truncate len))  ; number of samples in selection as integer.

;; Get the sound into an array
(setf ar (snd-fetch-array *track* ln ln))


;; remove all samples from index 1000 to 2000
(setf removed 0)  ; Number of samples removed

(dotimes (i ln)
  (when (and (>= i 1000) (< i 2000))
    (incf removed)  ; increment counter.
    (setf (aref ar i) nil)))


; Calculate number of samples to return
(setf remaining (- ln removed))

;; New array for output
(setf out (make-array remaining))

;; Copy non-nil samples to 'out' array.

(setf idx 0)  ; index counter to keep track of where we are in 'ar'

(dotimes (i (length ar))
  (when (aref ar i)  ; not nil
    (setf (aref out idx) (aref ar i))
    (incf idx)))


; Convert 'out' array to sound and return.
(snd-from-array 0 *sound-srate* out)

1 Like

Awesome code! I modified it for my needs, but I am wondering why Nyquist and Audacity completely freeze if I set maxlen to anything bigger than 1 million (which is only 22.676 seconds at 44.1 kHz sample rate). The processing happens really quickly if the maxlen is 1 million or less. Is there a limit for Lisp array lengths? Or a memory limit for Nyquist?

My current code (prevents stereo tracks from being processed on accident):

;type process
;version 4
;copyright Based on code by Steve Daulton from https://forum.audacityteam.org/t/how-to-delete-all-samples-that-meet-a-certain-condition-using-nyquist-per-sample-processing/105771/4?u=green_guy

;maxlen 1000000
;; max length: 22.676 s at 44100 Hz samplerate – everything above FREEZES Audacity

;; prevent stereo track from being processed – print error message instead (only in debug mode)
(when (arrayp *track*)
  (error "Please split the stereo track into two mono tracks first!"))
(unless (arrayp *track*)

  ; execute main program for mono tracks

  ;;; DSP conditional function
  (defun conditional_function (a)
    ; write your function here
    (< (abs a) 0.1)
  )

  (setf ln (truncate len))  ; number of samples in selection as integer.

  ;; Get the sound into an array
  (setf ar (snd-fetch-array *track* ln ln))


  ;; remove all samples on which the conditional function returns T (true)
  (setf removed 0)  ; Number of samples removed

  (dotimes (i ln)
    (when (conditional_function (aref ar i))
      (incf removed)  ; increment counter.
      (setf (aref ar i) nil)))


  ; Calculate number of samples to return
  (setf remaining (- ln removed))

  ;; New array for output
  (setf out (make-array remaining))

  ;; Copy non-nil samples to 'out' array.

  (setf idx 0)  ; index counter to keep track of where we are in 'ar'

  (dotimes (i (length ar))
    (when (aref ar i)  ; not nil
      (setf (aref out idx) (aref ar i))
      (incf idx)))


  ; Convert 'out' array to sound and return.
  (snd-from-array 0 *sound-srate* out)

)

Maybe I can utilize the code from the initial post (with the iterated_function) and insert this code into the loop that …

Arrays in Nyquist can be bigger than 1 million samples, but I think there is a limit to how big an array (about 1 million samples) you can grab from audio in one go.

In pure Nyquist (not in Audacity) it is not too difficult to iterate over a sound and grab an array at a time, process it, and return it to disk as a sound. Unfortunately it is a bit more tricky in Audacity because Nyquist code can only return one result to Audacity and then quit. There are a couple of workarounds, but they can both be quite tricky to get right.

1 Like

Hooray, I finally have the Nyquist code I wanted for so long! :partying_face:

It also works for stereo tracks now – although it is a bit unpractical to listen to as the channels drift away from each other over time. For that reason, I recommend:

  1. Convert the stereo track to mid and side mono tracks (easier with the “mid/side decode” preset in the Channel Mixer Nyquist plugin).
  2. Apply the effect on either track.
  3. Listen to mid, then to side.

The following version of my final code deletes all samples with an amplitude quieter than ±0.1 and also moves the remaining samples closer to 0 to reduce distortion.

;type process
;version 4
;copyright Based on example code by Steve Daulton from https://forum.audacityteam.org/t/how-to-operate-on-samples-directly-e-g-spec-f-t/31606/4

(setq threshold 0.1)

;;; DSP conditional function
(defun conditional_function (a)
  ; write your function here
  (< (abs a) threshold)
)

(setf total_remaining_dur 0.0)

;;; Iterate through audio selection
;;; grabbing an array full of samples at a time.
(defun iterated_function (sig)
  ; len is a global variable for number of samples in the selection
  (let* ((Samples (truncate len))
         (alen 10000)                   ; array size
         (it (truncate (/ len alen)))   ; iterations
         (remain (rem samples alen))    ; samples left over
         (adur (/ alen *sound-srate*))  ; duration of alen as sound
         (remaindur (/ remain *sound-srate*))
         (out (s-rest 0)))              ; initialise output
    (do ((i 0 (1+ i))
         tmpsnd
         (tnext 0 (+ remaining_dur tnext)))
        ((= i it))                      ; do until i=it

      ;; Get the samples into an array
      (setf sndarray (snd-fetch-array sig alen alen))

      ;; Remove all samples on which the conditional function returns T (true)
      (setf removed 0)  ; Number of samples removed
      (dotimes (orig_idx alen)
        (when (conditional_function (aref sndarray orig_idx))
          (incf removed)  ; increment counter
          (setf (aref sndarray orig_idx) nil)))
      ; Calculate number of samples to return
      (setf remaining (- alen removed))
      (setf remaining_dur (/ remaining *sound-srate*))
      (setf total_remaining_dur (+ total_remaining_dur remaining_dur))

      ;; New array for output
      (setf new_sndarray (make-array remaining))
      ;; Copy non-nil samples to 'new_sndarray'.
      (setf new_idx 0)  ; index counter to keep track of where we are in 'new_sndarray'
      (dotimes (orig_idx (length sndarray))
        (when (aref sndarray orig_idx)  ; not nil
          ;; (setf (aref new_sndarray new_idx) (aref sndarray orig_idx))
          (setf (aref new_sndarray new_idx) (if (< (aref sndarray orig_idx) 0)
            (+ (aref sndarray orig_idx) threshold)
            (- (aref sndarray orig_idx) threshold)))
          (incf new_idx)))

      ;; Convert 'new_sndarray' to sound
      ; correct: no zero-padding at end of new_sndarray
      (setf tmpsnd (snd-from-array 0 *sound-srate* new_sndarray))
      ; correct: tmpsnd has correct snd-length (varying for each iteration)
      ; cue might have a wrong start time from *warp* variable
      (print tnext) ; should use the value
      ;; (when (> i 0) (setf tnext (- tnext (/ remaining *sound-srate*))))
      ;; (print tnext)
      ;;Add 'tmpsnd' to the end of 'out'
      (setf out
        (sim out
          (at-abs tnext (cue tmpsnd)))))
    ;; Now do any remaining samples
    (if (> remain 0)
        (progn
          (print "remaining samples code block")
          ;;; repeating the code from above
          ;; Get the samples into an array
          (setf sndarray (snd-fetch-array sig remain remain))

          ;; Remove all samples on which the conditional function returns T (true)
          (setf removed 0)  ; Number of samples removed
          (dotimes (orig_idx remain)
            (when (conditional_function (aref sndarray orig_idx))
              (incf removed)  ; increment counter
              (setf (aref sndarray orig_idx) nil)))
          ; Calculate number of samples to return
          (setf remaining (- remain removed))

          ;; New array for output
          (setf new_sndarray (make-array remaining))
          ;; Copy non-nil samples to 'new_sndarray'.
          (setf new_idx 0)  ; index counter to keep track of where we are in 'new_sndarray'
          (dotimes (orig_idx (length sndarray))
            (when (aref sndarray orig_idx)  ; not nil
              (setf (aref new_sndarray new_idx) (if (< (aref sndarray orig_idx) 0)
                (+ (aref sndarray orig_idx) threshold)
                (- (aref sndarray orig_idx) threshold)))
              (incf new_idx)))

          ;; Convert 'new_sndarray' to sound
          (sim out
            (at-abs total_remaining_dur
              (cue (snd-from-array 0 *sound-srate* new_sndarray)))))
        out)))

; Duration in seconds for each processed segment
(setq dur (get-duration 1))

; 'expand' the iterated function for each channel of *track*.
(multichan-expand #'iterated_function *track*)

The following, different version of my final code deletes all samples with an amplitude louder than ±0.5.

;type process
;version 4
;copyright Based on example code by Steve Daulton from https://forum.audacityteam.org/t/how-to-operate-on-samples-directly-e-g-spec-f-t/31606/4

(setq threshold 0.5)

;;; DSP conditional function
(defun conditional_function (a)
  ; write your function here
  (> (abs a) threshold)
)

(setf total_remaining_dur 0.0)

;;; Iterate through audio selection
;;; grabbing an array full of samples at a time.
(defun iterated_function (sig)
  ; len is a global variable for number of samples in the selection
  (let* ((Samples (truncate len))
         (alen 10000)                   ; array size
         (it (truncate (/ len alen)))   ; iterations
         (remain (rem samples alen))    ; samples left over
         (adur (/ alen *sound-srate*))  ; duration of alen as sound
         (remaindur (/ remain *sound-srate*))
         (out (s-rest 0)))              ; initialise output
    (do ((i 0 (1+ i))
         tmpsnd
         (tnext 0 (+ remaining_dur tnext)))
        ((= i it))                      ; do until i=it

      ;; Get the samples into an array
      (setf sndarray (snd-fetch-array sig alen alen))

      ;; Remove all samples on which the conditional function returns T (true)
      (setf removed 0)  ; Number of samples removed
      (dotimes (orig_idx alen)
        (when (conditional_function (aref sndarray orig_idx))
          (incf removed)  ; increment counter
          (setf (aref sndarray orig_idx) nil)))
      ; Calculate number of samples to return
      (setf remaining (- alen removed))
      (setf remaining_dur (/ remaining *sound-srate*))
      (setf total_remaining_dur (+ total_remaining_dur remaining_dur))

      ;; New array for output
      (setf new_sndarray (make-array remaining))
      ;; Copy non-nil samples to 'new_sndarray'.
      (setf new_idx 0)  ; index counter to keep track of where we are in 'new_sndarray'
      (dotimes (orig_idx (length sndarray))
        (when (aref sndarray orig_idx)  ; not nil
          (setf (aref new_sndarray new_idx) (aref sndarray orig_idx))
          (incf new_idx)))

      ;; Convert 'new_sndarray' to sound
      ; correct: no zero-padding at end of new_sndarray
      (setf tmpsnd (snd-from-array 0 *sound-srate* new_sndarray))
      ; correct: tmpsnd has correct snd-length (varying for each iteration)
      ; cue might have a wrong start time from *warp* variable
      (print tnext) ; should use the value
      ;; (when (> i 0) (setf tnext (- tnext (/ remaining *sound-srate*))))
      ;; (print tnext)
      ;;Add 'tmpsnd' to the end of 'out'
      (setf out
        (sim out
          (at-abs tnext (cue tmpsnd)))))
    ;; Now do any remaining samples
    (if (> remain 0)
        (progn
          (print "remaining samples code block")
          ;;; repeating the code from above
          ;; Get the samples into an array
          (setf sndarray (snd-fetch-array sig remain remain))

          ;; Remove all samples on which the conditional function returns T (true)
          (setf removed 0)  ; Number of samples removed
          (dotimes (orig_idx remain)
            (when (conditional_function (aref sndarray orig_idx))
              (incf removed)  ; increment counter
              (setf (aref sndarray orig_idx) nil)))
          ; Calculate number of samples to return
          (setf remaining (- remain removed))

          ;; New array for output
          (setf new_sndarray (make-array remaining))
          ;; Copy non-nil samples to 'new_sndarray'.
          (setf new_idx 0)  ; index counter to keep track of where we are in 'new_sndarray'
          (dotimes (orig_idx (length sndarray))
            (when (aref sndarray orig_idx)  ; not nil
              (setf (aref new_sndarray new_idx) (aref sndarray orig_idx))
              (incf new_idx)))

          ;; Convert 'new_sndarray' to sound
          (sim out
            (at-abs total_remaining_dur
              (cue (snd-from-array 0 *sound-srate* new_sndarray)))))
        out)))

; Duration in seconds for each processed segment
(setq dur (get-duration 1))

; 'expand' the iterated function for each channel of *track*.
(multichan-expand #'iterated_function *track*)

Huge thanks for your help, @steve, and have a nice day!

Congratulations :slight_smile:

1 Like