2
$\begingroup$

I'm doing a project where I need to measure the rate of vibrato cycles in a sung note under various conditions. The stimulus will be a several-second-long audio sample of a held a capella note with vibrato; the output should be the rate of the vibrato repetition (e.g. 4Hz, 10.5Hz, etc). The audio will be captured specifically for this study; I won't be sampling existing audio, and don't need to deal with interfering signals. The underlying pitch of the note can be whatever is needed, although precision probably won't be possible.

Here's an example sonogram:

Example sonogram of a sung vibrato

Ideally, this would be done with a Linux command line chain or shell script, but if I need to write some Python that would be fine, too.

$\endgroup$
  • 1
    $\begingroup$ I'd probably try to first estimate the tone being sung (to be exact, it's fundamental frequency), so for example in your spectrogram something like 293 Hz (?), then filter out a bandwidth around that is significantly less than an octave wide, so maybe 193 to 393 Hz. After that, use a cyclostationary frequency estimator. (I'm sorry I'm throwing terms at you, but it's getting late and I won't be able to write an answer) $\endgroup$ – Marcus Müller Nov 11 '17 at 22:58
  • $\begingroup$ Yowza! Cyclostationary frequency estimator! (I want a tee shirt with that on it.) Thanks, but no way is this project worth that much effort. I've actually come up with a solution that uses the aubio library to do the main pitch-extraction work: see below. $\endgroup$ – Daniel Griscom Nov 11 '17 at 23:27
2
$\begingroup$

This publication may be of interest for you:

Jonathan Driedger, Stefan Balke, Sebastian Ewert, and Meinard Müller: Template-Based Vibrato Analysis of Music Signals, in: Proceedings of the International Conference on Music Information Retrieval, New York, USA, 2016, pp. 239–245. [PDF]

Furthermore, there is an accompanying website for this paper.

$\endgroup$
  • $\begingroup$ That's way more powerful and general than my problem requires. Someone else will find it helpful, though: thanks. $\endgroup$ – Daniel Griscom Nov 13 '17 at 13:06
1
$\begingroup$

I've implemented a simple vibrato measuring algorithm using the following open source tools:

  • sox for audio capture

  • aubiopitch for deriving the base frequency at regular intervals

  • Python for scripting the above and processing the results

aubiopitch, which is part of the aubio library, is the key here. It takes in an audio file and outputs a list of times and fundamental frequencies, spaced about every 6ms (aubiopitch uses a 2048-point FFT but steps by 256 samples, and the audio sample rate is 44.1kHz). The Python script then processes this as follows:

  • Keep two running averages, a short one of the last 5 frequency samples, and a long one of the last 10 frequency samples
  • If the short-term average is greater than the long-term average, then the frequency is rising; otherwise it's falling
  • Watch for when the frequency slope changes sign. Count the number of transitions, keeping track of the time between the first transition and the last one
  • Divide the number of transitions by the time, divide by two (two transitions per cycle), and you get the vibrato rate in Hz.

Python source code:

import subprocess
import re
import os

TEMP_SOUND = "/tmp/vibrato.delme.wav"
TEMP_DATA = "/tmp/vibrato.delme.txt"
DURATION = 2
VERBOSE = False


print("Recording for {} seconds...".format(DURATION))
subprocess.call(
    "/opt/local/bin/sox -d {} trim 0 {} >/dev/null 2>&1".format(TEMP_SOUND, DURATION), shell=True)
print("   ... done. Analyzing...")
subprocess.call(
    "/usr/local/bin/aubiopitch {}  | tail -n +10 > {}".format(TEMP_SOUND, TEMP_DATA), shell=True)


short_list = []
short_max = 5
long_list = []
long_max = 10
last_slope = None
first_crossing_time = None
time_freq_re = re.compile(r'([^ ]+) ([^ ]+)')
samples = 0

with open(TEMP_DATA, mode="rU") as f:
  for line in f:
    line = line.strip()
    time_freq_match = time_freq_re.match(line)
    if not time_freq_match:
      print("Whoa!")
      exit(1)
    time = float(time_freq_match.group(1))
    freq = float(time_freq_match.group(2))
    short_list.append(freq)
    if len(short_list) > short_max:
      short_list.pop(0)
    long_list.append(freq)
    if len(long_list) > long_max:
      long_list.pop(0)
    short_average = sum(short_list) / short_max
    long_average = sum(long_list) / long_max
    # If we've collected enough data to analyze
    if len(long_list) == long_max:
      slope = (short_average > long_average)
      # If this is the first time through, remember the slope
      if last_slope == None:
        last_slope = slope
      samples += 1
      # Crossing?
      if slope != last_slope:
        last_slope = slope
        # First crossing?
        if first_crossing_time == None:
          first_crossing_time = time
          samples = 0
          crossings = 0
        else:
          crossings += 1
        last_crossing_time = time
        last_crossing_samples = samples
        if VERBOSE:
          print("**** crossing ****")
      if VERBOSE:
        print("short {}, long {}, slope {}".format(short_average, long_average, slope))

  total_time = last_crossing_time - first_crossing_time
  print("total time {:.4}, samples {}, sample period {:.4} ms, crossings {}, vibrato rate {:.4} Hz".format(
      total_time, last_crossing_samples, total_time * 1000 / last_crossing_samples, crossings, crossings / (total_time * 2)))

os.remove(TEMP_SOUND)
os.remove(TEMP_DATA)

Example output:

DanMBPE:Vibrato griscom$ python ./vibrato.py
Recording for 2 seconds...
   ... done. Analyzing...
total time 1.84, samples 317, sample period 5.805 ms, crossings 17, vibrato rate 4.619 Hz
DanMBPE:Vibrato griscom$

Suggestions welcome!

$\endgroup$
  • $\begingroup$ The audio-to-pitch algorithm has a full-bandwidth audio signal going in ($f_s=$ 48 kHz) and a sub-audio signal representing pitch coming out (the sample rate can be perhaps 1 kHz or 500 Hz a little more than your 166 Hz). That pitch signal can be passed through complementary band-splitting filters, a HPF output that will have vibrato and very fast trills (this HPF has a linear gain of 0 at DC) and such and the complementary LPF output would have the basic pitch signal without the vibrato and trills. the HPF and LPF should be aligned in delay so that their outputs add to being a wire. $\endgroup$ – robert bristow-johnson Nov 11 '17 at 23:41
  • $\begingroup$ Noting that the aubiopitch tool is a black box, and assuming that it works as well as is necessary, how would this help? $\endgroup$ – Daniel Griscom Nov 12 '17 at 0:42
  • 1
    $\begingroup$ well, you can still get your pitch output at 167 Hz sample rate. at some frequency lower than 4 Hz, you need to decide what the low end of vibrato is and what is the high end if the remaining pitch contour is. let's say it's 3.5 Hz (i would hate to see it lower than that, but 4.6 Hz vibrato seems a little slow). so, with a HPF with sample rate of 167 Hz and 3.5 Hz cutoff, you can split your vibrato off from the rest of the pitch contour. then, when your HPF signal shows periodicity, you can determine the period of that with a similar pitch detect. $\endgroup$ – robert bristow-johnson Nov 12 '17 at 0:50
  • $\begingroup$ I'm still not clear about what deficiencies in my algorithm would be improved by your suggestions. Is it that it could find a vibrato section in a longer held tone? (And yes, 4.6Hz is slow; it's me....) $\endgroup$ – Daniel Griscom Nov 12 '17 at 14:25
  • 1
    $\begingroup$ it just seems to me useful to separate vibrato from other movement of the pitch contour if you're gonna be automatically extracting a parameter from that vibrato. you also want your automatic algorithm to have an idea whether vibrato is present or not. $\endgroup$ – robert bristow-johnson Nov 12 '17 at 19:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.