I just came across this thread, after having encountered the problem everyone has described. I was recording live and had the level set just a little too high so got some sections with digital overflow distortion.
So I went and wrote a Nyquist plug-in which I think is very similar to what Arnie and Stevethefiddle were talking about. Sorry that we’re all duplicating effort!
This plug-in checks sample by sample: if sample magnitude exceeds a user threshold, it starts looking for distortion; then, if it detects a big jump going the opposite direction of the waveform trend, it assumes it is overflow, and it patches in a half cycle of a sine wave between the threshold points.
I have tested it on some short segments and it looks ok. I suppose it is a processor and memory hog on longer segments.
I am trying to figure out how to upload a plug-in. Is there some repository? Until then, here is the whole thing inline, including debug code.
Please let me know if this helps, or if it has some horrible defect.
I sure there were primitives to convert sound to list and back!
Thanks.
;nyquist plug-in
;version 1
;type process
;name "Digital Distortion Post-Limiter..."
; rationale
; detects digital distortion = DAC overflow
; and replaces distorted intervals
; with interpolated half cycle of a sine wave
;
; control input
; sets the threshold level for interpolation
; if distortion is detected, all samples between
; threshold levels will be replaced by the interpolation.
;action "Detect and clean up Digital Distortion..."
;info "Digital Distortion Post-Limiternby Lex Lindsey May 2009nGNU Copyleft"
;control thresh "Limiting threshold" real "Peak" .8 0 1
; DEBUG get output stream
; (setq os (make-string-output-stream))
; REBUILD sound array after
; processing each channel separately
(defun process-s (a s)
; (prin1 a)(prin1 s)(terpri)
(if (arrayp s)
(vector (apply a (list (aref s 0))) (apply a (list (aref s 1))))
(apply a (list s))
)
)
; CONVERT list of sound samples into a sound
; whew wish there were some primitive to do this!
; must first convert the list into an array,
; then store all the list elements into the array,
; then convert the array into a sound.
; need sound rate from master
(defun snd-from-list (sr lst)
(let* ((nl (length lst)) (ar (make-array nl)))
(do ((n 0 (1+ n)))
((= n nl) (snd-from-array 0 sr ar))
(setf (aref ar n) (car lst))
(setq lst (cdr lst))
)
)
)
; CONVERT a sound into a list
; again, whew, wish there were a primitive!
; pass the count of samples because
; the duration argument to the sine function
; apparently does not work when called from inside Audacity
; (though it works fine when tested directly from ny shell)
(defun list-from-snd (s ns)
; (format os "list-from-snd len(s)=~S ns=~S~%" (snd-length s 2000) ns)
(let (lst (v1 (snd-fetch s)))
(while (> ns 0)
(setq lst (append lst (list v1)))
(setq v1 (snd-fetch s))
(setq ns (1- ns))
)
lst
)
)
; save samples while looking for possible digital distortion
(setq fi ()) ; save list
(setq nf 0) ; count
(setq v1prev nil)
(setq ddf nil) ; flag digital distortion
; DEBUG - track sample index
(setq sk 0)
; DEBUG - store values when fi is accessed
; (setq filook nil)
; (defun showfi () (print "sk,fun,v,v1prev,nf,ddf=") (printlist filook))
(defun defi (v fun)
; (setq filook (append filook (list (list sk fun v v1prev nf ddf))))
)
; THRESHOLD
; threshold is a constant, should be input from user
; DEBUG (setq thresh .8)
; the limiting range starts at the first saved prior value
; which crosses the threshold in either direction;
; remember all values between that position and the
; next position where the signal crosses the threshold
; going in the opposite direction
; SAVE sample
; return nil to flag defer output
; check for big jump in sample value => digital distortion
; then save previous sample value for test
(defun savefi (v1)
(setq fi (append fi (list v1)))
(setq nf (1+ nf))
(setq ddf (or ddf (> (abs (- v1 v1prev)) thresh)))
(defi v1 "savefi")
(setq v1prev v1)
nil
)
; START saving possible overflow samples
; must return nil so caller defers saving
; the list of sounds which may include digital distortion
; 2do: check to make sure this is not a false peak
; due to start of selection being at a high level
; could be logical loophole if selection actually starts over thresh
(defun startfi (v1)
(setq fi (list v1))
(setq nf 1)
(setq v1prev v1)
(setq ddf nil)
(defi v1 "startfi")
nil
)
; leftovers - if end of sample list while
; in the midst of defering output,
; must append saved list to output
; since appending nil is a no-op, not much to do
; but must call crossback since could be tracking possible DD
(defun leftover (s2)
(append s2 (cond (fi (crossback nil))))
)
; POST LIMITING
; fixup the overflow by post-limiting
;
; generate a positive-going sine envelope of
; frequency fe = fs / 2 / nf
; where fe = envelope frequency, fs = sample frequency,
; and nf = num of samples in the overflow range
; peak magnitude of envelope is difference between peak and threshold,
; which is constant
; NB: duration does not work when called from Audacity
; so just generate an *infinite* sine wave, but only use nf samples
; DEBUG: the big let* statement is to make debug display easier
(defun limit-envelope (fi nf)
(let* (ev (efreq (/ *sound-srate* (* nf 2)))
(ethresh (/ (+ v1prev (car fi)) 2))
(emagn (- 1 (abs ethresh)))
(ephase (if (> (car fi) 0) 1 -1))
(escale (* emagn ephase))
(epitch (hz-to-step efreq))
; generate half a cycle of a sine wave
; scaled by limited magnitude and phase
(esine (sine epitch))
(ess (scale escale esine))
(eoff (snd-offset ess ethresh))
)
; (format os "limit-envelope efreq=~S,emagn=~S,ephase=~S~%" efreq emagn ephase)
; (format os "limit-envelope ethresh=~S,epitch=~S~%" ethresh epitch)
; (format os "limit-envelope len:esine=~S~%" (snd-length esine 2000))
(setq ev (list-from-snd eoff nf))
; (format os "length:fi=~S,ev=~S~%" nf (length ev))
ev
)
)
; CROSSED BACK under threshold,
; check to see if dd was detected
; if dd, then return limited envelope
; else just return the saved sound list
(defun crossback (v1)
; check for nil - do not save if ending
(cond (v1 (savefi v1)))
(defi v1 "crossback")
; (format os "crossback:nf=~S~%" nf)
(cond (ddf (limit-envelope fi nf))
(T fi)
)
)
; CHECK SAMPLE
; check for threshold
; if already saving, check for threshold crossing
; if crossed, did we find any DD?
; if DD, output the limited envelope
; else output the saved samples
; else not saving, so if threshold crossed,
; start saving
; if checks returns non-nil,
; it is a list to be output as sound
(defun checks (v1)
; (prin1 (list 'v1= v1 'fi= fi))
(cond
(fi
; already saving - check to see if crossed back
; however, could cross under thresh while still
; in the overflow region, so must test to see
; that sign of v1 is same as that of first saved fi
(cond
((and (< (abs v1) thresh) (plusp (* v1 (car fi))))
; crossed back, check whether dd found
; returns fixup or saved sound list
; must clear the save list
(let ((f (crossback v1))) (setq fi nil) f))
(T (savefi v1)) ; also check for overflow
)
)
(T ; not in a potential overflow
(cond
((> (abs v1) thresh)
(startfi v1) ; crossed threshold
)
(T (list v1)) ; return sample to be appended to output
)
)
)
)
; DEBUG
; must define s2 global to use
; this single-step debug function
'(setq s2 ())
'(defun check1 (s1)
(let ((v (snd-fetch s1)))
; the main workhorse
; if checks returns non-nil,
; it is a sound list to append to s2
(let ((v2 (checks v)))
(cond (v2 (setq s2 (append s2 v2))))
)
; debug track index globally
(setq sk (1+ sk))
)
)
; CHECK CHANNEL Sample list
; mono - so needs to have process-s split and rebuild the channels
; needs to build the output sound list s2
(defun checkchannel (s1)
(let (s2 (srate (snd-srate s1)))
(do ( (v (snd-fetch s1) (setq v (snd-fetch s1))))
; remember to tack on any leftovers to s2
; if in the midst of an overflow check,
; there will be a sound list being saved
((null v) (snd-from-list srate (leftover s2)))
; the main workhorse
; if checks returns non-nil,
; it is a sound list to append to s2
(let ((v2 (checks v)))
(cond (v2 (setq s2 (append s2 v2))))
)
; debug track index globally
(setq sk (1+ sk))
)
)
)
; (format os "length:s0=~S~%" (snd-length (aref s 0) 2000))
; value of x is the return
(setq x (process-s 'checkchannel s))
; DEBUG output
; last value is string containing all previous stream output
; so will be displayed in a dialog
; (format os "length:x0=~S~%" (snd-length (aref x 0) 2000))
; (get-output-stream-string os)