My apologies to the list -- when I first sent this, it had an entire
audacity-nyquist digest appended to it. Maybe the moderator will see
this and just delete the big message that is pending approval. -Roger
This is a reply to Leland's post...
The general method used to get sounds from Audacity is flawed, and now
is probably the right time to think about fixing it. The solution (at
least I'm fairly confident about this) is to provide access functions to
retrieve sounds from Audacity, e.g. get-audio-selection(),
get-audio-track(track-num [, optional parameters ...]). I think that
currently Audacity limits the access (and the potential damage) to
selections, but there are cases where access to other tracks, labels,
etc. could make plugins much more powerful. Access functions that allow
the plugin to survey the project and pull in data would provide a
general, extensible interface. The main reason for access functions
though is to allow a sound to be accessed multiple times. To do this
now, you have to store all the samples in Nyquist as floats, and we've
seen innumerable discussions about this, especially for normalization.
With access functions, one could get and read a sound to compute the
maximum value; then one could get another copy of the same sound and
read it again to do dsp and return a result. This can be done without
accumulating samples in the Nyquist heap.
I suppose we can keep the current interface where the variable S is
bound to the selection: If the plugin ignores S, it will just sit there
with the potential to access samples and when the plugin returns, S will
be set to NIL. So if backward compatibility with S is good (at the
expense of being redundant with get-selection()), then this should work ok.
> ;nyquist plug-in
> ;version 3
> ;codetype sal
> ;type generate
> ;categories "http://lv2plug.in/ns/lv2core#GeneratorPlugin"
> ;name "Pluck..."
> ;action "Generating pluck sound..."
> ;control p "Pluck MIDI pitch" int "" 60 1 127
> ;control fade "Fade-out type" choice "abrupt,gradual" 0
> ;control dur "Duration [seconds]" real "" 1 0.1 30
> define function main()
> display "testing"
> with final-amp = #?(fade = 1, 0.001, 0.000001),
> pluck-sound = snd-pluck(*sound-srate*, step-to-hz(p), 0, dur,
;; alternatively: pluck-sound = pluck(p, 0, dur, final-amp)
> max-peak = peak(pluck-sound, ny:all)
> return (0.8 / max-peak) * pluck-sound
A few comments on the code: use WITH to declare local variables. Besides
the general software engineering issues, in this case, pluck-sound will
otherwise be a global variable that will accumulate all the pluck
samples in Nyquist memory. Note commas between local variables -- you
only get one WITH, but you can declare multiple locals.
I used PLUCK instead of SND-PLUCK. It calls SND-PLUCK but the API is a
I used "*" instead of SCALE -- after all that work on the SAL parser,
it's a waste not to use infix notation :-)
> There's 3 things to look for there:
> 1) There's a new "codetype" comment that tells Audacity the plugin is
> in SAL syntax. Anyone have a better suggestion for the keyword?
> 2) The plugin MUST define a function called "main" as that's what
> Audacity will execute. This was necessary because you need to
> "return" the result.
> 3) The plugin MUST "return" the desired result, be it, sound, string,
> double, or whatever.
These all sound like the right things to me. With "main", it occurred to
me that S could be passed in as a parameter. This would eliminate the
need to make S a global and to (inside nyx) set S to NIL when the plugin
returns. For that matter, ALL the plugin parameters could be passed in
as parameters. However, since the global S and global parameter value
mechanism is likely to stay for Lisp syntax, I think we should stick
with the same approach for Sal. Therefore, I agree with keeping main
> What Audacity actually passes to Nyquist is:
> (setf p 60)
> (setf fade 0)
> (setf dur 0.997000)
> (setf *sal-call-stack* nil)
> (setf *audacity-plugin-result* nil)
> (eval (sal-compile "define variable *audacity-plugin-result*
> define function main()
> display \"testing\"
> set final-amp = #?(fade = 1, 0.001, 0.000001)
> set pluck-sound = snd-pluck(*sound-srate*, step-to-hz(p), 0, dur,
> set max-peak = peak(pluck-sound, ny:all)
> return scale(0.8 / max-peak, pluck-sound)
> set *audacity-plugin-result* = main()
> " nil t nil))
> (eval *audacity-plugin-result*)
> And the SAL compiler produces:
> (PROG2 (SETF *SAL-LINE* 1)
> (SETF *AUDACITY-PLUGIN-RESULT*
> (IF (BOUNDP (QUOTE *AUDACITY-PLUGIN-RESULT*))
> (DEFUN MAIN
> (SAL-TRACE-ENTER (QUOTE MAIN)
> (QUOTE NIL))
> (PROG* NIL
> (PROG2 (SETF *SAL-LINE* 4)
> (DISPLAY "testing"))
> (PROGN (SETF *SAL-LINE* 5)
> (SETF FINAL-AMP
> (IF (EQL FADE 1) 0.001 1e-06)))
> (PROGN (SETF *SAL-LINE* 6)
> (SETF PLUCK-SOUND
> (SND-PLUCK *SOUND-SRATE*
> (STEP-TO-HZ P)
> (PROGN (SETF *SAL-LINE* 7)
> (SETF MAX-PEAK
> (PEAK PLUCK-SOUND NY:ALL)))
> (PROG2 (SETF *SAL-LINE* 8)
> (SAL-RETURN-FROM MAIN
> (SCALE (/ 0.8 MAX-PEAK)
> (SAL-RETURN-FROM MAIN NIL)))
> (PROGN (SETF *SAL-LINE* 11)
> (SETF *AUDACITY-PLUGIN-RESULT* (MAIN)))
Here's what I think should be passed to Nyquist:
(setf p 60)
(setf fade 0)
(setf dur 0.997000)
(setf *sal-call-stack* nil)
"define function main()
set final-amp = #?(fade = 1, 0.001, 0.000001)
set pluck-sound = snd-pluck(*sound-srate*, step-to-hz(p), 0, dur,
set max-peak = peak(pluck-sound, ny:all)
return scale(0.8 / max-peak, pluck-sound)
" nil t nil))
Notice that all of this is passed as a string to nyx_eval_expression.
This function uses XLISP to read one expression at a time, including
defining main and finally calling it. The value of the last expression
is returned, which is what we want. Do not create
*audacity-plugin-result*, a global, and assign the result of main to it,
because this binds a global variable to the sound. When the sound is
accessed to obtain samples for Audacity, the extra reference from the
global will prevent samples from being freed; instead the samples will
accumulate in Nyquist memory.
> All this works fine and I can run plugins using SAL syntax directly
> from the menus. But, I don't know if this is the best way to do it,
> so suggestions are welcome.
> (Remember, to be gentle and use baby speak since I don't under
> language too well yet. ;-))
Yes, well at least you have working code, and I'm just spewing ideas.
The concepts are right, but I may have made a few typos.