Snd Customization and Extension Part 2


related documentationsnd.htmlextsnd.htmlclm.htmlsndlib.htmlsndscm.htmlindex.html

Snd Startup


Snd resources

In the Motif version, there are a few X-style resources that Snd looks for (see Snd.ad):

initFile "~/.snd"
epsFile"snd.eps"
overwriteCheck 0
autoResize 1
horizontalPanes 0


buttonFont -*-times-medium-r-*-*-14-*-*-*-*-*-iso8859-1
boldbuttonFont -*-times-bold-r-*-*-14-*-*-*-*-*-iso8859-1
axisLabelFont -*-times-medium-r-normal-*-20-*-*-*-*-*-iso8859-1
axisNumbersFont -*-courier-medium-r-normal-*-14-*-*-*-*-*-iso8859-1
helpTextFont 9x15
listenerFont default


useSchemes none
highlightcolor ivory1
basiccolor ivory2
positioncolor ivory3
zoomcolor ivory4
cursorcolor red
selectioncolor lightsteelblue1
mixcolor lightgreen
mixfocuscolor yellow2
listenercolor aliceblue
envedwaveformcolor blue
filterwaveformcolor blue
mixwaveformcolor darkgray
graphcolor white
selectedgraphcolor white
datacolor black
selecteddatacolor black
markcolor red
pushedbuttoncolor lightsteelblue1
sashcolor lightgreen

If you have the HTML widget loaded, the following resources are also available:

htmlDir "."
htmlWidth 600
htmlHeight 400
htmlFontSizeList "14,10,24,24,18,14,12"
htmlFixedFontSizeList "14,10"

You can experiment with other choices by using the -xrm command line argument:

  snd -xrm '*Highlightcolor: Red' oboe.snd
  snd -xrm '*AxisNumbersFont: 6x10' oboe.snd
  snd -xrm '*overwriteCheck: 1' oboe.snd
  snd -xrm '*useSchemes: all' -xrm '*scheme: Pacific'
  snd -xrm '*fontList: 9x15' oboe.snd
  snd -xrm '*listenerFont: 6x10' oboe.snd
  snd -xrm '*mixwaveformcolor: red' oboe.snd -notebook
  snd oboe.snd pistol.snd -xrm '*selectedgraphcolor: black' -xrm '*selecteddatacolor: white'
  snd oboe.snd -title hiho -display hummer.hiho:0.0 -xrm '*chn-graph*backgroundPixmap: text.xpm'

The color names can be found in rgb.scm. If you use SGI color schemes (the useSchemes resource), most of the color resources mentioned above are ignored (the cursor and selection colors are never ignored). If color schemes are available they're listed in /usr/lib/X11/schemes, probably -- it's unfortunate that there is the language Scheme used by Guile, and the notion of an SGI color scheme -- there is no connection between the two. The last example sets the window title to "hiho", rather than "snd", displays the window on the machine hummer.hiho (presumably accessible over the net), and tiles the graph backgrounds with the contents of text.xpm. To get the -geometry argument to work, set the autoResize resource to 0:

  snd oboe.snd -geometry 800x200 -xrm '*autoResize: 0'

These resources can be set in your .Xdefaults file:

snd*buttonFont:         -adobe-times-medium-r-*-*-14-*-*-*-*-*-*-*
snd*boldbuttonFont:     -adobe-times-bold-r-*-*-14-*-*-*-*-*-*-*
snd*axisLabelFont:      -adobe-times-medium-r-normal-*-18-*-*-*-*-*-*-*
snd*axisNumbersFont:    9x15
snd*fontList:           9x15
snd*helpTextFont:       9x15

The autoResize resource determines how Snd acts when files are added or removed from its overall display. The default (1) causes Snd to expand or contract the main window's size to accommodate the sounds (many people find this distracting); if autoResize is 0, the outer window size remains the same, and the sounds try to fit as best they can. See also the variable auto-resize. If overwriteCheck is 1, Snd asks before overwriting existing files. The horizontalPanes resource is equivalent to the -h flag; if 1, sounds are layed out horizontally rather than vertically; if 2, you get a notebook widget holding the sounds.

The various color resources are:

basiccolordefault background color everywherebasic-color
cursorcolor color of the cursor cursor-color
datacolor unselected data color data-color
envedwaveformcolor color of envelope editor waveform enved-waveform-color
filterwaveformcolor color of control panel filter waveformfilter-waveform-color
graphcolor unselected channels' graph background graph-color
highlightcolor highlighting here and there highlight-color
listenercolor background color of the listener listener-color
listenertextcolor text color in the listener listener-text-color
markcolor color of the mark indicator mark-color
mixcolor used for mix waveforms mix-color
selectedmixcolor selected mix waveforms selected-mix-color
positioncolor color of position sliders position-color
pushedbuttoncolor color of pushed button pushed-button-color
sashcolor color of paned window sash handles sash-color
selecteddatacolor color of the data in selected channel selected-data-color
selectedgraphcolor background of selected channel's graphselected-graph-color
selectioncolor color of an active selection selection-color
textfocuscolor color of text field with focus text-focus-color
zoomcolor color of zoom sliders zoom-color

Each of these colors can be set in Guile using the second name given above ("basic-color"). Colors are defined by calling make-color with the three red/green/blue values, each a float between 0.0 and 1.0. (set! (basic-color) (make-color 1.0 0.0 0.0)) sets the overall background color of Snd to red. rgb.scm defines all the standard X11 color names (you probably don't want to load the whole thing; just use the names as needed).

There are several other resources that set various widget sizes: zoomSliderWidth, positionSliderWidth, toggleSize, sashSize, sashIndent, channelSashSize, channelSashIndent, and envedPointSize. And several more color resources: whitecolor (list background), blackcolor (recorder VU meter text), redcolor (buttons, VU clipping, etc), greencolor (a few buttons), yellowcolor (a few envelope editor buttons), lightbluecolor (the recorder), and lighterbluecolor (the fft option panel).

In the GTK version, you can load a gtkrc file, overriding all Snd defaults, with the function parse-rc-file:

  (parse-rc-file "/home/bil/test/gtk+-1.2.9/gtk/testgtkrc")

Snd invocation flags

The following flags are recognized by Snd (leaving aside all the usual Xt/X-related flags like -xrm).

-h -horizontallayout sounds as horizontal panes
-v -vertical layout sounds vertically (the default)
-notebook layout sounds in a notebook widget (Motif 2.0 or later)
-separate layout sounds each in a separate window (lisp listener in main window)
--help print some help, version info, and exit
--version print version info
-noglob don't read /etc/snd.conf
-noinit don't read ~/.snd, if any
-p -preload <dir> preload sound files in directory <dir> (for example, snd -p .)
-l -load <file> load guile (scheme) code in <file> (for example, snd -l test.scm)
-e -eval expr evaluate expr
-b -batch <file> load guile (scheme) code in <file> as a batch (no GUI) job

The -e switch evaluates its argument as though it had been passed to M-X. The initialization file, if any, is loaded first, then the arguments are processed in order. For example

snd -e "(set! (data-color) (make-color 1 0 0))" oboe.snd

reads ~/.snd, if any, then sets the (unselected) data color to red, then opens oboe.snd.

./snd -eval '(begin (display (+ 1 2)) (exit))'

prints "3" and exits. The "-title" argument works in both versions of Snd. The following adds "WAV" to the sound file extension table before preloading the directory:

snd -e '(add-sound-file-extension "WAV")' -p /home/bil/sounds

notebookcolors

The initialization file

When Snd starts up, it looks for an "initialization file", normally named "~/.snd" (its name can be set via the X resource mechanism, or through the environment variable SND_INIT_FILE). This optional file is supposed to be just like emacs' .emacs file, containing any customizations or extensions that you want loaded whenever Snd starts up. For example, say we want the Snd window to start out 800x500, want to predefine an envelope named "env1", and want the file selection box to default to showing just sound files. We make ~/.snd and put in it:

(set! (window-width) 800)
(set! (window-height) 500)
(defvar env1 '(0 0 1 1 2 0))
(set! (just-sounds) #t)

In addition, we could add our own analysis functions or whatever. In more complex situations, you may want an initialization file particular to a given machine, and global across users; the name of this optional global initialization file is "/etc/snd.conf". It is read before the user's local file; both can, of course, be absent. To override reading the global init file when Snd is invoked, include the switch -noglob. To override the local init file, use -noinit. Here's a more extended example:

(use-modules (ice-9 popen) (ice-9 debug) (ice-9 format))

(set! (window-width) 800)
(set! (window-height) 500)
(set! (button-font) "-adobe-times-medium-r-*-*-14-*-*-*-*-*-*-*")
(set! (bold-button-font) "-adobe-times-bold-r-*-*-14-*-*-*-*-*-*-*")
(set! (axis-label-font) "-adobe-times-medium-r-normal-*-18-*-*-*-*-*-*-*")
(set! (axis-numbers-font) "9x15")

(set! (show-mix-waveforms) #t)
(set! (listener-prompt) ":")
(show-listener)
(set! (show-indices) #t)

(define beige (make-color 0.96 0.96 0.86))
(define blue (make-color 0 0 1))
(set! (selected-graph-color) beige)
(set! (selected-data-color) blue)

(add-hook! mouse-enter-graph-hook 
  (lambda (snd chn) 
    (focus-widget (car (channel-widgets snd chn)))))
(add-hook! mouse-enter-listener-hook 
  (lambda (widget) 
    (focus-widget widget)))
(add-hook! mouse-enter-text-hook
  (lambda (w)
    (focus-widget w)))

If you loaded Snd with GSL, and have set the GSL_IEEE_MODE environment variable, it will override Snd's default arithmetic mode settings. GSL recommends the setting:

GSL_IEEE_MODE=double-precision,mask-underflow,mask-denormalized

Runtime modules and external programs

It is possible to load your own C code into Snd at run-time or use any external program from within Snd as an editing function. And, perhaps most useful, you can run Snd as an Emacs subjob.


Snd as an Emacs subjob

Snd watches stdin; any input received is evaluated as if typed in Snd's lisp listener; any subsequent output is sent to stdout; presumably any process could communicate with Snd in this manner. But the intention here was to connect to Emacs via ILISP (available at http://sourceforge.net/projects/ilisp, and possibly built into Xemacs). Once you have ILISP, put this code in your .emacs file:

(require 'ilisp)
(defdialect snd "Snd" ilisp
  (setq ilisp-block-command "(begin \n%s)")
  (setq ilisp-load-command "(load \"%s\")")
  (setq ilisp-init-hook '((lambda () (ilisp-init nil nil nil))))
  (setq comint-prompt-regexp "^>+")
  (setq ilisp-directory-command "(getcwd)")
  (setq	ilisp-set-directory-command "(chdir \"%s\")")
  (setq	ilisp-complete-command
	"(map (lambda (sym) 
                (list (symbol->string sym))) 
              (apropos-internal \"^%s\"))")
  (local-set-key "\C-x\C-h" '(lambda (s)
			       (interactive "sSnd help: ")
			       (insert "(snd-help " s ")")
			       (comint-send-input))))

  (setq snd-program "snd")

You can bind this to some key via:

  (global-set-key "\C-x\C-l" 'snd)

Now C-x C-l in Emacs starts Snd as a subjob; anything you type in the Emacs *snd* buffer is sent to Snd (very much as if you were running CLM as an Emacs subjob), and Snd's output is appended to the *snd* buffer. Also, C-x C-h prompts for some Snd entity and appends the help text for that entity in the snd buffer. The following code tries to implement Snd's name completion upon C-x C-i in the *snd* buffer:

(defun handle-snd-completion (process string)
  (let ((buffer (process-buffer process)))
    (save-excursion
      (set-buffer buffer)
      (goto-char (point-max))
      (insert (substring string 1 (- (length string) 3))))
    (goto-char (point-max))
    (display-buffer buffer)))
      
(defun snd-complete-symbol ()
  (interactive)
  (let* ((end (point))
	 (start (save-excursion (skip-syntax-backward "w_") (point)))
	 (pattern (buffer-substring-no-properties start end))
	 (proc (get-buffer-process (current-buffer)))
	 (old-filter (process-filter proc)))
    (delete-region start end)
    (set-process-filter proc 'handle-snd-completion)
    (process-send-string proc (format "(snd-completion \"%s\")\n" pattern))
    (accept-process-output proc)
    (set-process-filter proc old-filter)))

and add this to the defdialect block:

  (local-set-key "\C-x\C-i" 'snd-complete-symbol)

But for some reason it needs an open-paren? snd-program is the name of the snd image; you can include whatever startup switches you like:

(setq snd-program "snd -horizontal ~/cl/oboe.snd")

Dynamically loaded modules

You can import shared object files into Snd at any time. You need to build Snd with -lguile (that is, load it with the guile shared library, not libguile.a); if the loader can't find libguile.so.2 (or whatever), add its directory to your LD_LIBRARY_PATH; for example, if it's on /usr/local/lib, setenv LD_LIBRARY_PATH /usr/local/lib. Next add Guile wrappers to your C code:

  /* cscm.c */
  #include <math.h>
  #include <stdio.h>
  #include <stdlib.h>
  #include <guile/gh.h>

  int hiho (int a) 
  { /* this is the function we want to call from Snd */
    return(1 + a);
  }

  SCM hiho_wrapper(SCM a) 
  { /* this tells Guile how to interpret the arguments and return value of hiho */
    return(scm_long2num(hiho(scm_num2int(a, 0, "hiho"))));
  }

  void init_hiho() 
  { /* this declares hiho within Guile calling the wrapper which calls the C function hiho */
    scm_c_define_gsubr("hiho", 1, 0, 0, hiho_wrapper);
  }

Next compile your code into a shared object (this example is for Linux):

  cc -c cscm.c 
  ld -shared -o cscm.so cscm.o -lguile

Now go to Snd's lisp listener and,

  (define lib (dynamic-link "/home/bil/cl/cscm.so"))
  (dynamic-call "init_hiho" lib)
  (hiho 3)

The function we actually want loaded into Guile here is "hiho". We define a wrapper for it to handle the translation between Guile (Scheme) variable types and C ("hiho_wrapper"), and a procedure to define hiho in Guile ("init_hiho"). Once loaded ("dynamic-link"), we can call the initialization function ("dynamic-call"), and thereafter treat "hiho" as though it had been defined in Guile/Snd to begin with. After both the dynamic-link and dynamic-lib calls, the listener will print "#<unspecified>" or something equally obscure to indicate in its own peculiar way that all went well. M-x (hiho 4) will print 5 in the minibuffer.

As a slightly more useful example, let's import the bessel J0 function from GSL (Gnu Scientific Library); in this case, we need to build Snd with GSL (the easiest way is to include the --with-gsl option to configure). Then make a file (say "gsl-ex.c"):

#include <guile/gh.h>
#include <gsl/gsl_sf_bessel.h>

static SCM scm_j0(SCM x)
{ /* calls GSL function gsl_sf_bessel_J0_e */
  gsl_sf_result res;
  gsl_sf_bessel_J0_e(scm_num2dbl(x, "j0"), &res);
  return(scm_make_real(res.val));
}

void init_gsl_j0(void)
{ /* links scm_j0 into Snd under the name j0 */
  scm_c_define_gsubr("j0", 1, 0, 0, scm_j0);
}

Now the usual compile, load, link into Snd sequence:

/home/bil/snd-4/ cc gsl-ex.c -c -Wall
/home/bil/snd-4/ ld -shared gsl-ex.o -o gsl-ex.so -lguile
/home/bil/snd-4/ ./snd

>(define lib (dynamic-link "/home/bil/snd-4/gsl-ex.so"))
#<unspecified>
>(dynamic-call "init_gsl_j0" lib)
#<unspecified>
>(j0 0.0)
1.0
>(j0 2.0)
0.223890779141236
>(define (bes-fm dur freq amp ratio index)
 ;; bessel-FM from CLM
   (let* ((car-ph 0.0)
	  (mod-ph 0.0)
 	  (car-incr (hz->radians freq))
	  (mod-incr (* ratio car-incr))
	  (ampenv (make-env :envelope '(0 0 25 1 75 1 100 0) :scaler amp :end dur))
	  (output (make-vct dur)))
     (do ((i 0 (1+ i)))
	 ((= i dur))
       (vct-set! output i (* (env ampenv) (j0 car-ph)))
       (set! car-ph (+ car-ph car-incr (* index (j0 mod-ph))))
       (set! mod-ph (+ mod-ph mod-incr)))
     (vct->samples 0 dur output)))
#<unspecified>
>(bes-fm 22050 440 10.0 1.0 8.0)

It is possible to have the gsl-ex.so library loaded automatically, including the dynamic-link. (This part of Guile's module system may change, but they've been saying that for years). First, include this in gsl-ex.c:

void scm_init_gsl_bessel_module ()
{
  scm_register_module_xxx ("gsl bessel", init_gsl_j0);
}

This tells Guile to call init_gsl_j0 when the module is loaded (thereby defining j0 for us). Now we'll call this module (gsl bessel) just for laughs; this means when we type

>(use-modules (gsl bessel))

in Snd's listener, the loader will look for gsl/libbessel.so, calling scm_init_gsl_bessel_module if possible. So we create a subdirectory named gsl, and put gsl-ex.so in it under the name libbessel.so. Then whenever we want to use j0, we simply use the use-modules line given above.

To acess and edit sound data from such a module, use the Snd functions make-sample-reader and loop-samples. loop-samples takes a sample reader, a pointer to a (C) float function that takes one float argument (the current sample), the number of times to call that function, and a name for the editing operation for the edit history list. For example, the following module defines a function that scales the data by 2:

#include <guile/gh.h>

static float a2(float b) {return(b * 2.0);}

static SCM get_a2(void) {return(scm_ulong2num((unsigned long)a2));}

void init_hiho() {scm_c_define_gsubr("get-a2", 0, 0, 0, get_a2);}

The "a2" function will be called from Snd as follows; first make the shared object module and load it as above, then

(loop-samples (make-sample-reader 0) (get-a2) 50828 "a2")

There's a way to make a module like this loadable via the (use-modules ...) syntax in Guile, but I haven't delved into it yet.

To call internal Snd functions, you can do something like the following: declare an SCM variable to hold the procedure variable, in the init function set the variable to the value of scm_symbol_value0("function-name"), and in the rest of the code call it via scm_apply. The following is a sketch using the Snd internal "srate" function:

  #include <guile/gh.h>
  static SCM g_srate;
  static SCM srate_wrapper(SCM a) {return(scm_call_1(g_srate, a));}
  void init_srate(void) 
  { 
    scm_c_define_gsubr("my-srate", 1, 0, 0, srate_wrapper);
    g_srate = scm_symbol_value0("srate");
  }

Alternatively, you can simply use scm_eval_str0:

  scm_eval_str0("(recorder-dialog)");
  scm_eval_str0("(open-sound \"oboe.snd\")");
  srate = scm_num2int(scm_eval_str0("(srate)"), 0, "");

There is a Scheme to C compiler named Hobbit, but it's not very useful in our context. If you have pure Scheme functions, it may be able to speed them up, but if you want to call Snd/CLM/Sndlib functions, special support is needed, and even with that support my timing tests did not get more than a 50% improvement in speed. Recently, however, Keisuke Nishida started work on a Guile compiler that promises to speed up Snd-Scheme code by at least an order of magnitude. When it becomes available, I'll probably move the vct support stuff back into Scheme, and reduce some of the clutter of functions aimed at fast data access. I think all that's really needed from Snd are the sample-reader functions.

To use sndlib (clm) functions from a shared object file can be a bit tricky; first you need to build sndlib as a shared library (named "libsndlib.so" for loader convenience). If it's not in a directory the loader normally searches, you need to use -rpath in the loader arguments to force the loader to look in the right place. Here's an example that implements new-effects.scm's flanger in a file named eff.c:

#include <guile/gh.h>
#include <stdio.h>
#include <math.h>
#include "sndlib.h"
#include "clm.h"

typedef struct {
  mus_any *del,*ri;
} flg;

static flg *make_flange(float flg_speed, float flg_amount, float flg_time)
{
  flg *gens;
  int len;
  len = (int)(flg_time * 22050) + 1;
  gens = (flg *)calloc(1, sizeof(flg));
  gens->del = mus_make_delay(len, NULL, (int)(len + 1 + flg_amount));
  gens->ri = mus_make_rand_interp(flg_speed, flg_amount);
  return(gens);
}

static float run_flange(float inval, void *ugens)
{
  flg *gens = (flg *)ugens;
  return(0.75 * (inval + mus_delay(gens->del, inval, mus_rand_interp(gens->ri, 0.0))));
}

static void free_flange(flg *gens)
{
  mus_free(gens->del);
  mus_free(gens->ri);
  free(gens);
}

static SCM g_make_flange(SCM speed, SCM amount, SCM time)
{
  SCM result;
  flg *gens;
  gens = make_flange(scm_num2dbl(speed, "make-flange"), scm_num2dbl(amount, "make-flange"), scm_num2dbl(time, "make-flange"));
  return(scm_ulong2num((unsigned long)(gens)));
}

static SCM g_get_flange(void)
{
  return(scm_ulong2num((unsigned long)run_flange));
}

static SCM g_free_flange(SCM g_gens)
{
  free_flange((flg *)scm_num2ulong(g_gens));
}

void init_eff(void)
{
  mus_sound_initialize();
  init_mus_module();
  scm_c_define_gsubr("free-flange", 1, 0, 0, g_free_flange);
  scm_c_define_gsubr("get-flange", 0, 0, 0, g_get_flange);
  scm_c_define_gsubr("make-flange", 3, 0, 0, g_make_flange);
}

Now compile eff.c and turn it into eff.so, then

(define lib (dynamic-link "/home/bil/cl/eff.so"))
(dynamic-call "init_eff" lib)

If this gets the catch-all error "file not found: eff.so", it's actually complaining about the libraries, not eff.so itself. In a case where I had both guile and sndlib on unusual directories, I used:

ld -shared -o eff.so eff.o -rpath /home/bil/test/lib -L/home/bil/test/lib -lguile -rpath /home/bil/cl -L/home/bil/cl -lsndlib -ldl -lm

and changed the Snd makefile LIBS statement to:

LIBS = -Xlinker -rpath -Xlinker /home/bil/test/lib -L/home/bil/test/lib -lguile -Xlinker -rpath -Xlinker /home/bil/cl -L/home/bil/cl -lsndlib -lmcheck -L/usr/X11R6/lib -lXm -lXp -lXpm -lXt -lXext -lX11 -ldl -lm

If all else fails (as it usually does when using libtool's dynamic linking), I've included a simple dlopen call in Snd as a fallback -- (dlopen filename) will return a "handle" (like dynamic-link), or give you a truthful error message. The function corresponding to "dynamic-call" is dlinit: (dlinit handle func-name) where the function in question takes no arguments. If the sndlib.so business won't work for you, build your .so file with the sndlib object files included explicitly:

  ld -shared eff.o -o eff.so io.o headers.o audio.o sound.o clm.o vct.o sndlib2xen.o clm2xen.o

Then in Snd (assume we're on /home/bil/cl),

(define handle (dlopen "/home/bil/cl/eff.so"))
(dlinit handle "init_eff")

Once the dynamic linker is happy, the flanger can be invoked:

(loop-samples (make-sample-reader 0) (get-flange) (frames) "flange" (make-flange 2.0 5.0 0.001))

which is about 30 times faster than the interpreted version in new-effects.scm. There is (yet another) gotcha in this business: if Snd is built in the normal way incorporating the sndlib code directly, the call (mus-srate) in the listener does not refer to the same thing that mus_srate refers to in eff.c; the latter is using the shared library's variable whereas Snd is using the variable incorporated at compile time. In addition, the initialization used in sndlib doesn't carry over to the shared library (this is probably a bug...). So, you can (set! (mus-srate) 22050), and then find that mus_srate is returning 0! It's probably best to set the shared library's mus_srate explicitly when you initialize your module, or build Snd with sndlib.so (I've actually never done this...). I hope in the future to provide various modules (for the effects menu, for example).


External Programs

Any external program that knows about sound files can be used to perform editing operations from Snd. You thereby get Snd's display, analysis, header and format conversion, and edit-tree support, and can concentrate on the actual sound effect you're developing. The original impetus for Snd came from CLM, a large lisp-listener based program which normally runs without a graphical user interface, and without any simple way to move around in what Snd calls the edit history. Since interprocess communication proved problematic in this case, the communication path was simplified to consist of little more than shared files, with CLM treated as a batch program. A nice side-effect of this is that any other program can fit the same mold.

For example, say we have a sound processing CLM instrument we like; it takes two sound file names as its arguments, reading the first and writing the second. In Snd we write the current edited state to a temporary file, start CLM, call the instrument passing it the input and output filenames, then pass its output back to Snd. Snd then replaces the current data with the data our instrument wrote, as if it had incorporated that instrument as an editing operation from the beginning.

To write out the current data, we use either save-sound-as or save-selection. Our program then writes its changes, and Snd reads these back in as edits using set! samples. We can delete the Snd output at that point, though the input (changed) files should be left for Snd to handle. Here are some examples, based on the Snd-4 sound-to-temp functions and its friends; these are implemented in snd4.scm. The basic idea is that you can write out any portion of the current data, and (independent of any prior write) use any external sound file as an edit of any portion.


STK

STK is a synthesis toolkit developed by Perry Cook and Gary Scavone. Like many such programs, it reads a score file and produces an output file. We'll use it here to replace the current sound with a clarinet tone:

(define stk
  (lambda ()
    (let* ((str "")
           (data (sound-to-temp))
           (fil (open-pipe "syntmono Clarinet -s /tmp/test < scores/hiho.ski" "r")))
      (do ((val (read-char fil) (read-char fil)))
          ((eof-object? val))
        (set! str (string-append str (string val))))
      (close-pipe fil)
      (temp-to-sound data "/tmp/test.snd" "(STK clarinet)")
      str)))

hiho.ski is:

NoteOn          0.000000 1 60 127.000000
NoteOff         0.126032 1 60 63.500000

The basic sequence is: sound-to-temp writes out the current (possibly edited) state of the selected sound(s) in Snd as a temp file. sound-to-temp returns an opaque object which we will later pass to temp-to-sound to complete the edit. But first, we open a pipe, call STK as a batch job, and read in whatever it prints out (so we can see how the call went). Then we call temp-to-sound passing it the object mentioned earlier, the new filename (the data written by STK that will replace the current data in Snd), and the associated edit-history reference to the operation. In brief:

  [sound | selection]-to-[temp | temps]
  call external program on the data and write new data
  [temp | temps]-to-[sound | selection]

But this function can't safely be called twice because it always writes "test.snd", and it isn't very useful as an editing operation because it completely ignores the current Snd data. The next steps are to write our data using safe temporary filenames, and read the current data using temp-filenames. We'll also apply this to the current selection, rather than the full file. Since I don't know enough about STK to get it to read an input file, I'll use Sox for the next examples.


Sox

Sox is a widely available and well-known program for sound format conversions and various sound effects. In this case, we'll read and write NeXT files, and use Sox's copy "effect".

(define sox
  (lambda ()
    (let ((data (selection-to-temp)))
      (if data
	  (let* ((str "")
                 (input-names (temp-filenames data))
                 (output-name (string-append (tmpnam) ".snd"))
		 (cmd (string-append
		        "sox -t .au \""
                        (vector-ref input-names 0)
			"\" -t .au \""
			output-name
			"\" copy"))
		 (fil (open-pipe cmd "r")))
	    (do ((val (read-char fil) (read-char fil))) 
		((eof-object? val))
	      (set! str (string-append str (string val))))
	    (close-pipe fil)
	    (temp-to-selection data output-name "(sox copy)")
	    str)
	  (report-in-minibuffer "no current selection")))))

We use the Guile built-in function tmpnam to get an output file name that doesn't collide with any existing file; We then read the incoming filename that Snd wrote (temp-filenames), and pass that to Sox. This is a very complicated no-op, since Sox in this case merely copies its input to its output. We're assuming NeXT/Sun files (the "-t .au" business), and we're blithely ignored the possibility that we might be editing any number of sounds, each with any number of channels. To deal with the latter, we need to notice how many mono files have been passed to us (in the case of sound-to-temps), or our external program needs to be able to handle a file with arbitrarily many channels (sound-to-temp). In the next example, we'll loop through the mono files, processing each in turn. We'll also start packaging up the boilerplate a bit.

(define execute-and-wait
  (lambda (cmd)
    (let ((str "")
	  (fil (open-pipe cmd "r")))
      (do ((val (read-char fil) (read-char fil))) 
	  ((eof-object? val))
	(set! str (string-append str (string val))))
      (close-pipe fil)
      str)))

(define loop-through-files
  (lambda (description make-cmd)
    (let* ((data (sound-to-temps))
	   (input-names (temp-filenames data))
	   (files (vector-length input-names))
	   (output-names (make-vector files "")))
      (do ((i 0 (1+ i)))
	  ((= i files))
	(vector-set! output-names i (string-append (tmpnam) ".snd"))
	(execute-and-wait (make-cmd (vector-ref input-names i) (vector-ref output-names i))))
      (temps-to-sound data output-names description))))

(define sox-1
  (lambda ()
    (loop-through-files
     "(sox copy)"
     (lambda (in out)
       (string-append "sox -t .au \""	in "\" -t .au \"" out "\" copy")))))

Now our sox function can handle any number of files or channels that might be sync'd together in Snd. In case it's not obvious, the function loop-through-files takes as its second argument a function of two arguments, and calls it on each file as we march through the input file list, passing it the input and output file names as arguments. It (make-cmd) puts together the actual call on sox that we were making earlier. An equivalent using cp is:

(define copyfile
  (lambda ()
    (loop-through-files
      "(cp)"
      (lambda (in out)
        (string-append "cp " in " " out)))))

But we're still assuming NeXT/Sun format files, and we're throwing away the string we so laboriously created. A more friendly function would display its progress.


CLM

Reading, mixing, and writing sound files are no problem in CLM, but it's unusual to run it as a batch program. Assume for the moment we have loaded the CLM instruments we want (v.ins and jcrev.ins), and have saved the image using ACL 5.0 in Linux. The CLM image is invoked in this case with lisp -I clm.dxl. ACL provides a way (-e) to evaluate lisp code from the command line, so we'll use that along with the exit function to turn CLM into a batch program. For example, we can reverberate the current data:

(define reverb
  (lambda (reverb-amount)
    (loop-through-files
     (string-append "(reverb " (number->string reverb-amount) ")")
     (lambda (in out)
       (string-append
	"lisp -I clm.dxl "
	"-e '(progn (restart-clm) "
	"      (with-sound (:play nil :output \"" out "\" :reverb jc-reverb) "
	"        (mix \"" in "\") "
	"        (mix \"" in "\" :output *reverb* :amplitude " (number->string reverb-amount) "))"
	"      (exit))'")))))

This is a call on CLM's with-sound with a reverberator and two calls on mix, one for the direct signal, the other for the reverb input. The with-sound form is wrapped up in a progn that calls restart-clm (to make sure all dynamically allocated entities are setup properly), the with-sound itself, then exit to leave lisp (the latter is needed since we're waiting for EOF in the execute-and-wait function). The reverb function's argument sets the amount of reverb, and we save that value in the edit-history descriptor. Now, in Snd, M-x (reverb .1) reverbs the current data and extends the edit-history list with the string "(reverb .1)". This example also shows how to mix something into the current data. For example, to add an fm-violin note starting at the current cursor:

(define fm-violin
  (lambda (dur frq amp)
    (let* ((beg (/ (cursor) (srate)))
	   (fmv-call (string-append "(fm-violin "
				    (number->string beg) " "
				    (number->string dur) " "
				    (number->string frq) " "
				    (number->string amp) ")")))
      (loop-through-files
       fmv-call
       (lambda (in out)
       (string-append
	"lisp -I clm.dxl "
	"-e '(progn (restart-clm) "
	"      (with-sound (:play nil :output \"" out "\") "
	"        (mix \"" in "\") "
	         fmv-call
	"        ) (exit))'"))))))

But if anything goes wrong, the whole process gets hung, since Lisp drops into its error handler, and Snd is waiting for the Lisp job to exit -- we have to go to a shell and kill the Lisp subjob! So let's check for C-g in Snd, and send the subjob output to Guile's "current-output-port" (whatever that is):

(define read-or-run
  (lambda (fil)
    (let ((val (peek-char fil)))
      (or (and val (read-char fil))
	  (c-g?)
	  (read-or-run fil)))))

(define execute-and-wait
  (lambda (cmd)
    (let ((fil (open-pipe cmd "r")))
      (do ((val (read-or-run fil) (read-or-run fil))) 
	  ((or (eq? val #t) (eof-object? val))
	   (eq? val #t))
        (write-char val (current-output-port)))
      (close-pipe fil))))
      
(define loop-through-files
  (lambda (description make-cmd)
    (let* ((data (sound-to-temps))
	   (input-names (temp-filenames data))
	   (files (vector-length input-names))
	   (output-names (make-vector files ""))
	   (stopped #f))
      (do ((i 0 (1+ i)))
	  ((or stopped (= i files)))
	(vector-set! output-names i (string-append (tmpnam) ".snd"))
	(set! stopped (execute-and-wait (make-cmd (vector-ref input-names i) (vector-ref output-names i)))))
      (temps-to-sound data output-names description))))

If this is too ugly, we could probably use append-to-minibuffer instead of write-char. In Clisp, use the -x switch without the exit function call. Also, place the expression to be evaluated in double quotes, rather than ACL's single quotes.


Snd as a Widget

To include the entire Snd editor as a widget in some other program, first compile it with -DSND_AS_WIDGET. Then load it into your program, using the procedure snd_as_widget to fire it up. The program saw.c included with Snd is a very brief example.

  void snd_as_widget(int argc, char **argv, XtAppContext app, Widget parent, Arg *caller_args, int caller_argn)

starts up the Snd editor in the widget parent, passing the outer Snd form widget the arguments caller_args and caller_argn. The enclosing application context is app. parent needs to be realized at the time of the call, since Snd uses it to set up graphics contexts and so on. argc and argv can be passed to simulate a shell invocation of Snd. Remember that in this case, the first string argument is expected to be the application name, and is ignored by Snd.

In Gtk, the arguments are different, but the basic idea is the same. saw.c has an example.


Snd and the CLM module

The files clm.c, clm.h, and clm2xen.c implement CLM (a Common Lisp Music V implementation described in clm.html, available in clm-2.tar.gz at ccrma-ftp) as a Guile-loadable module. They are normally loaded into Snd when it is built. You can see what a generator does, or a group of generators, by running them in the lisp listener, and using the graph and spectrum functions. For example, say we have these declarations in ~/.snd:

(define data-size 1024)
(define data (make-vct data-size))

(define run 
  (lambda (fun)
    (do ((i 0 (1+ i))) 
        ((= i data-size))
      (vct-set! data i (fun)))
    (graph data)))

(define runf
  (lambda (fun)
    (do ((i 0 (1+ i))) 
        ((= i data-size))
      (vct-set! data i (fun)))
    (graph (snd-spectrum data blackman2-window data-size #t))))

Now we can open the listener, and type:

(define hi (make-oscil))
(run (lambda () (oscil hi)))
(define ho (make-oscil))
(runf (lambda () (oscil hi (* .5 (oscil ho)))))

Obviously, any CLM instrument or function can be used in this way to edit sounds, and so on. Say we want an echo effect:

(define echo 
  (lambda (scaler secs)
    (let ((del (make-delay (round (* secs (srate))))))
      (lambda (inval)
        (+ inval (delay del (* scaler (+ (tap del) inval))))))))

For readers who are new to Scheme, echo is a function of two arguments, scaler and secs. Scaler sets how loud subsequent echos are, and secs sets how far apart they are in seconds. echo uses the secs argument to create a delay line (make-delay) using the current sound's sampling rate to turn the secs parameter into samples. echo then returns a "closure", that is, a function with associated variables (in this case del and scaler); the returned function (the second lambda) takes one argument (inval) and returns the result of passing that value to the delay with scaling. The upshot of all this is that we can use:

(map-chan (echo .5 .75) 0 44100)

to take the current active channel and return 44100 samples of echos, each echo half the amplitude of the previous, and spaced by .75 seconds. map-chan's first argument is a function of one argument, the current sample; when we pass it (echo ...), it evaluates the echo call, which returns the function that actually runs the delay line, producing the echo. The CLM (common lisp) version might be something like:

(definstrument echo (beg dur scaler secs file)
  (let ((del (make-delay (round (* secs *srate*))))
	(inf (open-input file))
	(j 0))
    (run
     (loop for i from beg below (+ beg dur) do
       (let ((inval (ina j inf)))
	 (outa i (+ inval (delay del (* scaler (+ (tap del) inval)))))
	 (incf j))))
    (close-input inf)))

;;; (with-sound () (echo 0 60000 .5 1.0 "pistol.snd"))

CLM functions

See clm.html for full details. Optional args are in italics.


all-pass(gen input pm)all-pass filter
all-pass?(gen)#t if gen is all-pass filter
amplitude-modulate(carrier in1 in2)amplitude modulation
array-interp(arr x)interpolated array lookup
array->file(filename vct len srate channels)
write the contents of vct to the newly created sound file filename, giving the new file channels channels (data assumed to be interleaved in vct), sampling rate srate, and len samples (not frames).
asymmetric-fm(gen index fm)asymmetric-fm generator
asymmetric-fm?(gen)#t if gen is asymmetric-fm generator
buffer->frame(gen frame)buffer generator returning frame
buffer->sample(gen)buffer generator returning sample
buffer-empty?(gen)#t if buffer has no data
buffer-full?(gen)#t if buffer has no room for more data
buffer?(gen)#t if gen is buffer generator
clear-array(arr)set all elements of arr to 0.0
comb(gen input pm)comb filter
comb?(gen)#t if gen is comb filter
contrast-enhancement(input (index 1.0))a kind of phase modulation or companding
convolution(sig1 sig2 n)convolve sig1 with sig2 (size n), returning new sig1
convolve(gen input-function)convolve generator
convolve?(gen)#t if gen is convolve generator
convolve-files(f1 f2 maxamp outf)convolve f1 with f2, normalize to maxamp, write outf
db->linear(db)translate dB value to linear
degrees->radians(deg)translate degrees to radians
delay(gen input pm)delay line

delay is a built-in syntactic form. The name %delay is bound to the original meaning of delay in case you need to use it.

delay?(gen)#t if gen is delay line
dot-product(sig1 sig2)return dot-product of sig1 with sig2
env(gen)envelope generator
env-interp(x env (base 1.0))return value of env at x
env?(gen)#t if gen is env (from make-env)
mus-fft(rl im n sign)fft of rl and im (sign = -1 for ifft), result in rl
file->array(filename chan start len vct) load len samples of filename into vct starting at frame start in channel chan.
file->frame(gen loc frame)return frame from file at loc
file->frame?(gen)#t if gen is file->frame generator
file->sample(gen loc (chan 0))return sample from file at loc
file->sample?(gen)#t if gen is file->sample generator
filter(gen input)filter
filter?(gen)#t if gen is filter
fir-filter(gen input)FIR filter
fir-filter?(gen)#t if gen is fir filter
formant(gen input)formant generator
formant-bank(scls gens inval)
formant?(gen)#t if gen is formant generator
frame*(fr1 fr2 outfr)element-wise multiply
frame+(fr1 fr2 outfr)element-wise add
frame->buffer(buf frame)add frame to buffer
frame->file(gen loc frame)write (add) frame to file at loc
frame->file?(gen)#t if gen is frame->file generator
frame->frame(mixer frame outfr)pass frame through mixer
frame->list(frame)return list of frame contents
frame-ref(frame chan)return frame[chan]
frame->sample(frmix frame)pass frame through frame or mixer to produce sample
frame-set!(frame chan val)frame[chan]=val
frame?(gen)#t if gen is frame object
granulate(gen input-function)granular synthesis generator
granulate?(gen)#t if gen is granulate generator
hz->radians(freq)translate freq to radians/sample
iir-filter(gen input)IIR filter
iir-filter?(gen)#t if gen is iir-filter
in-any(loc chan stream)return sample in stream at loc and chan
in-hz(freq)translate freq to radians/sample
ina(loc stream)return sample in stream at loc, chan 0
inb(loc stream)return sample in stream at loc, chan 1
linear->db(val)translate linear val to dB
locsig(gen loc input)place input in output channels at loc
locsig-ref(gen chan)locsig-scaler[chan]
locsig-reverb-ref(gen chan)locsig-reverb-scaler[chan]
locsig-set!(gen chan val)locsig-scaler[chan] = val
locsig-reverb-set!(gen chan val)locsig-reverb-scaler[chan] = val
locsig?(gen)#t if gen is locsig generator

;; all the make function arguments are optional-key args
make-all-pass(feedback feedforward size initial-contents initial-element max-size)
make-asymmetric-fm(frequency initial-phase r ratio)
make-buffer(size fill-time)
make-comb(scaler size initial-contents initial-element max-size)
make-convolve(input filter fft-size filter-size)
make-delay(size initial-contents initial-element max-size)
make-env(envelope scaler duration offset base end start)
make-fft-window(type size beta)
make-file->frame(name)
make-file->sample(name)
make-filter(order xcoeffs ycoeffs)
make-fir-filter(order xcoeffs)
make-formant(radius frequency gain)
make-frame(chans &rest vals)
make-frame->file(name chans format type)
make-granulate(input expansion length scaler hop ramp jitter max-size)
make-iir-filter(order ycoeffs)
make-locsig(degree distance reverb output revout channels)
make-mixer(chans &rest vals)
make-notch(scaler size initial-contents initial-element max-size)
make-one-pole(a0 b1)
make-one-zero(a0 a1)
make-oscil(frequency initial-phase)
make-phase-vocoder(fftsize overlap interp pitch analyze edit synthesize)
make-ppolar(radius frequency)
make-pulse-train(frequency amplitude initial-phase)
make-rand(frequency amplitude)
make-rand-interp(frequency amplitude)
make-readin(file channel start)
make-sample->file(name chans format type comment)
make-sawtooth-wave(frequency amplitude initial-phase)
make-sine-summation(frequency initial-phase n a ratio)
make-square-wave(frequency amplitude initial-phase)
make-src(input srate width)
make-sum-of-cosines(frequency initial-phase cosines)
make-table-lookup(frequency initial-phase wave)
make-triangle-wave(frequency amplitude initial-phase)
make-two-pole(a0 b1 b2)
make-two-zero(a0 a1 a2)
make-wave-train(frequency initial-phase wave)
make-waveshape(frequency partials)
make-zpolar(radius frequency)
mixer*(mix1 mix2 outmx)matrix multiply of mix1 and mix2
mixer-ref(mix in out)mix-scaler[in,out]
mixer-set!(mix in out val)mix-scaler[in,out] = val
mixer?(gen)#t if gen is mixer object
multiply-arrays(arr1 arr2)arr1[i] *= arr2[i]
mus-a0(gen)a0 field (simple filters)
mus-a1(gen)a1 field (simple filters)
mus-a2(gen)a2 field (simple filters)
mus-array-print-length()how many array elements to print in mus_describe
mus-b1(gen)b1 field (simple filters)
mus-b2(gen)b2 field (simple filters)
mus-bank(gens amps args1 args2)
mus-channel(gen)channel of gen
mus-channels(gen)channels of gen
mus-cosines(gen)cosines of sum-of-cosines gen
mus-data(gen)data array of gen
mus-feedback(gen)feedback term of gen (simple filters)
mus-feedforward(gen)feedforward term of gen (all-pass)
mus-file-buffer-size()size of input/ouput buffers (default 8192)
mus-formant-radius(gen)formant radius
mus-frequency(gen)frequency of gen (Hz)
mus-hop(gen)hop amount of gen (granulate)
mus-increment(gen)increment of gen (src, readin, granulate)
mus-input?(gen)#t if gen is input source
mus-length(gen)length of gen
mus-location(gen)location (read point) of gen
mus-mix(outfile infile (outloc 0) frames (inloc 0) mixer envs)

mix infile into outfile starting at outloc in outfile and inloc in infile mixing frames frames of infile. frames defaults to the length of infile. If mixer, use it to scale the various channels; if envs (an array of envelope generators), use it in conjunction with mixerto scale/envelope all the various ins and outs.

mus-order(gen)order of gen (filters)
mus-output?(gen)#t if gen is output generator
mus-phase(gen)phase of gen (radians)
mus-ramp(gen)ramp time of gen (granulate)
mus-random(val)random numbers bewteen -val and val
mus-run(gen arg1 arg2)apply gen to args
mus-scaler(gen)scaler of gen
mus-rand-seed(val)random number generator seed (settable via set!)
mus-set-srate(val)set sampling rate to val -- (set! (mus-srate) val) is the same.
mus-srate()current sampling rate
mus-xcoeffs(gen)feedforward (FIR) coeffs of filter
mus-ycoeffs(gen)feedback (IIR) coeefs of filter
notch(gen input pm)notch filter
notch?(gen)#t if gen is notch filter
one-pole(gen input)one-pole filter
one-pole?(gen)#t if gen is one-pole filter
one-zero(gen input)one-zero filter
one-zero?(gen)#t if gen is one-zero filter
oscil(gen fm pm)sine wave generator
oscil-bank(scls gens invals)bank of oscils
oscil?(gen)#t if gen is oscil generator
out-any(loc samp chan stream)write (add) samp to stream at loc in channel chan
outa(loc samp stream)write (add) samp to stream at loc in chan 0
outb(loc samp stream)write (add) samp to stream at loc in chan 1
outc(loc samp stream)write (add) samp to stream at loc in chan 2
outd(loc samp stream)write (add) samp to stream at loc in chan 3
partials->polynomial(partials kind)create waveshaping polynomial from partials
partials->wave(synth-data table norm)load table from synth-data
partials->waveshape(partials norm size)create waveshaping table from partials
phase-partials->wave(synth-data table norm)load table from synth-data
phase-vocoder(pv input)phase vocoder generator
phase-vocoder?(pv)#t if pv is phase vocoder generator
polar->rectangular(rl im)translate from polar to rectangular coordinates
polynomial(coeffs x)evaluate polynomial at x
pulse-train(gen fm)pulse-train generator
pulse-train?(gen)#t if gen is pulse-train generator
radians->degrees(rads)convert radians to degrees
radians->hz(rads)convert radians/sample to Hz
rand(gen fm)random number generator
rand-interp(gen fm)interpolating random number generator
rand-interp?(gen)#t if gen is interpolating random number generator
rand?(gen)#t if gen is random number generator
readin(gen)read one value from associated input stream
readin?(gen)#t if gen is readin generator
rectangular->polar(rl im)translate from rectangular to polar coordinates
restart-env(env)return to start of env
ring-modulate(sig1 sig2)sig1 * sig2 (element-wise)
sample->buffer(buf samp)store samp in buffer
sample->file(gen loc chan val)store val in file at loc in channel chan
sample->file?(gen)#t if gen is sample->file generator
sample->frame(frmix samp outfr)convert samp to frame
sawtooth-wave(gen fm)sawtooth-wave generator
sawtooth-wave?(gen)#t if gen is sawtooth-wave generator
sine-summation(gen fm)sine-summation generator
sine-summation?(gen)#t if gen is sine-summation generator
spectrum(rl im win type)produce spectrum of data in rl (return rl)
square-wave(gen fm)square-wave generator
square-wave?(gen)#t if gen is square-wave generator
src(gen fm input-function)sample rate converter
src?(gen)#t if gen is sample-rate converter
sum-of-cosines(gen fm)sum-of-cosines (pulse-train) generator
sum-of-cosines?(gen)#t if gen is sum-of-cosines generator
sum-of-sines(amps phases)additive synthesis
table-lookup(gen fm)table-lookup generator
table-lookup?(gen)#t if gen is table-lookup generator
tap(gen pm)delay line tap
triangle-wave(gen fm)triangle-wave generator
triangle-wave?(gen)#t if gen is triangle-wave generator
two-pole(gen input)two-pole filter
two-pole?(gen)#t if gen is two-pole filter
two-zero(gen input)two-zero filter
two-zero?(gen)#t if gen is two-zero filter
wave-train(gen fm)wave-train generator
wave-train?(gen)#t if gen is wave-train generator
waveshape(gen index fm)waveshaping generator
waveshape?(gen)#t if gen is waveshape generator

formant-bank and oscil-bank are optimizations for situations like the phase vocoder or cross synthesis (see examp.scm). It is assumed that you have a vector of generators, all summing their outputs into a single float. The amplitude scalers (the first argument to the bank function) can be a float, a vector of floats, a vct of floats, or a function that returns a float each time it is called; similarly for the inputs (the third argument); the bank of generators (the second argument) is assumed to be a vector full of generators.

  (formant-bank amps gens inval)

is essentially the same as (but 30 times faster than)

  (do ((sum 0.0)
       (i 0 (1+ i))) 
      ((= i (vct-length gens)) sum)
    (set! sum (+ sum (* (vector-ref amps i)
                        (formant (vector-ref gens i) inval)))))

in the all-vector case. mus-bank is the general case. Here are a few more examples, taken from examp.scm.



(define comb-filter 
  (lambda (scaler size)
    (let ((cmb (make-comb scaler size)))
      (lambda (x) (comb cmb x)))))

; (map-chan (comb-filter .8 32))

;;; by using filters at harmonically related sizes, we can get chords:

(define comb-chord
  (lambda (scaler size amp)
    (let ((c1 (make-comb scaler size))
	  (c2 (make-comb scaler (* size .75)))
	  (c3 (make-comb scaler (* size 1.2))))
      (lambda (x)
        (* amp (+ (comb c1 x) (comb c2 x) (comb c3 x)))))))

; (map-chan (comb-chord .95 60 .3))

;;; or change the comb length via an envelope:

(define max-envelope
  (lambda (e mx)
    (if (null? e)
	mx
      (max-envelope (cddr e) (max mx (abs (cadr e)))))))

(define zcomb
  (lambda (scaler size pm)
    (let ((cmb (make-comb scaler size :max-size (+ size 1 (max-envelope pm 0))))
	  (penv (make-env :envelope pm :end (frames))))
      (lambda (x) (comb cmb x (env penv))))))

; (map-chan (zcomb .8 32 '(0 0 1 10)))

;;; to impose several formants, just add them in parallel:

(define formants
  (lambda (r1 f1 r2 f2 r3 f3)
    (let ((fr1 (make-formant r1 f1))
	  (fr2 (make-formant r2 f2))
	  (fr3 (make-formant r3 f3)))
      (lambda (x)
	(+ (formant fr1 x)
	   (formant fr2 x)
	   (formant fr3 x))))))

; (map-chan (formants .01 900 .02 1800 .01 2700))

;;; to get a moving formant:

(define moving-formant
  (lambda (radius move)
    (let ((frm (make-formant radius (cadr move)))
	  (menv (make-env :envelope move :end (frames))))
      (lambda (x)
        (let ((val (formant frm x)))
	  (set! (mus-frequency frm) (env menv))
	  val)))))

; (map-chan (moving-formant .01 '(0 1200 1 2400)))

;;; various "Forbidden Planet" sound effects:

(define sp
  (lambda (sr osamp osfrq)
    (let* ((os (make-oscil osfrq))
	   (sr (make-src :srate sr))
	   (len (frames))
	   (sf (make-sample-reader))
	   (out-data (make-vct len)))
      (vct-map! out-data
		  (lambda () 
		    (src sr (* osamp (oscil os))
			 (lambda (dir)
			   (if (> dir 0)
			       (next-sample sf)
			       (previous-sample sf))))))
      (free-sample-reader sf)
      (vct->samples 0 len out-data))))

; (fp 1.0 .3 20)


;;; -------- shift pitch keeping duration constant
;;;
;;; both src and granulate take a function argument to get input whenever it is needed.
;;; in this case, src calls granulate which reads the currently selected file.

(define expsrc
  (lambda (rate)
    (let* ((gr (make-granulate :expansion rate))
	   (sr (make-src :srate rate))
	   (vsize 1024)
	   (vbeg 0)
	   (v (samples->vct 0 vsize))
	   (inctr 0))
      (lambda (inval)
        (src sr 0.0
	  (lambda (dir)
	    (granulate gr
	      (lambda (dir)
		(let ((val (vct-ref v inctr)))
		  (set! inctr (+ inctr dir))
		  (if (>= inctr vsize)
		      (begin
			(set! vbeg (+ vbeg inctr))
			(set! inctr 0)
			(samples->vct vbeg vsize 0 0 v)))
		  val)))))))))

Geez, I haven't had this much fun in a long time! Check out examp.scm and snd-test.scm for more. CLM-in-CL users will be disappointed with the CLM-in-Scheme performance; my tests indicate that interpreted Scheme (as in Snd currently) is about 30 to 100 times slower than CLM instruments using the "run" macro. I may translate that macro to Scheme, but I'm waiting to see if there's any demand -- there is CLM-in-CL after all, and it's about 8000 lines of Lisp...


Snd and Motif

It is possible to add your own user-interface elements using the xm module included with Snd. 'make xm' should create a shared library named xm.so; you can load this at any time into Snd:

> (define hxm (dlopen "/home/bil/snd-5/xm.so"))
#<unspecified>
> (dlinit hxm "init_xm")
#t

and now we have access to all of X and Motif. As a very simple example, here's dialog window with a slider:

(define scale-dialog #f)
(define current-scaler 1.0)

(define (create-scale-dialog parent)
  (if (not (|Widget? scale-dialog))
      (let ((xdismiss (|XmStringCreate "Dismiss" |XmFONTLIST_DEFAULT_TAG))
	    (xhelp (|XmStringCreate "Help" |XmFONTLIST_DEFAULT_TAG))
	    (titlestr (|XmStringCreate "Scaling" |XmFONTLIST_DEFAULT_TAG)))
	(set! scale-dialog 
	      (|XmCreateTemplateDialog parent "Scaling"
                (list |XmNcancelLabelString   xdismiss
		      |XmNhelpLabelString     xhelp
		      |XmNautoUnmanage        #f
		      |XmNdialogTitle         titlestr
		      |XmNresizePolicy        |XmRESIZE_GROW
	              |XmNnoResize            #f
		      |XmNtransient           #f) ))
	(|XtAddCallback scale-dialog 
			|XmNcancelCallback (lambda (w context info)
					     (|XtUnmanageChild scale-dialog)))
	(|XtAddCallback scale-dialog 
			|XmNhelpCallback (lambda (w context info)
					   (snd-print "move the slider to affect the volume")))
	(|XmStringFree xhelp)
	(|XmStringFree xdismiss)
	(|XmStringFree titlestr)

	(let* ((mainform 
		(|XtCreateManagedWidget "formd" |xmFormWidgetClass scale-dialog
                  (list |XmNleftAttachment      |XmATTACH_FORM
		        |XmNrightAttachment     |XmATTACH_FORM
		        |XmNtopAttachment       |XmATTACH_FORM
		        |XmNbottomAttachment    |XmATTACH_WIDGET
		        |XmNbottomWidget        (|XmMessageBoxGetChild scale-dialog |XmDIALOG_SEPARATOR))))
	       (scale
		(|XtCreateManagedWidget "" |xmScaleWidgetClass mainform
		  (list |XmNorientation |XmHORIZONTAL
			|XmNshowValue   #t
			|XmNvalue       100
			|XmNmaximum     500
			|XmNdecimalPoints 2))))

      (|XtAddCallback scale 
		      |XmNvalueChangedCallback (lambda (w context info)
						 (set! current-scaler (/ (|value info) 100.0))))
      (|XtAddCallback scale |XmNdragCallback (lambda (w context info)
						 (set! current-scaler (/ (|value info) 100.0)))))))
  (|XtManageChild scale-dialog))

(create-scale-dialog (cadr (main-widgets)))

In Ruby, this would be:

$scale_dialog = false
$current_scaler = 1.0

def create_scale_dialog(parent)
  if !RWidget?($scale_dialog) 
    then
      xdismiss = RXmStringCreate("Dismiss", RXmFONTLIST_DEFAULT_TAG)
      xhelp = RXmStringCreate("Help", RXmFONTLIST_DEFAULT_TAG)
      titlestr = RXmStringCreate("Scaling", RXmFONTLIST_DEFAULT_TAG)
      $scale_dialog = RXmCreateTemplateDialog(parent, "Scaling",
                   	[RXmNcancelLabelString,  xdismiss,
                      	 RXmNhelpLabelString,    xhelp,
                      	 RXmNautoUnmanage,       false,
                      	 RXmNdialogTitle,        titlestr,
                      	 RXmNresizePolicy,       RXmRESIZE_GROW,
                      	 RXmNnoResize,           false,
                       	 RXmNtransient,          false])
      RXtAddCallback($scale_dialog, RXmNcancelCallback, 
                     Proc.new { |w, context, info| RXtUnmanageChild($scale_dialog)})
      RXtAddCallback($scale_dialog, RXmNhelpCallback, 
                     Proc.new { |w, context, info| snd_print "move the slider to affect the volume"})
      RXmStringFree xhelp
      RXmStringFree xdismiss
      RXmStringFree titlestr
      mainform = RXtCreateManagedWidget("formd", RxmFormWidgetClass, $scale_dialog,
                  	[RXmNleftAttachment,      RXmATTACH_FORM,
                         RXmNrightAttachment,     RXmATTACH_FORM,
                         RXmNtopAttachment,       RXmATTACH_FORM,
                         RXmNbottomAttachment,    RXmATTACH_WIDGET,
                         RXmNbottomWidget,        RXmMessageBoxGetChild($scale_dialog, RXmDIALOG_SEPARATOR)])
      scale = RXtCreateManagedWidget("", RxmScaleWidgetClass, mainform,
                  	[RXmNorientation, RXmHORIZONTAL,
                         RXmNshowValue,   true,
                         RXmNvalue,       100,
                         RXmNmaximum,     500,
                         RXmNdecimalPoints, 2])
      RXtAddCallback(scale, RXmNvalueChangedCallback, 
                     Proc.new { |w, context, info| $current_scaler = Rvalue(info) / 100.0})
      RXtAddCallback(scale, RXmNdragCallback, 
                     Proc.new { |w, context, info | $current_scaler = Rvalue(info) / 100.0})
      RXtManageChild $scale_dialog
    end
end

$Snd_widgets = main_widgets()
create_scale_dialog $Snd_widgets[1]

All of Snd is at your disposal once this module is loaded. As a more interesting example, the next function installs our own file filtering procedure into the File:Open dialog (it uses match-sound-files from extensions.scm):

(define (install-searcher proc)
  (define (XmString->string str)
    (cadr (|XmStringGetLtoR str |XmFONTLIST_DEFAULT_TAG)))
  (define (XmStringTable->list st len)
    (|XmStringTableUnparse st len #f |XmCHARSET_TEXT |XmCHARSET_TEXT #f 0 |XmOUTPUT_ALL))
  (define (list->XmStringTable strs)
    (|XmStringTableParseStringArray strs (length strs) #f |XmCHARSET_TEXT #f 0 #f))
  (|XtSetValues (let ((m (open-file-dialog #f)))
                         ; make sure the dialog exists
		  (list-ref (dialog-widgets) 6))
		(list |XmNfileSearchProc                             ; set dialog file search procedure
		       (lambda (widget info)
			 (let* ((dir (XmString->string (|dir info))) ; directory string
				(files (match-sound-files proc dir)) ; list of matching files
				(fileTable (list->XmStringTable      ; XmStringTable for XmNfileListItems
                                             (map (lambda (n)        ; every file needs prepended dir
                                                    (string-append dir n)) 
                                                  files))))
			   (|XtSetValues widget                      ; change the list of files
					 (list |XmNfileListItems fileTable
					       |XmNfileListItemCount (length files)
					       |XmNlistUpdated #t)))))))

;(install-searcher (lambda (file) (= (mus-sound-srate file) 44100)))
;(install-searcher (lambda (file) (= (mus-sound-chans file) 4)))

Now click the 'Filter' button to see only those files that fit the procedure in the dialog's files list. See snd-motif.scm and popup.scm for more examples. There are a few special Snd functions to support this module:

  snd-pixel color -- returns pixel of that color
  snd-gcs -- returns list of Snd graphics contexts
  all the widget-list functions

Snd and Gtk+

This part of Snd is in progress. Eventually I'll provide gtk equivalents of the stuff in snd-motif.scm and popup.scm. Once that gets squared away, most of the graphics routines currently built-into Snd (focus-widget, recolor-widget, etc) will be moved into Scheme. In the meantime, here's the scale-dialog in xg/gtk:

(define scale-dialog #f)
(define current-scaler 1.0)

(define (create-scale-dialog parent)
  (if (not scale-dialog)
      (begin
	(set! scale-dialog (|gtk_dialog_new))
	(|gtk_signal_connect (|GTK_OBJECT scale-dialog) "delete-event"
			     (lambda (w ev info)
			       (|gtk_widget_hide w)))
	(|gtk_window_set_title (|GTK_WINDOW scale-dialog) "Scale")
	(|gtk_widget_realize scale-dialog)
	(let ((dismiss (|gtk_button_new_with_label "Dismiss"))
	      (help (|gtk_button_new_with_label "Help")))
	  (|gtk_box_pack_start (|GTK_BOX (|action_area (|GTK_DIALOG scale-dialog))) dismiss #t #t 4)
	  (|gtk_box_pack_end (|GTK_BOX (|action_area (|GTK_DIALOG scale-dialog))) help #t #t 4)	
	  (|gtk_signal_connect (|GTK_OBJECT dismiss) "clicked"
			       (lambda (w info)
				 (|gtk_widget_hide scale-dialog)))
	  (|gtk_signal_connect (|GTK_OBJECT help) "clicked"
			       (lambda (w info)
				 (help-dialog "Scaler Dialog" "move the slider to affect the volume")))
	  (|gtk_widget_show dismiss)
	  (|gtk_widget_show help)
	  (let* ((adj (|gtk_adjustment_new 0.0 0.0 1.01 0.01 0.01 .01))
		 (scale (|gtk_hscale_new (|GTK_ADJUSTMENT adj))))
	    (|gtk_range_set_update_policy (|GTK_RANGE (|GTK_SCALE scale)) |GTK_UPDATE_CONTINUOUS)
	    (|gtk_scale_set_draw_value (|GTK_SCALE scale) #t)
	    (|gtk_scale_set_digits (|GTK_SCALE scale) 2)
	    (|gtk_signal_connect (|GTK_OBJECT adj) "value_changed"
				 (lambda (wadj info)
				   (set! current-scaler (|value (|GTK_ADJUSTMENT wadj)))))
	    (|gtk_box_pack_start (|GTK_BOX (|vbox (|GTK_DIALOG scale-dialog))) scale #f #f 6)
	    (|gtk_widget_show scale)))))
  (|gtk_widget_show scale-dialog))

(create-scale-dialog (cadr (main-widgets)))

The only change from the C code was the addition of GTK_ADJUSTMENT in the scale value_changed callback -- currently the xg module assumes the first argument to the two-argument callback is a GtkWidget, so we have to cast a GtkAdjustment back to its original type. Once I figure out how the "marshaller" works in Gtk, I may be able to fix this.


Snd with no GUI and scripting

If Snd is built without a graphical user interface (either by specifying --with-no-gui to configure, or by setting the USE_NO_GUI compile-time flag), it runs Guile's "repl" (read-eval-print loop) (or Ruby's equivalent) with input from stdin. All the non-interface related functions are available, so you can do things like:

snd> (new-sound "new.snd")
0
snd> (load "v.scm")
snd> (fm-violin 0 1 440 .1)
-1
snd> (frames 0)
22050
snd> (play)
#t
snd> (exit)

Guile's repl has its own error handlers, different from the normal Snd handlers; name completion, if it exists at all, won't complete Snd names; there are undoubtedly other differences that I haven't noticed.

Since this version of Snd is the same as the guile program with Snd loaded, you can treat it as a scripting engine. For example, if you have an executable file with:

#!/home/bil/test/snd-5/snd -l
!#
(define a-test 32)
(display "hiho")
(newline)

it can be executed just like any such script.

/home/bil/test/snd-5/ script
hiho
:a-test
32
:(exit)
/home/bil/test/snd-5/ 

The difference between this use of Snd, and using guile itself for scripts is that Snd uses the -l switch where guile would use -s. As noted above, you can use the -e switch to use Snd as a pure command-line program, and, of course, (exit) to drop back to the shell. Here's an example script that doubles every sample in "oboe.snd" and writes the result as "test.snd":

#!/home/bil/test/snd-5/snd -l
!#
(open-sound "oboe.snd")
(scale-by 2.0)
(save-sound-as "test.snd")
(exit)

The functions script-args and script-arg can be used to access the script's arguments, and if necessary (if not exiting) tell Snd to ignore arguments. script-args returns a list of strings giving the arguments. The first two are always "-l" and the script file name. The current argument is (script-arg). If you set this to a higher value, Snd will subsequently ignore the intevening arguments as it scans the startup arguments (see snd-test.scm for an example).

#!/home/bil/test/snd-5/snd -l
!#
(if (= (length (script-args)) 2) ;i.e. ("-l" "script")
  (display "usage: script file-name...\n")
  (begin
    (open-sound (list-ref (script-args) (+ (script-arg) 1)))
    (scale-by 2.0)
    (save-sound-as "test.snd")))
(exit)

This either grumbles if no argument is given, or scales its argument sound by 2.0:

script pistol.snd

And obviously we can run through the entire argument list, doubling all the sounds or whatever by using a do loop -- the following example displays all the comments it finds:

#!/home/bil/cl/snd -l
!#
(use-modules (ice-9 format))
(if (= (length (script-args)) 2) ;i.e. ("-l" "script")
  (display "usage: script file-name...\n")
  (do ((arg (+ (script-arg) 1) (1+ arg)))
      ((= arg (length (script-args))))
    (let ((name (list-ref (script-args) arg)))
      (display (format #f "~A: ~A~%" name (mus-sound-comment name))))))
(exit)

Say we save this as the file "comments".

/home/bil/cl/comments *.snd

If you like, you can use env:

#!/usr/bin/env snd
!#

But if that works, so will:

#!snd -l
!#

This scripting mechanism actually will work in any version of Snd; to keep the Snd window from popping up, use the -b (-batch) switch in place of -l.


Snd with Ruby

Ruby is an extension language described as an "object-oriented Perl". It provides a different syntax from that of Guile/Scheme. In Ruby, all the "-" are "_", "->" is "2", hooks and memo_sound have "$" prepended (since they are global variables from Ruby's point of view), and all the constants are capitalized (e.g. Autocorrelation). The generalized set! functions are replaced by "set_" plus the base name (e.g. set_window_width), with arguments reordered in some cases to place the optional values after the new value. That is, (set! (sync snd) 1) becomes set_sync(1, snd). Hooks in Ruby (which have little or nothing to do with Ruby's "hookable variables") are just procedures or nil, not lists of procedures as in Guile. Here's the Ruby version of the init file given above:

set_window_width 800
set_window_height 500

set_listener_font "9x15"
set_help_text_font "9x15"
set_axis_numbers_font "9x15"

set_show_mix_waveforms true
set_trap_segfault false
set_show_backtrace true
set_show_indices true

set_listener_prompt ":"
show_listener

beige = make_color 0.96, 0.96, 0.86
blue = make_color 0, 0, 1
set_selected_graph_color beige
set_selected_data_color blue

Procedures are created via Proc.new, so to set the open-hook to print the file name,

>$open_hook = Proc.new { |name| snd_print name }
#<Proc:0x40221b84>
>open_sound "oboe.snd"
/home/bil/cl/oboe.snd
0

(The trailing "0" is the result of open_sound). The Guile hook list support procedures aren't included in Ruby -- simply set the variable to the procedure you want, or false to clear it.

Vcts and sound-data objects mixin "Comparable" and "Enumerable", and respond to various array-like methods:

>v1 = make_vct 4
#<vct[len=4]: 0.000 0.000 0.000 0.000>
>v1[3] = 1.0
1.0
>v1.sort
0.00.00.01.0 # I don't know why it prints this way but ...
>v1
#<vct[len=4]: 0.000 0.000 0.000 1.000>
>v1.max
1.0

I'm thinking about making classes for things like sounds; you could then have sound + sound to mix, or sound * 2 to scale it, a given channel could be treated as an array, accessed via sound[0, 12345] and so on, These are extremely easy to add, but I'd like to coordinate this with Guile's object system. Keywords, CLM generic functions, and optional arguments work as in Scheme:

>osc = make_oscil(:frequency, 440)
oscil freq: 440.000Hz, phase: 0.000
>(oscil osc)
0.0
>(oscil osc)
0.1250506192
>osc.frequency
440.0

Lists (from the Scheme point of view) are arrays (vectors) in Ruby, and various built-in Scheme functions such as car aren't predefined, so to set up the focusing hooks as described in mouse-enter-graph-hook we need to do something along these lines:

def car(v)
  v[0]
end

$mouse_enter_graph_hook = Proc.new {|snd, chn| 
			            if sound? snd then
			               focus_widget car channel_widgets snd, chn
                                    end 
                                   }

$mouse_enter_listener_hook = Proc.new { |widget| 
                                        focus_widget widget 
                                      }

Here's one more example, a translation of display-energy in draw.scm:

def display_energy(snd, chn)
  ls = left_sample
  rs = right_sample
  data1 = make_graph_data(snd, chn)
  data = data1
  if not vct? data
    data = data1[1]
  end
  len = vct_length data
  sr = srate snd
  y_max = y_zoom_slider(snd, chn)
  vct_multiply!(data, data)
  graph(data, "energy", ls / sr, rs / sr, 0.0, y_max * y_max, snd, chn, false)
  end

# $lisp_graph_hook = Proc.new {|snd,chn| display_energy(snd,chn)}

In the listener, everything is line-oriented (that is, I'm not trying to catch incomplete expressions). And it appears that in Ruby, variables defined within a file are considered local to that file(?). The save state mechanism is incomplete, but the basic stuff works. Unimplemented (or untested) are: backtrace upon error, env lookup from the env name, and many cute methods. I'm slowly translating snd-test.scm to Ruby: see snd.rb. bird.rb is the Ruby version of bird.scm. (My very informal timing tests indicate that Guile and Ruby run at essentially the same speed: Guile might be about 10 to 20% faster).


Snd and gmeteor

gmeteor is a Guile-based filter design package written by Matteo Frigo, based on the Meteor system of Steiglitz, Parks, and Kaiser. It is freely available here. Once installed, it can be loaded into Snd and used to define filters very easily: (here I'm typing in Snd's listener and editing the numbers for legibility; the file gm.scm is taken nearly verbatim from the gmeteor script):

>(load "gm.scm")
#<unspecified>
>(load "../test/gmeteor-0.9/examples/example-1.scm")
#<unspecified>
>*coefficients*
#(0.0197 -0.0406 -0.0739 0.1340 0.4479 0.4479 0.13403 -0.0739 -0.0406 0.0197)
>(filter-sound (vector->vct *coefficients*) (vector-length *coefficients*))

There is one small problem: both Snd (CLM) and gmeteor define a function named make-filter. Someday I'll learn enough about the Guile module system to know how to keep gmeteor's make-filter from clobbering CLM's. (There may be others as well -- this is the one I happened to notice).


Snd and LADSPA

  init-ladspa
  list-ladspa
  analyse-ladspa library type
  apply-ladspa reader data duration origin

Richard Furse has provided a module to support LADSPA plugins in Snd. To get it loaded, either use the configure switch --with-ladspa, or include the compile flag HAVE_LADSPA. Here is documentation from Richard Furse:

Supporting functions are:

	(init-ladspa)

	Performs a search of LADSPA_PATH for plugins, doesn't need to be called 
as LADSPA automatically initialises on first use however can be used to 
reinitialise if new plugins have arrived.

	(list-ladspa)

	Returns a list of lists where each inner list contains a string to 
identify the plugin library and a string to identify the plugin type within 
the library.

	(analyse-ladspa plugin-library plugin-type)

	Returns a list of assorted data about a particular plugin including a 
list of port descriptions. plugin-library and plugin-type are as provided 
by list-ladspa.

The main function is:

	(apply-ladspa reader (plugin-library plugin-type [param1 [param2 ...]]) samples origin)

	Applies a LADSPA plugin in a way very similar to loop-samples - 
essentially a plugin identifier and parameter set takes the place of func. 
An example call to apply the low-pass-filter in the CMT plugin library is 
(apply-ladspa (make-sample-reader 0) (list "cmt" "lpf" 1000) 10000 "origin").

Dave Phillips in Linux Audio Plug-Ins: A Look Into LADSPA" adds this example:

  (apply-ladspa (make-sample-reader 57264) (list "cmt" "delay_5s" .3 .5) 32556 "ibm.wav")

"This sequence tells Snd to read a block of 32556 samples from the ibm.wav file, starting at sample number 57264, and apply the delay_5s LADSPA plug-in (Richard Furse's delay plug-in, also found in cmt.so) with a delay time of .3 seconds and a 50/50 dry/wet balance."

To help Snd find the plugin library, set either the Snd variable ladspa-dir or the environment variable LADSPA_PATH to the directory. If, for example, cmt.so is in /usr/local/lib/ladspa, (and you're using tcsh), then

  setenv LADSPA_PATH /usr/local/lib/ladspa

or

  (set! (ladspa-dir) "/usr/local/lib/ladspa")

Snd plugins may have any number of inputs and outputs; if more than one input is required, the first argument to apply-ladspa should be a list of readers:

  (apply-ladspa (list (make-sample-reader 0 0 0)  ;chan 0
                      (make-sample-reader 0 0 1)) ;chan 1
                (list "cmt" "freeverb3" 0 .5 .5 .5 .5 .5) 
                100000 "freeverb")

The "regularized" version of apply-ladspa could be defined:

(define* (ladspa-channel ladspa-data #:optional nbeg ndur nsnd nchn nedpos)
  (let* ((beg (or nbeg 0)) 
	 (snd (or nsnd (selected-sound)))
	 (chn (or nchn (selected-channel)))
	 (dur (or ndur (- (frames snd chn) beg)))
	 (edpos (or nedpos current-edit-position))
	 (reader (make-sample-reader beg snd chn 1 edpos)))
    (apply-ladspa reader ladspa-data dur "apply-ladspa")
    (free-sample-reader reader)))

Driving Snd remotely

It is possible to send Snd arbitrary scheme code from any other program; the program sndctrl.c is a simple example. Snd has two X window properties: "SND_VERSION" and "SND_COMMAND"; the former is the Snd version (a date), and the latter is the communication path for other programs. Any time such a program changes the SND_COMMAND property, Snd notices and evaluates the new value (as a string, as if typed in the Snd lisp listener). To get a response from Snd, use the function change-property(consat,name,command) where consat is the property name Snd should search for, name is the property to change, and command is the string that replaces the current property value. For example, CLM's communication with Snd function sends Snd this string:

"(change-property \"CLM_VERSION\" \"CLM_COMMAND\" " str ")"

where str is the form to be evaluated within Snd. It then waits for a change to the CLM_COMMAND property, returning its value to the user. The send-snd function itself, similarly, looks for SND_VERSION and sets SND_COMMAND to str, which Snd subsequently notices.


Snd and OpenGL

Snd can be used in conjunction with OpenGL, but due to the way GL uses X, it's not built into the Snd image. The files glfft.c and glfft.scm show one way to get GL graphics of Snd data. glfft.c is a program that sets up a Motif/Mesa GL drawing area widget, then sits in a loop watching for Snd-generated spectrogram data. Whenever any appears, it displays it using (exceedingly primitive) GL commands. glfft.scm is the Snd side of the process; it puts a function on the after-fft hook to write the spectrogram data to the shared file. To use this stuff, build glfft, start Snd, display spectrograms, load glfft.scm, and (start-gl).


Snd and gdb

Here are some gdb functions (for your ~/.gbdinit file) that might come in handy:

define gp
set gdb_print($arg0)
print gdb_output
end
document gp
Executes (object->string arg): gp memo_sound => #f
end

define ge
call gdb_read($arg0)
call gdb_eval(gdb_result)
set gdb_print(gdb_result)
print gdb_output
end
document ge
Executes (print (eval (read arg))): ge "(+ 1 2)" => 3
end

define gh
call g_help(scm_str2symbol($arg0), 20)
set gdb_print($1)
print gdb_output
end
document gh
Prints help string for arg: gh "enved-target"
end

SCM values are displayed as integers in gdb, so, for example, say Snd halts and you notice it's loading some unknown file:

#32 0x081ae8f4 in scm_primitive_load (filename=1112137128) at load.c:129

You can get the file name with gp:

(gdb) gp 1112137128
$1 = 0
$2 = 0x40853fac "\"/home/bil/test/share/guile/1.5.0/ice-9/session.scm\""

If you have a pointer to a CLM generator, you can use:

p mus_describe(arg)  --  show the user-view of arg
p mus_inspect(arg)  --  show every internal field of arg

The ge function can show current Snd state:

(gdb) ge "(eps-file)"
$5 = 0
$6 = 0
$7 = 0
$8 = 0x8296cf0 "\"snd.eps\""

Snd, CLM, CMN

Each of these programs sets up two X atoms for interprogram communication, *_VERSION which is guaranteed to have some value, and *_COMMAND which is evaluated as a string (i.e. (eval (read-from-string ...))) whenever it changes. In Snd, this happens via XEvents, but in the other two you have to poll for the change. In Snd, change-property can be used to talk to the others; in CLM clm-send-snd and clm-receive-snd, in CMN cmn-send-snd and cmn-receive-snd.

[this section under development...]


related documentationsnd.htmlextsnd.htmlclm.htmlsndlib.htmlsndscm.htmlindex.html