tachometer analysis via nyquist

First post so forgive me if I’m asking in the wrong place/way.

Background: I’m trying to use an Arduino to monitor a low frequency tach signal, and have made good progress, but I’d like to have a way of comparing the data it is returning with the actual input signal. It struck me that a Nyquist plugin should be able to do this, but i’m just starting to make sense of the language and would be grateful for any pointers you can give.

specifics: i have recorded several short mono tach signals. the signal is basically a sawtooth pulse followed by a short silence, with variable amplitude and frequency depending on the speed of the rotation. the stats i would like to obtain are:

  1. the maximum frequency (shortest number of samples between zero crossings?),
  2. the sample number (time) of this point,
  3. the minimum frequency and
  4. time of this point.

Frequencies range from approx 10Hz to 100Hz and the recordings are relatively short: between 1 and 3 seconds.
Ideally i would like to apply the analysis across the whole recording but i can narrow the selection to sections if necessary.

thanks for any suggestions or pointers to other posts

Please post a short sample recording in WAV or Flac format so that we can see what you are working with. See here for how to post an audio sample: https://forum.audacityteam.org/t/how-to-attach-files-to-forum-posts/24026/1

attached a very small section, with a range of frequencies. thanks

First I’d suggest normalizing the signals to 0 dB (“Normalize” effect). This makes the signals easier to work with. http://manual.audacityteam.org/o/man/normalize.html

If you want to batch process a lot of files, you can add the Normalize effect and your new Nyquist plug-in to a Chain. http://manual.audacityteam.org/o/man/chains_for_batch_processing_and_effects_automation.html

There’s a bit of noise in the sample which can easily be removed with a low-pass filter. For example:

(lowpass2 *track* 1000)

http://www.cs.cmu.edu/~rbd/doc/nyquist/part8.html#index444

but probably a better way to “filter” the signal is to resample it to a low frequency by taking the average amplitude of every, say 20, samples. This will effectively be a 1100Hz low-pass filter (the bandwidth at 44100 H is 22050 Hz, so at 1/20th of the sample rate the bandwidth will be 1102.5 Hz). It also reduces the amount of data to be processed to 1/20th of the original, so should be quicker and more efficient.

(snd-avg *track* 20 20 op-average)

http://www.cs.cmu.edu/~rbd/doc/nyquist/part8.html#index663

We can then loop through the samples and look at the amplitude.
One way of doing this is to use a “DO” loop (http://www.audacity-forum.de/download/edgar/nyquist/nyquist-doc/xlisp/xlisp-ref/xlisp-ref-093.htm) and grab each sample with SND-FETCH (http://www.cs.cmu.edu/~rbd/doc/nyquist/part8.html#index263). This is how “Sound Finder” and “Silence Finder” work (look for their “.NY” files in your Audacity Plugins folder.
However, since we are only looking at short amounts of audio (3 seconds at our low sample rate will be less than 7000 samples), we can do this more quickly by grabbing the whole lot as an array with SND-FETCH-ARRAY http://www.cs.cmu.edu/~rbd/doc/nyquist/part8.html#index265

So your code may look something like:

(let* ((sig (snd-avg *track* 20 20 op-average))
       (ln (snd-length sig ny:all))
       (data (snd-fetch-array sig ln ln)))
  (process data))

where “PROCESS” is your number crunching function (http://www.audacity-forum.de/download/edgar/nyquist/nyquist-doc/xlisp/xlisp-ref/xlisp-ref-087.htm)

Note that the size of the “STEP” determines the precision. Smaller values will give higher precision, but you may get some false hits due to noise, and the amount of data (hence the size of the data array) will increase). I don’t recall what the max size is for SND-FETCH-ARRAY, but it is at least 1 million.

thanks - that’s a great starting point. I’ll read through the example plugins and documentation and start trying out some code.

From a quick test, the limit appears to be a little over 1 million samples. Much more than that and Nyquist will crash, which will probably take Audacity down with it.

An alternative way to condition the signal would be to apply a “noise gate” (such as: http://wiki.audacityteam.org/wiki/Nyquist_Effect_Plug-ins#Noise_Gate) and then quantize the signal in the new plug-in code (http://www.cs.cmu.edu/~rbd/doc/nyquist/part8.html#index541).

Example (this creates labels at the positive to negative zero crossings):

(let* ((sig (quantize *track* 128))
       (ln (truncate len))
       (data (snd-fetch-array sig ln ln))
       (labels ())
       (oldval -1))
  (do* ((val (aref data 0)(aref data count))
        (count 0 (1+ count)))
      ((= count ln) labels)
    (when (and (> oldval 0)(< val 0))
      (push (list (/ count *sound-srate*) "") labels))
    (setf oldval val)))

“Conditioning” the signal allows us to deal with jitter around zero between the pulses. Alternatively you could handle the jitter when you process the data array.

In this example you will notice lots of false positives on an unconditioned signal due to noise between the pulses:

(setf step 2)
(let* ((sig (snd-avg *track* step step op-average))
       (ln (truncate len))
       (data (snd-fetch-array sig ln ln))
       (labels ())
       (oldval -1))
  (do* ((val (aref data 0)(aref data count))
        (count 0 (1+ count)))
      ((= count ln) labels)
    (when (and (> oldval 0)(<= val 0))
      (push (list (/ (- count 1) (/ *sound-srate* step)) "") labels))
    (setf oldval val)))

but we can avoid most (sometimes all) of them by looking for a non-zero crossing point (assuming that the signal has been normalized):

(setf step 2)
(setf offset 0.2) ; looks for the signal crossing this amplitude value

(let* ((sig (snd-avg *track* step step op-average))
       (ln (truncate len))
       (data (snd-fetch-array sig ln ln))
       (labels ())
       (oldval -1))
  (do* ((val (aref data 0)(aref data count))
        (count 0 (1+ count)))
      ((= count ln) labels)
    (when (and (> oldval offset)(<= val offset))
      (push (list (/ (- count 1) (/ *sound-srate* step)) "") labels))
    (setf oldval val)))

How are you getting on with this?
I had a play while my computer was busy doing another task, and wrote a bit more code that may have some useful features for you:

The code:

;; Looks for the signal crossing this amplitude value.
;; Increase if necessary to avoid false triggers
(setf v-offset 0.05)

;; Conditioning Filter frequency.
;; Reduce this if necessary to avoid false triggers.
;; Set to 0 to turn this off.
(setf filt 8000)

;; Time offset to calibrate detected zero crossing
;; points with waveform. Negative values move the
;; detected times (labels) to the left.
(setf t-offset -0.00001)

(let* ((sig (if (> filt 0)
                (lowpass2 *track* filt)
                *track*))
       (ln (truncate len))
       (data (snd-fetch-array sig ln ln))
       (labels ())
       (oldval -1)
       (prev -1)
       (maxhz -1)
       (minhz 99999999)
       (debug-text ""))
  (do* ((val (aref data 0)(aref data count))
        (count 0 (1+ count)))
      ((= count ln))
    (when (and (> oldval v-offset)(<= val v-offset))
      (let* ((time (+ (/ (- count 1) *sound-srate*))
                      t-offset)
             (cps (/ (- time prev)))
             (rpm (* cps 60))
             (text (format nil "~a Hz" cps)))
        (cond
          ((< prev 0)
            (setf debug-text
              (format nil "First impulse at ~a s." time))
            (push (list time debug-text) labels)
            (setf debug-text (strcat debug-text "\n\n")))
          (t
            (push (list time text) labels)
            (setf maxhz (max maxhz cps))
            (setf minhz (min minhz cps))
            (setf debug-text
              (format nil "~aTime: ~a s. \tSpeed: ~a rpm \t~a Hz~%"
                      debug-text time rpm cps))))
        (setf prev time)))
    (setf oldval val))
  (format t "Maximum speed is ~a Hz.~%Minimum speed = ~a Hz.~%~%~a"
          maxhz minhz debug-text)
  labels)

Apply in the Nyquist Prompt effect using the “Debug” button:
tracks002.png

The output from the Debug window:

Maximum speed is 42.8988 Hz.
Minimum speed = 41.8009 Hz.

First impulse at 0.0113152 s.

Time: 0.0346259 s. 	Speed: 2573.93 rpm 	42.8988 Hz
Time: 0.0579365 s. 	Speed: 2573.93 rpm 	42.8988 Hz
Time: 0.0812472 s. 	Speed: 2573.93 rpm 	42.8988 Hz
Time: 0.104671 s. 	Speed: 2561.47 rpm 	42.6912 Hz
Time: 0.128186 s. 	Speed: 2551.59 rpm 	42.5265 Hz
Time: 0.151723 s. 	Speed: 2549.13 rpm 	42.4855 Hz
Time: 0.175351 s. 	Speed: 2539.35 rpm 	42.3225 Hz
Time: 0.19907 s. 	Speed: 2529.64 rpm 	42.1606 Hz
Time: 0.222789 s. 	Speed: 2529.64 rpm 	42.1606 Hz
Time: 0.246621 s. 	Speed: 2517.6 rpm 	41.96 Hz
Time: 0.270544 s. 	Speed: 2508.06 rpm 	41.8009 Hz
Time: 0.294467 s. 	Speed: 2508.06 rpm 	41.8009 Hz

The main output is tab delimited, so it should be quite easy to import the data into a spreadsheet if you want.