Panning plug-ins

Topic split from:

An interesting way to create a low frequency random signal:

(lp (force-srate *sound-srate* (force-srate 4 (noise))) 2)

I had a similar idea:

(setf control-noise (sum 0.5 (mult 0.5 
  (snd-white 0 10 (get-duration 1)))))
(vector (mult (aref s 0)  control-noise)
        (mult (aref s 1)  (diff 1 control-noise)))

The thing I want to avoid the most, is that I do not have to normalize the noise after cration , but this seems to be always necessary with the usage of filters, although reson has an built-in normalize option.


The only slight problem with your snippet (though it may be acceptable for this plug-in) is that the interpolation between the random values is linear, so the transition from one pan position to the next is also linear. This tends to cause extreme left/right positions to exist only momentarily except for the unlikely event that two sequential random values both have values close to +1 or -1. Applying a low pass filter will make the pan smoother and allow extreme pan positions to linger a little longer.

This snippet produces peaks very close to +/- 1.0

(setq freq 4.0)
(mult 2.0 (lp (force-srate *sound-srate* (force-srate freq (clip (noise) 0.5))) (/ freq 2.0)))

To ensure that it does not exceed +/- 1 the waveform could be clipped (again) after it has been scaled.

(let ((freq 4.0)
      (sr *sound-srate*)
      (clip-noise (clip (noise) 0.5)))
    (mult 2.0 
      (lp (force-srate sr (force-srate freq clip-noise))
          (/ freq 2.0)))

Only a very few if any peaks are clipped to 1.0 because the noise is unlikely to be much (if at all) greater than 1.

By the way, I suspect (though I’ve not actually checked) that the original problem with Panrand.ny was not only excessive memory usage but also a stack overflow if the main function went through too many recursions. For a large number of loops it is generally better to use iteration so as to avoid the possibility of stack overflow.

It is probably better to use the “scale-srate” function instead of the inner force-srate. (something like ‘(scale-srate (noise) 0.0001)’).
If high precision for the position of the sound is not an absolute necessaty, it may also be bettter to use resample.
With squaring and drawing the square root, the course of the signal could be weighted differently.
There’s also the possibility to use “FMLFO” to produce a varying frequency. There’s a lot that can be done.

If you don’t need it to be truly “random” then you could create a fairly short random control signal and repeat it. If the control signal is more than a few seconds duration then unless there are any particularly distinctive moments it will still sound random even when repeated.
As you say, there are a lot of options and I’m happy to leave the choice to you :smiley:

Why not a X-mas special with 24 surprises…:smiley:

Here is the corrected plug-in "panning (random)"by David R. Sky.

What I’ve changed so far:

  • correct mono-conversion (-6 dB)
  • removed duration after “noise”. This was the main reason for the crashes, because the noise was ‘duration (sound)^2’ long, e.g. 2 min of sound produced 240 min of noise.
  • Some input correction
  • New default values
  • Stereo width offset in order to preserve the full gain for narrow signals
  • Simpler code layout, some minor bugs removed.

The code is not especially speed/resource improved. I’ve tested it with 30 min of a sweep sound without a crash - it still depends on how much memory you have.
The weak point is still to get the highest peak of the random noise. The peak function can’t be shortened because the highest peak could appear at the very end. I preserve further improvements for the bundled panning plug-in. However, suggestions are stil appreciated.
panrand.ny (1.33 KB)

Good catch.
I have applied (only) this fix and reverted my previous change.

I agree that you have made some other good enhancements, but as this plug-in is likely to be retired shortly I’d like to to keep the changes to David’s original code to a minimum.

More about your other changes to follow…

I see why you have done this and I think that this change is “correct”, but the desirability of this change depends on the way that the effect is used, so for anyone that is familiar with the current version it may not be a welcome change.
If the user has a mono track and they wish to pan it randomly, with the original version they would probably just add an empty track (Ctrl+shift+N) and join the two tracks to make a stereo track. If they do this with the updated code, the result will have only half the amplitude that the original version would have.
I think that this fix probably is a good idea for a new “panning” effect, but because it is a noticeably different behaviour I have not applied this to David’s plug-in.

Thanks - that’s fixed the problem and has now been applied.

I like user input checking. You can bet that if it is possible to enter invalid input, then someone will :smiley:
It may be a matter of personal preference as to how the user input is validated.
When is it better to assume that the user means a positive value when they enter a negative value (or vice versa). This approach could be useful for dB values where the user may just forget to enter a minus.
Sometimes it may be better to return an error message. This approach is probably best when the user input is complex, as may be the case when a text input widget is used for entering parameter values.

For this plug-in it would be better if it was impossible to enter negative values. Perhaps there is a case for an enhancement to the slider widget:

 ;control variable-name "text-left" variable-type "text-right" initial-value minimum maximum (&optional limit)

The additional “limit” parameter could be an optional value:

  • 0 or not present = not limited (as now)
  • 1 = input value cannot be less that minimum slider value
  • 2 = input value cannot be greater than maximum slider value
  • 3 = input value cannot be outside of slider range
  • (val1 val2) = minimum and maximum values through text input

If the minimum slider range is limited to a positive value, then Audacity would prevent typing a minus sign.
Audacity should probably also prevent typing a decimal point if the slider is set to integer.
I’m not certain that this suggestion is possible, but I think that it probably is.

What should happen if the user enters a “speed” of zero? This should probably be valid, but would need to be treated as a special case (using a conditional). A speed of zero would mean that the plug-in would randomly generate a constant value. This could be useful if the user was applying the effect to multiple tracks and wanted to spread the tracks (randomly) across the stereo field but without the individual tracks moving.

Again, probably not a bad idea, but a divergence from the original version so I’ve not applied to David’s plug-in. If I was choosing then I’d probably set the default to 1.0.

Nice enhancement.

The code could be simplified and memory usage reduced further still. I’ll start a new post as this one is already long.

Taking the “Normalize” function outside of the “Let” block certainly helps with readability.

Setting “S” to nil in line 27 does not actually make much difference to RAM usage. I presume that this is because the audio data is still bound to “sound”. An alternative approach that has a greater effect is to re-use the “S” variable.

The code could be simplified even further:
In line 25 the sound is converted to mono, so why use (vector) to make it 2 channel again?
Picking out a few lines from the new version:

(setf sound  (mult 0.5 (sum (aref s 0) (aref s 1))))
(setf s nil)

(defun pan2 (sound where)
    (mult (aref sound 0) (sum (1+ width-offset)  (mult -1 where)))
    (mult (aref sound 1) (sum width-offset  where))))

(pan2 (vector sound sound) signal)

As “sound” is now mono, we could easily make pan2 use a mono sound:

(setf s (mult 0.5 (sum (aref s 0) (aref s 1))))

(defun pan2 (sound where)
    (mult sound (sum (1+ width-offset)  (mult -1 where)))
    (mult sound (sum width-offset  where))))

(pan2 s signal)

I think that this raises another interesting possibility that could be used in a new “panning” plug-in. Rather than converting the input to mono, the left and right input channels could be panned independently, thus creating an interesting spacial effect rather than simply panning side to side. For this reason I would probably move the conversion to mono out into a separate function so that it can be used for “mono source” mode, or not used form “stereo source” mode.

(defun mono (sig)
  (mult 0.5 (sum (aref sig 0)(aref sig 1))))

We could also move creating the control signal out to a function, allowing it to be called as required, which then opens up the way to choosing alternative pan patterns, for example:

(defun pan (sig where)
    (mult sig (sum 1 (mult -1 where)))
    (mult sig where)))

(defun normalize (amp sig)
  (let ((x (peak sig ny:all)))
    (scale (/ amp x) sig)))

(defun mono (sig)
  (mult 0.5 (sum (aref sig 0)(aref sig 1))))

(defun rand-pan (width speed)
  (let* ((sig (lowpass2 (noise) speed))
         (sig (normalize width sig)))
    (sum 0.5 (mult 0.5 sig))))

(case pan-type
  (0 (pan (mono s) (rand-pan maxwidth maxspeed)))
  (t (pan (mono s) (sum 0.5 (mult 0.5 (hzosc 1))))))

I see that the subject fascinates you too.
Ok, firstly the changes I’ve made to the original.
The main fault was that the noise was calculated as if we had no environment (“abs-env”). We could gain a lot of resource if we made the noise up from a lower samplerate, since we only need the frequencies up to 20 Hz. The control-srate should be enough. This goes up to about 1102 Hz. A speed of 20 Hz would still give back 110 samples or curve points for one motion.
Thus “noise” could be replaced by “snd-white” with a proper samplerate and the original variable dur could now rightfully be applied.
The second fault was that the mono sound was 6 dB too high if made from 2-channel mono. I don’t think that this should remain, even if some users create their input stereo from one mono and one empty track (= 0 dB at most). 32-bit float formats can handle +6 dB and the sound can afterwards be normalised. If you use 16 or 24 bit, there is no “beyond” 0 dB. The output will clip. It is surely better to have 6 dB headroom which can be amplified afterwards to 0 dB.
The bundled panning effect will most likely check the input sound for empty channels.

A first control that has to be added is one that allows the user to decide if the source code is treated as stereo or should be summed up in a mono channel.
It depends on the preset, if the result will be meaningful. For example, if I chose “blend from left” and applied the effect to 10 seconds of music, I’d expect the sound to end up with the original stereo distribution. The second control for our BPP (from now on for “bundled panning plug-in”) is the effect catalog. There are dozens of presets that can be provided, such as “Random Pan”, “Random Width”, “Bee Flight”, “butterfly”, “Orbit” “blend from left-back” “Passing by”, “Fold and Swap L-R” and so on.
The big question is how many controls should follow this one. They should have a meaning to all presets, since we can’t grey out controls that are not used.
3 essential ones are:

  • “Speed”
  • “Width”
  • “Center offset”

Other optional controls are ponderable (Help, effect preview/overview etc).
A zero value in the speed control should be in fact have a special meaning to it. I propose to treat it as the “auto” mode. For a ping-pong effect, this could be 6 Hz (original vibrato speed of the first organs). For a blend effect this would be 1 (not Hz but once over the whole selection length).
Besides, the definition of the pan function is absolutely not necessary, since the function is already difined in Nyquist as it is (maybe not in very old versions).
However, I am personally not happy with this definition because it produces an incorrect panning (for speaker-output). This brings me to the 6th control, which I want to discuss in a seperate thread in the Nyquist Forum, namely the compensation for the center attenuation, made by Audacity.
My pan definition in “Panning (ranom)” was a first feeble attempt to even out this effect.
In general, the BPP will handle sounds on different levels, using the advantages of L-R and M-S stereophony, distance attenuation and maybe early reflections too. The output will be mixed via a similar procedure as in Steve’s Channel-Mixer.

Using SND-WHITE with a low sample rate certainly uses a lot less RAM, and even gives a bit of a speed increase, but we need to be a bit careful. If the selection is not exactly divisible by the low sample rate then the processed sound will be a bit shorter than the original. We can allow for this by generating a bit more noise than we really need.
For example, if we create the filtered noise with something like:

 (lowpass2 (snd-white 0 *control-srate* (1+ (get-duration 1))) speed)

Another slight peculiarity with using filtered white noise is that the very beginning is always centre-panned (which is obviously not “random”).

I think that’s due to the filter application. All filters need some time to establish themselves, because were always dealing with discreet signals. Some padding might help. Since you’ve already expanded the sound by one second, it’s easy to extract a desired portion.
Incidentally, I think that the user should be able to say if those effects start/end abruptly or if there should be a short fade-up/-down.
The random panning is not very useful when it begins its journey at the outer most left or right, especially when it is applied to the middle of a sound.

It’s difficult to guess what users might want to use random panning for. David may have made it just as an exercise or curiosity, or perhaps he had a specific application in mind - who knows.
Perhaps for a general purpose panning effect it would be useful to be able to specify the initial and final pan positions? I’m not sure how that might work if the panning is cyclical.

cycling effects shouldn’t be very difficult to program to certain pan positions.
We can pass the phase argument to the oscillators as the starting pan position. The end position is a little bit trickier, since we have to search for the right frequency that is close to the entered speed value and that ends with the right phase for the selection. Random signals are even harder, because we must search for the right values and afterwards stretch the signal to fit into the selection.
Another possibility for the random signals is to use “fmosc” (or “amosc”) to produce the exact speed frequency and to modulate it with random noise (all at a low sample rate).
This gives us more control and precision and reuses the calculations for regular wave-forms. Furthermore, we can avoid the normalisation problem, although it is replaced by the stretching issue…:wink:

For random panning, it might be easiest to end the random control signal a bit early, then swing across from the current pan position to the desired end position. Or to look at it another way, fade out the end of the random control signal to an offset value.
Something like:

(let ((dur (get-duration 1)))
    (sim (mult control (pwlv 1 (- dur (/ speed)) 1 dur 0))
      (pwlv 0 (- dur (/ speed)) 0 dur end-position))))

The snag with cyclical panning is that you can have an exact frequency, or an exact end phase, but not both.

Now, one should know what the user is the most keen to get, a precise frequency or an accurate pan position. The later can be bend the easiest, since sound location differs widely from one person to another. if we assume that only 5 positions can be localised with some precision and that extreme left or right is reached by a gain difference of only 18 dB (average 15 dB), it can easily be seen that a precise pan position (other than the centre) doesn’t need to be specified in degrees or even percent.
Here is a code snippet that works with a precise frequency but randomly chosen distances to the centre. It’s the plug-in within three lines…

(pan (osc c4) 
     (sum 0.5 (mult 0.5 (amosc (hz-to-step 1) 
                               (scale-srate (noise 5e-4) 5e-4)))))

The first sound within “pan” could of course be replaced by “s”. If you remove the “scale-srate” you’ll get a passing train…

If you don’t mind linear panning:

(pan (aref s 0)(snd-abs (snd-white 0 4 (1+ (get-duration 1)))))