Numbered Label

Hello,

here is a plugin that creates a new label and it sets the number as a label text. The number is increased by 1 for every new label.
The label is created on the same track as the build-in feature Add Label at Selection would do.

Feel free to share any feedback.

Thank you.
Numbered Label.ny (2.27 KB)
(v1; 2022/09/21)

Congratulations. It works nicely.

I don’t have time to study the code in detail, but a few general comments:

  1. Spaces not tabs.
    Unless the person reading the code has the same tab settings as the person writing the code, the indentation will be messed up by tabs (and indentation is very important for reading LISP languages).

  2. Try to avoid very long lines (for readability)
    Example:

(setf msg (format nil "~a- Label n ~a(~a); maxnum=~a; New label ID=~a~%" msg number_of_labels lval maxnum label_ID)))

might be better as:

(setf msg (format nil "~a- Label n ~a(~a); maxnum=~a; New label ID=~a~%"
                  msg number_of_labels lval maxnum label_ID)))
  1. Look to see if some of those global variables can be made local to functions. Global variables aren’t bad per se, but they are a common cause of bugs, and anyone reading the code has to jump around to follow the flow.

  2. Try to avoid long functions.
    Again for readability and to simplify debugging, small functions with clear purpose are easier to read and verify.
    Example, the comment “; get ID of the focused audio track” would be unnecessary if that loop was moved out into a separate function:
    Instead of:

(defun get_new_label_ID_and_value ()
  (let ((labels (aud-get-info "Labels"))
				(tracks (aud-get-info "Tracks"))
				(cursor_time (get '*selection* 'start))
				lval
				(track_id -1))
		(dolist (t tracks) ; get ID of the focused audio track
			(setf track_id (1+ track_id))
			(when (and (= (second (second t)) 1) (= focused_track_ID 0))
				(setf focused_track_ID track_id)))
				...

you could write something like:

(defun get_new_label_ID_and_value ()
  (let ((labels (aud-get-info "Labels"))
        (tracks (aud-get-info "Tracks"))
        (cursor_time (get '*selection* 'start))
        lval
        (track_id -1))
  (get-focus-track-id track)
		...

I think the code could be greatly simplified, but I’m not sure about some of your intentions. For example, if you have 3 labels:

Tracks000.png
What do you want the next label number to be?

In this specific case, when the existing labels are A, 7 and c, then the new numbered label would be 8. Of course, this specific example does not highlight the advantage of the Numbered Label plugin.

Imagine you are preparing the list of the labels that tag the audio parts like paragraphs, boxes, etc. This list then can be converted to the JSON file that can be used on the website, so the reader of article can playback any of the article part by clicking on it. Usually web article paragraphs have incremental numbered IDs for the reference.

Another advantage is that this provides a quick identification. Imagine that you want to label some parts of audio when you do checking. Then you want to write into the printed script that identification. Just with one hotkey, you place a label and then you note down the number of the label into the printed script.

The complexity of the code is increased by:

  • the possibility to have multiple label tracks
  • the possibility to have multiple audio tracks with multiple label tracks (multiple web-articles in one project)
  • it is not possible to create easily a label and set its text on the existing label track by nyquist plugin


Spaces not tabs.

OK, I will do my best to stick to this standard. But I have this problem: I am using Notepad++ and when I select Enter key, the next row will be intended by tab(s) according to the previous row, although the previous row has spaces at the beginning. So for me to stick to the mentioned standard requires extra “manual work”. Every time I hit the Enter I need to remember to check the beginning of the row and most of the time I have to delete tabs and replace them with the spaces. Or in my last project, I used tabs as usually and when I finished, I replaced all tabs with double space.

Of course, I will implement later all your precious suggestions from the first message.

Thank you.

When I was on Windows, I always used NotePad++ for writing Nyquist code, and I still recommend it :slight_smile:
I’ve not used it so much in recent years (I’m usually on Linux and use another app that, like NotePad++, is based on Scintilla, called “Scite”). I’m pretty sure that NotePad++ has an option to always use spaces for indentation, though I don’t recall where that option is.

Thanks for the tip!

So after quick googling, I found it and this is the right settings for Nyquist programming convention:
2022-09-22 17_00_26-Preferences.png
Finally!! :smiley:

I’ll post a series of short comments relating to this plug-in that you may find useful.
Here’s the first:

There’s a (undocumented?) function in the Nyquist source code (file name: “sal-parse.lsp”) called string-to-number.
This is the function definition:

(defun string-to-number (s)
  (read (make-string-input-stream s)))

There’s also a higher level function defined in “aud-do-support.lsp” called number-string-p. This is described as:
“like digit-char-p for strings”
If a string starts with a number (positive or negative, integer or float), then the number is returned, else NIL.
Note that for floats, the decimal separator must be a dot (not a comma).

Thus you can extract a number from a string (or start of a string) with:
(number-string-p string) ; returns number or NIL

Say you have a list like this:

((name "Audio Track") (focused 0) (selected 1) (kind "wave") (start 0) (end 30) (pan 0) (gain 1) (channels 1) (solo 0) (mute 0) (VZoomMin -1) (VZoomMax 1))

and you want to get the value of “focused”.
One way to get the value is from its position:

(second (second my-list))

A downside of this is “readability”. Unless the reader knows the precise form of “my-list”, they are reliant on comments to know what “(second (second my-list))” refers to.

Alternatively, you can use ASSOC to look up the required value:

(second (assoc "focused" my-list))

Even without knowing the precise form of “my-list”, the reader can immediately see that it is retrieving the value of “focused”.

Sorry for the delay, I’ve had a lot on.

There’s a minor issue that may or may not be important. It’s easiest to explain with an example:

  1. Generate a tone
  2. Add a few numbered labels in a label track below the audio track
  3. Make a selection in the audio track before the final label
  4. Press “down cursor” (focus is now on the label track)
  5. Apply “Numbered Label.ny”

A label is created, but it is not numbered.

Around line 29 you have:

    (dolist (ltrack labels) ; loop through all label tracks
      (when (and (> (first ltrack) focused_track_ID) (= label_track_ID -1))
          (setf label_track_ID	(first ltrack)))

I think the issue can be fixed by changing

(> (first ltrack) focused_track_ID)

to

(>= (first ltrack) focused_track_ID)

Alternatively, you could throw a meaningful error if no audio track has focus.

There’s another bug, and this one isn’t so easy to fix.
If the selection start is at exactly the same time as a label in the target track, then there’s no reliable way to determine the new label ID, because the label times returned by GetInfo: are not sufficiently precise.

Example:
If there’s an existing label that starts at: 3.494601 seconds,
then GetInfo : returns a start time of 3.4946 seconds (rounded to 4 decimal places).

Similarly, if there’s an existing label that starts at: 3.4945999 seconds
then GetInfo : returns a start time of 3.4946 seconds (rounded to 4 decimal places).

If the selection start time is exactly 3.494600 seconds, then both of these labels will appear to have the same start time as the selection, when in fact one is before and the other is after.

I’m not sure of the best way to fix this. I’ll think about it and get back to you.

I’m thinking that +/- 0.00005 seconds precision is probably acceptable.

When the new label is added, it is empty (no label text).
We can call “GetInfo: Labels” again after adding the new label, and ensure that the label ID that we think we’ve added is empty. If it’s NOT empty, then we’ve got the wrong label and we actually need the previous ID.

But what if we’ve got the wrong label and it’s an empty label?
If we’ve got the wrong label, it’s because, based on the “GetInfo: Labels” time, we thought that the existing label started before the new label, when in fact it doesn’t.
If both the new label and the “wrong” label are the same size, and the wrong label had no text, then it doesn’t really matter which one we label (assuming that we don’t need better than 4 decimal places precision). However, if the labels are different sizes, then we need to label the one that has start and end times that match the selection.

Can you think of a better solution / workaround?

If we don’t bother with that edge case where the selection starts at the beginning of an existing label, I think this code is reasonably easy to read:

;type tool

(defun newlabel-props ()
  ;;; Return '(ID NUM)
  ;;; ID = index of label to be added = number of labels before
  ;;; cursor in target track.
  ;;; NUM = 1 + highest number found in existing labels.
  (let ((labels (aud-get-info "Labels"))
        (targettrk (get-target-trk))
        (maxnum 0)
        tracknum
        (newlabelid 0))
    (dolist (ltrack labels (list newlabelid (1+ maxnum)))
      (setf tracknum (first ltrack))
      (dolist (label (second ltrack))
        (setf label-num (number-string-p (third label)))
        (when label-num
          (setf maxnum (max maxnum label-num)))
        (when (or (< tracknum targettrk)
                  (and (= tracknum targettrk)
                       (< (labelstart label) (get '*selection* 'start))))
          (setf newlabelid (1+ newlabelid)))))))


(defun get-target-trk ()
  ;; Return ID (zero indexed) of first label track >= track that has focus.
  (let ((tracks (aud-get-info "Tracks"))
        focusfound)
    (dotimes (i (length tracks) (1+ i))
      (when (hasfocus (nth i tracks))
        (setf focusfound t))
      (when (and focusfound
                 (islabeltrk (nth i tracks)))
        (return-from get-target-trk i)))))


(defun hasfocus (trk)
  (= (second (assoc 'focused trk)) 1))


(defun islabeltrk (trk)
  (equal (second (assoc 'kind trk)) "label"))


(defun labelstart (label)
  (first label))


(defun labelend (label)
  (second label))


(let* ((label-props (newlabel-props))
       (id (first label-props))
       (txt (format nil "~a" (second label-props))))
  (aud-do "AddLabel")
  (aud-do-command "SelectNone")
  (aud-do-command "SetLabel" :label id :text txt)
  "")

Unfortunately, adding in validation for that edge case gets a bit messy. I’d prefer a more elegant solution, but here it is anyway:
(I’ve not tested on Audacity 2.4.2 as I don’t have that version readily available).

;nyquist plug-in
;version 4
;type tool
;name "Numbered Label"
;author "JH and Steve Daulton"
;release 2.4.2-2
;copyright "GNU General Public License v2.0"


(defun newlabel-props ()
  ;;; Return '(ID NUM)
  ;;; ID = index of label to be added = number of labels before
  ;;; cursor in target track.
  ;;; NUM = 1 + highest number found in existing labels.
  (let ((labels (aud-get-info "Labels"))
        (targettrk (get-target-trk))
        (maxnum 0)
        tracknum
        (newlabelid 0))
    (dolist (ltrack labels (list newlabelid (1+ maxnum)))
      (setf tracknum (first ltrack))
      (dolist (label (second ltrack))
        (setf label-num (number-string-p (third label)))
        (when label-num
          (setf maxnum (max maxnum label-num)))
        (when (or (< tracknum targettrk)
                  (and (= tracknum targettrk)
                       (< (labelstart label) (get '*selection* 'start))))
          (setf newlabelid (1+ newlabelid)))))))


(defun get-target-trk ()
  ;; Return ID (zero indexed) of first label track >= track that has focus.
  (let ((tracks (aud-get-info "Tracks"))
        focusfound)
    (dotimes (i (length tracks) (1+ i))
      (when (hasfocus (nth i tracks))
        (setf focusfound t))
      (when (and focusfound
                 (islabeltrk (nth i tracks)))
        (return-from get-target-trk i)))))


(defun hasfocus (trk)
  (= (second (assoc 'focused trk)) 1))


(defun islabeltrk (trk)
  (equal (second (assoc 'kind trk)) "label"))


(defun labelstart (label)
  (first label))


(defun labelend (label)
  (second label))


(defun validate (id)
  ;;; Handle edge cases where cursor position on a label.
  ;;; Due to limited precision of (aud-get-info "Labels") we
  ;;; cannot be certain if original label or new label comes first.
  (let ((labels (aud-get-info "Labels"))
        (idcount 0)
        lcount
        txt)
    (dolist (ltrack labels)
      (setf lcount 0)
      (setf ltrack (second ltrack)) ;Just the labels.
      (dolist (label ltrack)
        (when (= idcount id)
          (cond ((string/= (third label) "")
                    ;Label not empty - we need the ID of the previous label.
                    (return-from validate (1- id)))
                ((= lcount 0)
                    ;First label in track, must be new label.
                    (return-from validate id)))
          (let* ((sel-len (- (get '*selection* 'end)
                          (get '*selection* 'start)))
                 (prev-label (nth (1- lcount) ltrack))
                 (label-len (- (labelend label) (labelstart label)))
                 (prev-len (- (labelend prev-label) (labelstart prev-label))))
            ;; Return ID of whichever is closest to the selection lenth
            (if (minusp (- (abs (- sel-len label-len))
                          (abs (- sel-len prev-len))))
                (return-from validate id)
                (return-from validate (1- id)))))
        (incf lcount)
        (incf idcount)))))


(let* ((label-props (newlabel-props))
       (id (first label-props))
       (txt (format nil "~a" (second label-props))))
  (aud-do "AddLabel")
  (setf id (validate id))
  (aud-do-command "SelectNone")
  (aud-do-command "SetLabel" :label id :text txt)
  "")