NYQUIST REFERENCE MANUAL
Version 3.00
Copyright 2008 by Roger B. Dannenberg
7 January 2008
Carnegie Mellon University
School of Computer Science
Pittsburgh, PA 15213, U.S.A.
.
Preface
This manual is a guide for users of Nyquist, a language for composition and
sound synthesis. Nyquist grew out of a series of research projects, notably
the languages Arctic and Canon. Along with Nyquist, these languages promote a
functional style of programming and incorporate time into the language
semantics.
Please help by noting any errors, omissions, or suggestions you may have.
You can send your suggestions to Dannenberg@CS.CMU.EDU (internet) via computer
mail, or by campus mail to Roger B. Dannenberg, School of Computer Science, or
by ordinary mail to Roger B. Dannenberg, School of Computer Science, Carnegie
Mellon University, 5000 Forbes Ave., Pittsburgh, PA 15213-3890, USA.
Nyquist is a successor to Fugue, a language originally implemented by Chris
Fraley, and extended by George Polly and Roger Dannenberg. Peter Velikonja and
Dean Rubine were early users, and they proved the value as well as discovered
some early problems of the system. This led to Nyquist, a reimplementation of
Fugue by Roger Dannenberg with help from Joe Newcomer and Cliff Mercer. Ning Hu
ported Zheng (Geoffrey) Hua and Jim Beauchamp's piano synthesizer to Nyquist
and also built NyqIDE, the Nyquist Interactive Development Environment for
Windows. Dave Mowatt contributed the original version of jNyqIDE, the
cross-platform interactive development environment. Dominic Mazzoni made a
special version of Nyquist that runs within the Audacity audio editor, giving
Nyquist a new interface and introducing Nyquist to many new users.
Many others have since contributed to Nyquist. Chris Tchou and Morgan Green
worked on the Windows port. Eli Brandt contributed a number of filters and
other synthesis functions. Pedro J. Morales, Eduardo Reck Miranda, Ann Lewis,
and Erich Neuwirth have all contributed nyquist examples found in the demos
folder of the Nyquist distribution. Philip Yam ported some synthesis functions
from Perry Cook and Gary Scavone's STK to Nyquist. Pedro Morales ported many
more STK instruments to Nyquist. Dave Borel wrote the Dolby Pro-Logic encoding
library and Adam Hartman wrote stereo and spatialization effects. Stephen
Mangiat wrote the MiniMoog emulator. Phil Light recorded the drum samples and
wrote drum machine software. The Xmusic library, particularly the pattern
specification, was inspired by Rick Taube's Common Music. The functions for
generating probability distributions were implemented by Andreas Pfenning.
Starting with Version 3, Nyquist supports a version of SAL, providing an
alternative to Lisp syntax. SAL was designed by Rick Taube, and the SAL
implementation in Nyquist is based on Taube's original implementation as part
of his Common Music system.
The current jNyqIDE includes contributions from many. Chris Yealy and Derek
D'Souza implemented early versions of the envelope editor. Daren Makuck and
Michael Rivera wrote the original equalizer editor. Priyanka Raghavan
implemented the sound browser.
Many others have made contributions, offered suggestions, and found bugs. If
you were expecting to find your name here, I apologize for the omission, and
please let me know.
I also wish to acknowledge support from CMU, Yamaha, and IBM for this work.
.
1. Introduction and Overview
Nyquist is a language for sound synthesis and music composition. Unlike
score languages that tend to deal only with events, or signal processing
languages that tend to deal only with signals and synthesis, Nyquist handles
both in a single integrated system. Nyquist is also flexible and easy to use
because it is based on an interactive Lisp interpreter.
With Nyquist, you can design instruments by combining functions (much as you
would using the orchestra languages of Music V, cmusic, or Csound). You can
call upon these instruments and generate a sound just by typing a simple
expression. You can combine simple expressions into complex ones to create a
whole composition.
Nyquist runs under Linux, Apple Mac OS X, Microsoft Windows NT, 2000, XP, and
Vista, and it produces sound files or directly generates audio. Recent
versions have also run on AIX, NeXT, SGI, DEC pmax, and Sun Sparc machines.
(Makefiles for many of these are included, but out-of-date). Let me know if
you have problems with any of these machines.
To use Nyquist, you should have a basic knowledge of Lisp. An excellent text
by Touretzky is recommended [Touretzky 84]. Appendix IV is the reference
manual for XLISP, of which Nyquist is a superset. Starting with Version 3,
Nyquist supports a variant of SAL, which is also available in Common Music.
Since there are some differences, one should generally call this implementation
``Nyquist SAL;'' however, in this manual, I will just call it ``SAL.'' SAL
offers most of the capabilities of Lisp, but it uses an Algol-like syntax that
may be more familiar to programmers with experience in Java, C, Basic, etc.
1.1. Installation
Nyquist is a C program intended to run under various operating systems
including Unix, MacOS, and Windows.
1.1.1. Unix Installation
For Unix systems, Nyquist is distributed as a compressed tar file named
nyqsrc2nn.zip, where nn is the version number (e.g. v2.36 was in
nyqsrc236.zip). To install Nyquist, copy nyqsrc2nn.zip to the directory on
your machine where you would like to install Nyquist, and type:
gunzip nyqsrc2nn.zip
cd nyquist
ln -s sys/unix/linux/Makefile Makefile
setenv XLISPPATH `pwd`/runtime:`pwd`/lib
make
The first line creates a nyquist directory and some subdirectories. The second
line (cd) changes directories to the new nyquist directory. The third line
makes a link from the top-level directory to the Makefile for your system. In
place of linux in sys/unix/linux/Makefile, you should substitute your system
type. Current systems are next, pmax, rs6k, sgi, linux, and sparc. The setenv
command tells Nyquist where to search for lisp files to be loaded when a file
is not found in the current directory. The runtime directory should always be
on your XLISPPATH when you run Nyquist, so you may want to set XLISPPATH in
your shell startup file, e.g. .cshrc. Assuming the make completes
successfully, you can run Nyquist as follows:
./ny
When you get the prompt, you may begin typing expressions such as the ones in
the following ``Examples'' section.
One you establish that Nyquist (ny) is working from the command line, you
should try using jNyqIDE, the Java-based Nyquist development environment.
First, make jny executable (do this only once when you install Nyquist):
chmod +x jny
Then try running jNyqIDE by typing:
./jny
If the jNyqIDE window does not appear, make sure you have Java installed (if
not, you probably already encountered errors when you ran make). You can also
try recompiling the Java files:
cd jnyqide
javac *.java
cd ..
Note: With Linux and the Macintosh OS X, jNyqIDE defines the environment
passed to Nyquist. If you set XLISPPATH as shown above, it will be passed along
to Nyquist under jNyqIDE. If not, a default XLISPPATH will have the lib and
runtime directories only. This does not apply to Windows because even though
the environment is there, the Windows version of Nyquist reads the XLISPPATH
from the Registry.
You can also specify the search path by creating the file nyquist/xlisppath,
which should have colon-separated paths on a single line of text. This file
will override the environment variable XLISPPATH.
It is good to have USER in the environment with your user ID. This string is
used to construct some file names. jNyqIDE will look for it in the environment.
You can also specify your user ID using the file nyquist/user, but if you have
a shared installation of Nyquist, this will not be very useful.
Note: Nyquist looks for the file init.lsp in the current directory. If you
look in the init.lsp in runtime, you will notice two things. First, init.lsp
loads nyquist.lsp from the Nyquist directory, and second, init.lsp loads
system.lsp which in turn defines the macro play. You may have to modify
system.lsp to invoke the right programs on your machine.
1.1.2. Win32 Installation
The Win32 version of Nyquist is packaged in three versions: the source
version and two runtime versions. The source version is a superset of the
runtime version intended for developers who want to recompile Nyquist. The
source version exists as a .zip file, so you need a utility like WinZip to
unpack them. The URL http://www.winzip.com/ has information on this product.
Typically, the contents of the zip file are extracted to the C:\nyquist
directory, but you can put it anywhere you like. You can then open the
workspace file, nyquist.sln, using Microsoft Visual C++. You can build and run
the command line and the NyqWin versions of Nyquist from within Visual C++.
The runtime versions contain everything you need to run Nyquist, including
the executable, examples, and documentation. Each runtime version is packaged
as an executable installer program. I recommend setupnyqiderun2xx.exe (``2xx''
refers to the current version number), a graphical interface written in Java
that runs nyquist.exe as a separate process. This IDE has a simple lisp editor
built in. Alternatively, you can install setupnyqwinrun2xx.exe, a different
graphical interface written in C++. Just copy the installer you want to your
system and run it. Then find Nyquist in your Start menu to run it. You may
begin typing expressions such as the ones in the following ``Examples''
section.
Optional: Nyquist needs to know where to find the standard runtime files. The
location of runtime files must be stored in the Registry. The installers
create a registry entry, but if you move Nyquist or deal with different
versions, you can edit the Registry manually as follows:
- Run the Registry editor. Under Windows NT, run
C:\WINNT\system32\regedt32.exe. Under Windows95, run
C:\WINDOWS\regedit.exe.
- Find and highlight the SOFTWARE key under HKEY_LOCAL_MACHINE.
- Choose Add key ... from the Edit menu, type CMU, and click the OK
button.
- Highlight the new CMU key.
- Choose Add key ... from the Edit menu, type Nyquist, and click the OK
button. (Note that CMU and Nyquist are case sensitive.)
- Highlight the new Nyquist key.
- Choose Add value ... from the Edit menu, type XLISPPATH, and click
the OK button. (Under WinXP the menu item is Edit:New:String Value,
after which you need to select the new string name that appears in
the right panel, select Edit:Rename, and type XLISPPATH.)
- In the String Edit box (select the Edit:Modify menu item in WinXP),
type a list of paths you want Nyquist to search for lisp files. For
example, if you installed Nyquist as C:\nyquist, then type:
C:\nyquist\runtime,C:\nyquist\lib
The paths should be separated by a comma or semicolon and no space.
The runtime path is essential, and the lib path may become essential
in a future release. You can also add paths to personal libraries of
Lisp and Nyquist code.
- Click the OK button of the string box and exit from the Registry
Editor application.
1.1.2.1. What if Nyquist functions are undefined?
If you do not have administrative privileges for your machine, the installer
may fail to set up the Registry entry that Nyquist uses to find initialization
files. In this case, Nyquist will run a lisp interpreter, but many Nyquist
functions will not be defined. If you can log in as administrator, do it and
reinstall Nyquist. If you do not have permission, you can still run Nyquist as
follows:
Create a file named init.lsp in the same directory as Nyquist.exe (the
default location is C:\Program Files\Nyquist, but you may have installed it in
some other location.) Put the following text in init.lsp:
(setf *search-path* "C:/Program Files/Nyquist/runtime,C:/Program Files/
(load "C:/Program Files/Nyquist/runtime/init.lsp")
Note: in the three places where you see C:/Program Files/Nyquist, insert the
full path where Nyquist is actually installed. Use forward slashes (/) rather
than back slashes (\) to separate directories. For example, if Nyquist is
installed at D:\rbd\nyquist, then init.lsp should contain:
(setf *search-path* "D:/rbd/nyquist/runtime,D:/rbd/nyquist/lib")
(load "d:/rbd/nyquist/runtime/init.lsp")
The variable *search-path*, if defined, is used in place of the registry to
determine search paths for files.
1.1.2.2. SystemRoot
(Ignore this paragraph if you are not planning to use Open Sound Control
under Windows.) If Nyquist prints an error message and quits when you enable
Open Sound Control (using osc-enable), check to see if you have an environment
variable SystemRoot, e.g. type set to a command prompt and look for the value
of SystemRoot. The normal value is C:\windows. If the value is something else,
you should put the environment entry, for example:
SystemRoot="D:\windows"
into a file named systemroot (no extension). Put this file in your nyquist
directory. When you run jNyqIDE, it will look for this file and pass the
contents as an environment variable to Nyquist. The Nyquist process needs this
to open a UDP socket, which is needed for Open Sound Control.
1.1.3. MacOS X Installation
The OS X version of Nyquist is very similar to the Linux version, but it is
developed using Xcode, Apple's programming environment. With a little work, you
can use the Linux installation instructions to compile Nyquist, but it might be
simpler to just open the Xcode project that is included in the Nyquist sources.
You can also download a pre-compiled version of Nyquist for the Mac. Just
download nyqosx2xx.tgz to the desktop and open it to extract the folder
nyqosx2xx. (Again, "2xx" refers to the current version number, e.g.
v2.31 would be named with "231".) Open the folder to find a Mac Application
named jNyqIDE and a directory named nyquist/doc. Documentation is in
the nyquist/doc directory.
The file jNyqIDE.app/Contents/Resources/Java/ny is the command line
executable (if you should need it). To run from the command line, you will need
to set the XLISPPATH environment variable as with Linux. On the topic of the
XLISPPATH, note that this variable is set by jNyqIDE when running with that
application, overriding any other value. You can extend the search path by
creating the file xlisppath in the same directory as the nyquist executable ny.
The xlisppath file should have colon-separated paths on a single line of text.
1.2. Helpful Hints
Under Win95 and Win98, the console sometimes locks up. Activating another
window and then reactivating the Nyquist window should unlock the output. (We
suggest you use JNyqIDE, the interactive development environment rather than a
console window.)
You can cut and paste text into Nyquist, but for serious work, you will want
to use the Lisp load command. To save even more time, write a function to load
your working file, e.g. (defun l () (load "myfile.lsp")). Then you can type (l)
to (re)load your file.
Using SAL, you can type
define function l () load "myfile.lsp"
and then
exec l()
to (re)load the file.
The Emacs editor is free GNU software and will help you balance parentheses
if you use Lisp mode. Also, the NyqIDE and jNyqIDE versions have built-in lisp
editors. If your editor does not help you balance parentheses, you may find
yourself counting parens and searching for unbalanced expressions. If you are
desparate, type (file-sexprs) and type the lisp file name at the prompt. This
function will read and print expressions from the file, reporting an error when
an extra paren or end-of-file is reached unexpectedly. By looking at the last
expression printed, you can at least tell where the unbalanced expression
starts. Alternatively, try the verbose mode of the load command.
1.3. Using jNyqIDE
The program named jNyqIDE is an ``integrated development environment'' for
Nyquist. When you run jNyqIDE, it starts the Nyquist program and displays all
Nyquist output in a window. jNyqIDE helps you by providing a Lisp and SAL
editor, hints for command completion and function parameters, some graphical
interfaces for editing envelopes and graphical equalizers, and a panel of
buttons for common operations. A more complete description of jNyqIDE is in
Chapter 2.
For now, all you really need to know is that you can enter Nyquist commands
by typing into the upper left window. When you type return, the expression you
typed is sent to Nyquist, and the results appear in the window below. You can
edit files by clicking on the New File or Open File buttons. After editing some
text, you can load the text into Nyquist by clicking the Load button. jNyqIDE
always saves the file first; then it tells Nyquist to load the file. You will
be prompted for a file name the first time you load a new file.
1.4. Using SAL
SAL mode means that Nyquist reads and evaluates SAL commands rather than
Lisp. The SAL mode prompt is "SAL> " while the Lisp mode prompt is "> ". When
Nyquist starts it normally enters SAL mode automatically, but certain errors
may exit SAL mode. You can reenter SAL mode by typing the Lisp expression
(sal).
In SAL mode, you type commands in the SAL programming language. Nyquist reads
the commands, compiles them into Lisp, and evaluates the commands. Commands can
be entered manually by typing into the upper left text box in jNyqIDE.
1.5. Using Lisp
Lsip mode means that Nyquist reads and evaluates Nyquist expressions in Lisp
syntax. Since Nyquist is build on a Lisp interpreter, this is the ``native'' or
machine language of Nyquist, and certain errors and functions may break out of
the SAL interpreter, leaving you with a prompt for a Lisp expression.
Alternatively, you can exit SAL simply by typing exit to get a Lisp prompt (>
). Commands can be entered manually by typing into the upper left text box in
jNyqIDE.
1.6. Examples
We will begin with some simple Nyquist programs. Throughout the manual, we
will assume SAL mode and give examples in SAL, but it should be emphasized that
all of these examples can be performed using Lisp syntax. See Section 6.2 on
the relationship between SAL and Lisp.
Detailed explanations of the functions used in these examples will be
presented in later chapters, so at this point, you should just read these
examples to get a sense of how Nyquist is used and what it can do. The details
will come later. Most of these examples can be found in the file
nyquist/demos/examples.sal. Corresponding Lisp syntax examples are in the file
nyquist/demos/examples.lsp.
Our first example makes and plays a sound:
;; Making a sound.
play osc(60) ; generate a loud sine wave
This example is about the simplest way to create a sound with Nyquist. The osc
function generates a sound using a table-lookup oscillator. There are a number
of optional parameters, but the default is to compute a sinusoid with an
amplitude of 1.0. The parameter 60 designates a pitch of middle C. (Pitch
specification will be described in greater detail later.) The result of the
osc function is a sound. To hear a sound, you must use the play command, which
plays the file through the machine's D/A converters. It also writes a
soundfile in case the computation cannot keep up with real time. You can then
(re)play the file by typing:
exec r()
This (r) function is a general way to ``replay'' the last thing written by
play.
15
Note: when Nyquist plays a sound, it scales the signal by 2 -1 and (by
default) converts to a 16-bit integer format. A signal like (osc 60), which
ranges from +1 to -1, will play as a full-scale 16-bit audio signal.
1.6.1. Waveforms
Our next example will be presented in several steps. The goal is to create a
sound using a wavetable consisting of several harmonics as opposed to a simple
sinusoid. In order to build a table, we will use a function that computes a
single harmonic and add harmonics to form a wavetable. An oscillator will be
used to compute the harmonics.
The function mkwave calls upon build-harmonic to generate a total of four
harmonics with amplitudes 0.5, 0.25, 0.125, and 0.0625. These are scaled and
added (using +) to create a waveform which is bound temporarily to *table*.
A complete Nyquist waveform is a list consisting of a sound, a pitch, and T,
indicating a periodic waveform. The pitch gives the nominal pitch of the
sound. (This is implicit in a single cycle wave table, but a sampled sound may
have many periods of the fundamental.) Pitch is expressed in half-steps, where
middle C is 60 steps, as in MIDI pitch numbers. The list of sound, pitch, and
T is formed in the last line of mkwave: since build-harmonic computes signals
with a duration of one second, the fundamental is 1 Hz, and the hz-to-step
function converts to pitch (in units of steps) as required.
define mkwave()
set *table* = 0.5 * build-harmonic(1.0, 2048) +
0.25 * build-harmonic(2.0, 2048) +
0.125 * build-harmonic(3.0, 2048) +
0.0625 * build-harmonic(4.0, 2048)
set *table* = list(*table*, hz-to-step(1.0), #t)
Now that we have defined a function, the last step of this example is to
build the wave. The following code calls mkwave the first time the code is
executed (loaded from a file). The second time, the variable *mkwave* will be
true, so mkwave will not be invoked:
if not boundp(quote(*mkwave*)) then
exec mkwave()
set *mkwave* = #t
1.6.2. Wavetables
When Nyquist starts, several waveforms are created and stored in global
variables for convenience. They are: *sine-table*, *saw-table*, and
*tri-table*, implementing sinusoid, sawtooth, and triangle waves, respectively.
The variable *table* is initialized to *sine-table*, and it is *table* that
forms the default wave table for many Nyquist oscillator behaviors. If you want
a proper, band-limited waveform, you should construct it yourself, but if you
do not understand this sentence and/or you do not mind a bit of aliasing, give
*saw-table* and *tri-table* a try.
Note that in Lisp and SAL, global variables often start and end with
asterisks (*). These are not special syntax, they just happen to be legal
characters for names, and their use is purely a convention.
1.6.3. Sequences
Finally, we define note to use the waveform, and play several notes in a
simple score:
define function note(pitch, dur)
return osc(pitch, dur, *table*)
play seq(note(c4 i), note(d4 i), note(f4 i),
note(g4 i), note(d4 q))
Here, note is defined to take pitch and duration as parameters; it calls osc to
do the work of generating a waveform, using *table* as a wave table.
The seq function is used to invoke a sequence of behaviors. Each note is
started at the time the previous note finishes. The parameters to note are
predefined in Nyquist: c4 is middle C, i (for eIghth note) is 0.5, and q (for
Quarter note) is 1.0. See Section 1.7 for a complete description. The result
is the sum of all the computed sounds.
Sequences can also be constructed using the at transformation to specify time
offsets. See sequence_example.htmdemos, sequence for more examples and
explanation.
1.6.4. Envelopes
The next example will illustrate the use of envelopes. In Nyquist, envelopes
are just ordinary sounds (although they normally have a low sample rate). An
envelope is applied to another sound by multiplication using the mult function.
The code shows the definition of env-note, defined in terms of the note
function in the previous example. In env-note, a 4-phase envelope is generated
using the env function, which is illustrated in Figure 1.
Figure 1: An envelope generated by the env function.
; env-note produces an enveloped note. The duration
; defaults to 1.0, but stretch can be used to change
; the duration.
;
define function env-note(p)
return note(p, 1.0) *
env(0.05, 0.1, 0.5, 1.0, 0.5, 0.4)
; try it out:
;
play env-note(c4)
While this example shows a smooth envelope multiplied by an audio signal, you
can also multiply audio signals to achieve what is often called ring
modulation. See the code and description in demos/scratch_tutorial.htm for an
interesting use of ring modulation to create ``scratch'' sounds.
In the next example, The stretch operator (~)is used to modify durations:
; now use stretch to play different durations
;
play seq(seq(env-note(c4), env-note(d4)) ~ 0.25,
seq(env-note(f4), env-note(g4)) ~ 0.5,
env-note(c4))
In addition to stretch, there are a number of transformations supported by
Nyquist, and transformations of abstract behaviors is perhaps the fundamental
idea behind Nyquist. Chapter 3 is devoted to explaining this concept, and
further elaboration can be found elsewhere [Dannenberg 89].
1.6.5. Piece-wise Linear Functions
It is often convenient to construct signals in Nyquist using a list of (time,
value) breakpoints which are linearly interpolated to form a smooth signal.
Envelopes created by env are a special case of the more general piece-wise
linear functions created by pwl. Since pwl is used in some examples later on,
we will take a look at pwl now. The pwl function takes a list of parameters
which denote (time, value) pairs. There is an implicit initial (time, value)
pair of (0, 0), and an implicit final value of 0. There should always be an
odd number of parameters, since the final value (but not the final time) is
implicit. Here are some examples:
; symetric rise to 10 (at time 1) and fall back to 0 (at time 2):
;
pwl(1, 10, 2)
; a square pulse of height 10 and duration 5.
; Note that the first pair (0, 10) overrides the default initial
; point of (0, 0). Also, there are two points specified at time 5:
; (5, 10) and (5, 0). (The last 0 is implicit). The conflict is
; automatically resolved by pushing the (5, 10) breakpoint back to
; the previous sample, so the actual time will be 5 - 1/sr, where
; sr is the sample rate.
;
pwl(0, 10, 5, 10, 5)
; a constant function with the value zero over the time interval
; 0 to 3.5. This is a very degenerate form of pwl. Recall that there
; is an implicit initial point at (0, 0) and a final implicit value of
; 0, so this is really specifying two breakpoints: (0, 0) and (3.5, 0):
;
pwl(3.5)
; a linear ramp from 0 to 10 and duration 1.
; Note the ramp returns to zero at time 1. As with the square pulse
; above, the breakpoint (1, 10) is pushed back to the previous sample.
;
pwl(1, 10, 1)
; If you really want a linear ramp to reach its final value at the
; specified time, you need to make a signal that is one sample longer.
; The RAMP function does this:
;
ramp(10) ; ramp from 0 to 10 with duration 1 + one sample period
;
; RAMP is based on PWL; it is defined in nyquist.lsp.
;
1.7. Predefined Constants
For convenience and readability, Nyquist pre-defines some constants, mostly
based on the notation of the Adagio score language, as follows:
- Dynamics Note: these dynamics values are subject to change.
lppp = -12.0 (dB)
lpp = -9.0
lp = -6.0
lmp = -3.0
lmf = 3.0
lf = 6.0
lff = 9.0
lfff = 12.0
dB0 = 1.00
dB1 = 1.122
dB10 = 3.1623
- Durations
s = Sixteenth = 0.25
i = eIghth = 0.5
q = Quarter = 1.0
h = Half = 2.0
w = Whole = 4.0
sd, id, qd, hd, wd = dotted durations.
st, it, qt, ht, wt = triplet durations.
- PitchesPitches are based on an A4 of 440Hz. To achieve a different
tuning, set *A4-Hertz* to the desired frequency for A4, and call
(set-pitch-names). This will recompute the names listed below with a
different tuning. In all cases, the pitch value 69.0 corresponds
exactly to 440Hz, but fractional values are allowed, so for example,
if you set *A4-Hertz* to 444 (Hz), then the symbol A4 will be bound
to 69.1567, and C4 (middle C), which is normally 60.0, will be
60.1567.
c0 = 12.0
cs0, df0 = 13.0
d0 = 14.0
ds0, ef0 = 15.0
e0 = 16.0
f0 = 17.0
fs0, gf0 = 18.0
g0 = 19.0
gs0, af0 = 20.0
a0 = 21.0
as0, bf0 = 22.0
b0 = 23.0
c1 ... b1 = 24.0 ... 35.0
c2 ... b2 = 36.0 ... 47.0
c3 ... b3 = 48.0 ... 59.0
c4 ... b4 = 60.0 ... 71.0
c5 ... b5 = 72.0 ... 83.0
c6 ... b6 = 84.0 ... 95.0
c7 ... b7 = 96.0 ... 107.0
c8 ... b8 = 108.0 ... 119.0
- Miscellaneous
ny:all = ``all the samples'' (i.e. a big number) = 1000000000
1.8. More Examples
More examples can be found in the directory demos, part of the standard
Nyquist release. In this directory, you will find the following and more:
- Gong sounds by additive synthesis(demos/pmorales/b1.lsp and
demos/mateos/gong.lsp
- Risset's spectral analysis of a chord (demos/pmorales/b2.lsp)
- Bell sounds (demos/pmorales/b3.lsp, demos/pmorales/e2.lsp,
demos/pmorales/partial.lsp, and demos/mateos/bell.lsp)
- Drum sounds by Risset (demos/pmorales/b8.lsp
- Shepard tones (demos/shepard.lsp and demos/pmorales/b9.lsp)
- Random signals (demos/pmorales/c1.lsp)
- Buzz with formant filters (demos/pmorales/buzz.lsp
- Computing samples directly in Lisp (using Karplus-Strong and physical
modelling as examples) (demos/pmorales/d1.lsp
- FM Synthesis examples, including bell, wood drum, brass sounds, tuba
sound (demos/mateos/tuba.lsp and clarinet sounds
(demos/pmorales/e2.lsp
- Rhythmic patterns (demos/rhythm_tutorial.htm
- Drum Samples and Drum Machine (demos/plight/drum.lsp.
2. The jNyqIDE Program
The jNyqIDE program combines many helpful functions and interfaces to help
you get the most out of Nyquist. jNyqIDE is implemented in Java, and you will
need the Java runtime system or development system installed on your computer
to use jNyqIDE. The best way to learn about jNyqIDE is to just use it. This
chapter introduces some of the less obvious features. If you are confused by
something and you do not find the information you need here, please contact the
author.
2.1. jNyqIDE Overview
The jNyqIDE runs the command-line version of Nyquist as a subtask, so
everything that works in Nyquist should work when using the jNyqIDE and
vice-versa. Input to Nyquist is usually entered in the top left window of the
jNyqIDE. When you type return, if the expression or statement appears to be
complete, the expression you typed is sent to Nyquist. Output from Nyquist
appears in a window below. You cannot type into or edit the output window text.
The normal way to use the jNyqIDE is to create or open one or more files. You
edit these files and then click the Load button. To load a file, jNyqIDE saves
the file, sets the current directory of Nyquist to that of the file, then
issues a load command to Nyquist. In this case and several others, you may
notice that jNyqIDE sends expressions to Nyquist automatically for evaluation.
You can always see the commands and their results in the output window.
Notice that when you load a selected file window, jNyqIDE uses setdir to
change Nyquist's current directory. This helps to keep the two programs in
sync. Normally, you should keep all the files of a project in the same
directory and avoid manually changing Nyquist's current directory (i.e. avoid
calling setdir in your code).
Arranging windows in the jNyqIDE can be time-consuming, and depending on the
operating system, it is possible for a window to get into a position where you
cannot drag it to a new position. The Window:Tile menu command can be used to
automatically lay out windows in a rational way. There is a preference setting
to determine the height of the completion list relative to the height of the
output window.
2.2. Command Completion
To help with programming, jNyqIDE maintains a command-completion window. As
you type the first letters of function names, jNyqIDE lists matching functions
and their parameters in the Completion List window. If you click on an entry in
this window, the displayed expression will replace the incompletely typed
function name. A preference allows you to match initial letters or any
substring of the complete function name. This is controlled by the ``Use full
search for code completion'' preference.
In addition, if you right click (or under Mac OS X, hold down the Alt/Option
key and click) on an entry, jNyqIDE will display documentation for the
function. Documentation can come from a local copy or from the online copy
(determined by the ``Use online manual instead of local copy'' preference).
Documentation can be displayed within the jNyqIDE window or in an external
browser (determined by the ``Use window in jNyqIDE for help browser''
preference.) Currently, the external browser option does not seem to locate
documentation properly, but this should be fixed in the future.
2.3. Browser
If you click on the Browse button or use the Window:Browse menu command,
jNyqIDE will display a browser window that is pre-loaded with a number of
Nyquist commands to create sounds. You can adjust parameters, audition the
sounds, and capture the expression that creates the sound. In many cases, the
expression checks to see if necessary functions are defined, loading files if
necessary before playing the sound. If you want to use a sound in your own
program, you can often simplify things by explicitly loading the required file
just once at the beginning of your file.
Since Nyquist now supports a mix of Lisp and SAL, you may find yourself in
the position of having code from the browser in one language while you are
working in the other. The best way to handle this is to put the code for the
sound you want into a function defined in a Lisp (.lsp) or SAL (.sal) file.
Load the file (from Lisp, use the sal-load command to load a SAL file), and
call the function from the language of your choice.
2.4. Envelope Editor
The envelope editor allows you graphically to design and edit piece-wise
linear and exponential envelopes. The editor maintains a list of envelopes and
you select the one to edit or delete using the drop down list in the Saved
Envelopes List area. The current envelope appears in the Graphical Envelope
Editor area. You can click to add or drag points. Alternatively, you can use
the Envelope Points window to select and edit any breakpoint by typing
coordinates. The duration of the envelope is controlled by the Stop field in
the Range area, and the vertical axis is controlled by the Min and Max fields.
When you click the Save button, all envelopes are written to Nyquist. You
can then use the envelope by treating the envelope name as a function. For
example, if you define an envelope named ``fast-attack,'' then you can create
the envelope within a Nyquist SAL program by writing the expression
fast-attack().
These edited envelopes are saved to a file named workspace.lsp in the current
directory. The workspace is Nyquist's mechanism for saving data of all kinds
(see Section 13.4.5). The normal way to work with workspaces is to (1) load the
workspace, i.e. load "workspace", as soon as you start Nyquist; (2) invoke the
envelope editor to change values in the workspace; and (3) save the workspace
at any time, especially before you exit jNyqIDE. If you follow these steps,
envelopes will be preserved from session to session, and the entire collection
of envelopes will appear in the editor. Be sure to make backups of your
workspace.lsp file along with your other project files.
The envelope editor can create linear and exponential envelopes. Use the Type
pull-down menu to select the type you want. Envelopes can be created using
default starting and ending values using pwl or pwe, or you can specify the
initial values using pwlv or pwev. The envelope editor uses pwl or pwe if no
point is explicitly entered as the initial or final point. To create a pwlv or
pwev function, create a point and drag it to the leftmost or rightmost edge of
the graphical editing window. You will see the automatically generated default
starting or ending point disappear from the graph.
Exponential envelopes should never decay to zero. If you enter a zero
amplitude, you will see that the envelope remains at zero to the next
breakpoint. To get an exponential decay to ``silence,'' try using an amplitude
of about 0.001 (about -60dB). To enter small values like this, you can type
them into the Amplitude box and click ``Update Point.''
The Load button refreshes the editor from data saved in the Nyquist process.
Normally, there is no need to use this because the editor automatically loads
data when you open it.
2.5. Equalizer Editor
The Equalizer Editor provides a graphical EQ interface for creating and
adjusting equalizers. Unlike the envelope editor, where you can type any
envelope name, equalizers are named eq-0, eq-1, etc., and you select the
equalizer to edit using a pull-down menu. The Set button should be use to
record changes.
3. Behavioral Abstraction
In Nyquist, all functions are subject to transformations. You can think of
transformations as additional parameters to every function, and functions are
free to use these additional parameters in any way. The set of transformation
parameters is captured in what is referred to as the transformation
environment. (Note that the term environment is heavily overloaded in computer
science. This is yet another usage of the term.)
Behavioral abstraction is the ability of functions to adapt their behavior to
the transformation environment. This environment may contain certain abstract
notions, such as loudness, stretching a sound in time, etc. These notions will
mean different things to different functions. For example, an oscillator
should produce more periods of oscillation in order to stretch its output. An
envelope, on the other hand, might only change the duration of the sustain
portion of the envelope in order to stretch. Stretching a sample could mean
resampling it to change its duration by the appropriate amount.
Thus, transformations in Nyquist are not simply operations on signals. For
example, if I want to stretch a note, it does not make sense to compute the
note first and then stretch the signal. Doing so would cause a drop in the
pitch. Instead, a transformation modifies the transformation environment in
which the note is computed. Think of transformations as making requests to
functions. It is up to the function to carry out the request. Since the
function is always in complete control, it is possible to perform
transformations with ``intelligence;'' that is, the function can perform an
appropriate transformation, such as maintaining the desired pitch and
stretching only the ''sustain'' portion of an envelope to obtain a longer note.
3.1. The Environment
The transformation environment consists of a set of special variables. These
variables should not be read directly and should never be set directly by the
programmer. Instead, there are functions to read them, and they are
automatically set and restored by transformation operators, which will be
described below.
The transformation environment consists of the following elements. Although
each element has a ``standard interpretation,'' the designer of an instrument
or the composer of a complex behavior is free to interpret the environment in
any way. For example, a change in *loud* may change timbre more than
amplitude, and *transpose* may be ignored by percussion instruments:
*warp* Time transformation, including time shift, time stretch, and
continuous time warp. The value of *warp* is interpreted as a
function from logical (local score) time to physical (global
real) time. Do not access *warp* directly. Instead, use
local-to-global(t) to convert from a logical (local) time to
real (global) time. Most often, you will call local-to-
global(0). Several transformation operators operate on *warp*,
including at (@), stretch (~), and warp.
*loud* Loudness, expressed in decibels. The default (nominal)
loudness is 0.0 dB (no change). Do not access *loud* directly.
Instead, use get-loud() to get the current value of *loud* and
either loud or loud-abs to modify it.
*transpose* Pitch transposition, expressed in semitones. (Default: 0.0).
Do not access *transpose* directly. Instead, use
get-transpose() to get the current value of *transpose* and
either transpose or transpose-abs to modify it.
*sustain* The ``sustain,'' ``articulation,'' ``duty factor,'' or amount
by which to separate or overlap sequential notes. For example,
staccato might be expressed with a *sustain* of 0.5, while very
legato playing might be expressed with a *sustain* of 1.2.
Specifically, *sustain* stretches the duration of notes
(sustain) without affecting the inter-onset time (the rhythm).
Do not access *sustain* directly. Instead, use get-sustain()
to get the current value of *sustain* and either sustain or
sustain-abs to modify it.
*start* Start time of a clipping region. Note: unlike the previous
elements of the environment, *start* has a precise
interpretation: no sound should be generated before *start*.
This is implemented in all the low-level sound functions, so it
can generally be ignored. You can read *start* directly, but
use extract or extract-abs to modify it. Note 2: Due to some
internal confusion between the specified starting time and the
actual starting time of a signal after clipping, *start* is not
fully implemented.
*stop* Stop time of clipping region. By analogy to *start*, no sound
should be generated after this time. *start* and *stop* allow
a composer to preview a small section of a work without
computing it from beginning to end. You can read *stop*
directly, but use extract or extract-abs to modify it. Note:
Due to some internal confusion between the specified starting
time and the actual starting time of a signal after clipping,
*stop* is not fully implemented.
*control-srate* Sample rate of control signals. This environment element
provides the default sample rate for control signals. There is
no formal distinction between a control signal and an audio
signal. You can read *control-srate* directly, but use
control-srate or control-srate-abs to modify it.
*sound-srate* Sample rate of musical sounds. This environment element
provides the default sample rate for musical sounds. You can
read *sound-srate* directly, but use sound-srate or
sound-srate-abs to modify it.
3.2. Sequential Behavior
Previous examples have shown the use of seq, the sequential behavior
operator. We can now explain seq in terms of transformations. Consider the
simple expression:
play seq(note(c4, q), note(d4, i))
The idea is to create the first note at time 0, and to start the next note when
the first one finishes. This is all accomplished by manipulating the
environment. In particular, *warp* is modified so that what is locally time 0
for the second note is transformed, or warped, to the logical stop time of the
first note.
One way to understand this in detail is to imagine how it might be executed:
first, *warp* is set to an initial value that has no effect on time, and
note(c4, q) is evaluated. A sound is returned and saved. The sound has an
ending time, which in this case will be 1.0 because the duration q is 1.0.
This ending time, 1.0, is used to construct a new *warp* that has the effect of
shifting time by 1.0. The second note is evaluated, and will start at time 1.
The sound that is returned is now added to the first sound to form a composite
sound, whose duration will be 2.0. *warp* is restored to its initial value.
Notice that the semantics of seq can be expressed in terms of
transformations. To generalize, the operational rule for seq is: evaluate the
first behavior according to the current *warp*. Evaluate each successive
behavior with *warp* modified to shift the new note's starting time to the
ending time of the previous behavior. Restore *warp* to its original value and
return a sound which is the sum of the results.
In the Nyquist implementation, audio samples are only computed when they are
needed, and the second part of the seq is not evaluated until the ending time
(called the logical stop time) of the first part. It is still the case that
when the second part is evaluated, it will see *warp* bound to the ending time
of the first part.
A language detail: Even though Nyquist defers evaluation of the second part
of the seq, the expression can reference variables according to ordinary
Lisp/SAL scope rules. This is because the seq captures the expression in a
closure, which retains all of the variable bindings.
3.3. Simultaneous Behavior
Another operator is sim, which invokes multiple behaviors at the same time.
For example,
play 0.5 * sim(note(c4, q), note(d4, i))
will play both notes starting at the same time.
The operational rule for sim is: evaluate each behavior at the current *warp*
and return the sum of the results. (In SAL, the sim function applied to sounds
is equivalent to adding them with the infix + operator. The following section
illustrates two concepts: first, a sound is not a behavior, and second, the sim
operator and and the at transformation can be used to place sounds in time.
3.4. Sounds vs. Behaviors
The following example loads a sound from a file in the current directory and
stores it in a-snd:
; load a sound
;
set a-snd = s-read(strcat(current-path(), "demo-snd.aiff"))
; play it
;
play a-snd
One might then be tempted to write the following:
play seq(a-snd, a-snd) ;WRONG!
Why is this wrong? Recall that seq works by modifying *warp*, not by operating
on sounds. So, seq will proceed by evaluating a-snd with different values of
*warp*. However, the result of evaluating a-snd (a variable) is always the
same sound, regardless of the environment; in this case, the second a-snd
should start at time 0.0, just like the first. In this case, after the first
sound ends, Nyquist is unable to ``back up'' to time zero, so in fact, this
will play two sounds in sequence, but that is a result of an implementation
detail rather than correct program execution. In fact, a future version of
Nyquist might (correctly) stop and report an error when it detects that the
second sound in the sequence has a real start time that is before the requested
one.
How then do we obtain a sequence of two sounds properly? What we really need
here is a behavior that transforms a given sound according to the current
transformation environment. That job is performed by cue. For example, the
following will behave as expected, producing a sequence of two sounds:
play seq(cue(a-snd), cue(a-snd))
This example is correct because the second expression will shift the sound
stored in a-snd to start at the end time of the first expression.
The lesson here is very important: sounds are not behaviors! Behaviors are
computations that generate sounds according to the transformation environment.
Once a sound has been generated, it can be stored, copied, added to other
sounds, and used in many other operations, but sounds are not subject to
transformations. To transform a sound, use cue, sound, or control. The
differences between these operations are discussed later. For now, here is a
``cue sheet'' style score that plays 4 copies of a-snd:
; use sim and at to place sounds in time
;
play sim(cue(a-snd) @ 0.0,
cue(a-snd) @ 0.7,
cue(a-snd) @ 1.0,
cue(a-snd) @ 1.2)
3.5. The At Transformation
The second concept introduced by the previous example is the @ operation,
which shifts the *warp* component of the environment. For example,
cue(a-snd) @ 0.7
can be explained operationally as follows: modify *warp* by shifting it by 0.7
and evaluate cue(a-snd). Return the resulting sound after restoring *warp* to
its original value. Notice how @ is used inside a sim construct to locate
copies of a-snd in time. This is the standard way to represent a note-list or
a cue-sheet in Nyquist.
This also explains why sounds need to be cue'd in order to be shifted in time
or arranged in sequence. If this were not the case, then sim would take all of
its parameters (a set of sounds) and line them up to start at the same time.
But cue(a-snd) @ 0.7 is just a sound, so sim would ``undo'' the effect of @,
making all of the sounds in the previous example start simultaneously, in spite
of the @! Since sim respects the intrinsic starting times of sounds, a special
operation, cue, is needed to create a new sound with a new starting time.
3.6. Nested Transformations
Transformations can be combined using nested expressions. For example,
sim(cue(a-snd),
loud(6.0, cue(a-snd) @ 3))
scales the amplitude as well as shifts the second entrance of a-snd.
Why use loud instead of simply multiplying a-snd by some scale factor? Using
loud gives the behavior the chance to implement the abstract property loudness
in an appropriate way, e.g. by including timbral changes. In this case, the
behavior is cue, which implements loudness by simple amplitude scaling, so the
result is equivalent to multiplication by db-to-linear(6.0).
Transformations can also be applied to groups of behaviors:
loud(6.0, sim(cue(a-snd) @ 0.0,
cue(a-snd) @ 0.7))
3.7. Defining Behaviors
Groups of behaviors can be named using define (we already saw this in the
definitions of note and note-env). Here is another example of a behavior
definition and its use. The definition has one parameter:
define function snds(dly)
return sim(cue(a-snd) @ 0.0,
cue(a-snd) @ 0.7,
cue(a-snd) @ 1.0,
cue(a-snd) @ (1.2 + dly))
play snds(0.1)
play loud(0.25, snds(0.3) ~ 0.9)
In the last line, snds is transformed: the transformations will apply to the
cue behaviors within snds. The loud transformation will scale the sounds by
0.25, and the stretch (~) will apply to the shift (@) amounts 0.0, 0.7, 1.0,
and 1.2 + dly. The sounds themselves (copies of a-snd) will not be stretched
because cue never stretches sounds.
Section 7.3 describes the full set of transformations.
3.8. Sample Rates
The global environment contains *sound-srate* and *control-srate*, which
determine the sample rates of sounds and control signals. These can be
overridden at any point by the transformations sound-srate-abs and
control-srate-abs; for example,
sound-srate-abs(44100.0, osc(c4)
will compute a tone using a 44.1Khz sample rate even if the default rate is set
to something different.
As with other components of the environment, you should never change
*sound-srate* or *control-srate* directly. The global environment is
determined by two additional variables: *default-sound-srate* and *default-
control-srate*. You can add lines like the following to your init.lsp file to
change the default global environment:
(setf *default-sound-srate* 44100.0)
(setf *default-control-srate* 1102.5)
You can also do this using preferences in jNyqIDE. If you have already started
Nyquist and want to change the defaults, the preferences or the following
functions can be used:
exec set-control-srate(1102.5)exec set-sound-srate(22050.0)
These modify the default values and reinitialize the Nyquist environment.
4. Continuous Transformations and Time Warps
Nyquist transformations were discussed in the previous chapter, but all of
the examples used scalar values. For example, we saw the loud transformation
used to change loudness by a fixed amount. What if we want to specify a
crescendo, where the loudness changes gradually over time?
It turns out that all transformations can accept signals as well as numbers,
so transformations can be continuous over time. This raises some interesting
questions about how to interpret continuous transformations. Should a loudness
transformation apply to the internal details of a note or only affect the
initial loudness? It might seem unnatural for a decaying piano note to perform
a crescendo. On the other hand, a sustained trumpet sound should probably
crescendo continuously. In the case of time warping (tempo changes), it might
be best for a drum roll to maintain a steady rate, a trill may or may not
change rates with tempo, and a run of sixteenth notes will surely change its
rate.
These issues are complex, and Nyquist cannot hope to automatically do the
right thing in all cases. However, the concept of behavioral abstraction
provides an elegant solution. Since transformations merely modify the
environment, behaviors are not forced to implement any particular style of
transformation. Nyquist is designed so that the default transformation is
usually the right one, but it is always possible to override the default
transformation to achieve a particular effect.
4.1. Simple Transformations
The ``simple'' transformations affect some parameter, but have no effect on
time itself. The simple transformations that support continuously changing
parameters are: sustain, loud, and transpose.
As a first example, Let us use transpose to create a chromatic scale. First
define a sequence of tones at a steady pitch. The seqrep ``function'' works
like seq except that it creates copies of a sound by evaluating an expression
multiple times. Here, i takes on 16 values from 0 to 15, and the expression for
the sound could potentially use i. Technically, seqrep is not really a
function but an abbreviation for a special kind of loop construct.
define function tone-seq()
return seqrep(i, 16,
osc-note(c4) ~ 0.25)
Now define a linearly increasing ramp to serve as a transposition function:
define function pitch-rise() return sustain-abs(1.0, 16 * ramp() ~ 4) This ramp
has a duration of 4 seconds, and over that interval it rises from 0 to 16
(corresponding to the 16 semitones we want to transpose). The ramp is inside a
sustain-abs transformation, which prevents a sustain transformation from having
any effect on the ramp. (One of the drawbacks of behavioral abstraction is that
built-in behaviors sometimes do the wrong thing implicitly, requiring some
explicit correction to turn off the unwanted transformation.) Now, pitch-rise
is used to transpose tone-seq: define function chromatic-scale() return
transpose(pitch-rise(), tone-seq())
Similar transformations can be constructed to change the sustain or ``duty
factor'' of notes and their loudness. The following expression plays the
previously constructed chromatic scale with increasing note durations. The
rhythm is unchanged, but the note length changes from staccato to legato: play
sustain((0.2 + ramp()) ~ 4, chromatic-scale()) The resulting sustain function
will ramp from 0.2 to 1.2. A sustain of 1.2 denotes a 20 percent overlap
between notes. The sum has a stretch factor of 4, so it will extend over the 4
second duration of chromatic-scale.
What do these transformations mean? How did the system know to produce a
pitch rise rather than a continuous glissando? This all relates to the idea of
behavioral abstraction. It is possible to design sounds that do glissando
under the transpose transform, and you can even make sounds that ignore
transpose altogether. As explained in Chapter 3, the transformations modify
the environment, and behaviors can reference the environment to determine what
signals to generate. All built-in functions, such as osc, have a default
behavior.
The default behavior for sound primitives under transpose, sustain, and loud
transformations is to sample the environment at the beginning of the note.
Transposition is not quantized to semitones or any other scale, but in our
example, we arranged for the transposition to work out to integer numbers of
semitones, so we obtained a chromatic scale anyway.
Transposition only applies to the oscillator and sampling primitives osc,
partial, sampler, sine, fmosc, and amosc. Sustain applies to osc, env, and
pwl. (Note that partial, amosc, and fmosc get their durations from the
modulation signal, so they may indirectly depend upon the sustain.) Loud
applies to osc, sampler, cue, sound, fmosc, and amosc. (But not pwl or env.)
4.2. Time Warps
The most interesting transformations have to do with transforming time
itself. The warp transformation provides a mapping function from logical
(score) time to real time. The slope of this function tells us how many units
of real time are covered by one unit of score time. This is proportional to
1/tempo. A higher slope corresponds to a slower tempo.
To demonstrate warp, we will define a time warp function using pwl:
define function warper()
return pwl(0.25, .4, .75, .6, 1.0, 1.0, 2.0, 2.0, 2.0)
This function has an initial slope of .4/.25 = 1.6. It may be easier to think
in reciprocal terms: the initial tempo is .25/.4 = .625. Between 0.25 and
0.75, the tempo is .5/.2 = 2.5, and from 0.75 to 1.0, the tempo is again .625.
It is important for warp functions to completely span the interval of interest
(in our case it will be 0 to 1), and it is safest to extend a bit beyond the
interval, so we extend the function on to 2.0 with a tempo of 1.0. Next, we
stretch and scale the warper function to cover 4 seconds of score time and 4
seconds of real time:
define function warp4()
return 4 * warper() ~ 4
Figure 2: The result of (warp4), intended to map 4 seconds of score
time into 4 seconds of real time. The function extends beyond 4
seconds (the dashed lines) to make sure the function is well-defined
at location (4, 4). Nyquist sounds are ordinarily open on the
right.
Figure 2 shows a plot of this warp function. Now, we can warp the tempo of
the tone-seq defined above using warp4:
play warp(warp4(), tone-seq())
Figure 3 shows the result graphically. Notice that the durations of the tones
are warped as well as their onsets. Envelopes are not shown in detail in the
figure. Because of the way env is defined, the tones will have constant attack
and decay times, and the sustain will be adjusted to fit the available time.
Figure 3: When (warp4) is applied to (tone-seq-2), the note onsets
and durations are warped.
4.3. Abstract Time Warps
We have seen a number of examples where the default behavior did the ``right
thing,'' making the code straightforward. This is not always the case.
Suppose we want to warp the note onsets but not the durations. We will first
look at an incorrect solution and discuss the error. Then we will look at a
slightly more complex (but correct) solution.
The default behavior for most Nyquist built-in functions is to sample the
time warp function at the nominal starting and ending score times of the
primitive. For many built-in functions, including osc, the starting logical
time is 0 and the ending logical time is 1, so the time warp function is
evaluated at these points to yield real starting and stopping times, say 15.23
and 16.79. The difference (e.g. 1.56) becomes the signal duration, and there
is no internal time warping. The pwl function behaves a little differently.
Here, each breakpoint is warped individually, but the resulting function is
linear between the breakpoints.
A consequence of the default behavior is that notes stretch when the tempo
slows down. Returning to our example, recall that we want to warp only the
note onset times and not the duration. One would think that the following
would work:
define function tone-seq-2 ()
return seqrep(i, 16,
osc-note(c4) ~~ 0.25)
play warp(warp4(), tone-seq-2())
Here, we have redefined tone-seq, renaming it to tone-seq-2 and changing the
stretch (~) to absolute stretch (~~). The absolute stretch should override the
warp function and produce a fixed duration.
If you play the example, you will hear steady sixteenths and no tempo
changes. What is wrong? In a sense, the ``fix'' works too well. Recall that
sequences (including seqrep) determine the starting time of the next note from
the logical stop time of the previous sound in the sequence. When we forced
the stretch to 0.25, we also forced the logical stop time to 0.25 real seconds
from the beginning, so every note starts 0.25 seconds after the previous one,
resulting in a constant tempo.
Now let us design a proper solution. The trick is to use absolute stretch
(~~) as before to control the duration, but to restore the logical stop time to
a value that results in the proper inter-onset time interval:
define function tone-seq-3()
return seqrep(i, 16,
set-logical-stop(osc-note(c4) ~~ 0.25, 0.25))
play warp(warp4(), tone-seq-3())
Notice the addition of set-logical-stop enclosing the absolute stretch (~~)
expression to set the logical stop time. A possible point of confusion here is
that the logical stop time is set to 0.25, the same number given to ~~! How
does setting the logical stop time to 0.25 result in a tempo change? When used
within a warp transformation, the second argument to set-logical-stop refers to
score time rather than real time. Therefore, the score duration of 0.25 is
warped into real time, producing tempo changes according to the enviroment.
Figure 4 illustrates the result graphically.
Figure 4: When (warp4) is applied to (tone-seq-3), the note onsets
are warped, but not the duration, which remains a constant 0.25
seconds. In the fast middle section, this causes notes to overlap.
Nyquist will sum (mix) them.
4.4. Nested Transformations
Transformations can be nested. In particular, a simple transformation such
as transpose can be nested within a time warp transformation. Suppose we want
to warp our chromatic scale example with the warp4 time warp function. As in
the previous section, we will show an erroneous simple solution followed by a
correct one.
The simplest approach to a nested transformation is to simply combine them
and hope for the best:
play warp(warp4(),
transpose(pitch-rise(), tone-seq()))
This example will not work the way you might expect. Here is why: the warp
transformation applies to the (pitch-rise) expression, which is implemented
using the ramp function. The default behavior of ramp is to interpolate
linearly (in real time) between two points. Thus, the ``warped'' ramp function
will not truly reflect the internal details of the intended time warp. When
the notes are moving faster, they will be closer together in pitch, and the
result is not chromatic. What we need is a way to properly compose the warp
and ramp functions. If we continuously warp the ramp function in the same way
as the note sequence, a chromatic scale should be obtained. This will lead to
a correct solution.
Here is the modified code to properly warp a transposed sequence. Note that
the original sequence is used without modification. The only complication is
producing a properly warped transposition function:
play warp(warp4(),
transpose(
control-warp(get-warp(),
warp-abs(nil, pitch-rise())),
tone-seq()))
To properly warp the pitch-rise transposition function, we use control-warp,
which applies a warp function to a function of score time, yielding a function
of real time. We need to pass the desired function to control-warp, so we
fetch it from the environment with get-warp(). Finally, since the warping is
done here, we want to shield the pitch-rise expression from further warping, so
we enclose it in warp-abs(nil, ...).
An aside: This last example illustrates a difficulty in the design of
Nyquist. To support behavioral abstraction universally, we must rely upon
behaviors to ``do the right thing.'' In this case, we would like the ramp
function to warp continuously according to the environment. But this is
inefficient and unnecessary in many other cases where ramp and especially pwl
are used. (pwl warps its breakpoints, but still interpolates linearly between
them.) Also, if the default behavior of primitives is to warp in a continuous
manner, this makes it difficult to build custom abstract behaviors. The final
vote is not in.
5. More Examples
This chapter explores Nyquist through additional examples. The reader may
wish to browse through these and move on to Chapter 7, which is a reference
section describing Nyquist functions.
5.1. Stretching Sampled Sounds
This example illustrates how to stretch a sound, resampling it in the
process. Because sounds in Nyquist are values that contain the sample rate,
start time, etc., use sound to convert a sound into a behavior that can be
stretched, e.g. sound(a-snd). This behavior stretches a sound according to the
stretch factor in the environment, set using stretch. For accuracy and
efficiency, Nyquist does not resample a stretched sound until absolutely
necessary. The force-srate function is used to resample the result so that we
end up with a ``normal'' sample rate that is playable on ordinary sound cards.
; if a-snd is not loaded, load sound sample:
;
if not(boundp(quote(a-snd))) then
set a-snd = s-read("demo-snd.aiff")
; the SOUND operator shifts, stretches, clips and scales
; a sound according to the current environment
;
define function ex23()
play force-srate(*default-sound-srate*, sound(a-snd) ~ 3.0)
define function down()
return force-srate(*default-sound-srate*,
seq(sound(a-snd) ~ 0.2,
sound(a-snd) ~ 0.3,
sound(a-snd) ~ 0.4,
sound(a-snd) ~ 0.6))
play down()
; that was so much fun, let's go back up:
;
define function up()
return force-srate(*default-sound-srate*,
seq(sound(a-snd) ~ 0.5,
sound(a-snd) ~ 0.4,
sound(a-snd) ~ 0.3,
sound(a-snd) ~ 0.2))
; and write a sequence
;
play seq(down(), up(), down())
Notice the use of the sound behavior as opposed to cue. The cue behavior
shifts and scales its sound according to *warp* and *loud*, but it does not
change the duration or resample the sound. In contrast, sound not only shifts
and scales its sound, but it also stretches it by resampling or changing the
effective sample rate according to *warp*. If *warp* is a continuous warping
function, then the sound will be stretched by time-varying amounts. (The
*transpose* element of the environment is ignored by both cue and sound.)
Note: sound may use linear interpolation rather than a high-quality
resampling algorithm. In some cases, this may introduce errors audible as
noise. Use resample (see Section 7.2.2) for high-quality interpolation.
In the functions up and down, the *warp* is set by stretch (~), which simply
scales time by a constant scale factor. In this case, sound can ``stretch'' the
signal simply by changing the sample rate without any further computation. When
seq tries to add the signals together, it discovers the sample rates do not
match and uses linear interpolation to adjust all sample rates to match that of
the first sound in the sequence. The result of seq is then converted using
force-srate to convert the sample rate, again using linear interpolation. It
would be slightly better, from a computational standpoint, to apply force-srate
individually to each stretched sound rather than applying force-srate after
seq.
Notice that the overall duration of sound(a-snd) ~ 0.5 will be half the
duration of a-snd.
5.2. Saving Sound Files
So far, we have used the play command to play a sound. The play command
works by writing a sound to a file while simultaneously playing it. This can
be done one step at a time, and it is often convenient to save a sound to a
particular file for later use:
; write the sample to a file,
; the file name can be any Unix filename. Prepending a "./" tells
; s-save to not prepend *default-sf-dir*
;
exec s-save(a-snd, 1000000000, "./a-snd-file.snd")
; play a file
; play command normally expects an expression for a sound
; but if you pass it a string, it will open and play a
; sound file
play "./a-snd-file.snd"
; delete the file (do this with care!)
; only works under Unix (not Windows)
exec system("rm ./a-snd-file.snd")
; now let's do it using a variable as the file name
;
set my-sound-file = "./a-snd-file.snd"
exec s-save(a-snd, 1000000000, my-sound-file)
; play-file is a function to open and play a sound file
exec play-file(my-sound-file)
exec system(strcat("rm ", my-sound-file))
This example shows how s-save can be used to save a sound to a file.
This example also shows how the system function can be used to invoke Unix
shell commands, such as a command to play a file or remove it. Finally, notice
that strcat can be used to concatenate a command name to a file name to create
a complete command that is then passed to system. (This is convenient if the
sound file name is stored in a parameter or variable.)
5.3. Memory Space and Normalization
Sound samples take up lots of memory, and often, there is not enough primary
(RAM) memory to hold a complete composition. For this reason, Nyquist can
compute sounds incrementally, saving the final result on disk. However,
Nyquist can also save sounds in memory so that they can be reused efficiently.
In general, if a sound is saved in a global variable, memory will be allocated
as needed to save and reuse it.
The standard way to compute a sound and write it to disk is to pass an
expression to the play command:
play my-composition()
Often it is nice to normalize sounds so that they use the full available
dynamic range of 16 bits. Nyquist has an automated facility to help with
normalization. By default, Nyquist computes up to 1 million samples (using
about 4MB of memory) looking for the peak. The entire sound is normalized so
that this peak will not cause clipping. If the sound has less than 1 million
samples, or if the first million samples are a good indication of the overall
peak, then the signal will not clip.
With this automated normalization technique, you can choose the desired peak
value by setting *autonorm-target*, which is initialized to 0.9. The number of
samples examined is *autonorm-max-samples*, initially 1 million. You can turn
this feature off by executing:
(autonorm-off)
and turn it back on by typing:
(autonorm-on)
This normalization technique is in effect when *autonorm-type* is
quote(lookahead), which is the default.
An alternative normalization method uses the peak value from the previous
call to play. After playing a file, Nyquist can adjust an internal scale factor
so that if you play the same file again, the peak amplitude will be
*autonorm-target*, which is initialized to 0.9. This can be useful if you want
to carefully normalize a big sound that does not have its peak near the
beginning. To select this style of normalization, set *autonorm-type* to the
(quoted) atom quote(previous).
You can also create your own normalization method in Nyquist. The peak
function computes the maximum value of a sound. The peak value is also
returned from the play macro. You can normalize in memory if you have enough
memory; otherwise you can compute the sound twice. The two techniques are
illustrated here:
; normalize in memory. First, assign the sound to a variable so
; it will be retained:
set mysound = sim(osc(c4), osc(c5))
; now compute the maximum value (ny:all is 1 giga-samples, you may want
; smaller constant if you have less than 4GB of memory:
set mymax = snd-max(mysound, NY:ALL)
display "Computed max", mymax
; now write out and play the sound from memory with a scale factor:
play mysound * (0.9 / mymax)
; if you don't have space in memory, here's how to do it:
define function myscore()
return sim(osc(c4), osc(c5))
; compute the maximum:
set mymax = snd-max(list(quote(myscore)), NY:ALL)
display "Computed max", mymax
; now we know the max, but we don't have a the sound (it was garbage
; collected and never existed all at once in memory). Compute the soun
; again, this time with a scale factor:
play myscore() * (0.9 / mymax)
You can also write a sound as a floating point file. This file can then be
converted to 16-bit integer with the proper scaling applied. If a long
computation was involved, it should be much faster to scale the saved sound
file than to recompute the sound from scratch. Although not implemented yet in
Nyquist, some header formats can store maximum amplitudes, and some soundfile
player programs can rescale floating point files on the fly, allowing
normalized soundfile playback without an extra normalization pass (but at a
cost of twice the disk space of 16-bit samples). You can use Nyquist to
rescale a floating point file and convert it to 16-bit samples for playback.
5.4. Frequency Modulation
The next example uses the Nyquist frequency modulation behavior fmosc to
generate various sounds. The parameters to fmosc are:
fmosc(pitch modulator table phase)
Note that pitch is the number of half-steps, e.g. c4 has the value of 60 which
is middle-C, and phase is in degrees. Only the first two parameters are
required:
; make a short sine tone with no frequency modulation
;
play fmosc(c4, pwl(0.1))
; make a longer sine tone -- note that the duration of
; the modulator determines the duration of the tone
;
play fmosc(c4, pwl(0.5))
In the example above, pwl (for Piece-Wise Linear) is used to generate sounds
that are zero for the durations of 0.1 and 0.5 seconds, respectively. In
effect, we are using an FM oscillator with no modulation input, and the result
is a sine tone. The duration of the modulation determines the duration of the
generated tone (when the modulation signal ends, the oscillator stops).
The next example uses a more interesting modulation function, a ramp from
zero to C , expressed in hz. More explanation of pwl is in order. This
4
operation constructs a piece-wise linear function sampled at the
*control-srate*. The first breakpoint is always at (0, 0), so the first two
parameters give the time and value of the second breakpoint, the second two
parameters give the time and value of the third breakpoint, and so on. The
last breakpoint has a value of 0, so only the time of the last breakpoint is
given. In this case, we want the ramp to end at C , so we cheat a bit by
4
having the ramp return to zero ``almost'' instantaneously between times 0.5 and
0.501.
The pwl behavior always expects an odd number of parameters. The resulting
function is shifted and stretched linearly according to *warp* in the
environment. Now, here is the example:
; make a frequency sweep of one octave; the piece-wise linear function
; sweeps from 0 to (step-to-hz c4) because, when added to the c4
; fundamental, this will double the frequency and cause an octave sweep
;
play fmosc(c4, pwl(0.5, step-to-hz(c4), 0.501))
The same idea can be applied to a non-sinusoidal carrier. Here, we assume
that *fm-voice* is predefined (the next section shows how to define it):
; do the same thing with a non-sine table
;
play fmosc(cs2, pwl(0.5, step-to-hz(cs2), 0.501),
*fm-voice*, 0.0)
The next example shows how a function can be used to make a special frequency
modulation contour. In this case the contour generates a sweep from a starting
pitch to a destination pitch:
; make a function to give a frequency sweep, starting
; after seconds, then sweeping from
; to in seconds and then
; holding at for seconds.
;
define function sweep(delay, pitch-1, sweep-time,
pitch-2, hold-time)
begin
with interval = step-to-hz(pitch-2) - step-to-hz(pitch-1)
return pwl(delay, 0.0,
; sweep from pitch 1 to pitch 2
delay + sweep-time, interval,
; hold until about 1 sample from the end
delay + sweep-time + hold-time - 0.0005,
interval,
; quickly ramp to zero (pwl always does this,
; so make it short)
delay + sweep-time + hold-time)
end
; now try it out
;
play fmosc(cs2, sweep(0.1, cs2, 0.6, gs2, 0.5),
*fm-voice*, 0.0)
FM can be used for vibrato as well as frequency sweeps. Here, we use the lfo
function to generate vibrato. The lfo operation is similar to osc, except it
generates sounds at the *control-srate*, and the parameter is hz rather than a
pitch:
play fmosc(cs2, 10.0 * lfo(6.0), *fm-voice*, 0.0)
What kind of manual would this be without the obligatory FM sound? Here, a
sinusoidal modulator (frequency C ) is multiplied by a slowly increasing ramp
4
from zero to 1000.0.
set modulator = pwl(1.0, 1000.0, 1.0005) *
osc(c4)
; make the sound
play fmosc(c4, modulator)
For more simple examples of FM in Nyquist, see demos/warble_tutorial.htm.
Another interesting FM sound reminiscent of ``scratching'' can be found with a
detailed explanation in demos/scratch_tutorial.htm..
5.5. Building a Wavetable
In Section 1.6.1, we saw how to synthesize a wavetable. A wavetable for osc
also can be extracted from any sound. This is especially interesting if the
sound is digitized from some external sound source and loaded using the s-read
function. Recall that a table is a list consisting of a sound, the pitch of
that sound, and T (meaning the sound is periodic).
In the following, a sound is first read from the file demo-snd.nh. Then, the
extract function is used to extract the portion of the sound between 0.110204
and 0.13932 seconds. (These numbers might be obtained by first plotting the
sound and estimating the beginning and end of a period, or by using some
software to look for good zero crossings.) The result of extract becomes the
first element of a list. The next element is the pitch (24.848422), and the
last element is T. The list is assigned to *fm-voice*.
if not(boundp(quote(a-snd))) then
set a-snd = s-read("demo-snd.aiff")
set *fm-voice* = list(extract(0.110204, 0.13932, cue(a-snd)),
24.848422,
#T)
The file demos/examples.sal contains an extensive example of how to locate
zero-crossings, extract a period, build a waveform, and generate a tone from
it. (See ex37 through ex40 in the file.)
5.6. Filter Examples
Nyquist provides a variety of filters. All of these filters take either real
numbers or signals as parameters. If you pass a signal as a filter parameter,
the filter coefficients are recomputed at the sample rate of the control
signal. Since filter coefficients are generally expensive to compute, you may
want to select filter control rates carefully. Use control-srate-abs (Section
7.3) to specify the default control sample rate, or use force-srate (Section
7.2.2) to resample a signal before passing it to a filter.
Before presenting examples, let's generate some unfiltered white noise:
play noise()
Now low-pass filter the noise with a 1000Hz cutoff:
play lp(noise(), 1000.0)
The high-pass filter is the inverse of the low-pass:
play hp(noise(), 1000.0)
Here is a low-pass filter sweep from 100Hz to 2000Hz:
play lp(noise(), pwl(0.0, 100.0, 1.0, 2000.0, 1.0))
And a high-pass sweep from 50Hz to 4000Hz:
play hp(noise(), pwl(0.0, 50.0, 1.0, 4000.0, 1.0))
The band-pass filter takes a center frequency and a bandwidth parameter.
This example has a 500Hz center frequency with a 20Hz bandwidth. The scale
factor is necessary because, due to the resonant peak of the filter, the signal
amplitude exceeds 1.0:
play reson(10.0 * noise(), 500.0, 20.0, 1)
In the next example, the center frequency is swept from 100 to 1000Hz, using a
constant 20Hz bandwidth:
play reson(0.04 * noise(),
pwl(0.0, 200.0, 1.0, 1000.0, 1.0),
20.0)
For another example with explanations, see demos/wind_tutorial.htm.
5.7. DSP in Lisp
In almost any signal processing system, the vast majority of computation
takes place in the inner loops of DSP algorithms, and Nyquist is designed so
that these time-consuming inner loops are in highly-optimized machine code
rather than relatively slow interpreted lisp code. As a result, Nyquist
typically spends 95% of its time in these inner loops; the overhead of using a
Lisp interpreter is negligible.
The drawback is that Nyquist must provide the DSP operations you need, or you
are out of luck. When Nyquist is found lacking, you can either write a new
primitive signal operation, or you can perform DSP in Lisp code. Neither option
is recommended for inexperienced programmers. Instructions for extending
Nyquist are given in Appendix I. This section describes the process of writing
a new signal processing function in Lisp.
Before implementing a new DSP function, you should decide which approach is
best. First, figure out how much of the new function can be implemented using
existing Nyquist functions. For example, you might think that a tapped-delay
line would require a new function, but in fact, it can be implemented by
composing sound transformations to accomplish delays, scale factors for
attenuation, and additions to combine the intermediate results. This can all
be packaged into a new Lisp function, making it easy to use. If the function
relies on built-in DSP primitives, it will execute very efficiently.
Assuming that built-in functions cannot be used, try to define a new
operation that will be both simple and general. Usually, it makes sense to
implement only the kernel of what you need, combining it with existing
functions to build a complete instrument or operation. For example, if you
want to implement a physical model that requires a varying breath pressure with
noise and vibrato, plan to use Nyquist functions to add a basic pressure
envelope to noise and vibrato signals to come up with a composite pressure
signal. Pass that signal into the physical model rather than synthesizing the
envelope, noise, and vibrato within the model. This not only simplifies the
model, but gives you the flexibility to use all of Nyquist's operations to
synthesize a suitable breath pressure signal.
Having designed the new ``kernel'' DSP operation that must be implemented,
decide whether to use C or Lisp. (At present, SAL is not a good option because
it has no support for object-oriented programming.) To use C, you must have a
C compiler, the full source code for Nyquist, and you must learn about
extending Nyquist by reading Appendix I. This is the more complex approach, but
the result will be very efficient. A C implementation will deal properly with
sounds that are not time-aligned or matched in sample rates. To use Lisp, you
must learn something about the XLISP object system, and the result will be
about 50 times slower than C. Also, it is more difficult to deal with time
alignment and differences in sample rates. The remainder of this section gives
an example of a Lisp version of snd-prod to illustrate how to write DSP
functions for Nyquist in Lisp.
The snd-prod function is the low-level multiply routine. It has two sound
parameters and returns a sound which is the product of the two. To keep things
simple, we will assume that two sounds to be multiplied have a matched sample
rate and matching start times. The DSP algorithm for each output sample is
simply to fetch a sample from each sound, multiply them, and return the
product.
To implement snd-prod in Lisp, three components are required:
1. An object is used to store the two parameter sounds. This object
will be called upon to yield samples of the result sound;
2. Within the object, the snd-fetch routine is used to fetch samples
from the two input sounds as needed;
3. The result must be of type SOUND, so snd-fromobject is used to
create the result sound.
The combined solution will work as follows: The result is a value of type
sound that retains a reference to the object. When Nyquist needs samples from
the sound, it invokes the sound's ``fetch'' function, which in turn sends an
XLISP message to the object. The object will use snd-fetch to get a sample from
each stored sound, multiply the samples, and return a result.
Thus the goal is to design an XLISP object that, in response to a :next
message will return a proper sequence of samples. When the sound reaches the
termination time, simply return NIL.
The XLISP manual (see Appendix IV describes the object system, but in a very
terse style, so this example will include some explanation of how the object
system is used. First, we need to define a class for the objects that will
compute sound products. Every class is a subclass of class class, and you
create a subclass by sending :new to a class.
(setf product-class (send class :new '(s1 s2)))
The parameter '(s1 s2) says that the new class will have two instance
variables, s1 and s2. In other words, every object which is an instance of
class product-class will have its own copy of these two variables.
Next, we will define the :next method for product-class:
(send product-class :answer :next '()
'((let ((f1 (snd-fetch s1))
(f2 (snd-fetch s2)))
(cond ((and f1 f2)
(* f1 f2))
(t nil)))))
The :answer message is used to insert a new method into our new product-class.
The method is described in three parts: the name (:next), a parameter list
(empty in this case), and a list of expressions to be evaluated. In this case,
we fetch samples from s1 and s2. If both are numbers, we return their product.
If either is NIL, we terminate the sound by returning nil.
The :next method assumes that s1 and s2 hold the sounds to be multiplied.
These must be installed when the object is created. Objects are created by
sending :new to a class. A new object is created, and any parameters passed to
:new are then sent in a :isnew message to the new object. Here is the :isnew
definition for product-class:
(send product-class :answer :isnew '(p1 p2)
'((setf s1 (snd-copy p1))
(setf s2 (snd-copy p2))))
Take careful note of the use of snd-copy in this initialization. The sounds s1
and s2 are modified when accessed by snd-fetch in the :next method defined
above, but this destroys the illusion that sounds are immutable values. The
solution is to copy the sounds before accessing them; the original sounds are
therefore unchanged. (This copy also takes place implicitly in most Nyquist
sound functions.)
To make this code safer for general use, we should add checks that s1 and s2
are sounds with identical starting times and sample rates; otherwise, an
incorrect result might be computed.
Now we are ready to write snd-product, an approximate replacement for
snd-prod:
(defun snd-product (s1 s2)
(let (obj)
(setf obj (send product-class :new s1 s2))
(snd-fromobject (snd-t0 s1) (snd-srate s1) obj)))
This code first creates obj, an instance of product-class, to hold s1 and s2.
Then, it uses obj to create a sound using snd-fromobject. This sound is
returned from snd-product. Note that in snd-fromobject, you must also specify
the starting time and sample rate as the first two parameters. These are copied
from s1, again assuming that s1 and s2 have matching starting times and sample
rates.
Note that in more elaborate DSP algorithms we could expect the object to have
a number of instance variables to hold things such as previous samples,
waveform tables, and other parameters.
6. SAL
Nyquist supports two languages: XLISP and SAL. In some sense, XLISP and SAL
are the same language, but with differing syntax. This chapter describes SAL:
how it works, SAL syntax and semantics, and the relationship between SAL and
XLISP, and differences between Nyquist SAL and Common Music SAL.
Nyquist SAL is based on Rick Taube's SAL language, which is part of Common
Music. SAL offers the power of Lisp but features a simple, Algol-like syntax.
SAL is implemented in Lisp: Lisp code translates SAL into a Lisp program and
uses the underlying Lisp engine to evaluate the program. Aside from the
translation time, which is quite fast, SAL programs execute at about the same
speed as the corresponding Lisp program. (Nyquist SAL programs run just
slightly slower than XLISP because of some runtime debugging support
automatically added to user programs by the SAL compiler.)
From the user's perspective, these implementation details are hidden. You can
enter SAL mode from XLISP by typing (SAL) to the XLISP prompt. The SAL input
prompt (SAL> ) will be displayed. From that point on, you simply type SAL
commands, and they will be executed. By setting a preference in the jNyqIDE
program, SAL mode will be entered automatically.
It is possible to encounter errors that will take you from the SAL
interpreter to an XLISP prompt. In general, the way to get back to SAL is by
typing (top) to get back to the top level XLISP interpreter and reset the
Nyquist environment. Then type (sal) to restart the SAL interpreter.
6.1. SAL Syntax and Semantics
The most unusual feature of SAL syntax is that identifiers are Lisp-like,
including names such as ``play-file'' and even ``*warp*.'' In SAL, most
operators must be separated from identifiers by white space. For example,
play-file is one identifier, but play - file is an expression for ``play minus
file,'' where play and file are two separate identifiers. Fortunately, no
spaces are needed around commas and parentheses.
In SAL, whitespace (any sequence of space, newline, or tab characters) is
sometimes necessary to separate lexical tokens, but otherwise, spaces and
indentation are ignored. To make SAL readable, it is strongly advised that you
indent SAL programs as in the examples here. The jNyqIDE program is purposely
insistent about SAL indentation, so if you use it to edit SAL programs, your
indentation should be both beautiful and consistent.
As in Lisp (but very unlike C or Java), comments are indicated by semicolons.
Any text from an unquoted semicolon to the end of the line is ignored.
; this is a comment
; comments are ignored by the compiler
print "Hello World" ; this is a SAL statement
As in Lisp, identifiers are translated to upper-case, making SAL
case-insensitive. For example, the function name autonorm can be typed in lower
case or as AUTONORM, AutoNorm, or even AuToNoRm. All forms denote the same
function. The recommended approach is to wirte programs in all lower case.
SAL is organized around statements, most of which contain expressions. We
will begin with expressions and then look at statements.
6.1.1. Expressions
6.1.1.1. Simple Expressions
As in XLISP, simple expressions include:
- integers (FIXNUM's), such as 1215,
- floats (FLONUM's) such as 12.15,
- strings (STRING's) such as "Magna Carta", and
- symbols (SYMBOL's) such as magna-carta. A symbol with a leading colon
(:) evaluates to itself as in Lisp. Otherwise, a symbol denotes
either a local variable, a formal parameter, or a global variable. As
in Lisp, variables do not have data types or type declarations. The
type of a variable is determined at runtime by its value.
Additional simple expressions in SAL are:
- lists such as {c 60 e 64}. Note that there are no commas to separate
list elements, and symbols in lists are not evaluated as variables
but stand for themselves. Lists may contain numbers, booleans,
symbols, strings, and other lists.
- Booleans: SAL interprets #t as true and #f as false. (As far as the
SAL compiler is concerned, t and nil are just variables. Since these
are the Lisp versions of true and false, they are interchangeable
with #t and #f, respectively.)
A curious property of Lisp and Sal is that false and the empty list are the
same value. Since SAL is based on Lisp, #f and {} (the empty list) are equal.
6.1.1.2. Operators
Expressions can be formed with unary and binary operators using infix
notation. The operators are:
- + - addition, including sounds - - subtraction, including sounds * -
multiplication, including sounds / - division (due to divide-by-zero
problems, does not operate on sounds) % - modulus (remainder after
division) ^ - exponentiation = - equal (using Lisp eql) != - not
equal > - greater than < - less than >= - greater than or equal <= -
less than or equal ~= - general equality (using Lisp equal) & -
logical and | - logical or ! - logical not (unary) @ - time shift @@
- time shift to absolute time ~ - time stretch ~~ - time stretch to
absolute stretch factor
Again, remember that operators must be delimited from their operands using
spaces or parentheses. Operator precedence is based on the following levels of
precedence:
@ @@ ~ ~~
^
/ *
% - +
~= <= >= > ~= =
!
&
|
6.1.1.3. Function Calls
A function call is a function name followed by zero or more comma-delimited
argument expressions enclosed within parentheses:
list()
piano-note(2.0, c4 + interval, 100)
Some functions use named parameters, in which case the name of the argument
with a colon precedes the argument expression.
s-save(my-snd(), ny:all, "tmp.wav", play: #t, bits: 16)
6.1.1.4. Array Notation
An array reference is a variable identifier followed by an index expression
in square brackets, e.g.:
x[23] + y[i]
6.1.1.5. Conditional Values
The special operator #? evaluates the first argument expression. If the
result is true, the second expression is evaluated and its value is returned.
If false, the third expression is evaluated and returned (or false is returned
if there is no third expression):
#?(random(2) = 0, unison, major-third)
#?(pitch >= c4, pitch - c4) ; returns false if pitch < c4
6.1.2. SAL Statements
SAL compiles and evaluates statements one at a time. You can type statements
at the SAL prompt or load a file containing SAL statements. SAL statements are
described below. The syntax is indicated at the beginning of each statement
type description: this font indicates literal terms such as keywords, the
italic font indicates a place-holder for some other statement or expression.
Bracket [like this] indicate optional (zero or one) syntax elements, while
braces with a plus {like this}+ indicate one or more occurrences of a syntax
element. Braces with a star {like this}* indicate zero or more occurrences of a
syntax element: { non-terminal }* is equivalent to [ {non-terminal}+ ].
6.1.2.1. begin and end
begin [with-stmt] {statement}+ end
A begin-end statement consists of a sequence of statements surrounded by the
begin and end keywords. This form is often used for function definitions and
after then or else where the syntax demands a single statement but you want to
perform more than one action. Variables may be declared using an optional with
statement immediately after begin. For example:
begin
with db = 12.0,
linear = db-to-linear(db)
print db, "dB represents a factor of", linear
set scale-factor = linear
end
6.1.2.2. chdir
chdir expression
The chdir statement changes the working directory. This statement is provided
for compatibility with Common Music SAL, but it really should be avoided if you
use jNyqIDE. The expression following the chdir keyword should evaluate to a
string that is a directory path name. Note that literal strings themselves are
valid expressions.
chdir "/Users/rbd/tmp"
6.1.2.3. define variable
[define] variable name [= expression] {, name [= expression]}*
Global variables can be declared and initialized. A list of variable names,
each with an optional initialization follows the define variable keywords.
(Since variable is a keyword, define is redundant and optional in Nyquist SAL,
but required in Common Music SAL.) If the initialization part is omitted, the
variable is initialized to false. Global variables do not really need to be
declared: just using the name implicitly creates the corresponding variable.
However, it is an error to use a global variable that has not been initialized;
define variable is a good way to introduce a variable (or constant) with an
initial value into your program.
define variable transposition = 2,
print-debugging-info, ; initially false
output-file-name = "salmon.wav"
6.1.2.4. define function
[define] function name ( [parameter], {, parameter}* ) statement
Before a function be called from an expression (as described above), it must
be defined. A function definition gives the function name, a list of
parameters, and a statement. When a function is called, the actual parameter
expressions are evaluated from left to right and the formal parameters of the
function definition are set to these values. Then, statement is evaluated.
The formal parameters are essentially local variables that exist only until
statement completes or a return statement causes the function evaluation to
end. As in Lisp, parameters are passed by value, so assigning a new value to a
formal parameter has no effect on the actual value. However, lists and arrays
are not copied, so internal changes to a list or array produce observable side
effects.
The parameters are meaningful only within the lexical (static) scope of
statement. They are not accessible from within other functions even if they are
called by this function.
Use a begin-end statement if the body of the function should contain more
than one statement or you need to define local variables. Use a return
statement to return a value from the function. If statement completes without a
return, the value false is returned.
6.1.2.5. display
display string {, expression}*
The display statement is handy for debugging. At present, it is only
implemented in Nyquist SAL. When executed, display prints the string followed
by a colon and then, for each expression, the expression and its value are
printed, after the last expression, a newline is printed. For example,
display "In function foo", bar, baz
prints
In function foo : bar = 23, baz = 5.3
SAL may print the expressions using Lisp syntax, e.g. if the expression is
``bar + baz,'' do not be surprised if the output is ``(sum bar baz) = 28.3.''
6.1.2.6. exec
exec expression
Unlike most other programming languages, you cannot simply type an expression
as a statement. If you want to evaluate an expression, e.g. call a function,
you must use an exec statement. The statement simply evaluates the expression.
For example,
exec set-sound-srate(22050.0) ; change default sample rate
6.1.2.7. if
if test-expr then true-stmt [else false-stmt]
An if statement evaluates the expression test-expr. If it is true, it
evaluates the statement true-stmt. If false, the statement false-stmt is
evaluated. Use a begin-end statement to evaluate more than one statement in
then then or else parts.
if x < 0 then x = -x ; x gets its absoute value
if x > upper-bound then
begin
print "x too big, setting to", upper-bound
x = upper-bound
end
else
if x < lower-bound then
begin
print "x too small, setting to", lower-bound
x = lower-bound
end
Notice in this example that the else part is another if statement. An if may
also be the then part of another if, so there could be two possible if's with
which to associate an else. An else clause always associates with the closest
previous if that does not already have an else clause.
6.1.2.8. when
when test statement
The when statement is similar to if, but there is no else clause.
when *debug-flag* print "you are here"
6.1.2.9. unless
unless test statement
The unless statement is similar to when (and if) but the statement is
executed when the test expression is false.
unless count = 0 set average = sum / count
6.1.2.10. load
load expression
The load command loads a file named by expression, which must evauate to a
string path name for the file. To load a file, SAL interprets each statement in
the file, stopping when the end of the file or an error is encountered. If the
file ends in .lsp, the file is assumed to contain Lisp expressions, which are
evaluated by the XLISP interpreter. In general, SAL files should end with the
extension .sal.
6.1.2.11. loop
loop [with-stmt] {stepping}* {stopping* action+ [finally] end
The loop statement is by far the most complex statement in SAL, but it offers
great flexibility for just about any kind of iteration. The basic function of a
loop is to repeatedly evaluate a sequence of action's which are statements.
Before the loop begins, local variables may be declared in with-stmt, a with
statement.
The stepping clauses do several things. They introduce and initialize
additional local variables similar to the with-stmt. However, these local
variables are updated to new values after the action's. In addition, some
stepping clauses have associated stopping conditions, which are tested on each
iteration before evaluating the action's.
There are also stopping clauses that provide additional tests to stop the
iteration. These are also evaluated and tested on each iteration before
evaluating the action's.
When some stepping or stopping condition causes the iteration to stop, the
finally clause is evaluated (if present). Local variables and their values can
still be accessed in the finally clause. After the finally clause, the loop
statement completes.
The stepping clauses are the following:
repeat expression
Sets the number of iterations to the value of expression, which
should be an integer (FIXNUM).
for var = expression [ then expr2 ]
Introduces a new local variable named var and initializes it to
expression. Before each subsequent iteration, var is set to the
value of expr2. If the then part is omitted, expression is
re-evaluated and assigned to var on each subsequent iteration.
Note that this differs from a with-stmt where expressions are
evaluated and variables are only assigned their values once.
for var in expression
Evaluates expression to obtain a list and creates a new local
variable initialized to the first element of the list. After
each iteration, var is assigned the next element of the list.
Iteration stops when var has assumed all values from the list.
If the list is initially empty, the loop action's are not
evaluated (there are zero iterations).
for var [from from-expr] [to | below to-expr] [by step-expr]
Introduces a new local variable named var and intialized to the
value of the expression from-expr (with a default value of 0).
After each iteration of the loop, var is incremented by the
value of step-expr (with a default value of 1). The iteration
ends when var is greater than or equal to the value of to-expr
if there is a to clause, or when var is less than or equal to
the value of to-expr when there is a below clause. If there is
no to or below clause, no interation stop test is created for
this stepping clause.
The stopping clauses are the following:
while expression
The iterations are stopped when expression evaluates to false.
Anything not false is considered to mean true.
until expression
The iterations are stopped when expression evaluates to true.
The finally clause is defined as follows:
finally statement
The statement is evaluated when one of the stepping or stopping
clauses ends the loop. As always, statement may be a begin-end
statement. If an action evaluates a return statement, the
finally statement is not executed.
Loops often fall into common patterns, such as iteratiing a fixed number of
times, performing an operation on some range of integers, collecting results in
a list, and linearly searching for a solution. These forms are illustrated in
the examples below.
; iterate 10 times
loop
repeat 10
print random(100)
end
; print even numbers from 10 to 20
; note that 20 is printed. On the next iteration,
; i = 22, so i >= 22, so the loop exits.
loop
for i from 10 to 22 by 2
print i
end
; collect even numbers in a list
loop
with lis
for i = 0 to 10 by 2
set lis @= i ; push integers on front of list,
; which is much faster than append,
; but list is built in reverse
finally result = reverse(lis)
end
; now, the variable result has a list of evens
; find the first even number in a list
result = #f ; #f means "false"
loop
for elem in lis
until evenp(elem)
finally result = elem
end
; result has first even value in lis (or it is #f)
6.1.2.12. print
print expr {, expr}*
The print statement prints a newline, then evaluates expressions and prints
their values. A blank space is printed after each value.
print "The value of x is", x
6.1.2.13. return
return expression
The return statement can only be used inside a function. It evaluates
expression and then the function returns the value of the expression to its
caller.
6.1.2.14. set
set var op expression {, var op expression}*
The set statement changes the value of a variable var according to the
operator op and the value of the expression. The operators are:
= The value of expression is assigned to var.
+= The value of expression is added to var.
*= The value of var is multiplied by the value of the expression.
&= The value of expression is inserted as the last element of the
list referenced by var. If var is the empty list (denoted by
#f), then var is assigned a newly constructed list of one
element, the value of expression.
^= The value of expression, a list, is appended to the list
referenced by var. If var is the empty list (denoted by #f),
then var is assigned the (list) value of expression.
@= Pushes the value of expression onto the front of the list
referenced by var. If var is empty (denoted by #f), then var is
assigned a newly constructed list of one element, the value of
expression.
<= Sets the new value of var to the minimum of the old value of
var and the value of expression.
>= Sets the new value of var to the maximum of the old value of
var and the value of expression.
; example from Rick Taube's SAL description
loop
with a, b = 0, c = 1, d = {}, e = {}, f = -1, g = 0
for i below 5
set a = i, b += 1, c *= 2, d &= i, e @= i, f <= i, g >= i
finally display "results", a, b, c, d, e, f, g
end
6.1.2.15. with
with var [= expression] {, var [= expression]}*
The with statement declares and initializes local variables. It can appear
only after begin or loop. If the expression is omitted, the initial value is
false. The variables are visible only inside the begin-end or loop statement
where the with statement appears. Even in loop's the variables are intialized
only when the loop is entered, not on each iteration.
6.1.2.16. exit
exit [nyquist]
The exit statement is unique to Nyquist SAL. It returns from SAL mode to the
XLISP interpreter. (Return to SAL mode by typing ``(sal)''). If nyquist is
included in the statement, then the entire Nyquist process will exit.
6.2. Interoperability of SAL and XLISP
When SAL evaluatas command or loads files, it translates SAL into XLISP. You
can think of SAL as a program that translates everything you write into XLISP
and entering it for you. Thus, when you define a SAL function, the function
actually exists as an XLISP function (created using Lisp's defun special form).
When you set or evaluate global variables in SAL, these are exactly the same
Lisp global variables. Thus, XLISP functions can call SAL functions and
vice-versa. At run time, everything is Lisp.
6.2.1. Function Calls
In general, there is a very simple translation from SAL to Lisp syntax and
back. A function call is SAL, for example,
osc(g4, 2.0)
is translated to Lisp by moving the open parenthesis in front of the function
name and removing the commas:
(osc g4 2.0)
Similarly, if you want to translate a Lisp function call to SAL, just reverse
the translation.
6.2.2. Playing Tricks On the SAL Compiler
In many cases, the close coupling between SAL and XLISP gives SAL unexpected
expressive power. A good example is seqrep. This is a special looping construct
in Nyquist, implemented as a macro in XLISP. In Lisp, you would write something
like:
(seqrep (i 10) (pluck c4))
One might expect SAL would have to define a special seqrep statement to express
this, but since statements do not return values, this approach would be
problematic. The solution (which is already fully implemented in Nyquist) is to
define a new macro sal-seqrep that is equivalent to seqrep except that it is
called as follows:
(sal-seqrep i 10 (pluck c4))
The SAL compiler automatically translates the identifier seqrep to sal-seqrep.
Now, in SAL, you can just write
seqrep(i, 10, pluck(c4))
which is translated in a pretty much semantics-unaware fashion to
(sal-seqrep i 10 (pluck c4))
and viola!, we have Nyquist control constructs in SAL even though SAL is
completely unaware that seqrep is not actually a special form.
7. Nyquist Functions
This chapter provides a language reference for Nyquist. Operations are
categorized by functionality and abstraction level. Nyquist is implemented in
two important levels: the ``high level'' supports behavioral abstraction, which
means that operations like stretch and at can be applied. These functions are
the ones that typical users are expected to use, and most of these functions
are written in XLISP.
The ``low-level'' primitives directly operate on sounds, but know nothing of
environmental variables (such as *warp*, etc.). The names of most of these
low-level functions start with ``snd-''. In general, programmers should avoid
any function with the ``snd-'' prefix. Instead, use the ``high-level''
functions, which know about the environment and react appropriately. The names
of high-level functions do not have prefixes like the low-level functions.
There are certain low-level operations that apply directly to sounds (as
opposed to behaviors) and are relatively ``safe'' for ordinary use. These are
marked as such.
Nyquist uses both linear frequency and equal-temperament pitch numbers to
specify repetition rates. Frequency is always specified in either cycles per
second (hz), or pitch numbers, also referred to as ``steps,'' as in steps of
the chromatic scale. Steps are floating point numbers such that 60 = Middle C,
61 = C#, 61.23 is C# plus 23 cents, etc. The mapping from pitch number to
frequency is the standard exponential conversion, and fractional pitch numbers
(pitch-69)/12
are allowed: frequency=440*2 . There are many predefined pitch
names. By default these are tuned in equal temperament, with A4 = 440Hz, but
these may be changed. (See Section 1.7).
7.1. Sounds
A sound is a primitive data type in Nyquist. Sounds can be created, passed
as parameters, garbage collected, printed, and set to variables just like
strings, atoms, numbers, and other data types.
7.1.1. What is a Sound?
Sounds have 5 components:
- srate M the sample rate of the sound.
- samples M the samples.
- signal-start M the time of the first sample.
- signal-stop M the time of one past the last sample.
- logical-stop M the time at which the sound logically ends, e.g. a
sound may end at the beginning of a decay. This value defaults to
signal-stop, but may be set to any value.
It may seem that there should be logical-start to indicate the logical or
perceptual beginning of a sound as well as a logical-stop to indicate the
logical ending of a sound. In practice, only logical-stop is needed; this
attribute tells when the next sound should begin to form a sequence of sounds.
In this respect, Nyquist sounds are asymmetric: it is possible to compute
sequences forward in time by aligning the logical start of each sound with the
logical-stop of the previous one, but one cannot compute ``backwards'',
aligning the logical end of each sound with the logical start of its successor.
The root of this asymmetry is the fact that when we invoke a behavior, we say
when to start, and the result of the behavior tells us its logical duration.
There is no way to invoke a behavior with a direct specification of when to
stop[Most behaviors will stop at time 1, warped according to *warp* to some
real time, but this is by convention and is not a direct specification.].
Note: there is no way to enforce the intended ``perceptual'' interpretation
of logical-stop. As far as Nyquist is concerned, these are just numbers to
guide the alignment of sounds within various control constructs.
7.1.2. Multichannel Sounds
Multichannel sounds are represented by Lisp arrays of sounds. To create an
array of sounds the XLISP vector function is useful. Most low-level Nyquist
functions (the ones starting with snd-) do not operate on multichannel sounds.
Most high-level functions do operate on multichannel sounds.
7.1.3. Accessing and Creating Sound
Several functions display information concerning a sound and can be used to
query the components of a sound. There are functions that access samples in a
sound and functions that construct sounds from samples.
(sref sound time)
Accesses sound at the point time, which is a local time. If time does not
correspond to a sample time, then the nearest samples are linearly
interpolated to form the result. To access a particular sample, either
convert the sound to an array (see snd-samples below), or use snd-srate
and snd-t0 (see below) to find the sample rate and starting time, and
compute a time (t) from the sample number (n):t=(n/srate)+t0 Thus, the
th
lisp code to access the n sample of a sound would look like: (sref
sound (global-to-local (+ (/ n (snd-srate sound)) (snd-t0 sound)))) Here
is why sref interprets its time argument as a local time: > (sref (ramp
1) 0.5) ; evaluate a ramp at time 0.5 0.5 > (at 2.0 (sref (ramp 1) 0.5)) ;
ramp is shifted to start at 2.0 ; the time, 0.5, is shifted to 2.5 0.5 If
you were to use snd-sref, which treats time as global, instead of sref,
which treats time as local, then the first example above would return the
same answer (0.5), but the second example would return 0. Why? Because
the (ramp 1) behavior would be shifted to start at time 2.0, but the
resulting sound would be evaluated at global time 0.5. By definition,
sounds have a value of zero before their start time.
(sref-inverse sound value)
Search sound for the first point at which it achieves value and return the
corresponding (linearly interpolated) time. If no inverse exists, an
error is raised. This function is used by Nyquist in the implementation
of time warping.
(snd-from-array t0 sr array)
Converts a lisp array of FLONUMs into a sound with starting time t0 and
sample rate sr. Safe for ordinary use. Be aware that arrays of
floating-point samples use 14 bytes per sample, and an additional 4 bytes
per sample are allocated by this function to create a sound type.
(snd-fromarraystream t0sr object)
Creates a sound for which samples come from object. The starting time is
t0 (a FLONUM), and the sample rate is sr. The object is an XLISP object
(see Section IV.11 for information on objects.) A sound is returned. When
the sound needs samples, they are generated by sending the message :next
to object. If object returns NIL, the sound terminates. Otherwise, object
must return an array of FLONUMs. The values in these arrays are
concatenated to form the samples of the resulting sound. There is no
provision for object to specify the logical stop time of the sound, so the
logical stop time is the termination time.
(snd-fromobjectt0 sr object)
Creates a sound for which samples come from object. The starting time is
t0 (a FLONUM), and the sample rate is sr. The object is an XLISP object
(see Section IV.11 for information on objects. A sound is returned. When
the sound needs samples, they are generated by sending the message :next
to object. If object returns NIL, the sound terminates. Otherwise, object
must return a FLONUM. There is no provision for object to specify the
logical stop time of the sound, so the logical stop time is the
termination time.
(snd-extent sound maxsamples)
Returns a list of two numbers: the starting time of sound and the
terminate time of sound. Finding the terminate time requires that samples
be computed. Like most Nyquist functions, this is non-destructive, so
memory will be allocated to preserve the sound samples. If the sound is
very long or infinite, this may exhaust all memory, so the maxsamples
parameter specifies a limit on how many samples to compute. If this limit
is reached, the terminate time will be (incorrectly) based on the sound
having maxsamples samples. This function is safe for ordinary use.
(snd-fetch sound)
Reads samples sequentially from sound. This returns a FLONUM after each
call, or NIL when sound terminates. Note: snd-fetch modifies sound; it is
strongly recommended to copy sound using snd-copy and access only the copy
with snd-fetch.
(snd-fetch-array sound len step)
Reads sequential arrays of samples from sound, returning either an array
of FLONUMs or NIL when the sound terminates. The len parameter, a FIXNUM,
indicates how many samples should be returned in the result array. After
the array is returned, sound is modified by skipping over step (a FIXNUM)
samples. If step equals len, then every sample is returned once. If step
is less than len, each returned array will overlap the previous one, so
some samples will be returned more than once. If step is greater than len,
then some samples will be skipped and not returned in any array. The step
and len may change at each call, but in the current implementation, an
internal buffer is allocated for sound on the first call, so subsequent
calls may not specify a greater len than the first. Note: snd-fetch-array
modifies sound; it is strongly recommended to copy sound using snd-copy
and access only the copy with snd-fetch-array.
(snd-flatten sound maxlen)
This function is identical to snd-length. You would use this function to
force samples to be computed in memory. Normally, this is not a good thing
to do, but here is one appropriate use: In the case of sounds intended for
wavetables, the unevaluated sound may be larger than the evaluated (and
typically short) one. Calling snd-flatten will compute the samples and
allow the unit generators to be freed in the next garbage collection.
Note: If a sound is computed from many instances of table-lookup
oscillators, calling snd-flatten will free the oscillators and their
tables. Calling (stats) will print how many total bytes have been
allocated to tables.
(snd-length sound maxlen)
Counts the number of samples in sound up to the physical stop time. If
the sound has more than maxlen samples, maxlen is returned. Calling this
function will cause all samples of the sound to be computed and saved in
memory (about 4 bytes per sample). Otherwise, this function is safe for
ordinary use.
(snd-maxsamp sound)
Computes the maximum of the absolute value of the samples in sound.
Calling this function will cause samples to be computed and saved in
memory. (This function should have a maxlen parameter to allow
self-defense against sounds that would exhaust available memory.)
Otherwise, this function is safe for ordinary use. This function will
probably be removed in a future version. See peak, a replacement ( page
27).
(snd-play expression)
Evaluates expression to obtain a sound or array of sounds, computes all of
the samples (without retaining them in memory), and returns. If this
happens faster than real time for interesting sounds, you might want to
modify Nyquist to actually write the samples directly to an audio output
device. Meanwhile, since this function does not save samples in memory or
write them to a disk, it is useful in determining how much time is spent
calculating samples. See s-save (Section 7.5) for saving samples to a
file, and play (Section 7.5) to play a sound. This function is safe for
ordinary use.
(snd-print-tree sound)
Prints an ascii representation of the internal data structures
representing a sound. This is useful for debugging Nyquist. This
function is safe for ordinary use.
(snd-samples sound limit)
Converts the samples into a lisp array. The data is taken directly from
the samples, ignoring shifts. For example, if the sound starts at 3.0
seconds, the first sample will refer to time 3.0, not time 0.0. A maximum
of limit samples is returned. This function is safe for ordinary use, but
like snd-from-array, it requires a total of slightly over 18 bytes per
sample.
(snd-srate sound)
Returns the sample rate of the sound. Safe for ordinary use.
(snd-time sound)
Returns the start time of the sound. This will probably go away in a
future version, so use snd-t0 instead.
(snd-t0 sound)
Returns the time of the first sample of the sound. Note that Nyquist
operators such as add always copy the sound and are allowed to shift the
copy up to one half sample period in either direction to align the samples
of two operands. Safe for ordinary use.
(snd-print expression maxlen)
Evaluates expression to yield a sound or an array of sounds, then prints
up to maxlen samples to the screen (stdout). This is similar to snd-save,
but samples appear in text on the screen instead of in binary in a file.
This function is intended for debugging. Safe for ordinary use.
(snd-set-logical-stop sound time)
Returns a sound which is sound, except that the logical stop of the sound
occurs at time. Note: do not call this function. When defining a
behavior, use set-logical-stop or set-logical-stop-abs instead.
(snd-sref sound time)
Evaluates sound at the global time given by time. Safe for ordinary use,
but normally, you should call sref instead.
(snd-stop-time sound)
Returns the stop time of sound. Sounds can be ``clipped'' or truncated at
a particular time. This function returns that time or MAX-STOP-TIME if he
programmer has not specified a stop time for the sound. Safe for ordinary
use.
(soundp sound)
Returns true iff sound is a SOUND. Safe for ordinary use.
(stats)
Prints the memory usage status. See also the XLISP mem function. Safe
for ordinary use. This is the only way to find out how much memory is
being used by table-lookup