PD-Scheme

Version 0.2

Please note that this is still very much a work in progress

Introduction

PD-Scheme is a system for writing objects for the computer-music software called Pure Data. It lets you use the Scheme programming language to write PD objects. It is an alternative to writing such objects in C, or to implementing them as PD abstractions. It does not in essence facilitate doing anything that can not be done by connecting together existing PD objects, although it offers some possibilities because all objects created in pd-scheme share the variable space of a single scheme interpreter. pd-scheme currently uses the "Scheme in One Day" (SIOD) Scheme implementation..

A couple of important notes: First, this distribution currently only works on Linux, although it should be say to port to other operating systems. Second, a major failing in the present version is that in many cases, errors in Scheme will cause PD to unexpectedly quit! Sorry about this - I will work on this problem.

"Why write externs in Scheme?" I created pd-scheme because I found that in some cases, connecting conventional PD externs together to implement all but the simplest logic was cumbersome, difficult to understand later, and to me at least, was more difficult then using a procedural language. On the other hand, I was not eager to drop down to the C language to write externs which in many cases would be highly specific to particular compositions, and changing by the minute. My goal was to have an easier way to quickly implement parts of a patch that would otherwise require a lot of mouse-clicking and a rats-nest of connections if done solely using the available PD externs. Of course (and this is a good thing!) there alternative ways in some cases to reduce this problem; for example, we could use the "expr" object. So pd-scheme is not the only answer to such things; it's just one more alternative.

Of course, there are drawbacks to the Scheme approach. First, is the problem of garbage collection - if this happens in the middle of a time-sensitive computation, it could easily cause a missed audio deadline. Second, using an interpreted Scheme implementation like SIOD means that externs written in Scheme will execute more slowly then equivalent externs written in C, and probably more slowly then the same thing implemented as a PD abstraction using connected PD externs (My hunch is that the PD message processing overhead is generally more efficient than that of the SIOD interpreter - considering that SIOD has no byte-code compiler, and the overhead to calling a Scheme object is currently non-trivial).

These drawbacks have not as of yet been a problem with my own use of pd-scheme. At some point, I hope to get a chance to measure both the execution speed, and the rate of garbage accumulation. For my personal purposes, I think that I have enough RAM to cover the garbage accumulation over the typical period of time that I am running the PD session. If this becomes a problem in the future, there are two solutions. First, in pd-scheme, garbage collection can be triggered dynamically from PD (perhaps at a quiet moment). Second, it is always possible to rewrite things in C as the need arises. In other words, we could consider pd-scheme as a kind of prototyping language, to let us quickly get things working that later could be implemented in C if such a need arises for performance reasons.

Another point is that pd-scheme only handles control messages; it does not deal with audio at all. Given its purpose, I don't consider this to be a problem.

"Why use SIOD instead of a different Scheme/Lisp implementation?" After looking at a few alternatives, SIOD seemed to be the quickest route to get something working. It is very easy to plug SIOD into an application. If pd-scheme ever gets a user base greater than one, I would consider using something better (faster, more complete).

Installation

Ok, so you want to try it. First, remember that this package only works on Linux for now.

1a. Get SIOD

1b. Unfortunately, there seems to be a symbol name clash between SIOD and some other library or extern that is used by PD. To fix this, add the following line to the top of siod.h in the SIOD distribution: #define err siod_err. The symptom if you don't do this, is that PD will exit whenever an error is encountered in scheme code.

1c. Now compile and install SIOD according to its directions.

2. If you haven't already, extract the pd-scheme source archive.

3. Go into the pd-scheme directory (modulo the version number) and look over the makefile for any changes that you might want to make. In particular, you must change the SIOD definition to point to the directory where you have installed SIOD.

4. In the pd-scheme directory do a make followed by make install

5. Assuming no errors from step 4, then you need to tell PD to load the pd-scheme extern. As shipped, the makefile puts this in /usr/lib/pd/externs. So either on the PD command line, or in .pdrc, add -lib /usr/lib/pd/externs/scheme.

6. The pd-scheme extern looks for a file called pdinit.scm somewhere in the PD search (specified by the PD -path option), and loads it into the Scheme interpreter. This file provides the Scheme-side implementation of pd-scheme. Make a symbolic link somewhere in your PD path to point to the pdinit.scm file in the pd-scheme distribution. This is important, because unless the pd-scheme directory happens to be in your pd search path, PD will not find the file.

7. Copy the pdsite.scm and pdexampl.scm files from the pd-scheme distribution to somewhere in your PD path, or else make symbolic links for these as described in step 6. pdsite.scm gets loaded by pdinit.scm and is the file that customizes pd-scheme for your use. You will eventually want to modify this file to load your own libraries of scheme externs.

8. Now, start PD and you if you see a pretentious message on your console indicating "scheme is here", it's a a good sign. If, on the other hand, PD quits, it probably means something is wrong with the paths (or else something is missing in these installation instructions!)

Using pd-scheme

Messages

You can evaluate an arbitrary expression in the scheme interpreter by sending a message to scm. For convenience, the outermost set of parens will be supplied for you.

For example, select File/Message from the PD menu and type scm print (+ 1 2) (or use a message box in a patch). You should see the answer on the PD console.

Two such messages you might use a lot are scm gc to run the garbage collector, and scm reload to reload your scheme files. (Note, however, that reloading a scheme definition of an object will not change objects that are already instantiated; you will have to recreate the object in your patch for changes to take effect.)

Using scheme externs (objects)

First, there is a patch that tests/demos some of the pd-scheme object that are defined in pdexampl.scm. This patch is pd-scheme-examples.pd. It uses the zexy "segregate" object, but if you don't have zexy, you can delete the box and the patch should otherwise work.

To use an object that has been defined in scheme, type in an object box () name arg1 arg2, where name is the name of the object as defined in scheme, and arg1 arg2 are the initialization arguments (if the object uses them).

For example, type () addn 3 to create an object that adds the numbers at its three inlets. (This object is defined in pdexampl.scm).

To create an object in debugging mode, use ()?. Currently this has the effect that the scheme expressions that the pd-scheme passes to and receives from scheme, in the course of creating the object and triggering it, are printed to the PD console.

Creating scheme objects

When an object starting with () is in a patch, pd-scheme expects to find a function in scheme named make-name, where name is the name of the object (the next item after the "()" in the object box). This function should return a list. The first item in the list is a lambda expression that will be called when something is received at the object's first inlet. The cdr of the list is a list of inlet/outlet specifiers, which tell pd-scheme how many inlets and outlets to create, and their types.

A quick tutorial

Here's an example from pdexampl.scm. This code defines an object called >10.

;;; Echo the float input, only if greater than 10.
;;; 
(define (make->10)
  (cons
   (lambda (arg)
	 (if (> (cadr arg) 10)
		 (list 0 (cadr arg)) ; ok - output it.
		 '())) ;; empty list will result in nothing sent to any outlets.
   `(o f)))

This function is what will get called when you type () >10 in an object box. In the list that it returns, the first item is a new function (lambda expression). That function gets stored away by pd-scheme, and is what will be called when something is received at the first inlet of the object.

The rest of the list (after the lambda expression) that gets returned is a set of symbols defining the inlets and outlets. The o signifies that the symbols that follow this will each specify an outlet, until an i is reached (not used here), in which case subsequent symbols specify inlets. In this case, the f specifies a single float outlet.

There is always at least one inlet created, and it is always an "anything" inlet. Any i specifiers thus are only needed to define additional inlets.

When something is received at the inlet, the lambda expression will be called, and each inlet will be bound to the lambda arguments. Since there is only one inlet in this example, we only have one argument. For "anything" inlets (which the first one always is), the value of this argument is the PD message that is received, translated into a scheme list. So, if we connect a number box to the inlet and fire it, arg would get a value like (float 10), because PD will send a message with the "float" message selector. To access the float value that we received, we thus have to use cadr to look at the second item of the message.

The return value of the lambda expression is a list that specifies what values will be sent to what outlets. It is a list of alternating outlet numbers and values. In this case, if the input was greater than ten, we return a list consisting of 0 (for the first outlet) and the value we received. If the input was not greater than ten, then we output nothing by returning an empty list.

Next, let's look at an object that uses initialization arguments, has more than one outlet, and fires an outlet more than once when it gets triggered.

;;; count-loop 
;;;
;;; creation argument = loop size = (max loop value - 1)
;;; any input : starts the loop (from zero)
;;; outlet 0: 0 through loop size -1
;;; outlet 1: bang when done
(define (make-count niters)
  (cons
   (lambda (arg)
	 (let ((i 0) (output '()))
	   (while (< i niters)
			  (set! output (append output (list 0 i)))
			  (set! i (+ 1 i)))
	   (set! output (append output (list 1 'bang)))
	   output))
   '(o f s)))

This time, the object creation function expects one initialization argument, niters. This will take the value of the first (only, in this case) item that is typed into the object box after the object name.

The outlet specifier in this case causes two outlets to be created, a float and a symbol.

The run-time lambda dynamically builds an output list in output. The loop will add a number of 0 n pairs to the list, which will cause the first outlet to be repeatedly fired with the values from 0 to niters-1. Finally, a "bang" message at outlet 1 (the second outlet) is tacked on, and this list is returned to actually generate the outputs.

One thing to note is that this object could be considered to be in bad style, since it violates the right-to-left convention of PD objects with multiple outlets. In general, for an object with multiple outlets, it might be better to generate the list in reverse outlet-number order.

Finally, here is an object that needs to keep some state.


;;; subdivide inputs
;;;
;;; In a sense, this is a beat subdivider.
;;;
;;; For every input other than "reset", a float is sent
;;; to the outlet and incremented, unless it unless it reaches the creation
;;; arg in which case it goes back to zero.
;;;
;;; A "reset" message resets the output value to zero without outputting anytning.
;;;
(define (make-subdiv n)
  (cons
   (let ((i 0))
	 (lambda (arg)
	   (let ((output '()))
		 (if (= (car arg) 'reset)
			 (begin
			   (set! output '())
			   (set! i 0))
			 (begin
			   (set! output (list 0 i))
			   (set! i (+ i 1))
			   (if (>= i n)
				   (set! i 0))))
		 output)))
   '(o f)))

Here, i keeps its value as part of the object's state, since it is captured as part of the closure created by the lambda expression.

Also of note, we check the message selector, and take a different action if it is a "reset" message.

How pd-scheme finds object definitions

When an object is created, pd-scheme first looks for a source file named name.scm in the PD search path. If it finds it, it first loads that file, which should contain the definition of the object.

Otherwise, we hope that the definition has already been loaded, which typically would happen through the loading of a file by site.scm.

The scheme function pdload is usefull for this. It is similar to load, with two differences. First, only the base file name is specified (it will add the ".scm" extension). Second, it searches the PD path to find the file. So you would typically customize your site.scm to load your own scheme files (that might define multiple objects) using pdload.

An easier way

There are some macros provided as a shortcut to create objects with one inlet.

pddef creates an object with an arbitrary outlet specification. Its parameters are:


(pddef name-and-initargs outlet-specifier state-variables function-body)

"name-and-initargs" is the name of the object to be created, along with any initialization arguments. With creation args, it must be a list, but otherwise, just the name can be used.

"outlet-specifier" is the outlet specification list.

"state-variables" is a list of state variables and their values, in the same form as would be used as the first argument to a "let" expression. It can be an empty list if the object doesn't need to keep state.

"function-body" is the the body of the run-time lambda expression. Use args to reference the function argument.

An example:


;;; Output our inlet to both outlets
;;;
(pddef taa (o l l) ()
   (list 1 args 0 args))

To facilitate even further laziness, pddeff and pddefa let you omit the outlet specifier, and they create objects with a single "float" outlet, and a single "anything" outlet, respectively:


;;; Output 0.0 upon receiving anything at our inlet
;;;
(pddeff out0 () (list 0 0.0))

;;; Echo the inlet to the outlet
;;;
(pddefa echo () (list 0 args))

Clocks

PD clocks can be used to run a scheme object at a certain time or after a certain delay. For an example of this, see the "timeout" object in the examples.

To specify that an object will use a clock, include c in the I/O list specifier.

To set the clock to trigger the object at a given absolute time, include s time in the return value list. To set the clock to trigger after a certain delay, use d delay. To unset the clock, use u 0, where the 0 is an ignored argument but required for consistency.

When an object is triggered by a clock, it acts as if it received the message "clock" at its first inlet.

Utility Functions

There are various functions provided in pdinit.scm that may be usefull. For example, there are functions that convert symbols like "a4" into an integer corresponding to the equivalent midi key number.

Changes

From version 0.1 to 0.2:

1. Bug fixes, thanks mostly to Orm Finnendahl and David Casal

2. Outlet type "l" has been changed to "a", to outlet an "anything" rather than a list. "l" still works but for compatibility now works identically to "a". You can easily output a list if you want, see "out-type" in the examples.

2. PD clocks have been added.

3. Not really a change in pd-scheme, but the cause for the crashes on scheme errors has been traced to a name conflict. See the installation section for details.

4. "pdlogicaltime" function returns the PD logical time in ticks. But this has not been tested yet.

Suggestions, Acknowledgements, Contact info

There are quite a few ideas I have as to how to extend pd-scheme in the future. But if anybody has any ideas of their own, please e-mail me and let's do it!

Thanks to Orm Finnendahl and David Casal for bug fixes and suggestions. lt@westnet.com