Previous Section | Next Section | Table of Contents | Index | Title Page

Developing and Debugging in Nyquist

There are a number of tools, functions, and techniques that can help to debug Nyquist programs. Since these are described in many places throughout this manual, this chapter brings together many suggestions and techniques for developing code and debugging. You really should read this chapter before you spend too much time with Nyquist. Many problems that you will certainly run into are addressed here.

Debugging

Probably the most important debugging tool is the backtrace. There are two kinds of backtrace: one for SAL, and one for Lisp.

SAL mode is actually just an XLISP function (sal) that reads input and evaluates it. When SAL encounters an error, it normally prints a trace of the SAL stack (all the active functions written in SAL), exists the current command, and reads the next command.

If you call XLISP functions from SAL, including most Nyquist sound processing functions, and an error occurs within these XLISP functions, you will only see the SAL function that called the XLISP functions listed in the stack trace. Sometimes you need more details.

When Nyquist encounters an error when it is not running SAL, it normally suspends execution and prints an error message. To find out where in the program the error occurred and how you got there, start by typing (bt). This will print out the last several function calls and their arguments, which is usually sufficient to see what is going on.

In order for (bt) to work, you must have a couple of global variables set: *tracenable* is ordinarily set to NIL. If it is true, then a backtrace is automatically printed when an error occurs; *breakenable* must be set to T, as it enables the execution to be suspended when an error is encountered. If *breakenable* is NIL (false), then execution stops when an error occurs but the stack is not saved and you cannot get a backtrace. Finally, bt is just a macro to save typing. The actual backtrace function is baktrace, which takes an integer argument telling how many levels to print. All of these things are set up by default when you start Nyquist.

To get this XLISP backtrace behavior when SAL encounters an error, you need to have *breakenable* set while SAL is running. The best way to do this is to run within the NyquistIDE program, open the Preferences dialog, and choose the desired settings, e.g. “Enable XLISP break on SAL error.”

Since Nyquist sounds are executed with a lazy evaluation scheme, some errors are encountered when samples are being generated. In this case, it may not be clear which expression is in error. Sometimes, it is best to explore a function or set of functions by examining intermediate results. Any expression that yields a sound can be assigned to a variable and examined using one or more of: s-plot, snd-print-tree, and of course play. The snd-print-tree function prints a lot of detail about the inner representaion of the sound. Keep in mind that if you assign a sound to a global variable and then look at the samples (e.g. with play or s-plot), the samples will be retained in memory. At 4 bytes per sample, a big sound may use all of your memory and cause a crash.

Another technique is to use low sample rates so that it is easier to plot results or look at samples directly. The calls:

set-sound-srate(100)
set-control-srate(100)

set the default sample rates to 100, which is too slow for audio, but useful for examining programs and results. The function

snd-samples(sound, limit)

will convert up to limit samples from sound into a Lisp array. This is another way to look at results in detail.

The trace function is sometimes useful. It prints the name of a function and its arguments everytimg the function is called, and the result is printed when the function exits. To trace the osc function, type:

trace(osc)

and to stop tracing, type untrace(osc).

If a variable needs a value or a function is undefined, and if *breakenable* was set, you will get a prompt where you can fix the error (by setting the variable or loading the function definition) and keep going. At the debug (or break) prompt, your input must be in XLISP, not SAL syntax. Use (co), short for (continue) to reevaluate the variable or function and continue execution.

When you finish debugging a particular call, you can “pop” up to the top level by typing (top), a short name for (top-level). There is a button named "Top" in the NyquistIDE that takes you back to the top level (ready to accept XLISP expressions), and another button named "SAL" that puts you back in SAL mode.

Useful Functions

grindef(name) [SAL]
(grindef name) [LISP]
Prints a formatted listing of a lisp function. This is often useful to quickly inspect a function without searching for it in source files. Do not forget to quote the name, e.g. (grindef 'prod).

args(name) [SAL]
(args name) [LISP]
Similar to grindef, this function prints the arguments to a function. This may be faster than looking up a function in the documentation if you just need a reminder. For example, (args 'lp) prints “(LP S C),” which may help you to remember that the arguments are a sound (S) followed by the cutoff (C) frequency.

The following functions are useful short-cuts that might have been included in XLISP. They are so useful that they are defined as part of Nyquist.

incf(symbol) [SAL]
(incf symbol) [LISP]
Increment symbol by one. This is a macro, and symbol can be anything that can be set by setf. Typically, symbol is a variable: “(incf i),” but symbol can also be an array element: “(incf (aref myarray i)).”

decf(symbol) [SAL]
(decf symbol) [LISP]
Decrement symbol by one. (See incf, above.)

push(val, lis) [SAL]
(push val lis) [LISP]
Push val onto lis (a Lisp list). This is a macro that is equivalent to writing (in Lisp) (setf lis (cons val lis)).

pop(lis) [SAL]
(pop lis) [LISP]
Remove (pop) the first item from lis (a Lisp list). This is a macro that is equivalent to writing (in Lisp) (setf lis (cdr lis)). Note that the remaining list is returned, not the head of the list that has been popped. Retrieve the head of the list (i.e. the top of the stack) using first or, equivalently, car.

The following macros are useful control constructs.

while(test, expr1, expr2, ...) [SAL]
(while test expr1 expr2 ...) [LISP]
A conventional “while” loop. If test is true, evaluate expressions (expr1, expr2, etc.) and repeat. If test is false, return. This expression evaluates to NIL unless the expression (return expr) is evaluated, in which case the value of expr is returned. In SAL, the loop statement is preferred.

when(test, action) [SAL]
(when test action) [LISP]
A conventional “if-then” statement. If test is true, action is evaluated and returned. Otherwise, NIL is returned. (Use if or cond to implement “if-then-else” and more complex conditional forms.

It is often necessary to load a file only if it has not already been loaded. For example, the pianosyn library loads somewhat slowly, so if some other file already loaded it, it would be good to avoid loading it again. How can you load a file once? Nyquist does not keep track of files that are loaded, but you must be loading a file to define some function, so the idea is to tell Nyquist "I require function from file"; if the function does not yet exist, Nyquist satisfies the requirement by loading the file.

require-from(fnsymbol, filename [, path]) [SAL]
(require-from fnsymbol filename [path]) [LISP]
Tests whether fnsymbol, an unquoted function name, is defined. If not, filename, a STRING, is loaded. Normally fnsymbol is a function that will be called from within the current file, and filename is the file that defines fnsymbol. The path, if a STRING, is prepended to filename. If path is t (true), then the directory of the current file is used as the path.

Sometimes it is important to load files relative to the current file. For example, the lib/piano.lsp library loads data files from the lib/piano directory, but how can we find out the full path of lib? The solution is:

current-path() [SAL]
(current-path) [LISP]
Returns the full path name of the file that is currently being loaded (see load). Returns NIL if no file is being loaded.

Finally, there are some helpful math functions:

real-random(from, to) [SAL]
(real-random from to) [LISP]
Returns a random FLONUM between from and to. (See also rrandom, which is equivalent to (real-random 0 1)).

power(x, y) [SAL]
(power x y) [LISP]
Returns x raised to the y power.

Previous Section | Next Section | Table of Contents | Index | Title Page