Building an audio track from labels: take 2, with label-determined pitch

Someone asked earlier how to make a sound repeat at some labels. The solution there was to use a tool plug-in. If you can generate the sound within Nyquist, you can also do, just with the present Audacity a label-driven “Ni” synth. :laughing:

First Makes some labels that say “pitch:XX”. You can actually gen this “score” from Ny too, e.g. type at the prompt:

(quote ((1.47447 2.92571 "pitch:60") (2.21751 3.1463 "pitch:70") (2.65868 4.60916 "pitch:80")))

The labels overlap on purpose in the above example.

Then here comes the Ni synth:

;version 4
;type generate

(defun pitch-from-label (str)
   (eval-string (string-left-trim "pitch:" str))) ; not much error checking here

;; Audacity ignores the initial shift, so must add a s-rest
;; shift-time won't work here
(defun note-snd (beg end pitch)
   (seq (s-rest beg)
        (pluck (pitch-from-label pitch)
               (- end beg))))

(defun note-snd-from-label (lbl)
   (apply #'note-snd lbl))

(setq lbls (second (first (aud-get-info "Labels"))))
(setq snd-list (mapcar #'note-snd-from-label lbls)) 

;; sim is a macro, so we can't use apply
(eval (cons 'sim snd-list))

Result of running that:

You can replace pluck with osc to see that the note spans are correct (they are), but you’d need to add something like velocity param besides pitch for nicer overlaps of sine waves (no clipping).

Nyquist has even nicer STK instruments than just pluck, e.g. clarinet, but they have somewhat weird “breath” parameters that I haven’t figured out how to use just yet.

Also you can’t use sampler with type generate, since there’s no easy way to read a sound sample and return sound that doesn’t overwrite it, i.e. with type process, so no easy way make the output go to some other sound track. One could probably come up with some scratch trick to select two tracks, one with the sample, and one as the target to be written, but it’s annoying, especially having to deal with different lengths. I’ll try to make something nicer with my return to new track Audacity patch, which is basically working now, just needs some code cleanup for a PR.

Nice bit of code :ugeek:

It would be more efficient to generate just the notes and offset them. In your code, if there’s a label from 1000 seconds to 1001 seconds, then NOTE-SND generates 1001 seconds of sound. Using SIM and AT, you would only need to generate 1 second of audio for that note.

Also, if you want the notes to align with the labels, irrespective of the selection, you need to account for a possible offset of the start of the selection.

Example:

;version 4
;type generate

(defun pitch-from-label (str)
  (eval-string (string-left-trim "pitch:" str)))

(defmacro add-note (data offset)
  `(at (- (first ,data) offset)
    (pluck (pitch-from-label (third ,data))
           (- (second ,data)(first ,data)))))

(let ((lbls (second (first (aud-get-info "Labels"))))
      (offset (get '*selection* 'start))
      (out (s-rest 0)))
  (dolist (data lbls out)
    (setf out (sim out (add-note data offset)))))

One would be better off with score-gen for a serious solution. My quick sim hack has the downside that if you play 1000 notes that starts 1000 sound gens, most of which output silence. It’s not remotely close to being useful for music production, but for cueing a few sounds works.

Or as you hinted at elsewhere, the ability to read / write Note tracks.

That’s an AND not an or. :stuck_out_tongue: score-gen is the Nyquist thing that essentially uses a priory queue for playback scheduling (well, the underlying timed-seq does), as I understand it somewhat similar to how a Pbind drives an EventPlayer in SuperCollider.

For our purposes in this discussion, using timed-seq directly may be more reasonable. From the comments, which describe this better than the manual…

;; (timed-seq '((time1 stretch1 expr1) (time2 stretch2 expr2) ...))
;; a timed-seq takes a list of events as shown above
;; it sums the behaviors, similar to 
;;     (sim (at time1 (stretch stretch1 expr1)) ...)
;; but the implementation avoids starting all expressions at once
;; 
;; Notes: (1) the times must be in increasing order
;;   (2) EVAL is used on each event, so events cannot refer to parameters
;;        or local variables

I had not actually used timed-seq directly before, and it’s a bit tricky due to how it evals stuff

;version 4
;type generate

(defun pitch-from-label (str)
   (eval-string (string-left-trim "pitch:" str))) ; not much error checking here

(defun note-event (beg end pitch)
   `(,beg 1.0 ; <- that's a stretch factor
      (pluck ,(pitch-from-label pitch)
             ,(- end beg))))

(defun note-event-from-label (lbl)
   (apply #'note-event lbl))

(setq lbls (second (first (aud-get-info "Labels"))))
(print (setq event-list (mapcar #'note-event-from-label lbls)))

; still needs to add that 1st note s-rest, not done here
(timed-seq event-list)

Although in this case it oddly has to be like

(seq (s-rest 0) (timed-seq event-list))

If you put the actual time there of the first note entry, it will be doubled.

A simple implementation using a score.
This assumes that label text is in the form “C4” …
It also requires that the selection starts at time = 0.

;version 4
;type generate

(defun make-note (start end pitch-str)
  (list start (- end start)
        `(pluck (eval-string ,pitch-str))))

(defun make-score (label)
  (setf note (apply 'make-note label)))

(let ((labels (second (first (aud-get-info "Labels")))))
  (setf score (mapcar 'make-score labels))
  (push (list 0 0 '(s-rest 0)) score)
  (timed-seq score))