Skip to content
Graham Wakefield edited this page Apr 28, 2019 · 38 revisions

What is Gibberwocky for Max?

Brings together the marvelous magic of Max/MSP, and the glorious goo of Gibber, a widely-used web-based live coding language. That means a quick way to augment a Max patcher with live coded sequencing & modulations. Live coding happens in a browser-based text editor, and modulations can be messages, UI knob tweaks, complex patterns, and arbitrary signal graphs.

Installing

Open Max's Package Manager, find & install the gibberwocky package.

Alternatively, download/checkout https://github.com/gibber-cc/gibberwocky.max into Documents/Max 8/Packages (or My Documents/Max 8/Packages on windows)

Make sure you have a recent version of Chrome (or Firefox or Safari (or maybe even Edge now)).

Using Gibberwocky

  • Add a [gibberwocky] object to your Max patcher.
    • make it [gibberwocky @sync link] if you want to play together over a local network
  • Turn on audio in the patcher, if it isn't already on.

Note: you should only have 1 [gibberwocky] object open at any time. It might not behave nicely otherwise.

Send the [gibberwocky] object a bang to open the editor (alternatively, just open http://gibberwocky.cc/burble in your browser). In the editor's console, if it says "gibberwocky.max is ready to burble", you're all set!

Note: You might need to open port 8081 on your firewall.

Editing cheat-sheet

Triggering/stopping key commands

Single lines: put the cursor on the line and press Ctrl-Enter (starts at the next measure) or Shift-Enter (starts ASAP)

Multiple contiguous lines: place the cursor, press Option-Enter (starts at the next measure) or Shift-Option-Enter (starts ASAP)

Any selected text: Ctrl-Enter (starts at the next measure) or Shift-Enter (starts ASAP)

You can stop all sequences in gibberwocky with the Ctrl+. keyboard shortcut (Ctrl + period).

Drag and drop from the MOM

Open the "schemas" tab and select the mom (Max object model) to see what entry points have been detected. This list gets updated whenever you save the patcher with the [gibberwocky] object in it. You can drag any item in the list into the editor to quickly create the corresponding code to modulate it.

Tips

Remember, audio must be enabled in Max.

Patcher must be saved for the MoM schema to update.

If you lost the connection (e.g. Max crashed & restarted), you can reconnect to it by triggering connect(). Or you can refresh the webpage (but you'll lose your code).


What can Gibberwocky modulate?

Gibberwocky => Max:

  • message => messages/lists to receive objects or to [gibberwocky] 1st outlet
  • param => messages objects with scripting names
  • device => messages to Live device objects (including sending midinotes and setting parameters)
  • signal => audio outlets of [gibberwocky]

All of the above can also be triggered in repeating sequences, using the seq(values, timings) method. Gibberwocky's seq() can also handle functions in place of arrays, to generate patterns for us. Any Javascript function can be used to generate values for seq(), but Gibberwocky provides a few rich and useful examples oriented to specific tasks:

  • Pattern(): an arbitrary sequential pattern that can return a sequence of values over and over again, which can also be modifed as it plays
  • Generators designed to create timing values for seq():
    • Euclid(): timings based on Euclidean rhythms
    • Hex(): timings based on binary patterns
    • Automata(): timings based on 1D cellular automata
  • Signal generators can also be used in sequences; the signals will be "sampled" at the timing points to generate new values

Gibberwocky also provides some objects to quickly spawn multiple sequences:

  • Steps(): a set of generators optimized for drum machine patterns with strings
  • HexSteps: Multiple note sequences like Steps(), but using patterns like Hex()

Resources

Docs: http://gibberwocky.cc

For a complete description of the gibberwocky API, see the gibberwocky reference. Otherwise the various tutorial are the best place to get started.

For questions, answers, and announcements try the mailing list or the gibber Slack channel for more informal discussion. If you aren't already a member of the Live Coding Slack group there's a site that generates automatic invites.

Source code: https://github.com/gibber-cc/gibberwocky.max


Messages

message("hello")("world")

...will send the message hello world out of the gibberwocky 1st outlet, or to any [receive hello] objects (if they exist). So will:

message("hello.world")    
message(“hello.world”) => “hello world”

This can get as long as you need:

synth1.a.b.c.d.e.f.g.h( 'i?' ) // sends 'synth1 a b c d e f g h i?'

A message path of this kind can also be stored as an callable object, for re-use:

hello = message("hello")
hello("world") 

yo = message("yo")
yo("hi") => "yo hi"
yo("lo") => "yo lo"
yo.seq("yo", 1/4) => “yo yo”  “yo yo”  “yo yo”  

Sequencing

Messages can also be sequenced into repeating patterns, using .seq(values, timings); where timings are normally expressed as multiples/fractions of a whole note.

// sends "hello world" every whole note:
message("hello").seq("world", 1)

// sends "hello world" every quarter note:
message("hello").seq("world", 1/4)

If the arguments to seq(value, timing) are arrays, they will be iterated over each time:

// alternates between sending "hello me" and "hello you" every quarter note
message("hello").seq(["me", "you"], 1/4)

Randomness

rndf() and rndi() are used to generate a single random float or integer

// make sure you have the console tab in the gibberwocky sidebar
log( rndf() ) // outputs floats between 0-1
log( rndi() ) // outputs either 0 or 1

// although 0 and 1 are the default min/max values, we can pass
// arbitrary bounds:
log( rndf(-1,1) )
log( rndi(0,127) )

Arrays of random values: if we pass a third value, we can create multiple random numbers at once, returned as an array.

log( rndf( 0,1,4 ) ) // array of four integers, each either 0 or 1
log( rndi( 0,127,3 ) )

Random generators: Sometimes you don't want a random number immediately, but rather want to sequence calls to generate random numbers. The Capitalized versions of rndi and rndf do that job:

message("die").seq(Rndi(6), 1/4)   // random die roll every beat

Param

Forwards messages directly to Max objects with scripting names.

For example, can be handy to name attrui or other UI objects and address them directly without drawing patch cords everywhere:

params["foo"](74)  // sends a message `74` to the object with scripting name "foo"

params["foo"].seq(Rndi(127), 1/4) // on every quarter note, sends a random integer up to 127 to the object with scripting name "foo"

Devices

Live Devices ([amxd~]) are usually given scripting names by default, which makes it easy to address them. The MOM also grabs all the parameters within a device so you can modulate those directly.

parameters

// set the synth resonance:
devices['synth']['filter_resonance'](.95)

// alternate filter cutoff between 100Hz and 10kHz every beat:
devices['synth']['cutoff'].seq([100, 10000], 1/4)

Devices that are also instruments will also respond to note and midinote events, opening up some more musical parameterization and pattern transformation options.

Note

devices['bass'].note( 'fb2' )

devices['synth'].note.seq( ['c4','e4','g4'], 1/8 )

note() and scales

In gibberwocky, the default scale employed is C minor, starting in the fourth octave. This means that if we pass 0 as a value to note(), C4 will also be played:

// same:
devices['synth'].note( 'c4' )
devices['synth'].note( 0 )

Passing integers to note() thus selects relative values in the current scale:

devices['synth'].note.seq( [-7,-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,7], 1/16 )

To change the current scale (root and mode):

Scale.root( 'd4' )
Scale.mode( 'lydian' )

Scale.root( 'c4' )
Scale.mode( 'phrygian' )

We can also sequence changes to the root / mode:

Scale.root.seq( ['c4','d4','f4','g4'], 2 )
Scale.mode.seq( ['lydian', 'ionian', 'locrian'], 2 )

Stop the scale sequencing:

Scale.root[0].stop()
Scale.mode[0].stop()
Scale.root( 'c4' )

We can also define our own scales using chromatic scale indices. Scales can have arbtrary numbers of notes.

Scale.modes[ 'my mode' ] = [ 0,1,2,3,5,6,10 ]
Scale.mode( 'my mode' )

We can also change the octave centre of an instrument:

devices['synth'].octave(-2)

chords

Using note names:

devices['synth'].chord( ['c4','eb4','gb4','a4'] )

Or chord names:

devices['synth'].chord( 'c4maj7' )
devices['synth'].chord( 'c#4sus7b9' )

Or using scale indices:

devices['synth'].chord( [0,2,4,5] )

midinote

// midi note number:
devices['synth'].midinote( 60 )  
// midi note number, midi velocity, duration in ms:
devices['synth'].midinote( 60, 120, 1000 )  

Note, velocity, and duration can also be sequenced independently:

devices['bass'].midinote.seq( 60, 1/2 )
devices['bass'].velocity.seq( [16, 64, 127], 1/2 )
devices['bass'].duration.seq( [10, 100,500], 1/2 )

midichord

// midi note numbers in an array:
devices['synth'].midichord( [60, 63, 67] )  

Note on sequencing chord and midichord

We can also sequence calls to chord() and midichord(). To move between different chords, we need to pass an array of arrays:

devices['synth'].midichord.seq( [[60,64,68], [62,66,72]], 1/2 )

devices['synth'].chord.seq( [[0,2,4,5], [1,3,4,6]], 1 )

Sequencing with "tracks"

Everything that can be sequenced can also have multiple "tracks". Normally, without specifying a track, only one sequence will operate at once. For example, only one of these will actually run, even if you trigger both:

devices['synth'].midinote.seq( 72, 1/4 )
devices['synth'].midinote.seq( 48, 1/4 )

That is, by default, gibberwocky will replace an existing sequence with a new one. To set up multiple sequences, you can add a "track" ID number as a third argument to .seq():

devices['synth'].midinote.seq( 60, 1/2, 1 ) 
devices['synth'].midinote.seq( 72, 1/3, 2 ) 
devices['synth'].midinote.seq( 84, 1/7, 3 ) 

You can access the data of track 0 like this:

log( devices['synth'].midinote[0].values.toString() ) 
log( devices['synth'].midinote[0].timings.toString() ) 

When you launch a sequence on a "track" that has the same ID as another running sequence, the older sequence is stopped. If the sequences have different IDs they run concurrently. This makes it really easy to create polyrhythms.

Note: In the examples of sequencing we've seen so far, no ID has been given, which means gibberwocky is assuming a default ID of 0 for each sequence.

devices['synth'].midinote.seq( 48, 1 ) // assumes ID of 0

You can also stop all sequences on a specific object:

devices['synth'].stop()

Signals

We've seen that the first outlet of gibberwocky is used for messaging. The remaining outlets are used for signals. You can determine the number of outlets using the @signals property; for example, [gibberwocky @signals 4] has four outputs for audio signals in addtion to its messaging output (for a total of 5).

You can send gen~ graphs from the browser to gibberwocky to be compiled and run within Max. Most gen~ operators are available in gibberwocky.

Here's a simple ramp from 0 to 1 at 1Hz -- hook up the 2nd outlet of [gibberwocky] to a [scope~] to see it in action:

signals[0]( phasor(1) )

In gen~, the cycle function generates a sine oscillator (as opposed to sin, which calculates the mathematical sin of a number). Let's start by passing a straight sine oscillator to our leftmost signal output.

osc = cycle(4)
signals[0]( osc )

Sequencing gen parameter changes

We can access the properties of any gen~ object using bracket notation, starting from 0. In the above example, the first parameter of our osc object is the frequency (which we set to four) which means we can change it as follows:

osc[0]( 2 )
// sequence changes to frequency over time
osc[0].seq( [2,4,8], 1/2 )

Note that the cycle ugen generates a full bandwidth audio signal with a range of {-1,1}. Oftentimes we want to specify a center point (bias) for our sine oscillator, in addition to a specific amplitude and frequency. Here's an LFO with scale and bias applied (so we can control the low and high values of the curve in addition to the frequency).

osc = add( 1, mul( 0.5, cycle(2) )  // has 3 parameters: 1 (bias), 0.5 (scale), 2 (frequency)

In the above codeblock, osc[0] controls the bias, osc[1] controls the gain, and osc[2] controls the frequency. That is, the number in the [] brackets refers to the position of the parameter in the original code of the graph.

Other operators

Gibberwocky also provdes a few richer functions not present in gen, including an lfo() equivalent to the above:

// frequency, bias, amplitude 
mylfo = lfo( 2, 1, 0.5 )
signals[0]( mylfo )

Some more useful operators:

sine(period_in_beats, center_bias, amplitude) // sine wave (-1..1) with named properties: "period", "bias", "amp", "phase"
lfo(freq_in_hz, center_bias, amplitude) // sine wave (-1..1) with named properties: "period", "bias", "amp", "phase"
beats(period_in_beats) // ramp wave (0..1), not bandlimited
btof(period_in_beats) // convert period_in_beats to a frequency in Hz

See full list of generators here

Pattern

An arbitrary sequential pattern that can return a sequence of values over and over again.

mypattern = Pattern( 60,62,64,65 )
log( mypattern() ) // 60
log( mypattern() ) // 62
log( mypattern() ) // 64
log( mypattern() ) // 65
log( mypattern() ) // back to 60...

Patterns can be used in place of arrays for sequencing:

devices['bass'].midinote.seq( mypattern, 1/8 )

We can access these patterns using the "track" ID, and thus apply transformations:

devices['bass'].midinote[0].values.reverse()
devices['bass'].midinote[0].values.transpose( 1 ) // add 1 to each value
devices['bass'].midinote[0].values.scale( 1.5 )    // scale each value by .5
devices['bass'].midinote[0].values.rotate( 1 )    // shift values to the right
devices['bass'].midinote[0].values.rotate( -1 )   // shift values to the left
devices['bass'].midinote[0].values.reset()        // reset to initial values

Just like anything else, we can even sequence these transformations:

devices['bass'].midinote[0].values.rotate.seq( 1,1 )
devices['bass'].midinote[0].values.reverse.seq( 1, 2 )
devices['bass'].midinote[0].values.transpose.seq( 1, 2 )
devices['bass'].midinote[0].values.reset.seq( 1, 8 )

Euclid

Designed to generate patterns of timings for seq()

You can also specify Euclidean rhythms using the Euclid() function, which returns a pattern:

devices['drums'].midinote.seq( 36, Euclid(5,8) )

Euclidean rhythms are specifcations of rhythm using a number of pulses allocated over a number of beats. The algorithm attempts to distribute the pulses as evenly as possible over all beats while maintaining a grid. Examples of these distributions are given below (where 'x' represents a pulse and '.' represents a rest):

1,4 : x...
2,3 : x.x
2,5 : x.x..
3,5 : x.x.x
3,8 : x..x..x.
5,8 : x.xx.xx. 
4,9 : x.x.x.x..
5,9 : x.x.x.x.x

As in Gibber, by default the number of beats chosen also determines the time used by each beat; selecting '5,8' means 5 pulses spread across 8 1/8 notes. However, you can also specify a different temporal resolution for the resulting pattern: '5,8,1/16' means 5 pulses spread across 8 beats where each beat is a 1/16th note.

You can read a paper describing Euclidean rhythms here: http://archive.bridgesmathart.org/2005/bridges2005-47.pdf

Hex

The Hex function creates rhythmic patterns by turning hexadecimal numbers into binary patterns, based off an idea originally implemented by Steven Yi.

Hex objects accept strings of hexadecimal numbers (0-9, a-f). Each hexadecimal number is responsible for populating four 1/16th notes (by default) with pulses and rests. A value of 0 means no pulses are present.

devices['drums'].midinote.seq( 36, Hex('0') ) /* 0000 */
devices['drums'].midinote.seq( 36, Hex('1') ) /* 0001 */
devices['drums'].midinote.seq( 36, Hex('2') ) /* 0010 */
devices['drums'].midinote.seq( 36, Hex('3') ) /* 0011 */
devices['drums'].midinote.seq( 36, Hex('4') ) /* 0100 */
devices['drums'].midinote.seq( 36, Hex('5') ) /* 0101 */
devices['drums'].midinote.seq( 36, Hex('6') ) /* 0110 */
devices['drums'].midinote.seq( 36, Hex('7') ) /* 0111 */

devices['drums'].midinote.seq( 36, Hex('8') ) /* 1000 */
devices['drums'].midinote.seq( 36, Hex('9') ) /* 1001 */
devices['drums'].midinote.seq( 36, Hex('a') ) /* 1010 */
devices['drums'].midinote.seq( 36, Hex('b') ) /* 1011 */
devices['drums'].midinote.seq( 36, Hex('c') ) /* 1100 */
devices['drums'].midinote.seq( 36, Hex('d') ) /* 1101 */
devices['drums'].midinote.seq( 36, Hex('e') ) /* 1110 */
devices['drums'].midinote.seq( 36, Hex('f') ) /* 1111 */

Longer strings create longer patterns:

devices['drums'].midinote.seq( 36, Hex('92') ) /* 10010010 */

Normally the step size is 1/16th notes, however you can change this with a second argument:

// set step size to quarter notes:
devices['drums'].midinote.seq( 36, Hex('8', 1/4) ) /* 1000 */

Signals as patterns

Gibberwocky enables you to define continuous signals that are periodically sampled to create patterns, a common technique in the modular synthesis community that was popularized in live coding by the Impromptu and Extempore environments. For example, to use a sine oscillator to generate a repeating musical scale:

devices['bass'].note.seq( sine( 1, 0, 4 ) /* period (in beats), center, amplitude */, 1/8 )

As another example, in multi-samplers / drum machines different midi notes trigger different sounds. Using signals to select sounds can yield interesting patterns over time.

The below oscillator will range from 36–44. Note that we initialize the sine() with phase 0.75 to make sure that the kick drum triggers on beat 1.

devices['drums'].midinote.seq( sine( 1, 40, 4, .75 ), 1/16 )

Signals as patterns to select from arrays

A Lookup() object can lookup a value in an array based on a signal.

For example, here's an example specifically sequencing a snare drum pattern:

devices['drums'].midinote.seq( 38, Lookup( beats(4), [ 1/32, 1/16, 1/8, 1/4 ] ))

Steps

Steps() creates a group of sequencer objects. The first argument to Steps is an object that maps MIDI note numbers to sequencer patterns. The second argument to Steps is the instrument to target.

Each sequencer is responsible for playing a single note. The velocity of each note is determined by a hexadecimal value (0-f), where f is the loudest note. A value of '.' means that no MIDI note message is sent

The lengths of the patterns can differ. By default, the amount of time for each step in a pattern equals 1 divided by the number of steps in the pattern. In the example below, most patterns have sixteen steps, so each step represents a sixteenth note. However, the first two patterns (60 and 62) only have four steps, so each is a quarter note.

Note that while the example below is designed to work with the Analogue Drums device found in the gibberwocky help file, that instrument is actually NOT velocity sensitive.

steps = Steps({
  [36]: 'ffff', 
  [38]: '.a.a',
  [41]: '........7.9.c..d',
  [43]: '..6..78..b......',
  [45]: '..c.f....f..f..3',  
  [42]: '.e.a.a...e.a.e.a',  
  [46]: '..............e.',
}, devices['drums'] )

The individual patterns can be accessed using the note numbers they are assigned to:

// rotate one pattern (assigned to midinote 71)
// in step sequencer  every measure
steps[42].rotate.seq( 1,1 )

// reverse all steps each measure
steps.reverse.seq( 1, 2 )

HexSteps

Multiple note sequences like Steps(), but using patterns like Hex():

h = HexSteps({
  // kick
  36:'82008224',
  // snare
  38:'0808',
  // closed hat
  42:'bbbf',
  // "cowbell"
  45:'ab5a'
}, devices.drums )

Sequencing functions

In fact, any function can be used inside of a sequence:

myvalues = function() {
	// generate a random note number:
  	return 60 + 2*Math.ceil(Math.random() * 3);
}
devices['bass'].midinote.seq( mypattern, 1/8 )

So, if we wanted to sequence a random midinote to the 'bass' device in the gibberwocky help patcher, we could sequence a function as follows:

devices['synth'].midinote.seq( ()=> rndi(40,100), 1/8 )

// or
devices['bass'].note.seq( Rndi(0,7), 1/8 )

Arrays can also be turned into random picker functions:

// randomly play open or closed hi-hat every 1/16th note
devices['drums'].midinote.seq( [42,46].rnd(), 1/16 )