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.
I don’t have time to study the code in detail, but a few general comments:
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).
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)))
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.
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:
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.
When I was on Windows, I always used NotePad++ for writing Nyquist code, and I still recommend it
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.
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
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”.
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.
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)
"")