Nyquist LPC Analysis and Synthesis

Nyquist provides functions for LPC analysis and synthesis on streams of audio data. For analysis, the audio stream is broken into successive frames, each of which is analyzed, resulting in a small set of coefficients. For synthesis, these frames control a time-varying filter which can be applied to an audio signal.

As with FFT frames, Nyquist does not have a special data type corresponding to a sequence of LPC frames. Instead, a sequence of frames is represented by an XLISP object. Whenever you send the selector :next to the object, you get back either NIL, indicating the end of the sequence, or you get an LPC frame. (Note that FFT frames and LPC frames are not compatible.)

This tutorial is based on the file lpcdemo.lsp in the demos folder fo the Nyquist release. Not every line is covered, but this tutorial should give you a pretty full explanation of how to use LPC in Nyquist.

Preliminaries

Before using LPC functions, you must load the LPC library:
(load "lpc")
At the top of lpcdemo.lsp, you see the following lines:
(setf *lpc-path* (current-path))
(setf *lpc-data* (strcat *lpc-path* "lpc-exmpl.dat"))
The (current-path) function call returns the full file system path to the file being loaded (lpcdemo.lsp). We save this into *lpc-path* so we can find other related files later, even if the current directory is changed. *lpc-data* is one of the file names we will use later.

LPC Analysis

Another file we want is a-snd-file.snd, a very short vocal sound that is part of the Nyquist release (it is also used by examples.lsp). To get this file, we concatenate it with the path we obtained above. The resulting full path can be passed to s-read to read the sound. Find the expressions in the following function that build a file name and read the file as a sound:
(defun do-lpc-analysis ()
  (let ((myfile (strcat *lpc-path* "a-snd-file.snd")))
    (save-lpc-file (make-lpanal-iterator (s-read *myfile*) 0.08 0.04 50)
                   "temp.dat")))
You can analyze a sound using make-lpanal-iterator. This function takes a sound, a frame size, a step size, and the number of poles. In do-lpc-analysis (above), we used 0.08 seconds, 0.04 seconds, and 50 poles for these parameters. The result of make-lpanal-iterator is an object that will deliver LPC frames on demand. In this function, we will grab all of the frames and write them to a file using save-lpc-file. The data is written to "temp.dat". You should run this function and look at the file, which contains ASCII data. Later, we will use this data to filter a sound.

Since a-snd-file.snd is a very short and uninteresting sound, we will use a different set of LPC frames in the following synthesis examples. The data is in lpc-exmpl.dat, and the full path for this file was computed earlier and stored in *lpc-data*.

The first example will just show how to read the LPC data file using make-lpc-file-iterator, which takes the file name as a parameter. You can print some frame data using show-lpc-data as shown in the following:

(defun ex-1 () 
  (show-lpc-data (make-lpc-file-iterator *lpc-data*) 100 120 NIL))
The additional parameters to show-lpc-data are the starting frame (100), the ending frame (120), and a flag (NIL)  indicating whether to print filter coefficients. If you want to look a little closer at the inner workings of this code, you can send a :next message to an LPC iterator object as follows:
(setf iterator (make-lpc-file-iterator *lpc-data*))
(send iterator :next)
This will return the first frame of the LPC analysis. Send :next again  to get the next frame.

LPC Synthesis With Fixed All-Pole Filter

The next example creates a sequence of vowel sounds. The vowel filters are based on the
30th, 60th, and 100th frames taken from the file lpc-exmpl.dat. We use make-lpc-file-iterator as before to read the LPC frames from the file, but we use nth-frame to get the desired frames.

The function allpoles-from-lpc constructs a filter from the frame and applies it to a sound. In this case, the source sound is created by buzz with a little vibrato provided by lfo:
 

(defun ex-4 () 
  (play (seq
          (allpoles-from-lpc (buzz 12 f2 (lfo 5.0 4.0)) 
                             (nth-frame (make-lpc-file-iterator *lpc-data*)
                                        30))
          (allpoles-from-lpc (buzz 12 f2 (lfo 5.1 4.0)) 
                             (nth-frame (make-lpc-file-iterator *lpc-data*)
                                        60))
          (allpoles-from-lpc (buzz 12 g2 (lfo 5.3 4.0))  
                             (nth-frame (make-lpc-file-iterator *lpc-data*)
                                        100)) )))
Rather than iterate through a file to find the desired frame, you can also just store the desired frames as in the following:
(setf a-lpcdata
  '(63.2144 0.674387 0.103287
    #(-0.0381026 0.00804115 0.0109905 0.0145117 0.00199174 
      -0.00129314 0.0171826 0.0181176 0.00179391 -0.0114089 
      -0.0120949 -0.000410595 -0.0122539 -0.0209354 -0.00804976 
      -0.00345041 -0.00409532 -0.00227011 0.014224 0.0135451
      0.0056023 -0.00651142 -0.00564953 -0.0168921 -0.0377939
      -0.0449506 -0.0355592 -0.0339316 -0.0454434 1.19336)))
(setf e-lpcdata
  '(40.7157 0.149753 0.0606467
    #(0.0244574 -0.0225545 -0.0172724 -0.0122709 -0.0042946
      0.00886974 0.0121516 0.0120936 0.00197545 -0.00582163
      -0.018367 -0.0201546 -0.00440599 0.00638936 0.0166275
      0.0185066 0.00890464 -0.00158013 -0.00494974 -0.00479037
      0.0130814 0.0138648 -0.0022018 -0.021368 -0.0343532 
      -0.0312712 -0.0574975 -0.0918824 -0.112016 1.31398)))
(setf i-lpcdata
  '(5.5391 0.0321825 0.0762238 
    #(-0.0341124 -0.0149688 -0.00585657 -0.0111572 0.00769712
      0.0190367 0.00885366 0.0112762 0.0118286 -0.00059044 
      -0.0140864 -0.0123688 -0.0151128 0.00214354 -0.00810219 
      -0.00538188 0.00631382 0.020771 0.0356498 0.0295531
      0.0242797 0.0124296 0.00445127 -0.013062 -0.0387178 
      -0.0527783 -0.0685511 -0.076575 -0.0846335 1.24521)))
The following function applies a filter to noise:
(defun noise-vocal (lpcdata dur)
  (allpoles-from-lpc (noise dur) lpcdata))
Combining this with our definitions of different frames, we can write a little sequence
of vowel sounds:
(defun ex-5 ()
  (play
    (seq (noise-vocal a-lpcdata 1)
         (noise-vocal e-lpcdata 1)
         (noise-vocal i-lpcdata 1))))
We can do the same thing using a buzz sound rather than noise:
(defun buzz-vocal (lpcdata dur)
  (allpoles-from-lpc (buzz 16 e2 (const 0.0 dur))
                     lpcdata))
(defun ex-6 ()
  (play
     (seq (buzz-vocal a-lpcdata 1)
          (buzz-vocal e-lpcdata 1)
          (buzz-vocal i-lpcdata 1))))

Time-Varying LPC Filter

The most interesting LPC effect is to use a sequence of frames to drive a time-varying all-pole filter. If the frames were analyzed from a vocal source, then the resulting filter will transfer the speech articulation (or at least some of it) to another sound. The time-varying LPC filter is called lpreson.

Here, the LPC data from *lpc-data* is used to modulate noise:

(defun ex-7a ()
  ;; parameters are sound, lpc-iterator, skiptime
  (lpreson (noise 6.5) (make-lpc-file-iterator *lpc-data*) 0.04))
(defun ex-7 ()
  (play (ex-7a)))
The same thing can be done to filter a buzz sound. This example generates some vocal-like sounds in two-part harmony:
(defun ex-8a (p dur)
  (lpreson (buzz 16 p (scale 1.5 (lfo 3.0 dur))) 
           (make-lpc-file-iterator *lpc-data*)
           0.04))

(defun ex-8 ()
  (play
    (sim (seq (ex-8a c4 1) (ex-8a g3 1) (ex-8a a3 1) (ex-8a b3 1) (ex-8a c4 1))
         (seq (ex-8a c2 2) (ex-8a f2 1) (ex-8a g2 1) (ex-8a e2 1)))))

Note that you can change the skiptime parameter to lpreson to change the rate at which the filter moves through the frames. The result is to caues the speech articulations to go faster or slower.

More examples can be found in lpcdemo.lsp.

Acknowledgments

Pedro J. Morales created the LPC functions for Nyquist and wrote lpcdemo.lsp on which this tutorial is based.