5  The RT programming language

5.1  Features

5.2  Limitations

5.3  Types

5.4  Closures

Closures are not supported. And worse, there is currently no checking whether the code is safe in a language that doesn't support closures.

The following code:

(define a (rt-compile (lambda ()
		         (let ((a (lambda (b)
			            (declare (<int> b))
			            (lambda ()
			              b))))
		           ((a 50))))))
(rt-funcall a)

...returns 0. 4

(Note, I manually had to add (declare (<int> b)) to make it compile because of a bug in the compiler.)

5.5  Functions

(define-rt (add-really a b)
  (+ a b))

(define-rt (add a b)
  (add-really a b))

(rt-funcall (rt-c (lambda ()
		    (add 2 3))))
=> 5

Optional, rest or keyword arguments are not supported.

5.6  Macros

Macros are straight forward:

(define-rt-macro (add . args)
  `(+ ,@args))

(rt-funcall (rt-compile (lambda (a b c)
			  (add a b c)))
	    2 3 4)
=> 9

And keyword arguments:

(define-rt-macro (add a1 a2 (#:a3 3) (#:a4 4) (#:a5 5))
  `(+ ,a1 ,a2 ,a3 ,a4 ,a5))

(rt-funcall (rt-compile (lambda ()
			  (add 1 2 #:a4 9))))
=> 20

[1+2+3+9+5]

The function rt-macroexpand works the same as macroexpand, but for rt-macros. It can be used inside other rt-macros, and is currently used in the if, min, max, and and or macros to speed up some situations.

When letting a variable name start with the prefix expand/, like this:

 (define-rt-macro (add expand/a expand/b)
   `(+ a b))
  

..a and b are macroexpanded automatically. In some situations, this can cause increased performance. (But not in the short add macro above though.)

For the define-rt-macro macro, I have the following lines in my .emacs file:

(font-lock-add-keywords
 'scheme-mode
 '(("(\\(define-rt-macro\\)\\>\\s-*(?\\(\\sw+\\)?"
    (1 font-lock-keyword-face)
    (2 font-lock-constant-face
       nil t))))

5.7  Reading and writing rt-variables from the guile-side

(definstrument (instrument)
  (let ((osc (make-oscil))
	(vol 0.8))
    (<rt-play> 0 10
	      (lambda ()
	        (out (* (oscil osc)
		        vol))))))

(define i (instrument))

(-> i vol)
=> 0.8

(-> i osc)
=> #<oscil freq: 440.000Hz, phase: 0.256>

To change the volume:

(set! (-> i vol) 0.2)
(-> i vol)
=> 0.2

To change the frequency:

(set! (mus-frequency (-> i osc)) 200)
=> 200

This will return an error:

(set! (-> i osc) (make-oscil))

...because only numbers and buses can be set.

5.8  Shared variables

The guile-function make-var (with an optional value argument for value) allocates a variable that can be both read from and written to both from the guile and the rt side with the functions read-var and write-var:

(let ((vol (make-var)))
  (<rt-play> 0 10
	     (lambda ()
	       (out 0 (* (read-var vol) (in 0)))))
  (<rt-play> 0 10
	     (lambda ()
	       (out 1 (* (read-var vol) (in 1)))))
  (write-var vol 0.2)
  (in 5000
      (lambda ()
	(write-var vol 1.0))))

(API might change)

make-glide-var, read-glide-var and write-glide-var are functions which interpolates the read values to avoid large jumps. Check out source code for syntax.

5.9  Midi

Receiving alsa midi is supported. Check out rt-examples.scm for a quite large example. If you don't want alsa-midi (for example if you're running osx), set *use-alsa-midi* to #f before loading rt-compiler.scm.

(<rt-play> (lambda ()
              (receive-midi (lambda (control data1 data2)
                               (declare (<int> control data1 data2))
                               (printf "gakk! %x %x %x\\n" control data1 data2)))))

5.10  Ladspa plugins

Ladspa-plugins are supported, altough a bit inefficiently. Using ladspa-plugins might also trigger bugs in some plugins because the framesize is currently always 1. The three plugins I have tried so far have worked fine though. 5

make-ladspa is a guile function that creates a plugin object. First argument is filename, and second is the name of the label.

ladspa-set! is implemented both for guile and rt, and sets an input control-number.

ladspa-run is an rt function. First argument is the plugin object, and second argument is a vct holding sound-data. The function returns a vct.

make-ladspa-gui is a guile function that automatically makes a gui for the ladspa object. This one should hopefully be convenient to use to find default values. (see rt-examples.scm for an example)

(definstrument (ladspatest)
  (let ((am-pitchshift (make-ladspa "am_pitchshift_1433" "amPitchshift")))
    (<rt-play> (lambda ()
		 (out (ladspa-run am-pitchshift
				  (vct (in 0))))))))

(define l (ladspatest))
(ladspa-set! (-> l am-pitchshift) 0 0.5)
(make-ladspa-gui l)
(-> l stop)

(API might change)

5.11  Routing signals

5.11.1  Buses

To create a bus, use make-bus. make-bus takes one optional argument, which is number of channels. Default is 1.

To read from a bus, use read-bus. To write to a bus, use write-bus.

When writing to a bus, you add your signal if the bus had been written to in the current cycle. If not, the old value is overwritten.

When reading, you always get the current value, unless the bus hadn't been written to for two or more cycles. Then you'll get zero.

This behaviour is a bit different from Supercollider, but I think its better. I migh change the behaviour later though.

5.11.2  The in and out macros

The in and out macros are supposed to provide a convenient interface to the bus system. The basic way to play a sound to the soundcard is like this: (write-bus *out-bus* sound). Instead, you can just write (out sound).

But thats not all. in and out also tries to automatically find out whether you are playing a VCT or just a single value, and which channel(s) to write to or read from.

But thats not all either. The compiler automatically creates two special bus-variables called out-bus and in-bus, which, if out-bus in-bus aren't already declared locally 6 is set to *out-bus* or *in-bus*. And since buses are settable, you can redefine outputs and inputs the way you like:

    (definstrument (oscillator)
      (let ((osc (make-oscil)))
        (<rt-play> (lambda ()
  		      (out (oscil osc))))))
    (define o (oscillator))
    (define bus (make-bus 2))
    (set! (-> o out-bus) bus)
    (definstrument (volume vol)
      (<rt-play> (lambda ()
	           (out (* vol (in))))))
    (volume 0.5 #:in-bus bus)
    (rte-silence!)

This way, you very seldom should have the need to use read-bus and write-bus directly.

In my .emacs file, I have the following lines to colorize out and in:

(font-lock-add-keywords
 'scheme-mode 
 '(("(\\(out\\)\\>\\s-*(?\\(\\sw+\\)?"
    (1 font-lock-keyword-face
       nil t))))
(font-lock-add-keywords
 'scheme-mode
 '(("(\\(in\\)\\>\\s-*(?\\(\\sw+\\)?"
    (1 font-lock-keyword-face
       nil t))))

5.11.3  The syntax for in and out

This simple function will software monitor the two first channels for 10 seconds:

(<rt-play> 0 10
	   (lambda ()
	     (out 0 (in 0))
	     (out 1 (in 1))))

This function does the same, but swaps the channels:

(<rt-play> 0 10
	   (lambda ()
	     (out 0 (in 1))
	     (out 1 (in 0))))

This function does the same, but will mix both input-channels before sending the result to both channel 0 and 1.

(<rt-play> 0 10
	   (lambda ()
	     (out 0 1 (in 0 1))))

This function does exactly the same as the one above, but by using a shorter syntax:

(<rt-play> 0 10
	   (lambda ()
	     (out (in))))

This function will send the sum of the first two input-channels to the 10 first even-numbered output-channels:

(<rt-play> 0 10
	   (lambda ()
	     (out 0 2 4 6 8 10 12 14 16 18 (in))))

This last argument for out can also be a vct. The following function will software monitor the two first channels for 10 seconds:

(<rt-play> 0 10
	   (lambda ()
	     (out (vct (in 0)
                       (in 1)))))               

This function does the same, but for channel 2, 3 and 4:

(<rt-play> 0 10
	   (lambda ()
	     (out 2 (vct (in 2)
                         (in 3)
                         (in 4)))))

This function does the same as the one above, but halves the volume:

(<rt-play> 0 10
	   (lambda ()
	     (out 2 (vct-scale! (vct (in 2)
                                     (in 3)
                                     (in 4))
                                0.5))))

Note that the API for out and in might change. (although probably not too radically...)

5.11.4  The syntax for definstrument

When using the functions in or out inside a definstrument block, you can call the instrument with the hidden key-word arguments out-bus and in-bus.

Example:

  1. First create an instrument, a simple oscillator.

        (definstrument (oscillator)
          (let ((osc (make-oscil)))
            (<rt-play> (lambda ()
      		      (out (oscil osc))))))
    

  2. Make a bus with two channels

        (define bus (make-bus 2))
    

  3. Let the oscillator play to the bus

        (oscillator #:out-bus bus)
    

    [No sound in the loudspeakers]

  4. Connect the output from the oscillator to the soundcard

        (<rt-play> (lambda ()
    	          (out (read-bus bus))))
    

5.12  Using CLM

Almost all CLM classes are supported, as well as all their methods, and other functions. Most things should work as expected, hopefully.

Exceptions:

Note that calling the CLM methods are not very efficient (that is, the (mus-*) functions). This will hopefully change, but until then, you can in certain situation significantly improve the efficiency of your code by avoid using CLM methods as far as possible. For example,

(let ((das-env (make-env `(0 400 1 500)
			 #:duration 5)))
  (<rt-play> (lambda ()
	       (let ((envval (env das-env)))
	         (if (>= envval 500)
		     (remove-me))))))

use more than reasonable less CPU than:

(let ((das-env (make-env `(0 400 1 500)
			 #:duration 5)))
  (<rt-play> (lambda ()
	       (let ((envval (env das-env)))
	         (if (>= (mus-location das-env) (mus-length das-env))
		     (remove-me))))))

The only exception is the methods for the rt-readin class, which access the attributes directly instead of doing indirect jumps.
So this is as efficient as possible:

      (let ((rs (make-readin "/home/kjetil/t1.wav")))
        (<rt-play> (lambda ()
	             (if (>= (mus-location rs) (mus-length rs))
		         (set! (mus-location rs) 0))
	             (out (readin rs)))))

5.13  Lockfree Ringbuffer

(not implemented)

Use the ringbuffer clm-like generators to exchange data-streams between guile and the realtime thread.

5.13.1  ringbuffer

(define osc (make-oscil))
(define rb (make-ringbuffer (* 8192 256)       ;; Number of samples to buffer. This one should be \underline{huge} to avoid clicking.
			    (lambda ()
			      (oscil osc))))
(<rt-play> 0 10
	   (lambda ()
	     (out (* 0.8 (ringbuffer rb)))))

The above example is not very good, because you can run oscil directly in the realtime thread. A better example is below, because you can't call readin in the realtime thread. This is how you can play a file without buffering the whole file into memory, which the rt-version of readin does:

(define file (make-readin "/home/kjetil/t1.wav"))
(define rb (make-ringbuffer (* 8192 256)
			    (lambda ()
			      (readin file))))
(<rt-play> 0 10
	   (lambda ()
	     (out (* 0.8 (ringbuffer rb)))))

5.13.2  ringbuffer-location

Assumes that location doesn't change to radically, only 0 or 1 steps more or less compared to the last one. It can receive request for any step though, but it might not be able to catch the value in time.

(define file (file->sample "/home/kjetil/t1.wav"))
(define rb (make-ringbuffer-location (* 8192 256)
				     (lambda (location)
				       (file->sample file location))))
(define position 0)
(<rt-play> 0 100
	   (lambda ()
	     (out (* 0.8 (ringbuffer-location rb position))) ;; If data is not available, a value from the buffer is returned instead. Might produce less clicks than zero.
	     (set! position (1+ position))))

To delay playing until data is available:

(<rt-play> 0 100
	   (lambda ()
	     (if (ringbuffer-location? rb position)         ;; ringbuffer-location? whether data at the position is available. If \#f, a request is sent.
	         (begin
                    (out (* 0.8 (ringbuffer-location rb position)))
                    (set! position (1+ position))))))

5.14  Provided Functions, Macros and Special Forms

5.14.1  Blocks

(begin <body>)
    Special form

Works as in scheme

5.14.2  Control Flow

(call-with-current-continuation proc)
    Special form

I think it works as in scheme, but I'm surprised how simple it was to implement...

(Not a very efficient function)

5.14.3  Functions

(lambda ...)
    Special form

Works as in scheme, except:

  • Functions might not be tail-recursive if possible. Its not very difficult to guarantee a function to be tail-recursive for single functions, but I think gcc already supports tail-recursive functions, so I hope its not necessary to add it explicitly. But I might be wrong!

  • Rest argument is not supported: (lambda (a . rest) ...) (error) (You can work around this to a certain degree by using macros with keywords or optional arguments)

(let ...)
    Macro

named let is implemented as a macro.

5.14.4  Variables

(define ...)
    Special form

Works nearly as in scheme, but unlike scheme it can be placed anywhere in a block. For example:

    (lambda()
      (set! a 2)
      (define d 9)
      (+ a d))
  

...is legal.

(let ...)
    Special form

Works as in scheme

(let* ...)
    Special form

Works as in scheme.

(letrec ...)
    Special form

Works as in scheme.

(letrec* ...)
    Special form

Like let*, but with the functions available everywhere:

           (rt-funcall (rt-compile (lambda ()
				     (letrec* ((a 2)
					       (b (lambda ()
						    (c)))
					       (c (lambda ()
						    a)))
				       (b)))))
            => 2.0

(There is also a letrec* macro for guile in oo.scm.)

(set! ...)
    Special form

Works as in scheme, except that setting Guile variables will not affect the Guile side:

        (let* ((a 5)
	       (b (rt-compile (lambda ()
				(set! a 9)
				a)))
	       (c (rt-funcall b)))
	  (list a c))
	 => (5 9.0)

(For setting a large number of variables to be visible from the Guile-side, you can use vct-set!)

5.14.5  Conditionals

(if <test> <consequent> <alternate>)
(if <test> <consequent>)

Works as in scheme. But beware that there is no boolean type, and #f=0 and #t=1. Therefore, the following expression will return 1, which is not the case for scheme: (if 0 0 1)

(case ...)
    Macro

works as in scheme, except that = is used for testing instead of eqv?

(cond ...)
    Macro

works as in sheme, but => is not supported

(< ...)
    Macro

works as in scheme

(> ...)
    Macro

works as in scheme

(< ...)
    Macro

works as in scheme

(= ...)
    Macro

works as in scheme

(>= ...)
    Macro

works as in scheme

(= ...)
    Macro

works as in scheme

(not ...)
    Function

works as in scheme

5.14.6  Iteration

(while <test> <body>)
    Special form

Works as in Guile, including both break and continue. (Does not expand to a recursive function.)

(break)
    Special form

Used to break out of a while loop. (Not a very efficient function)

(continue)
    Special form

goto the top of a while loop. (Not a very efficient function)

(do ...)
    Macro

works as in scheme (Using while)

(range ...)
    Macro

       (range i 5 10
              (printf "%d " i))
        => 5 6 7 8 9
        (range i 10 5
	       (printf "%d " i))
        => 10 9 8 7 6
     

(Using while)

5.14.7  Logical Operators

(and ...)
    Special form

works as in scheme

(or ...)
    Special form

works as in scheme

5.14.8  Mathematics

(+ ...)
    Function

works as in scheme

(- ...)
    Macro

works as in scheme

(* ...)
    Function

works as in scheme

(/ ...)
    Macro

works as in scheme

(1+ ...)
    Function

works as in Guile

(1- ...)
    Function

works as in Guile

(min ...)
    Macro

works as in scheme

(max ...)
    Macro

works as in scheme

And:

sin cos tan abs log exp expt acos asin atan sqrt asinh acosh atanh cosh sinh tanh atan2 (see ``man atan2'') hypot (see ``man hypot'')

zero? positive? negative? odd? even? remainder modulo quotient

floor ceiling truncate round truncate

logand logior lognot logxor ash random

5.14.9  Lists, pairs, vectors and VCT's

VCT's are extremely efficiently implemented.

Pairs, lists and vectors are not, and should be avoided.

The provided functions are:

vct make-vct vct-map! vct-length vct-ref vct-set! vct-scale! vct-offset! vct-fill!

vector? vector-length vector-ref

pair? null? car cdr cadr caddr cadddr caddddr cddr cdddr cddddr cdddddr cdar cdadr cdaddr cdadddr caar caadr caaddr caadddr list-ref for-each

5.14.10  Others

(declare ...)
    Special form

Works as in common lisp, except that the name of the types are different: <int>, <float> and <double> .

           (define-rt (int-fib n)
	     (declare (<int> n))
	     (if (< n 2)
		 n
		 (+ (fib (- n 1))
		    (fib (- n 2)))))

...which is the same as:

           (define-rt (int-fib n)
	     (declare (<int> n))
	     (the <int>
		  (if (< n 2)
		      n
		      (+ (fib (- n 1))
			 (fib (- n 2))))))

As you see, the compiler is a little bit intelligent when determine types, so it should not be necessary to use declare on all numeric variables and the for every expression, (although it shouldn't hurt either).

(is-type? <type> <variable>)
    Special form

Mostly for internal use:

             (let ((a 5))
	       (is-type? <int> a))
             => 1
             (let ((a 5))
	       (is-type? <float> a))
             => 0

The first argument must be a type, and the third argument must be a variable-name. This is not legal: (is-type? <int> (+ 2 3)) Used to implement exact?/inexact?/etc., and for various optimizations.

(the ...)
    Special form

Works as in common lisp, except that the name of the types are different: <int>, <float> and <double> are the currently supported numeric types.

       (define-rt (int-cast-add a b)
          (the <int>
	       (+ a b)))

       [...which is the same as:

       (define-rt (int-cast-add a b)
          (declare (<float> a b))
          (the <int>
	       (+ a b))) ]

       

(include-guile-func ...)
    Macro

Includes the code of a Guile function.

                      (define (add a b)
			(+ a b))
                      (rt-funcall (rt (lambda ()
					(define add (include-guile-func add))
					(add 2 3))))
                      => 5
    

(remove-me)
    Macro

Removes the function from the realtime engine. See rt-examples.scm for an example-

(unquote ...)
    Macro

           (define a 9)
           (rt-funcall (rt (lambda()
			     (+ ,pi ,(+ 5 a))))
           => 17.1415926535898

    

And:

exact? inexact? number? string? exact->inexact inexact->exact

printf (Using c's fprintf with stderr as the first argument. Warning, this one is not realtime safe!)

5.15  Various

* In addition to the functions and macros described above, theres a bunch of very internal functions and macros that used wrongly can hang your machine or destroy your harddisk. Most of them start with a prefix rt- or rt_.

One very useful function might be rt_alloc which does realtime-safe memory allocation. All memory allocated with rt_alloc is automatically freed when the function returns.

* Theres still a lot of smaller optimizations thats possible to do. However, gcc (V>=3) should be able to fix most of these.

* Note to myself, define-ec-struct in eval-c.scm needs to be documented.


2 Writing Guile variables only half-worked, and sometimes made guile segfault, so I removed it.

3 Note also that although the compiler tries to determine the most fitted type for a numeric variable, it often fails. So in some situations you have to declare numeric variables although it shouldn't have been necesarry. For an example, study the C-code generated by the instrument called extremely-simple-delay found in the file rt-examples.scm. Two of the local float variables should have been ints. This is because the compiler is buggy. It will hopefully be fixed though.

4 The current behavior guarantee the return value to be 0. But that behavior might change.

5 I think at least all swh-plugins should work fine because they seem to use a common ringbuffer to buffer up sound-data.

6 Never declare in-bus and out-bus in the toplevel!