Nyquist FFT and Inverse FFT Tutorial

# 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 files `demos/src/lpcdemo.lsp` and `demos/src/lpcdemo.sal` in 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.

The code here is written in SAL syntax, with LISP syntax following in fine print.

## Preliminaries

Before using LPC functions, you must load the LPC library:

    load "lpc.lsp"

    (load "lpc")

At the top of `lpcdemo.lsp`, you see the following lines:

    variable *lpc-path* = current-path()
    variable *lpc-data* = strcat(*lpc-path*, "lpc-exmpl.dat")

    (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.sal or 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:

    function do-lpc-analysis()
      begin
        with myfile = strcat(*lpc-path*, "demo-snd.aiff")
        exec save-lpc-file(make-lpanal-iterator(s-read(myfile), 0.08, 0.04, 50),
                       "temp.dat")
      end

    (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:

    function ex-1()
      exec show-lpc-data(make-lpc-file-iterator(*lpc-data*), 100, 120, NIL)

    (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:

    set iterator = make-lpc-file-iterator(*lpc-data*)
    exec send(iterator, :next)

    (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`:

    functioni 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)))

    (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:

    set a-lpcdata = list(63.2144, 0.674387, 0.103287,
        vector(-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))

    set e-lpcdata = list(40.7157, 0.149753, 0.0606467,
        vector(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))

    set i-lpcdata = list(5.5391, 0.0321825, 0.0762238,
        vector(-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))

    (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:

    function noise-vocal(lpcdata, dur)
      return allpoles-from-lpc(noise(dur), lpcdata)

    (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:

    function ex-5()
      play seq(noise-vocal(a-lpcdata, 1),
               noise-vocal(e-lpcdata, 1),
               noise-vocal(i-lpcdata, 1))

    (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:

    function buzz-vocal(lpcdata, dur)
      return allpoles-from-lpc(buzz, 16, e2, const(0.0, dur),
                               lpcdata)

    (defun buzz-vocal (lpcdata dur)
      (allpoles-from-lpc (buzz 16 e2 (const 0.0 dur))
                         lpcdata))

    function ex-6()
      play seq(buzz-vocal(a-lpcdata, 1),
               buzz-vocal(e-lpcdata, 1)
               buzz-vocal(i-lpcdata, 1))

    (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:

    function ex-7a()
      ;; parameters are sound, lpc-iterator, skiptime
      return lpreson(noise(6.5),
                     make-lpc-file-iterator(*lpc-data*), 0.04)

    function ex-7()
      play ex-7a()

    (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:

    function ex-8a(p, dur)
      return lpreson(buzz(16, p, 1.5 * lfo(3.0, dur)),
                     make-lpc-file-iterator(*lpc-data*), 0.04)

    function 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)))

    (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.