S4M Scheme API

Below are details of the Scheme API. Core S7 functions are not listed here, only functions added by S4M. Scheme functions are either defined in s4m.c using the S7 Foreign Function Interface, or in Scheme code boostrapped from the initial load of s4m.scm, including the s74.scm file.

File Loading

(load-from-max {filename string})

Load a file into Scheme. Acts excactly like Scheme standard load but finds the full path on the Max file search path first.

Console Output

(post {args…})

Post to the Max console. All arguments will be converted to string representations automatically. Post returns the null list.

(define a 99)
(post "my var a is" a)
s4m> my var a is 99

Note: Console output by default does not print null responses, so that the console does not print a message on every function call for side effects, such as out. This can be changed by setting log-null to 1, with either a message, the attribute, or the inspector.

(s4m-filter-result res)

Hook function to allow users to customize which results will be logged to the console. Redefine or alter this to return the keyword value :no-log to indicate result should not be logged.

;; if we replace what would be returned (res) by :no-log, s4m will not print to console
(define (s4m-filter-result res)
   (cond
     ;; turn off logging of the returned integers
     ((int? res) :no-log)
     ;; for everything else, do the normal thing
     (else res)))

Outputs

(out {outlet} {value})

Output {value} through outlet {outlet}. Value must be a single object. Returns the null list, thus by default does not log to console.

;; output number 99
(out 0 99)
;; output a max list of ints
(out 0 (list 1 2 3))
(out 0 '(1 2 3))
;; output a bang
(out 0 'bang)
;; output the value of my-var
(out 0 my-var)
;; output the max symbol "set"
(out 0 'set)
;; output the max message "set 99"
(out 0 (list 'set 99))
(out-0 {value})

Convenience helper for out X, as sometimes a single argument function is helpful. Defined for outs 0-7, to add more, edit s4.scm Returns the null list, thus by default does not log to console.

(out* {list or vector})

Unpacks the argument and spreads across all available outlets. Input lists with more items than there are outlets have the remaining items sent out as list out the final outlet.

;; assuming there are 3 outlets
(out* '(a b c d))
;; out 0: a, out 1: b, out 2: c d

Sending Messages

We can send arbitrary Max messages to other Max objects that have been given a scripting name. Before doing so, we must send the scan message to the s4m object, which will scan the current patcher and all descendents, registering scripting names interally in the s4m object (in the C code).

(send {target} {msg} … {atoms})

Send the message {msg}, which may be followed by any number of values to be handled as Max atoms.

;; update the contents of a number box that has scripting name num-target
;; we quote num-target below as we want the symbol num-target, not the
;; value of a variable named num-target.
(send 'num-target 99)

;; send a message box a message to update to the contents "foobar 1 2 3"
(send 'msg-target 'set 'foobar 1 2 3)

;; this means we can send a message as list using scheme's apply function
(define msg-list (list 'set 1 2 3))
(apply send (cons 'msg-target msg-list))

There are also two convenience helper, send-list and send*.

(send-list {target} {msg-list})

Send the message {msg-list}, a single arg of a list.

(define msg-list (list 'set 1 2 3))
(send-list 'msg-target msg-list)
(send* {args})

Flattens all lists passed as arguments and calls send

(send-list 'msg-target 'set '(a b c) '(1 2 3))
; calls (send 'msg-target 'a 'b 'c 1 2 3)

This can be used to integrate with all kinds of Max objects, including updating colls, dicts, tables, etc. We can copy whatever message the object receives and send them.

Table I/O

We can write and read integer data directly to and from named Max tables, as well as query for the existence and size of Max tables. Max tables are useful for storing integer data that we want to share between an s4m object and the rest of Max (or another s4m object. Tables are queried in each function, so if you want to read or write a lot of data, the vector conversions functions will be faster than a loop of single point table operations.

Short form aliases exist for all functions and are identical except for the function name.

(table? {table-name})

Returns #true if the symbol {table-name} corresponds to a named Max table.

(table-length {table-name})

Returns integer length of table {table-name}. Error if not a valid table. Alias: tabl

(table-ref {table-name} {index})

Returns value at {index} in table {table-name}. Alias: tabr

(table-set! {table-name} {index} {value})

Sets value at {index} in table {table-name} to {value}. Alias: tabs

(table->vector {table-name} {opt. index} {opt. count})

Returns a new Scheme vector from contents of a table, starting at index 0 or {index} in the table, copying entire table or {count} points if optional {count} given. Alias t->v

(table-set-from-vector! {table-name} {table-index} {vector} {opt. vector-index} {opt. count})

Sets contents in a table from a Scheme vector, starting at {table-index}. Copies the entire vector, or from {vector-index} for a total of {count} points if given. If table is not large enough, copies whatever fits. Returns a new vector of the contents copied. Alias: tabsfv

(vector-set-from-table! {vector-name} {vector-index} {table-name} {opt. table-index} {opt. count})

Sets content in an existing vector from a Max table, starting at {vector-index}. Copies the entire table, or from {table-index} for a total of {count} points if given. If vector is not large enough, copies whatever fits. Returns a new vector of the contents copied. Alias: vecsft

Buffer I/O

We can write and read float data directly to and from named Max buffers, as well as query for the existence and size of Max buffers. Max buffers are useful for storing float data that we want to share between an s4m object and the rest of Max (or another s4m object, they do not need to be used for audio samples but can hold any collection of floats. Buffers are queried and locked in each function, so if you want to read or write a lot of data, the vector conversions functions will be faster than a loop of single point buffer operations.

Short form aliases exist for all functions and are identical except for the function name.

(buffer? {buffer-name})

Returns #true if the symbol {buffer-name} corresponds to a named Max buffer.

(buffer-samples {buffer-name})

Returns integer length of buffer {buffer-name}. Error if not a valid buffer. Note that this is called buffer-size and not buffer-length to match Max naming, where length gives the length in seconds and size in samples. Alias: bufsmp

(buffer-ref {buffer-name} {opt. channel} {index})

Returns value at {index} in buffer {buffer-name}. If called with two arguments, channel defaults to zero. Alias: bufr

(buffer-set! {buffer-name} {opt. channel} {index} {value})

Sets value at {index} in buffer {buffer-name} to {value}. If called with two arguments, channel defaults to zero. Alias: bufs

(buffer->vector {buffer-name} {opt. channel} {opt. index} {opt. count})

Returns a new Scheme vector from contents of a buffer, reading from {channel} or channel zero, starting at index 0 or {index} in the buffer, copying entire buffer or {count} points if optional {count} given. Alias b->v

(buffer-set-from-vector! {buffer-name} {opt. buffer-channel} {opt. buffer-index} {vector} {opt. vector-index} {opt. count})

Sets contents in a buffer from a Scheme vector, using {buffer-channel} or channel zero and starting at {buffer-index} or index zero. Copies the entire vector, or from {vector-index} for a total of {count} points if given. If buffer is not large enough, copies whatever fits. Returns a new vector of the contents copied. Alias: bufsv

Note: there is not yet a buffer version of table-set-from-vector!

(vector-set-from-buffer! {vector-name} {vector-index} {buffer-name} {opt. buffer-index} {opt. count})

Sets content in an existing vector from a Max buffer, starting at {vector-index}. Copies the entire buffer, or from {buffer-index} for a total of {count} points if given. If vector is not large enough, copies whatever fits. Returns a new vector of the contents copied. Alias: bufsft

Dictionary I/O & hash-tables

We can read and write key-value stores in Max dictionaries and S7 Scheme hash-tables, which are unordered key-value hashmaps. When converting nested values in Max dictionaries to Scheme, arrays become Scheme vectors, and nested dictionaries become Scheme hash-tables. Only symbol, keyword, or string keys are supported by the conversion functions.

In Lisp dialects that support keywords, the idiomatic practice is to generally use keywords for hash-table keys. A keyword is a symbol that starts with a colon, and always evaluates to itself, which means that they do not need to be quoted. This works well in Max too, Max will simply treat them as string keys that happen to start with a colon. This ensures you never get mixed up about whether a symbol in a Max message is supposed to be evaluated or not - if it’s a keyword it won’t matter if this gets eval’d by the interpreter. Keywords are recommended as dict and hash-table keys wherever possible.

Note that in S7, querying a hash-table for a non-existent key returns #false. In Max, there is no boolean false value, and booleans are usually expressed as 0. This means 0 is often a valid value to store, and thus getting #false back can be misleading. Instead of returning a potentially valid value on a key error, the Max dict functions raise Scheme errors, which can be caught to return a default value of your choosing.

S7 hash-table and keyword examples

;; a keyword always evaluates to itself
(eval :foobar)
s4m> :foobar

(define key :foobar)
(eval key)
s4m> :foobar

;; which means quoting is unnecessary and does nothing
(eval ':foobar)
s4m> foobar

;; make a hashtable with two keyword slots
(define my-hash (hash-table :a 1 :b 2))
s4m> (hash-table :a 1 :b 2)

;; get value at :a
(hash-table-ref my-hash :a)
s4m> 1

;; applicative syntax of the same
(my-hash :a)
s4m> 1

;; a non-existing key from an S7 hash-table returns false (not an error!)
(my-hash :z)
s4m> #f

;; set a value in a hash-table, sets value and returns value
(hash-table-set! my-hash :foobar 99)
s4m> 99

;; applicative syntax of the same
(set! (my-hash :foobar) 99)
s4m> 99

;; make a nested hash-table
(set! (my-hash :pets) (hash-table))
s4m> (hash-table)

;; now we can set and get recursively using applicative syntax
(set! (my-hash :pets :dog) 'Poppy-Poodle)
s4m> Poppy-Poodle

(eval my-hash)
s4m> (hash-table :a 1 :b 2 :foobar 99 :pets (hash-table :dog Poppy-Poodle))

(hash-table-ref my-hash :pets :dog)
s4m> Poppy-Poodle
(my-hash :pets :dog)
s4m> Poppy-poodle

Max Dictionary API

Examples below can be used with the tests-dict.maxpat test patcher in the patchers directory.

(dict-ref {dict-name} {symbol|list key} )

Return value in dict {dict-name} at {key}, where dict-name is a symbol, and key can be either a list or symbol. If key is a list, recurses through the list. Raises an ‘key-error’ error if the key is invalid. Alias dictr

;; get a value from max dict named "test-dict", at key "a"
(dict-ref 'test-dict 'a)
s4m> 1

;; get a value that is a nested dict, becomes a hash-table
(dict-ref 'test-dict 'b)
s4m> (hash-table 'ba 2 'bb 3)

;; get value at key "ba" in nested dict at key "b"
(dict-ref 'test-dict (list 'b 'ba) )
;; same thing with alternative quoting syntax
(dict-ref 'test-dict '(b ba))
s4m> 2

;; get the value at index 2 in the nested vector at key "c"
(dict-ref 'test-dict '(c 2) )
s4m> 33

;; out of range index 3 raises error
(dict-ref 'test-dict '(c 3) )
s4m> ERROR
(dict-set! {dict-name} {symbol|list key} {value} )

Sets value in dict {dict-name} at {key}, where dict-name is a symbol, and key can be either a list or symbol. If key is a list, recurses through the list. Returns value set. Raises error if key invalid. Alias dicts

;; set a value in max dict named "test-dict", at key "z"
(dict-set! 'test-dict 'z 44)
s4m> 44

;; set a value that is a hash-table, becomes a nested dict
(dict-set! 'test-dict 'y (hash-table :a 1 :b 2))
s4m> (hash-table :a 1 :b 2)

;; set value at key "bc" in nested dict at key "b"
(dict-set! 'test-dict (list 'b 'bc) 111)
s4m> 111

;; attempt set at invalid key list (there is no 'foo entry to recurse through)
(dict-set! 'test-dict (list 'foo 'b) 99)
s4m> ERROR
(dict-replace! {dict-name} {symbol|list key} {value} )

Sets value in dict {dict-name} at {key}, where dict-name is a symbol, and key is a list of symbols or integers. If the list recursion hits an unused key, creates a nested dict for it, similar to the Max “replace” message to dicts. (Nested arrays do not get automatically created, also like the Max replace message). Returns value set. Raises error message if key or dict invalid. Alias dicts*

;; set a value that is a hash-table, creating an intermediate hash-table automatically
(dict-replace! 'test-dict (list 'foo 'bar) 99)
s4m> 99
(dict->hash-table {dict-name})

Return a hash-table of a Max dictionary. Nested dicts become nested hash-tables, arrays become vectors. Raises error on bad dict-name. Alias d->h

(hash-table->dict {hash-table} {dict-name})

Write entire contents of a hash-table to a named Max dict. If {dict-name} does not exist, creates a dictionary. If {dict-name} already exists, replaces entire contents. Alias h->d.

Max Time and Transport API

Transport functions exist to interact with the Max master transport. Named transports in addition to the master transport are not yet supported, and behaviour in Max For Live is unknown (but will be tackled in future!).

(time)

Returns current time in float ms. This is the global Max time, not the transport time. This only resets to 0 on restarting Max.

(transport-state)

Returns #t if transport is running, #f otherwise. Alias t-state

(transport-state-set! {boolean|int state})

Starts master transport on #t or 1, stops on #f or 0. Returns state set. Alias t-state!.

(transport-bpm-set! {int bpm})

Starts master transport tempo to bpm. Note, there is no get version in the Max C SDK, strangely. Returns bpm set. Alias t-bpm!

(transport-time-sig)

Returns current time signature as list of (numerator denominator). Alias t-time-sig

(transport-time-sig-set! {int numerator} {int denominator})

Sets master transport time signature. Alias t-time-sig!

(transport-ticks)

Returns current master transport location in ticks (float). Alias t-ticks

(transport-seek {opt. bars} {opt. int beats} {float|int ticks})

Sets master transport location immediately. If called with three arguments, sets with Max bbu (bars beats units) format, otherwise sets location in ticks. Returns new transport location in ticks (float). Alias t-seek

(ticks->ms {number ticks})

Converts ticks to float ms according to current settings of the master transport.

(ticks->bbu {number ticks})

Converts ticks to list of (bars, beats, ticks) according to current settings of the master transport.

(ms->ticks {number ms})

Converts ms to float ticks according to current settings of the master transport.

(ms->bbu {number ms})

Converts ms to list of (bars beats units) according to current settings of the master transport.

(bbu->ms {int bars} {int beats} {number units})

Converts bars-beats-units to float ms according to current settings of the master transport.

(bbu->ticks {int bars} {int beats} {number units})

Converts bars-beats-units to float ticks according to current settings of the master transport.

Scheduling, Delays, & Timers

S4M uses Max’s clock facilities to interact with the Max scheduler to create accurate delays and timers. As of 0.3, these can be run from both high priority and low priority thread objects. In the low priority thread, they are scheduled with the high priority scheduler, but defered when they start.

Delays can also interact with the global transport, including quantizing to transport-aware time values.

Internally, delays register callback functions in the global s4m-callback-registry hash-table. Cancelling an event consists of removing the callback function. Functions that are scheduled with the delay functions or used as clock listeners must take no arguments, with the exception of the tick listeners, which will get the current tick as a single argument.

Helpful Functions

Some helpful functions for working with timers and delay functions.

(isr?)

Returns true if exectuting in the the high-priority scheduler thread.

(time)

Returns current Max time in ms.

Delay Functions

Functions to schedule execution of a Scheme function in the Max scheduler.

(delay {number time} {function})

Schedule function to execute in {time} ms. Returns a symbol of the callback key, which is the key underwhich the generated callback function is registered, and which can be used to cancel an event.

;; schedule a function that posts to the console in 1 sec
(delay 1000 (lambda () (post :tada)))

;; or
(define (delayed-func) (post :tada))
(delay 1000 delayed-func)
(delay-eval {number time} {var-args} )

Convenience function to allow calling delay without having to make a function. If called with 1 arg in var-args and it is a list, it will be evaluated at the time scheduled. If called with many args, they will be treated as a list and evaluated at the time scheduled.

;; schedule a function that outputs 99 from outlet 0 in 1 second
(delay-eval 1000 (list out 0 99) )

;; or
(delay-eval 1000 out 0 99)
(delay-t {number|symbol time} {function})

Transport aware version of delay. Time can be number of ticks, or Max time notation such as 4n. If using time notation as a symbol, you will need to quote it or pass it in a variable. Otherwise identical to delay.

;; delay for 480 ticks (1 quarter note)
(delay-t 480 my-function)

;; or
(delay-t '4n my-function)

;; or
(define quarter '4n)
(delay-t quarter my-function)
(delay-t-eval {number|symbol time} {var args})

Transport aware version of delay-eval.

(delay-tq {number|symbol time} {number|symbol quantize-time} {function})

Transport aware and quantized version of delay. Time and quantize-time can be number of ticks, or Max time notation such as 4n. Execution is scheduled for the next boundary of {quantize-time}. Note that this is independent of whether the transport is playing - the correct time is calculated and scheduled. Stopping the transport does not cancel the event.

(delay-tq-eval {number|symbol time} {number|symbol quantize-time} {var args})

Eval version of delay-tq.

(cancel-delay {handle} )

Given a delay call that returns {handle}, cancel execution of the function. Note that this function is used for all the delay variants, as they all return guaranteed unique callback handles.

;; schedule a function and then cancel it
(define cb-handle (delay 1000 (lambda () (post :tada))))
(cancel-delay cb-handle)

Timer Functions

Several functions are available to register periodic timers, with or without following the global transport. These are implemented at present only for the high-priority thread. (Low priority timers to be implemented in future). These all return the keyword :clock-registered to let you know they succeeded.

(clock-ms {number ms} {function callback})

Register function {callback} to be executed every {ms} milliseconds. The callback registered should accept no arguments, but can get the the current time by calling (time) in its body.

;; register a time callback to post current time in ms every second
(define (time-callback) (post "current time" (time)))
(clock-ms 1000.0 time-callback)
(cancel-clock-ms)

Cancels the clock-ms timer.

(clock-ms-t {number ms} {function callback})

Transport aware version of clock-ms. The clock used only advances if transport is playing.

(cancel-clock-ms-t)

Cancels the clock-ms-t timer.

(clock-ticks {number ticks} {function callback})

Transport aware version that receives the current transport tick location as its argument.

;; register a time callback to post current ticks every quarter note
(define (tick-callback current-tick) (post "current tick:" (current-tick)))
(clock-ticks 480 tick-callback)
(cancel-clock-ticks)

Cancels the clock-ticks timer.

Garbage Collector Functions

Advanced users may want to tweak how the garbage collector runs to allow running with lower latency and larger programs. For example, one might set the heap-size quite low and deliberately run the gc very frequently off a timer, quantized such that it runs at an unimportant time musically such as between 16th notes every 4th 16th note or something. Or we might decide not to run it at all until a song is over, and then run when the music is paused.

(gc-disable)

Turn off the gc. Note that your heap-size will grow, and once you turn it back on, it will take a lot longer to run the gc if the heap is now big.

(gc-enable)

Enables the gc. This does not tell the gc to run though.

(gc-try)

Runs the gc if it’s enabled, does nothing if not.

(gc-run)

Forces the gc to run, whether or not it’s enabled. Does not change the enable status.

(set! (*s7* ‘gc-stats #t))

Turns on and off gc stats logging to the console, which will show you how fast its running and over how much memory.

(*s7* ‘heap-size)

Returns the current heap size. You can use this to see how big the heap got while you had the gc disabled.

(*s4m* :heap-size)

Returns the s4m starting heap-size. You can use this to set how much s4m allocates at the beginning to prevent unnecessary heap resizes if you’re planning on locking out the gc.

Note that once the heap has grown, the only way to shrink it again is to recreate your s4m object by editing the s4m box. Reset does not shrink the heap.

If you want to run with no gc, the best thing to do is to turn on stats, play for as long as you plan to run, and note the heap-size. Then start your s4m object off with this heap-size. This will avoid the work of resizing the heap during playback. However, this does mean any gc run will be slow, so you will need to keep it disabled until you can afford an underrun.

Alternately, you can set the heap-size very low, and run the gc proactively very frequently. This will not run as fast as locking it out entirely (assuming you’ve made sure the heap won’t resize), but avoids the problem of eventually needing to run a really slow gc round.