Plugin to get duration percentages of tracks

Further to this thread:

My attempt (with help from Steve) to write a plugin to be able to work out as a percentage, duration of an audio track
to total sections of multiple labels.

As a start, to get the information of the first audio track:
(For now, have left out any error checking, i.e. check if there is an audio track).

(setf audio-track (first (aud-get-info "Tracks")))
(print audio-track)

The returned result is:

((NAME "Audio Track") (FOCUSED 1) (SELECTED 1) (KIND "wave") (START 0.986854) (END 6.03719) (PAN 0) (GAIN 1) (CHANNELS 1) (SOLO 1) (MUTE 0) (VZOOMMIN -1) (VZOOMMAX 1))

The same could be used for the label track:

(setf labels (first (aud-get-info "Labels")))
(print labels)

Which correctly returns the required information:

(1 ((0.273752 0.734153 "1") (1.02168 4.01995 "2") (5.00971 5.42526 "3")))

This however, is where I am stuck.
All the times are there, but how to extract them individually, so that they can be added together then work it out as a percentage of the audio track.

EDIT:

@Steve
No help yet please, going to read up more about lists first.

I’ll offer a hint :wink: Look up ASSOC in the Xlisp manual.

It’s getting there. :wink:

I’m determined to have at least a rough working script today.

Calling it a night, the label tracks are driving me nuts.

The audio track is easy enough:

 
(setf aud-start (second (assoc 'START audio-track)))
(setf aud-end (second (assoc 'END audio-track)))
(setf audio-track-len (- aud-end aud-start))

Since they have a “START” and “END”, assoc works perfectly and the total length is in “audio-track-len”.

However, for the label tracks, it’s very different as there is nothing constant.
Tried the “dotimes” function but it’s zero based whilst the first data I need in the label track (start time) in the nth 1 and not nth 0 item.

Will sleep on it and start again fresh tomorrow.

I’m stuck on extracting the start and end times of the labels.
Tried “dolist” and “dotimes” but I still don’t fully understand the mechanics of those two.

All credit to you for having a go :slight_smile:
Yes, it’s a bit tricky.

Firstly, the difference between (aud-do “GetInfo: …”) and (aud-get-info …)

;type tool
(setf info (aud-do "GetInfo: Type=Tracks Format=LISP"))
(print info)

In the Debug window, that will print something like:

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

Observe the double quotes.
(aud-do “GetInfo: …”) returns a dotted list with two elements:

("a long  string value that looks like a list" . T)

“GetInfo” was initially designed to return JSON data for Python scripting, where JSON is a way of encoding JavaScript data as a string.
The “Format=LISP” argument tells Audacity to format the JSON data in a way that is easier for Nyquist to handle (Nyquist does not directly support JSON).

The important thing here is that although the first part of the list “looks” like a LISP list, it is actually just a string (“text”).

;type tool
(setf info (aud-do "GetInfo: Type=Tracks Format=LISP"))
(print info)

(if (stringp (car info))  ;ist the first part of 'info' a string?
    "Yes it's a string"
    "No it is not a string")
;; Returns "Yes it's a string".

Now try (aud-get-info …)

;type tool
(setf info (aud-get-info "tracks"))
(print info)

returns something like:

(((NAME "Audio Track") (FOCUSED 1) (SELECTED 1) (KIND "wave") (START 0) (END 30) (PAN 0) (GAIN 1) (CHANNELS 1) (SOLO 0) (MUTE 0) (VZOOMMIN -1) (VZOOMMAX 1)))

“AUD-GET-INFO” is a wrapper around “AUD-DO” that parses the returned string and evaluates it as a LISP expression.

Using aud-get-info with two tracks:

(((NAME "Audio Track") (FOCUSED 1) (SELECTED 1) (KIND "wave") (START 0) (END 30) (PAN 0) (GAIN 1) (CHANNELS 1) (SOLO 0) (MUTE 0) (VZOOMMIN -1) (VZOOMMAX 1)) ((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)))

It’s a bit tricky to see, but if we reformat that we can see:

((<fist-strack-info>) (<second-track-info))

Where is comprised of a list of 2 element lists:
(list (symbol1 value1) (symbol2 value2)…)

This post is already quite long, so I’ll end here and write a new post about how to handle the lists returned by “AUD-GET-INFO”.

Hi Steve,

Thanks for the extra info.
This challenge has taught me a lot more about Lisp.
During my “adventures”, actually learned other stuff relating to Nyquist and Lisp which will come in very handy.

Back to the track data, the audio tracks are actually easy to get the required info from, it’s the Label tracks that I’m struggling with.

My code always errors out with either:
Bad form (I’m obviously doing something stupid), or
bad argument type - 1

The “1” I suspect is due to the way the Label track data is returned.

(1 ((0.273752 0.734153 “1”) (1.02168 4.01995 “2”) (5.00971 5.42526 “3”)))

Since I’m using nth x, it errors as it’s not like the other fields that have 3 items, i.e. the first entry:
0.273752 0.734153 “1”
Which are the 0.273…
the 0.734…
and the “1”.

Moving on to Label tracks.
As previously described, (aud-do “GetInfo: Type=Labels Format=LISP”) will get us the information, but only as a text description.
We will use AUD-GET-INFO instead as that gives us a LISP expression which is much easier to work with.

We will also run these examples as “;type tool”.
Unlike other effect types, “tool” plug-ins do not iterate over the tracks. The effect just runs once, regardless of how many tracks are selected.
An important limitation of “;type tool” is that it does not have access to track, but that does not matter in this case. We don’t need to use track.

I’ll use this project for demonstration purposes:

project.png

;type tool
(setf info (aud-get-info "Labels"))
(print info)

Returns:

((1 ((0 2 "The") (3 5 "First") (6 8 "Label") (9 11 "track"))) (2 ((2 3 "Second") (5 6 "Label") (8 9 "Track"))))

Let’s format that a bit more clearly:

((1 ((0 2 "The") (3 5 "First") (6 8 "Label") (9 11 "track")))
 (2 ((2 3 "Second") (5 6 "Label") (8 9 "Track"))))

Observe that the outer list contains two lists, one for each label track.

Each of these contains two elements. The first element is a number, and the second element is a list.
The number is the “track index”, counting from zero. (track index 0 is the audio track at the top).
The lists in each label track contain one list for each label, in the form:

(list <start-time> <end-time> "<label text>")

So first of all, we need to iterate through ‘info’ to get each label track.

;type tool
(setf info (aud-get-info "Labels"))

;; DOLIST is a convenient way to iterate through lists
(dolist (one-label-track info)
  (print one-label-track))

which returns:

(1 ((0 2 "The") (3 5 "First") (6 8 "Label") (9 11 "track")))
(2 ((2 3 "Second") (5 6 "Label") (8 9 "Track")))

We don’t need to know the track index. We’re only interested in the labels, which are in lists as the second element of “one-label-track”:

;type tool
(setf info (aud-get-info "Labels"))

;; DOLIST is a convenient way to iterate through lists
(dolist (one-label-track info)
  (print (second one-label-track)))

which returns:

((0 2 "The") (3 5 "First") (6 8 "Label") (9 11 "track"))
((2 3 "Second") (5 6 "Label") (8 9 "Track"))

— Take a quick breather — back in a couple of minutes :wink:

— Take a quick breather — back in a couple of minutes > :wink: > —

Perfect, gives me enough time to go through your latest info and have one of these…
Screen Shot 2021-05-27 at 3.04.23 PM.png

Recapping on my previous post, we have seen that we can get a list of labels for each label track with:

;type tool
(setf info (aud-get-info "Labels"))

;; DOLIST is a convenient way to iterate through lists
(dolist (one-label-track info)
  (print (second one-label-track)))

We are probably making things too complicated by dealing with all label tracks, so from here on we shall just use the first label track

;type tool
(setf first-label-track (first (aud-get-info "Labels")))
(print first-label-track)

which returns:

(1 ((0 2 "The") (3 5 "First") (6 8 "Label") (9 11 "track")))

and we only need the second element of this, so we can write:

;type tool
(setf first-label-track (first (aud-get-info "Labels")))
(setf labels (second first-label-track))

;; Do not return "labels" because it is a specially formatted list.
;; Just print it as text:
(format nil "~s" labels)

which prints:

((0 2 "The") (3 5 "First") (6 8 "Label") (9 11 "track"))

So now we can iterate through “labels” and calculate the length of each label.

;type tool
(setf first-label-track (first (aud-get-info "Labels")))
(setf labels (second first-label-track))

(dolist (label labels)
  (setf duration (- (second label) (first label)))
  (format t "Label ~s is ~a seconds.~%"
          (third label)
          duration))

"See debug output."

Wow, I never would have worked it out.

Although only been messing about with Nyquist for a short while, manipulating audio is more my thing.
Lists and the like are definitely out of my comfort zone.

This exercise will be good practice for me, you have given me a lot to work on so will have a go now.

Manipulating audio is Nyquist’s forte. That’s what it was designed for.

Parsing lists of data is a bit cumbersome, but at least we don’t have to parse the raw string data from “GetInfo:”.
Manipulating strings (text) in Nyquist is painful :smiley:

If you’re interested in how AUD-GET-INFO works, it is defined here: https://github.com/audacity/audacity/blob/master/nyquist/init.lsp

For Effect / Analyze / Generate type plug-ins, we are usually saved from having to extract data from GetInfo.
The reasons that we have to do this the hard way are:

  1. We want to get data about the entire project, irrespective of the current selection.
  2. Nyquist cannot directly read label tracks.
  3. We don’t want to iterate over selected tracks, which is why we are using “;type tool”.

For normal “Effects” (;type process) we have:

;The number of selected samples
(print len)

;The selection duration (seconds)
(print (get-duration 1))

;The selection start time
(print (get '*selection* 'start))

;The selection end time
(print (get '*selection* 'end))

See also: https://wiki.audacityteam.org/wiki/Nyquist_Plug-ins_Reference#globals
and on the same page: https://wiki.audacityteam.org/wiki/Nyquist_Plug-ins_Reference#property_lists

Making progress.
Below, the code so far.

I have kept each step separate as opposed to combing them, still feeling a bit fragile. :smiley:
This is what I have so far, percentage still to be worked out but that is the easy part.

(setf audio-track (first (aud-get-info "Tracks")))
(setf track-name (get '*TRACK* 'NAME))

(setf aud-start (second (assoc 'START audio-track)))
(setf aud-end (second (assoc 'END audio-track)))
(setf audio-track-len (- aud-end aud-start))

(setf first-label-track (first (aud-get-info "Labels")))
(setf labels (second first-label-track))

(setf tot-dur-restless 0)
(setf percentage 0)
(setf sleep-time 0)

(dolist (label labels)
  (setf duration-restless (- (second label) (first label)))
  (setf tot-dur-restless(+ tot-dur-restless duration-restless)))

(setf sleep-time (- audio-track-len tot-dur-restless))
(print sleep-time)

Seems to be working.
The returned time is the uninterrupted sleep time.
I’ve named the variables WRT the OP’s original purpose.

No problem. The first task is to get something that works. It can be tidied up later :smiley:


Note that (setf track-name (get 'TRACK 'NAME)) will only work with “normal” effects (;type generate, process or analyze).
For these types of effects, Audacity iterates through the selected audio tracks (processes each selected track in turn) and TRACK links to the audio data in the track that is currently being processed.

For “;type tool”, the TRACK variable is not set because these type of effects do not iterate through the selected tracks.

I’ve written a version that has a few additional features:

  • Get the end time of all audio tracks (not just the first track)
  • Ignore labels that are beyond the end of the audio tracks.
  • If a label overlaps the end of the audio, only count the part that is overlapping the audio.

Like your version, if there are multiple label tracks, it only uses labels from the first label track.

I’ve attached the code as a file so that you can avoid seeing it until you want to :wink:

Note that it is not an installable plug-in as it does not have a full set of the “required headers”.
percent-labelled.ny (963 Bytes)

I purposely have not looked at your version yet, will look at it shortly.

Revised code:
(Does the function “number-to-string” look familiar ? :wink: ).

;type tool

(defun number-to-string (number)
  (format nil "~a" number))

(setf audio-track (first (aud-get-info "Tracks")))

(setf aud-start (second (assoc 'START audio-track)))
(setf aud-end (second (assoc 'END audio-track)))
(setf audio-track-len (- aud-end aud-start))

(setf first-label-track (first (aud-get-info "Labels")))
(setf labels (second first-label-track))

(setf tot-dur-restless 0)
(setf percentage 0)
(setf sleep-time 0)
(setf percent "")

(dolist (label labels)
  (setf duration-restless (- (second label) (first label)))
  (setf tot-dur-restless(+ tot-dur-restless duration-restless)))

(setf sleep-time (- audio-track-len tot-dur-restless))
(setf percentage (* (/ sleep-time audio-track-len) 100))
(setf percent (strcat "Uninterrupted sleep is " (number-to-string percentage) " % of the total time."))
(print percent)

OK had a look at your code, much cleaner, shorter and you didn’t go from New York to Baltimore via Tokyo like I did. :smiley:

Two things I am happy about:

  1. I got there, sort of, and
  2. My percentage tallies with yours.

Congratulations :ugeek:

Your code is actually slightly shorter than mine (27 lines vs 29), though I’m using 7 lines to calculate the longest audio track.

(defun audio-end ()
  (let ((tracks (aud-get-info "tracks"))
        (max-end 0))
    (dolist (track tracks max-end)
      (when (member (list 'kind "wave") track :test 'equal)
        (let ((end (second (assoc 'end track))))
          (setf max-end (max max-end end)))))))

(print (audio-end))

(defun number-to-string (number)
  (format nil "~a" number))

(setf percent (strcat "Uninterrupted sleep is " (number-to-string percentage) " % of the total time."))
(print percent)

A neater and shorter way to write that:

(format nil "Uninterrupted sleep is ~a % of the total time." percentage)

FORMAT is commonly used with either NIL or T (true) as the first argument.

(format nil “string”) simply returns the string, but also supports “format directives”.
Example:

(format nil "Value is: ~a" 42) ;returns "Value is: 42"

More examples here: XLISP format

As the manual says: “If the ‘destination’ is T , the printing occurs to standard-output.”
In the case of Audacity, the standard-output is the debug window, or if the debug window is not opened, it prints to the Audacity log (“Help menu > Diagnostics > Show Log”)

Just for fun, here’s a short version of your code. It is a little less easy to read, but much shorter:

;type tool

(setf audio-track-len
  (let ((audio-track (first (aud-get-info "Tracks"))))
    (- (second (assoc 'END audio-track))
       (second (assoc 'START audio-track)))))

(let ((labels (second (first (aud-get-info "Labels"))))
      (total 0.0))
  (dolist (label labels)
    (setf total (+ total (- (second label) (first label)))))
  (format nil "Uninterrupted sleep is ~a % of the total time."
          (* (/ total audio-track-len) 100)))

I did notice a bug in your code.
If all the label lengths, and the audio track are exact (integer) number of seconds, then both “sleep-time” and “audio-track-len” are integers, so “percentage” is rounded to 0.

There are a number of ways to fix that.
The way I avoided that in my “percent-labelled.ny” code was to force one of them to be a floating point number:

(/ (float a) b)

In the code above, I initialised “total” as a floating point value 0.0
(adding an integer to a float gives a float.)