Additive Tone

This plugin is for generating a tone consisting of a fundamental frequency and its harmonics, with the harmonics’ amplitudes specified by the user and allowed to vary over time.

The “Decay Coefficients” parameter is used to determine the timbre, and expects a (possibly deep) list of positive numbers.
The numbers specify a power law which describes how fast the harmonics attenuate according to frequency.
For example, entering “(2)” will produce a tone where amplitude decays with the square of frequency: the second harmonic is four times as quiet as the fundamental, the third is nine times as quiet, and the fourth sixteen.
If several terms are entered, the tone will interpolate smoothly between the specified timbres. For example, entering “(2) (5.72) (2)” will produce a tone that starts relatively bright, is dimmest after one third of the duration, returns to its normal brightness by two thirds of the duration, and remains bright until the end.
If a given list has k sublists, this is interpreted as specifying what happens to every kth harmonic. For example, entering “(2 4.2)” will produce a tone where the even harmonics are louder than the odd harmonics. Entering “(2 4 3)” has the 0 mod 3 harmonics loudest, the 2 mod 3 harmonics quieter, and the 1 mod 3 harmonics quietest.
This list may be arbitrarily deep. For example, “((5 (4 2)) 3)” gives the following relative amplitudes for each harmonic:

Harmonic    Frequency    Amplitude    Amplitude
1           440          1^-5         1.000000
2           880          2^-3         0.125000
3           1320         3^-4         0.012345
4           1760         4^-3         0.015625
5           2200         5^-5         0.000320
6           2640         6^-3         0.004630
7           3080         7^-2         0.020408
8           3520         8^-3         0.001953

The remaining parameters are more self explanatory.
“Pitch (Steps)” specifies the MIDI pitch of the tone, where 69 is 440Hz, and an increase of 1 increases the pitch by one semitone.
“Length (Seconds)” specifies the length of the tone in seconds.
“Vibrato Speed (Hz)” specifies the frequency of the sine LFO which modifies the pitch. The vibrato applied to the entire tone, i.e. the harmonics are still harmonics of the fundamental frequency. If this is set to 0, no vibrato is applied. If this is set higher than the control-srate (one twentieth of the sample rate by default), aliasing artifacts may occur.
“Vibrato Depth (Hz)” specifies the depth of the vibrato.
ADDITIVE3.ny (5.24 KB)

I like it. It produces some nice sounds. :smiley:

;If the rth element is a number x, then the ith harmonic (where i is r modulo k) has ;amplitude i^(-x).

>

Does it make sense to allow negative numbers?
\
\
Although it is "legal" to use most standard set ASCII characters in symbol names, it is generally considered best practice to stick with numbers, letters and hyphen ("-"). Other characters are prone to encoding errors or misinterpretation when reading the code. For example, "list-from-string" is clearer than "list<-string" (and in my opinion, looks less weird :wink:)

In this particular case, there is a new function in Audacity 2.3.1 that can be used (2.3.1 is due for release later this month).
The new function is: EVAL-STRING
It is similar to the Nyquist EVAL function (http://www.cs.cmu.edu/~rbd/doc/nyquist/part19.html#index1462) except that it specifically works on strings. You can use it like this:

```text
(setf coefs (eval-string (format nil "'(~a)" coefs-string)))

I also notice that your function “floats<-list” actually does two things:

  1. Casts all numbers to float - this appears to be unnecessary. The code work correctly with integers.
  2. Checks that all user supplied elements are numeric. (this is where a check for negative numbers could be inserted if desired)

I probably wouldn’t bother about listing all errors in the “parameters” string. In an extreme case, this could create an error window that is too big to fit on screen. I’d just return the first error, which can also makes error handling less clunky. Although several old plug-ins use the recipe of a global “err” message and a test for string length, a nicer way is with THROW and CATCH.
http://www.audacity-forum.de/download/edgar/nyquist/nyquist-doc/xlisp/xlisp-ref/xlisp-ref-281.htm
http://www.audacity-forum.de/download/edgar/nyquist/nyquist-doc/xlisp/xlisp-ref/xlisp-ref-048.htm

Example:

(defun gen-tone (pitch duration coef-lists)
  ;;; Get wavetables and mod. env., pass to built-in Nyquist oscillators.
  ;validate before processing
  (checklist coef-lists)
  ...


(defun checklist (lst)
  ;;; Check that all coefficients are numbers
  (unless (numberp lst)
    (if (listp lst)
        (mapcar #'checklist lst)  ;recursion
        (throw 'err
          (format nil "Error.~%Coefficient '~a' is not a number." lst)))))

(catch 'err (gen-tone pch dur coefs))

There’s a minor bug here:

(defun gen-table (coef-list)
  (do* ((current-harmonic 1 (1+ current-harmonic))
        (wavetable (s-rest 0))
        (amplitude 1.0 (get-amplitude current-harmonic coef-list)))
      ((> current-harmonic top-harmonic) wavetable)
    (if (> amplitude quiet)
        (setf wavetable (sim wavetable 
          (scale amplitude 
            (build-harmonic current-harmonic 2048)))))))

You are creating a table with 2048 sample points.
Because it is being added to “wavetable”, and “wavetable” has a sample rate of 44100 Hz, your harmonic table is resampled to 44100 Hz.

Also, I’m unsure of the purpose of “(if (> amplitude quiet)”.

At first glance I assumed that it was an optimisation to stop calculating wavetables once the “quiet” threshold was reached, but it doesn’t actually do that. The loop continues until “top-harmonic” is reached and just discards very quiet wavetable, which has little effect on the processing time.

If you really need to reduce the number of loops, then you need to escape the loop once the “quiet” threshold is reached. For example, you could have something like:

(defun gen-table (coef-list)
  ;;; Build harmonic table
  (do* ((current-harmonic 1 (1+ current-harmonic))
        (wavetable (s-rest 0))
        (amplitude 1.0 (get-amplitude current-harmonic coef-list)))
      ((or (> current-harmonic top-harmonic)
           (<= amplitude quiet)) wavetable)
    (setf wavetable
      (sim wavetable 
           (scale amplitude (build-harmonic current-harmonic 2048))))))

or

(defun gen-table (data)
  ;;; Build harmonic table
  (simrep (i max-harmonic)
    (let* ((count (1+ i))
           (gain (get-amplitude count data)))
      (when (= count 1) (setf gain 1))
      (when (<= gain quiet) (return))
      (scale gain (build-harmonic count 2048)))))

(Note that in the second example, “max-harmonic” must be an integer)

GEN-TABLES could be written a bit more succinctly, though both of these versions work correctly:

;;;Build list of tables/breakpoints to pass to siosc. 
(defun gen-tables (coef-lists pitch interval)
  (setf time interval)
  (setf tables ())
  (reverse (cdr                    ;list built in reverse. loop adds extra element.
    (dolist (coef-list coef-lists tables)
      ;;append wavetable and breakpoint, then increment.
      (setf tables (cons (normalize-table (gen-table coef-list)) tables))
      (setf tables (cons time tables))
      (setf time (sum time interval))))))



(defun gen-tables (coef-lists pitch interval)
  ;;; Push wavetable and breakpoint onto a list 'tables'.
  ;;; Discard the final breakpoint, then return 'tables' for siosc.
  (let ((count 1) tables)
    (dolist (coef-list coef-lists (reverse (cdr tables)))
      (push (normalize (gen-table coef-list)) tables)
      (push (* interval count) tables)
      (incf count))))

Similarly:

(defun normalize-table (tab)
  (let ((tab-peak (peak tab ny:all)))
    (scale (/ 0.75 tab-peak) tab)))



(defun normalize (sig)
  (scale (/ 0.75 (peak sig ny:all)) sig))

Thanks for the thorough look at this, steve!

Does it make sense to allow negative numbers?

Not really, no! I had figured someone might want to create a sound where the highest frequencies were loudest, but on second thought these “highest” frequencies would be just below my arbitrary choice of high frequency, and I bet it’s pretty miserable when someone plays such a sound by accident. Negative numbers throw errors now.

Weird, arrow-y, grotesque function names now fall in line with exemplary, well-to-do functions such as db-to-linear and hz-to-step.
Floats<-list is renamed to validate-coefs since that’s what it really does. Casting to float is removed (“bad integer operation” was the bane of my existence when I first wrote this). This is where negative numbers throw errors. If a user enters something cheeky like ’ or ))<>((, nothing happens, but maybe they had that coming.

The new function is: EVAL-STRING

Neat, can’t wait! Is this part of version 5?

There’s a minor bug here:

Thank you! normalize-table, which used 2048 instead of ny:all, stopped working correctly one day and I couldn’t figure out why. I’ve replaced gen-table with a function using simrep, and changed the top-harmonic variable to be an integer low enough to simultaneously avoid argument stack overflows, aliasing, and irritated house pets. Also, I’ve changed get-amplitude since gen-table now starts looping from 0.

Also, I’m unsure of the purpose of “(if (> amplitude quiet)”.

You guessed right, an optimization. I agree it doesn’t need to be there, it runs fast enough as is. The idea wasn’t to reduce the number of loops, but instead not to bother building, scaling, and adding a harmonic if it wouldn’t be audible. I don’t think it would be appropriate to exit the loop after encountering a quiet harmonic, since a user might input “(99 0)”.
From the way I wrote the if, I would expect that the harmonic wouldn’t be computed at all if (> amplitude quiet) returns nil. Is this not the case?

GEN-TABLES could be written a bit more succinctly

Ooh! yoink
ADDITIVE3.ny (5.12 KB)

No, this is still version 4.
Version 5 will come eventually, but version 4 still has a lot of possibilities to be discovered before we go there. The recent work on “Scripting” has opened up a lot of new territory for version 4. “eval-string” is a by-product of that. (2.3.1 alpha manual page: https://alphamanual.audacityteam.org/man/Scripting). There’s still a lot of this that has not yet been documented, but that is on the way.

There has also been some new widgets added to v4 plug-ins. I’m still in the process of writing the documentation:

Yes I think that’s the case, but I doubt that it makes a significant difference to the performance.
The “build-harmonic” function is just one line in nyquist.lsp (https://github.com/audacity/audacity/blob/master/nyquist/nyquist.lsp)

(defun build-harmonic (n table-size) (snd-sine 0 n table-size 1))

As “table-size” is small, and “snd-sine” is a primitive written in C, I’d expect this to be very fast, unlike interpreted LISP loops. Perhaps worth doing some experimentation to test the speed / performance.

:smiley:

If the user enters something like this, the plug-in silently fails (error in debug only):

(2.25 3 2.0 5 3) (3.3 2.7) (5.4 2 7.5 (3.2 5) (4 2)) 4 (7.8 9.2) (13.3)

and I think that kind of user error would be easy to make but not so easy to spot what is wrong.

To rigorously validate the user input (as should happen with plug-ins that are shipped with Audacity) is quite complex and probably requires stepping through every character and matching parentheses. I don’t think that is necessary here, but I think it would be worth checking that all top level items are lists.

Perhaps something like this:

(defun validate-coefs (lst)
  ;;; All items in top level list must be lists.
  (dolist (coef lst)
    (when (not (listp coef))
      (throw 'err (format nil "Coefficient ~s is invalid (not a list)." x))))
  (validate-items lst))

(defun validate-items (item)
  ;;; Items must be lists or numbers. Test recursively.
  (cond
    ((and (numberp item) (>= item 0)))  ;valid
    ((listp item) (mapc 'validate-items item))  ;recurse
    (t (throw 'err (format nil "Coefficient ~s is invalid." item)))))

I like the new widgets. Duration is now a time button.
The read/write commands have me wondering: is it already possible for a LADSPA or LV2 plugin, or possibly a malicious version of the FFMpeg library to execute arbitrary code, or is there some kind of limitation on what they can do if Audacity calls them? Are there any restrictions on what files a Nyquist plugin could read from or write to?

Right, I just noticed the Run Benchmark… tool and it told me my computer could play a couple dozen 44.1kHz tracks at once, in real time. I suppose a big round number like 2048 spooked me a bit, but it makes sense that most computers that can play sound should be able to crush this.
I’m struggling a bit trying to figure out how to time how fast Nyquist runs something, since I can’t get any information on the system time when code executes. I’m guessing the general idea is to write a loop in Nyquist to run the same code some large, fixed number of times, time that with a stopwatch or something, and use that to calculate the average time. I’m unsure how to account for the extra time it takes to interpret and execute the loop itself.

Totally right, my bad! I’ve changed validate-coefs to your suggestion, but this has me wondering whether it was a good idea to require the top level items to be lists. I guess I wanted the syntax to differentiate between top level items and deeper ones, since they mean different things, but I’m wondering if it makes sense to just silently convert that 4 to a (4) and keep going.
ADDITIVE3.ny (5.19 KB)

For me “ADDITIVE3.ny (5.24 KiB)” produces sound,
but “ADDITIVE3.ny (5.19 KiB)” does not produce sound, just an error message …

ADDITIVE3 ny (5 19 KiB).png

Your version of Audacity is too old.

I’ve got it working (on Audacity 2.3.0) …

1481.gif