GeoGraphy: Audio

The classes responsible for audio using GeoGraphy must (at least in the proposed approach) inherit from the abstract class GeoAudio.

Lets' consider this:

s = Server.local.boot ;

// An available synth

h = Sinusoider.new(b).initAudio ;

Each GeoAudio-child class (here Sinusoider) requires a Runner: it works by spawning an audio event each time it receives an updated message from the runner. The audio class instance is registered to the runner. This means the each time the sequencing mechanism (i.e.e an actant) activates a vertex, a message is sent to the audio device.

The message is an array like this:

[vID, vertex[..4], eID, edge, aID, weight, offsetWeight, count]

Here is the class definition with some comments.

You have to provide three methods.

initAudio: allows you to perform all the required initialization passes (e.g. filling buffers). It is optional (i.e. you could not need initialization at all)

sendDef: assigns to the aDef variable the SynthDef you want and send it to the server

play: the play method allows to define mappings between the GeoGraphy values and your synthDef. The method is called each time a new event is spawned by GeoGraphy. Typically a new synth is created from the provided synthDef and its arguments are mappings from GeoGraphy message elements.

This is the class definition of Sinusoider with some comment:

Sinusoider : GeoAudio {

// we need to inherit from GoeAudio

// initAudio is not necessary here

// so we use it just to let you know that it can be used

initAudio { "nothing to do here".postln }

// this method is simply a container for the synthDef you like

sendDef {

// simply replace the SynthDef with yours

var aDef = SynthDef(\Sinusoider, { arg pitch, amp = 0.1, out = 0 ;

var dur = 30/pitch ; // --> duration of the event is proportional to pitch

Out.ar(out, Pan2.ar(

EnvGen.kr(Env.perc(dur*0.01, dur, 1, -8), 1.0, doneAction:2)

// doneAction=2 avoids the freeing issue

*

SinOsc.ar(pitch.midicps),

LFNoise1.kr(1/dur), // --> panning is proportional to duration and to pitch

amp

)

)}) ;

// here we send it to server, a variable representing the local server

// aDef and server are declared in GeoAudio

aDef.send(server) ;

}

// each time there's an update, a new synth is spawned

// don't change the method's name

// message is the array passed by GeoGraphy to the audio devices

// it is made by the following values

// [vID, next, duration, eID, options, aID, offsetWeight, weight, count]

play { arg message ;

var label, weight ;

var label, weight, offsetWeight, amp ;

label = message[1][3].asString ;

// message[1] is the array of a vertex without the edges

// its [3] element is the label: see Graph

weight = message[5] ;

offsetWeight = message[6] ;

amp = (weight+offsetWeight).thresh(0) ;

if ( label[0].asSymbol == \s, { // --> reacting to s+num messages

label = label[1..].asFloat ;

Synth.new(\Sinusoider, [\pitch, label, \amp, amp])

})

}

}

Note that in this case the label for sinusoider is a string in form s+midiNumber. So it is possible to extract the midi number from it and pass as an argument for synth creation. In this case the play method checks if the first letter of the label is "s".

Only if it is so (e.g. s90), then the the midiNumber is extracted from the label itself, converted to float, and passed as a pitch argument to a new Synth from the SynthDef. This means that Sinusoider reacts only to certain labels. This allows to filter messages to different synth devices. More, the sum of weight+offsetWeight is used to control amp arg. The following is a varation on the theme just to show the mechanism. This time a square wave is generated with the same envelope and the instance reacts to q+num messages, instead of s+num.

// 1b. The same, just for comparison, using a square

// It reacts to a different format: e.g. "q60" (pitch --> 60, 60 is extracted)

Squarer : GeoAudio {

// initAudio is not necessary here

// we assume pitch as midi notation

sendDef {

//the synthDef being sent

var aDef = SynthDef(\Squarer, { arg pitch, amp = 0.1, out = 0 ;

var dur = 30/pitch ;

Out.ar(out, Pan2.ar(

EnvGen.kr(Env.perc(dur*0.01, dur, 1, -8), 1.0, doneAction:2)

*

Pulse.ar(pitch.midicps, 0.5),

LFNoise1.kr(1/dur),

amp

)

)}) ;

aDef.send(server) ;

}

play { arg message ;

var label, weight, offsetWeight, amp ;

label = message[1][3].asString ;

weight = message[5] ;

offsetWeight = message[6] ;

amp = (weight+offsetWeight).thresh(0) ;

if ( label[0].asSymbol == \q, {

label = label[1..].asFloat ;

Synth.new(\Squarer, [\pitch, label, \amp, amp])

})

}

}

The following is a sample player. You pass a folder and then all the audio samples in the folder and its subfolders are available by assigning their name to a vertex label.

GraphSampler : GeoAudio {

var <>bufDict, <>samplesPath ;

initAudio { arg aSamplesPath ; // init is relevant

var p, l, name ;

samplesPath = aSamplesPath ;

bufDict = IdentityDictionary.new ;

server.doWhenBooted({ // load a buffer for all the files in samplePath

p = Pipe.new("ls -R" + samplesPath, "r") ;

l = p.getLine ;

while({l.notNil}, {

case

{ l.contains("/") }

{ samplesPath = l.split($:)[0] }

{ (l.contains("/").not).and(l.contains(".")) }

{ name = (l.split($.)[0].asSymbol).postln ;

bufDict.add(name

-> Buffer.read(server, samplesPath++"/"++l))} ;

l = p.getLine;

}) ;

p.close ;

})

}

sendDef {

// a simple file player

var aDef = SynthDef(\graphSampler, { arg bufnum, amp = 1, out = 0, dur = 1 ;

Out.ar(out, Pan2.ar(

Line.kr(amp, amp, dur, doneAction:2)

*

PlayBuf.ar(1, bufnum, BufRateScale.kr(bufnum), loop: 0),

LFNoise1.kr(1/dur)

)

)}) ;

aDef.send(server) ;

}

play { arg message ;

var label, weight, offsetWeight, dur, amp ;

label = message[1][3] ; // the label is used to refer to the buffer dictionary

weight = message[5] ;

offsetWeight = message[6] ;

amp = (weight+offsetWeight).thresh(0) ;

if (bufDict[label] != nil,

{ dur = bufDict[label].numFrames/server.sampleRate ;

// the duration fi the sample is used for the EnvGen

// so that after playing the synth is freed

Synth.new(\graphSampler,

[\bufnum, bufDict[label].bufnum, \amp, amp, \dur, dur]) }

// ,{ "NO SAMPLE WITH THIS NAME".postln }

)

}

}

Classes provided with GeoGraphy (and used in the help files) are Sinusoider, Squarer, GraphSampler and SlicePlayer (using vertex' coordinates to read fragments of an audio file at different rates).

Note that you can have as many as instances of GeoAudio-inheriting classes as you want. They will all receive a play message when GeoGraphy is updated: the way they will react depends on you.

Note also that very different approaches to audio handling are surely possible. A generic audio synthesis handler has only to register to the runner in order to be notified about incoming events spawned by actants. Then it's up to the developer to decide how to map the data contained in runner's messages to audio syntheis algorithms and design.