How to export every track as an individual wav file using macros?

I have projects with up to 18 audio tracks and I’m looking for a way to do the following using a macro:

  • Create a subdirectory called “wav” in the current project directory
  • Save every track as an individual wav file inside that “wav” directory using the track names (e.g. “Guitar 1.wav”)

Regarding the first step, I could not find a way to set the export path dynamically based on the project’s location.
Regarding the second step, I was not able to find a way to export a specific track by deleting all other tracks first, then export, then undo, but since the number of tracks that I have isn’t always the same, I don’t see how I could create a reliable macro like that. Also I wasn’t able to find a way to tell it to use the track’s name.

Any ideas how I could achieve this?

OS: Fedora/Linux

That can’t be done with a macro, as a macro is just a list of commands.
However, there is “Export Multiple”: Exporting multiple audio files - Audacity Manual

Thank you, I see. Well, I would really like to automate this.

I just asked ChatGPT, just to see what would happen and it suggests to use this plugin:

;;; export-tracks.ny
;;; Exports every track in the project as a separate WAV file using the track name as the file name, saving the tracks in a subdirectory called "wav".
;;; Author: ChatGPT (OpenAI)
;;; Date: 2023-01-31

(require "audacity.lsp")

;; exports the selected track as a WAV file
(defun export-track (filename)
  (save-audio filename :format :wav))

;; create the subdirectory "wav" inside the directory in which the current project is saved
(defun create-directory (directory)
  (if (not (file-directory-p directory))
      (mkdir directory)))

;; main function that loops through all tracks in the project and exports each one as a separate WAV file using the track name as the file name
(defun main ()
  (let ((project-directory (aud-get-project-directory)))
    (create-directory (concat project-directory "/wav"))
    (let ((num-tracks (aud-num-tracks)))
      (dotimes (i num-tracks)
        (let ((track (aud-get-track i)))
          (aud-select-track track)
          (export-track (concat project-directory "/wav/" (aud-get-selected-track-name) ".wav")))))))

Would that actually work?

It gave me a chuckle, but although it looks like sensible code, it’s nonsense.

The one part of ChatGPT’s answer that isn’t nonsense, is the suggestion of doing it with a (Nyquist) plug-in. Everything else is nonsense because it is not valid Nyquist code - it’s a kind of pseudocode). About “Nyquist”: Nyquist - Audacity Manual

Personally I’d use “Export Multiple”.

I have used Export Multiple before, but it’s just too many steps. If I only had to do this only once a year, I would be fine with it, but since I have to do this countless times a day, it would be very valuable for me to get this automated.

If you say this can be done with Nyquist, I will give it a try. But the syntax is very different from any other programming language I have used before, so it’s a bit tough for me to wrap my head around it.

Edit:
So this would get me the number of wav tracks in a project, right?

(let ((wavetrack-count (get '*project*' wavetracks)))

I’m a bit confused why there is a variable containing the number of existing tracks rather than a list containing all the tracks that I could then iterate over.

Edit2:
Is the only way to get all tracks, by selecting them one by one?

If you are using the same settings every time, then it’s just:

  1. “Shift + Ctrl + L”
  2. “Enter”

If you need different settings each time, then you will need to enter the differences, whether you use Export Multiple, a Nyquist script, or any other method.


It’s called “S-expressions” (symbolic expression), and it’s the syntax used by the LISP family of languages.
The basics are pretty simple and consistent:

; semicolon for a comment
(command arguments...)
; operators
(operator arguments...)
; functions
(function arguments...)
; macros
(macro arguments...)
; nesting a function as an argument:
(func1 (inner-func args...))

So, whereas we write in English:

result = 3 + 2

in Lisp we write:

(setf result (+ 3 2))



There are basically 4 “types” of Nyquist plug-in:

;type process
;type generate
;type analyze
;type tool

The “process” type are generally “effects”, and when applied to a selection of more than one track, the effect iterates over the selected tracks.


There you have part of a LET block.
The actual command is:

(get '*project* 'wavetracks)

Pay attention to the quotes. It’s:

'*project*

and

'wavetracks

This command is an Audacity specific addition to Nyquist. GET is a command that gets a value from a symbol’s property list. Audacity creates several special symbols that provide information to Nyquist about Audacity’s current state.

This page is worth reading through: Missing features - Audacity Support

Thank you, that’s very valuable information.

So I would have to write a macro that selects all tracks and then invokes a ‘process’-plugin which just checks if TRACK.TYPE != “wave” and if so, it somehow(?) exports it using the PROJECT.NAME, TRACK.NAME and SYSTEM-DIR variables. Something like this:

export(TRACK, SYSTEM-DIR.HOME + "/Audacity/" + PROJECT.NAME + "/wav/" + TRACK.NAME + ".wav")

But I couldn’t find a way to invoke “export” using Nyquist.

And unfortunately I also couldn’t find a way to get the path of the directory where the current project is saved.

Edit:

I think I found a way to export data to a file.

My understanding is that Nyquist plug-ins basically have system-wide access, which I was not expecting.

Further if I’m not mistaken, TRACK holds the actual audio data. So maybe I could pass that to the function in order to export it.

That’s one way.
Another way is to use an AUD-DO command to tell Audacity to do the export. See: Nyquist-Macros - Audacity Manual

If the project has been saved, or opened from a saved project, the .AUP3 file will be the first entry in the “recent files” list, which you should be able to access with the “GetPreference:” command (Scripting Reference - Audacity Manual). Use “AUD-DO” to send the “GetPreference:” command.

TRACK (with asterisks) is a symbol, which points to the track’s audio data.
The TRACK symbol is “unbound” (not defined) for “;type tool” plug-in code (there’s a feature request to remove that limitation: Don't disable *TRACK* in ";type tool" plugins · Issue #3907 · audacity/audacity · GitHub)

Because you want a valid audio file, you would need to use S-SAVE (Nyquist Functions).
Note that S-SAVE does not support metadata, and does not support all of the formats that Audacity supports.

Unfortunately I saw your response too late, but I also found the S-SAVE function you mentioned in your last response:

 s-save(expression, [maxlen, filename, progress], format: format, mode: mode, bits: bits, swap: flag, play: play) [SAL]
(s-save expression [maxlen filename progress] :format format :mode mode :bits bits :swap flag :play play) [LISP]
    Evaluates the expression, which should result in a sound or an array of sounds, and writes the result to the given filename. (If omitted, *default-sound-file* is used instead.) A FLONUM is returned giving the maximum absolute value of all samples written. (This is useful for normalizing sounds and detecting sample overflow.) If play is not NIL, the sound will be output through the computer's audio output system. (play: [SAL] or :play [LISP] is not implemented on all systems; if it is implemented, and filename is NIL, then this will play the file without also writing a file.) The latency (length of audio buffering) used to play the sound is 0.3s by default, but see snd-set-latency. If a multichannel sound (array) is written, the channels are up-sampled to the highest rate in any channel so that all channels have the same sample rate. The maximum number of samples written per channel is optionally given by maxlen, which allows writing the initial part of a very long or infinite sound. Progress is indicated by printing the sample count after writing each 10 seconds of frames. If progress is specified and greater than 10,000, progress is printed at this specified frame count increment. A header is written according to format, samples are encoded according to mode, using bits bits/sample, and bytes are swapped if flag is not NIL. Defaults for these are *default-sf-format*, *default-sf-mode*, and *default-sf-bits*. The default for flag is NIL. The bits parameter may be 8, 16, or 32. The values for the format and mode options are described below: 

Format

snd-head-none
    The format is unknown and should be determined by reading the file.

snd-head-raw
    A raw format file has no header.

snd-head-AIFF
    AIFF format header.

snd-head-IRCAM
    IRCAM format header.

snd-head-NeXT
    1024-byte NeXT/SUN format header followed by IRCAM header ala CMIX. Note that the NeXT/SUN format has a header-length field, so it really is legal to have a large header, even though the normal minimal header is only 24 bytes. The additional space leaves room for maximum amplitudes, which can be used for normalizing floating-point soundfiles, and for other data. Nyquist follows the CMIX convention of placing an IRCAM format header immediately after the NeXT-style header.

snd-head-Wave
    Microsoft Wave format header.

snd-head-WaveX
    Microsoft Wave with WAVEFORMATEX format header.

snd-head-flac
    FLAC lossless compressed audio.

snd-head-ogg
    OGG-VORBIS compressed audio.

snd-head-*
    See sndfnint.lsp in the nyquist/runtime directory for more formats. The current list includes paf, svx, nist, voc, w64, mat4, mat5, pvf, xi, htk, sds, avr, sd2, and caf. 

Mode

snd-mode-adpcm
    ADPCM mode (not supported).

snd-mode-pcm
    signed binary PCM mode.

snd-mode-ulaw
    8-bit U-Law mode.

snd-mode-alaw
    8-bit A-Law mode (not supported).

snd-mode-float
    32-bit floating point mode.

snd-mode-upcm
    unsigned binary PCM mode.

snd-mode-*
    See sndfnint.lsp in the nyquist/runtime for more modes. The current list includes double, gsm610, dwvw, dpcm, and msadpcm. 

The defaults for format, mode, and bits are as follows:

NeXT and Sun machines:
    snd-head-NeXT, snd-mode-pcm, 16

SGI and Macintosh machines:
    snd-head-AIFF, snd-mode-pcm, 16

So my understanding is I don’t necessarily have to use the Lisp syntax, I can use the SAL syntax as well. It seems way more familiar to me. Using that syntax I would end up with something like this:

;nyquist plug-in
;version 4
;type process
;name "Multi Export"
;debugbutton disabled
;author "randomuser111"
;release 1.0.0
;copyright "Released under terms of the GNU General Public License version 2 or later."


if get(quote(*TRACK*), quote(TYPE)) = "wave" then
  s-save(TRACK, 999999999999, "./wav/" + get(quote(*TRACK*), quote(NAME)) + ".wav", snd-head-Wave, snd-mode-float, 32, NIL, NIL)

Would that work?

Or maybe something like this instead?

AUD-DO("Export2: Filename=\"./wav/" + get(quote(*TRACK*), quote(NAME)) + ".wav\" NumChannels=\"" + get(quote(*TRACK*), quote(CHANNELS)) + "\"")

I’m not sure if the “recent files” method would be reliable. My workflow is to create a copy of MyTemplate.AUP3 and then open that. So that project was never actually saved, it was just copied and renamed.

Yes, you can use SAL syntax. It was developed to make Nyquist programming more familiar to people with experience with C/C++ like languages. However, I’ve always use LISP syntax so I’m not familiar with SAL.


Here’s a simple script in LISP syntax that will export selected tracks to a specified directory:

;type process
;version 4

(setf path "fully/qualified/path/")
(setf fname "test")
(setf ext ".wav")

(setf fname (format nil "~a~a~a~a"
                        path
                        fname
                        (get '*track* 'index)
                        ext))

(s-save *track* ny:all fname :format snd-head-Wave :mode snd-mode-pcm :bits 16)
""

Note that because it is “;type process” it automatically iterates through each selected track.
Note also that it only exports “selected” audio.

The two double quotes (an empty string) suppresses messages so that the script will run in the Nyquist Prompt without pausing after each track.

(get 'track 'index) is a track counter that increments after each track is processed.

Hello. I have managed something like this in the past with a simple DOS batch file (I’m om old man !!)
I would set up an Audacity macro to split off the first track (of how ever many there are) and save it in an external WAV file with a fixed name, then exit Audacity.
The batch file would then pick up the fixed-name WAV file and RENAME it to whatever number it had reached, then re-invoke Audacity … until no WAV file was produced, which would signal that all tracks has been processed.

FOR %A in (1 2 3 4 5 6 7 8 9 10 11 _____) do Audacity_Parse.bat %a

That sort of thing.
Let me know if you think this is feasible.
Cheers, Chris

Thank you very much, steve! I’m almost there, but I just had to go back to SAL because the LISP syntax just makes me wanna die. :confused:

I ended up with the following code, which almost works:
https://pastebin.com/c5y5z0CX
(Had to put it in pastebin because apparently someone configured the cloudflare filter to consider my if-statement to be an SQL-injection.)

But I don’t understand how to use get in SAL and set trackname = get(*track*, "NAME") causes this error:

error: bad argument type - #<Sound: #0x55c71d71ad60>
Call traceback:

Regarding the batch file, I would really like to avoid closing and reopening audacity up to 18 times in a row.
But since I have a script that creates a copy of my template project and then launches Audacity with the copy, I could see myself extending it to add a custom entry into audacity.cfg for example something like this:

[ProjectLocations]
New Project 1=/home/fedora/Music/Recordings/New Project 1
TestProject=/home/fedora/tmp/TestProject

This would allow me to reliably get the path for the current project, even if I open multiple ones.

In LISP syntax, the symbols are quoted (to avoid evaluation of the symbols)

(get '*track* 'name)

So I guess in SAL it will be:

get(quote(*track*), quote(name))

Thank you, that did the job! :slight_smile:

I was just thinking if it might be possible to tell the plugin to select all tracks and then trigger itself again, so that it exports all tracks without me having to create a macro that first selects all tracks and then triggers the plugin.

Something like this:

;nyquist plug-in
;version 4
;type process
;name "MultiTrackExport"
set selectedtrackindexes = get(quote(*selection*), quote(tracks))
set selectedtrackscount = length(selectedtrackindexes)
set totaltrackscount = get(quote(*project*), quote(wavetracks))
set currenttrackindex = get(quote(*track*), quote(index))

; If all tracks are selected:
if selectedtrackscount = totaltrackscount then
    begin
        ; Code that exports the tracks
    end
else
    ; If the plugin is currently running on the last selected track:
    if selectedtrackscount = currenttrackindex then
        begin
            AUD-DO("SelectAll")
            AUD-DO("RunNyquistPlugin MultiTrackExport")
        end
return 0

But obviously RunNyquistPlugin does not exists, so maybe it’s not possible. I mean I could use the NyquistPrompt and try to pass 20 lines of code merged into one to it, but that would make the code very unreadable…

Besides that I’m noticing odd behaviour with quote(selection) and quote(project).

This code for example shows a message box with a “1” for every selected track:

set x = quote(*selection*)
return 1

But after closing the last message box, it always gives me this error:

>>> parse warning: Identifier contains operator character(s).
        Perhaps you omitted spaces around an operator.
>>> in NIL, line 1, col 15.

set x = quote(*selection*)
              ^

Interestingly this doesn’t happen if I use quote(track) instead.

Any ideas why?

Perhaps because track has a value and selection doesn’t?
(As far as I’m aware, none of the regular contributors to this forum are well versed in SAL)

Hello. I quite see your point. I was aware of this about twelve months ago when I began using Audacity to collaborate on audio books.
A complete load-and-quit-and-exit of Audacity seemed to be excessive use of a laptop until I realized that I had to walk to the supermarket for groceries.

I already had worked out the Audacity-macro code to do a single, explicitly-named file, as I am sure that you have by now, so it was simply a matter of setting up the FOR loop in a batch file and starting the run.

It completed in less than the time it took me to return home with my groceries!

Sometimes nerdy geeks like me get carried away with speed and timing because of those 13-microsecond machine-language instruction cycle times from The Good Old Days. I Grew up hungering for speed but had to get over it.
Today, what do I care if a chunk of plastic and metal busts its gut churning away, as long as I can upload the assembled audio-book before the sun sets!

Just a thought …
Cheers, Chris

Thank you Chris, I really appreciate your input on this, but I think the plugin solution is working pretty well for me. :slight_smile:

@Steve You’re right, track and system-time are the only symbols that actually have a value and are not unbound by themselves. It turns out that the “error” I was getting was more of a warning that I can just ignore. It only shows up when running from the Nyquist prompt anyway.

I spend countless hours learning the SAL syntax and studying the documentation and after writing a few nasty algorithms I ended up with something that works well enough to consider this a success. :slight_smile:

Since the entry in the Audacity config that contains the path to the current project is dynamic I had to write an algorithm that iterates over all the values of RecentFiles and ActiveProjects… and it’s pretty slow unfortunately because it’s invoking GetPreference countless times.

Anyway, I published it on GitHub:
https://github.com/T-vK/MultiTrackExport