Binson Echorec 2 plugin

New to Nyquist, Lisp and Audacity I wanted to implement a software variant of the famous Binson Echorec 2 Delay, used by Dave Gilmour (Pink Floyd). The Original Binson has a rotating drum with one recording and four replay magnetic heads. The maximum delay for the fourth head is 300 ms. I could not resist extending the functionality with adjustable “rotation speed”, a better tone control and a control to “move the heads” to get other echo characteristics, although I am not saying that they are better. The original delay times 75, 150, 225, and 300 ms were the result of many hours of testing, and this setting is really good and used in the example. Of course I used code written by others, I looked at the multitap delay by Steve Daulton (thanks). As I said I am new to Nyquist and Lisp so any suggestions for better code is appreciated! I have been writing code since the early days of punched cards and so on, but it is still fun to explore new areas. Thanks a lot for making Audacity and Nyquist available. Mixing is really made easy!
binson.ny (5.1 KB)

Latest Version: binson.ny

That seems to work OK, though I did encounter a couple of problems.

Avoid using special characters (such as “å”) in Nyquist plug-ins. Nyquist does not understand multi-byte characters, and they can cause unexpected and hard to find bugs.

Avoid trailing parentheses.

For some reason, when I open binson.ny in a text editor, there’s an empty line between each line. I suspect this is due to the text editor you are using. On Windows, I’d recommend NotePad++ ( NotePad++ also has parentheses matching, and syntax highlighting for LISP (possibly “Common Lisp”, though it does not matter too much which dialect of Lisp).

Line indentation is quite important in Lisp for readability. There’s a short but useful guide here:

Thanks a lot Steve
I have used notepad, but I will immediately move to Notepad ++, and of course I will follow the style guide.
The code does not check for a stereo track, is that a flaw?

Checking for a stereo track is only necessary if the code needs to do something different with stereo tracks.
For example, a ping-pong delay “requires” a stereo track, so you might have something like:

(if (arrayp *track*)
    (ping-pong *track* arg1 arg2)
    "Error.\nStereo Track Required")

or you might do:

(if (arrayp *track*)
    (ping-pong *track* arg1 arg2)
    (mono-delay *track* arg1 arg2))

If the code works with mono or stereo, then no need to test for it.

Ah, that explains it. NotePad++ is MUCH better :slight_smile:

Thanks Steve!
I have downloaded the Notpad++. The indention rules says indent two characters, but I can not find a way to make “Tab” work. The default seems to be four. Should I bother?
I have corrected the code to make it more readable, should I update?

I’ve not used NotePad++ for quite a while (I usually work on Linux), but I’m pretty sure there is a way to set it to use 2 spaces.

What I normally do when updating a plug-in on the forum, is to post the new version in a new post, with a brief description of the changes, and then add a link to the bottom of the first post and highlighting it as Latest Version:

The code is now more readable. Thanks Steve!
binson.ny (5.12 KB)

I’ve merged your latest version post into this topic, and added a link in the first post.

I think you should now be able to edit previous posts, so you can take a look and see exactly what I’ve done. In most cases it is better not to modify your old posts because it can make topics difficult to follow, but there are cases like this where it is useful. Just keep in mind that other Audacity users may be reading the topic.

That’s a lot more readable.

There’s some things that I would change (if it were my code), which are more to do with coding style than functionality. Would you like me to describe those changes?

Of course!!! Please do. I have never coded in Lisp, and it was quite a few years since I wrote code at all, and that was for military surveillance systems.

Will the result be the same if I apply high shelf on the sum of all delays instead of applying it on each delay separately?
I have found your desk EQ , is it ok to use your code in my echo to get a much better tone control?

My disclaimer - I’m self taught, but I’ve read up quite a lot and always try to follow “best practices” :wink: I don’t claim to be an expert, but hopefully I can offer some tips that will save you some time and effort.

Below are some points, in no particular order.

The first point is an error that I’ve just spotted.
The “;version” header refers to the Nyquist code syntax version. Valid values are integers between 1 and 4. Version 1 was the earliest plug-in version. If I recall correctly, the only widgets supported by version 1 plug-ins were the slider widget and the string widget.
The latest version is version 4. It is highly recommended that all new plug-ins are written as version 4 plug-ins.
Note that the audio passed from the track to Nyquist is “S” in version 3 or lower, and “TRACK” in version 4.

If you wish to log a version number for the plug-in, use the “;release” header. For example:

;version 4
;release 1.1

More info here:

Input validation:
In current versions of Audacity, there is some input validation done by Audacity. For example, in the line:

;control fback "Length of Swell" int "" 2 0 10

The “int” widget enforces that fback is an integer in the range 0 to 10.
This was not the case in early versions of Audacity, which is why you will see some older plug-ins checking the type and range of input values.

An exception to this is the “Numeric Text” widget, which can be defined with an indefinite range, by using “NIL” as the max and/or min value.
More information here:

“IF” statements:

(if condition

“IF” is similar to the ternary operator in other languages.
Note that the “CONDITION”, “THEN” and “ELSE” clauses all line up one above the other.

“IF” can be used with just one case, for example:

(if (oddp val)
    (setf val (1+ val)))

though if there is no “ELSE” clause, I think it’s a bit easier to see the intent by using “WHEN” rather than “IF”:

(when (oddp val)
   (setf val (1+ val)))

(Note that the “WHEN” expression is indented by only 2 spaces)


Several of the plug-ins that are shipped with Audacity have copious amounts of comments. In some cases this is to help new users understand the code. In the case of David Sky plug-ins, it’s because he was blind and he found it easier to program with lots of comments (sadly David Sky passed away a few years ago. He was a lovely man, and a great help when I was first learning to write Nyquist scripts).

As a general rule, it’s best practice to try and write code that is “self documenting”. That is, the code itself describes clearly what the code does. The correct reason for using comments in real-world code, is to say “why” something is the way that it is in cases where the reason is not obvious.

Examples of unnecessary comments:

(setf a 23)  ;a=23
(setf b "David")  ;b= David
(format nil "~a is ~a years old" b a)

much better would be:

(setf client-name "David")
(setf client-age 23)
(format nil "~a is ~a years old" client-name client-age)

Example of when a comment may be useful:

(setf volume (* 4.1888 val))  ;volume of sphere = (4*PI/3) * (r^3)

and a good use of a comment from line 79 of your code:

(setq delay1 (* 75.0 m-echo))	;Binson

Line length:

As a general rule, try to avoid long lines of code. They are difficult to read, and easy to miss mistakes. If you find you have very long lines of code, that’s usually a sign that it could be written better.

Duplicate code:

This often goes hand in hand with the above point about line length. If you find that you have the same (or very similar) code repeated several times, consider rewriting it as a function, a macro, or pre-evaluating the expression.

I’ll give an example of how some of your code lines may be reduced in length, with less repetition, in a follow-on post.

All of my code on this forum, and all of my published plug-ins are licensed under GPL v2, which means that you can use them pretty much any way you like provided that you retain the GPL license.
(Full details of GPL v2 can be found here: GNU General Public License v2.0 - GNU Project - Free Software Foundation)

The last part of your code:

;; The ECHO
(scale mv 
(sim s 
  (if (and (> delay1 0)(> gain1 0)(= delay-on 1))
    (eq-lowshelf (eq-highshelf (scale gain1 (feedback-delay s delay1 fback)) f-high dB-high ) f-low dB-low) 0)
  (if (and (> delay2 0)(> gain2 0)(= delay-on 1))
    (eq-lowshelf (eq-highshelf (scale gain2 (feedback-delay s delay2 fback)) f-high dB-high ) f-low dB-low) 0)

  (if (and (> delay3 0)(> gain3 0)(= delay-on 1))
    (eq-lowshelf (eq-highshelf (scale gain3 (feedback-delay s delay3 fback)) f-high dB-high ) f-low dB-low) 0)
  (if (and (> delay4 0)(> gain4 0)(= delay-on 1))
    (eq-lowshelf (eq-highshelf (scale gain4 (feedback-delay s delay4 fback)) f-high dB-high ) f-low dB-low) 0)

  (if (= delaytype 2) (scale  gain-swell (feedback-delay s delay1 0)) 0)	; Binson returns a fraction of the signal for all heads in the Swell-pos.
  (if (= delaytype 2) (scale  gain-swell (feedback-delay s delay2 0)) 0)
  (if (= delaytype 2) (scale  gain-swell (feedback-delay s delay3 0)) 0)
  (if (= delaytype 2) (scale  gain-swell (feedback-delay s delay4 0)) 0)))

We can see earlier in the code that delay1 to delay4 are always > 0.

We can also see that the shelf filtered delay is almost the same for each of the delays, so that’s a prime candidate for wrapping in a function.
Note that fback, f-high, dB-high, f-low and dB-low are all global in scope, so no need to explicitly pass them to the new filter function (they are already available in the function because they are “global”).

Assuming I’ve not made any errors, this should be functionally identical to your code, but in my opinion, easier to read and debug:

;; The ECHO

(defun filter (sig gain delay)
  (if (and (> gain 0) (= delay-on 1))
      (let* ((delayed (feedback-delay sig delay fback))
             (delayed (mult gain delayed))
             (hs (eq-highshelf delayed f-high dB-high)))
         (eq-lowshelf hs f-low dB-low))

(scale mv 
  (sum *track*
    (filter *track* gain1 delay1)
    (filter *track* gain2 delay2)
    (filter *track* gain3 delay3)
    (filter *track* gain4 delay4)
    (if (= delaytype 2)
        ;; Binson returns a fraction of the signal for all heads in the Swell-pos.
        (mult gain-swell
          (sum (feedback-delay *track* delay1 0)
               (feedback-delay *track* delay2 0)
               (feedback-delay *track* delay3 0)
               (feedback-delay *track* delay4 0)))

Good question. It will probably be more efficient. Try it and see. :slight_smile:

Steve, I don’t know how to thank you enough! So generous with your time and knowledge. Otroligt, as we say in Sweden. Version 1.2 will be so much better!