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