«mpd18UseCase -- FRP style» by LFSaw

on 03 Nov'14 17:40 in mpd18modalitycookbook

Example Modality implementation for the MPD18

usecase

Jeffs "simple" MPD18 use case (JNCv2)

The MPD18 has 16 Buttons and a slider.

  • [Sound Buttons] Buttons 1-3 are mapped to adsr enveloped sound sources.
    • By pushing them down sound turns on; releasing: sound off.
  • the Slider sets amplitude (or pitch) for the (sound)source of the currently depressed button.

  • [Memory Slots] Buttons 5-16 represent 'memory' positions (initially not mapped)

    • if sound is assigned (see below), sound is played when button depressed.
  • [Shift Button] Button 4 is a 'shift key'. When depressed

    1. Sound Buttons don't trigger any sound but select the active slot. This can be followed by
    2. depressing a Memory Slot button, which assigns the selected sound to that pad.
    3. if you release the shift key before assignment, nothing happens.
    4. assigning a copy to an already assigned memory slot replaces existing
    5. mute copy +[Sound Button then Shift button]
    6. Sound Button triggers sound
    7. depress Memory Slot button, assigning the sound to the pad, with sound

Variant

  • Several (up to three) sound buttons can be assigned to a memory slot
  • slider informs all sounds assigned to a memory slot

Further Variation

  • include velocity and aftertouch from pads
  • press shift button then memory slot (w/o pressing sound button) clears memory

  • play sound using 2 to 4 dim fan out w/ aftertouch and slider

    • slider no longer applies to amplitude
    • velocity to amplitude
  • button then shift.

    • shift starts to record control data from aftertouch
    • release shift to stop recording.
    • aftertouch recording (looped) is applied to that source for some param
    • double click shift key then sound source to remove recorded source param
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
(
q = Namespace();

q.n = 15;

//Declare synthdefs
q.sources = [
	{
		var snd = RLPF.ar(Pulse.ar(\freq.kr(200), 0.2), 2500, 0.8) * 0.3;
		var env = EnvGen.kr(Env.asr, \gate.kr(1), doneAction: 2);
		Out.ar(0, snd * env);
	},
	{
		var snd = SinOsc.ar(\freq.kr(200)) * 0.3;
		var env = EnvGen.kr(Env.asr, \gate.kr(1), doneAction: 2);
		Out.ar(0, snd * env);
	},
	{
		var snd = Saw.ar(\freq.kr(200)) * 0.3;
		var env = EnvGen.kr(Env.asr, \gate.kr(1), doneAction: 2);
		Out.ar(0, snd * env);
	}
];

q.names = [\def1, \def2, \def3];

[q.sources, q.names].flopWith{ |func, name|
	SynthDef(name, func).add
};

//array to store running synths
q.synths = 15.collect{ [] };

//GUIS
q.mpdwin = Window("MPD18 use case (JNCv2)").front;
q.butvals = 0!4!4;

q.buts = 4.collect { |i|
	4.collect {|j|
		Button(q.mpdwin, Rect(i * 80 + 5, 240 - (j * 80) + 5, 75, 75))
		.states_([["up" + (i + 1 + (j * 4)), Color.black], ["DOWN", Color.black, Color.green]]);
	}
}.flop;

q.playButs = q.buts[0][..2];
q.memButs = q.buts[1..].flatten;

q.sl = Slider(q.mpdwin, Rect(340, 25, 40, 280));

q.shifter = q.buts [0][3];
q.shifter.states_([["shift", Color.black], ["SHIFT", Color.black, Color.green]]);

CmdPeriod.add({ q.mpdwin !? _.close });

//IO actions
//creating, setting frequency and stopping synths

//IO{ } is essentially the same as { } and it is only used
//to delay execution of the action until reaching the end
//of the event graph
//If an event stream is carrying IO but is not actually registered for output
//using .enOut, then that action will not be performed. This is also useful
//for dynamic event switching where we might want to use an event stream only at
//some times

q.startSynths = { |i, freqs, sources|
	IO{
		q.synths[i] = [sources[i], freqs].flopWith{ |j, freq|
			Synth(q.names[j], [\freq, freq.linlin(0.0,1.0,300,2000)])
		};
	}
};

q.stopSynths = { |i|
	IO{ q.synths[i].do(_.release); q.synths[i] = [] }
};

q.setFreq = { |i, freqs|
	IO{ [q.synths[i], freqs].flopWith{ |s,v| s.set(\freq,v.linlin(0.0,1.0,300,2000)) } }
};

//soft set

//takes an array of event streams and a value for the distance
//necessary for accepting an incoming event
//based on the algorithm on the SoftSet
//uses a recursive relation since 'checked' depends on 'outSig',
//'outES' depends on 'checked', and 'outSig' depends on 'outES'.
//the recursive relation allows us to use the last outputted value
//to determine the next value.
q.softset = { |es, delta = 0.1|
	var outSig;
	var checked =  { |e|
		var eSig = e.hold(0.0);
		({ |last, new|
			var current = outSig.now;
			if( (absdif(current, last) < delta) || (absdif(current, new) < delta) ){Some(new)}{None()}
		} <%> eSig <@> e).selectSome
	};
	var outES = es.collect(checked).mreduce;
	outSig = outES.hold(0.0);
	outES
};

//FRP
/*
Notes:

The logic is essentially separated into two parts, the copy logic and the logic for each pad.
Both of them can essentially be tested separatedelly from everything else

The logic for each pad is essentially also separate from one pad to another, with the exception
that there is a recursive relationship to the last outputted frequencies of each pad
since the copy mechanism needs access to those when copying.

The last value of the freqs for each pad
is stored in a variable freqArraySig, which is declared up front, so that it can be used
half-way through the pad function before it is event assigned to anything (assignment of that
variable is last thing done in the FRP code).

ENdef works like Ndef for FRP.

ENdef(\x, {  }) //set frp network
ENdef(\x).start //start processing events
ENdef(\x).stop //stop processing events
ENdef(\x).clear //remove all actions

*/
ENdef(\x,{

	var freqArraySig; //declare now for recursive use

	//*** declare all the input event streams (and signals) ***
	//button value are converted to boolean since we will be doing checks on their value mostly
	var flatPads = q.buts.flat;
	var allButtonsESs = (flatPads[0..2]++flatPads[4..]).collect{ |x| x.enInES.collect(_.booleanValue) };

	//the first 3 buttons are the play buttons
	var playButsESs = allButtonsESs[..2];
	//the last 12 are the mem buttons
	var memButsESs = allButtonsESs[3..];

	var shiftES = q.shifter.enInES.collect(_.booleanValue);
	var shiftSig = shiftES.hold(false);
	//pads are only interested on when shift is not pressed so we create that signal here
	//for convenince
	var shiftSigNot = shiftSig.collect(_.not);

	var sliderES = q.sl.enInES;
	var sliderSig = sliderES.hold(0);

	//*** copying logic ***

	// copyFromTo is constructed by running a state function
	// each button press is associated with a specific function
	// to alter the current state.

	// The is state is T( [Int], Option Int)

	// Option Int can be Some(4) or Nothing()

    // The state means copy settings from play buttons with
	// indexes in array in first element of tuple
	// to membutton index stored in Option ( Some(index) ).
	// the data is only ready when whe get a Some on the second element
	// of the tuple.

	var copyFromTo =
	// merge all button pressing events
	// associating them with state function
	// only let them through when shift is pressed
	(shiftSig.when(                  //only let through pad down presses
		                             //(I.d is the identity function
		                             //so select only let's through true values of pad
		(playButsESs.collect({ |es,i| es.select(I.d).collect{
			//here we declare the state function for this play pad press
			{|state|
				//already made assignment so clean state
				if(state.at2.isDefined) {
					T([i],None())
				}
				//collecting things to assign
				{
					if(state.at1.includes(i).not){
						T(state.at1++[i],None())
					}{
						state
					}
				}
		} } }) ++
		memButsESs.collect({ |es,i| es.select(I.d).collect{
			//here we declare the state function for this mem pad press
			{|state|
				//if we already have a Some in the second element
				if(state.at2.isDefined) {
					//then it means two mem button were pressed in sequence
					//copy description is complete, do nothing
					state
				} {
					//otherwise store the mem button to copy to
					//mem buttons start at index 4
					T(state.at1, Some(i+3) )
				}
	} } })).mreduce )
	//shift is pressed, clean state and start over
	| shiftES.collect{ { T([],None()) } })
	//injectF is what actually runs the state functions
	//first argumetn is initial state
	.injectF( T([],None()) )
	//transform
	//    T([x,y,z],None())  into None()
	//and T([x,y,z],Some(v)) into Some( T([x,y,z], v ) )
	.collect{ |tup|
		tup.at2.collect{ |x| tup.at2_(x) }
	}
	//.enDebug("state")
	//only let through when we have a Some( T([x,y,z], v ) )
	//this means we have a complete copy description
	//selectSome only let's through Some(..) values and unwraps
	//them from the Some()
	.selectSome;
	//.enDebug("swap");
	//copying the settings causes pickup mode

	//sources stores which synthdefs each button is using
	//each button has an array associated since it can
	//play multiple synthdefs at the same time
	var sources = copyFromTo.collect{ |tup|
		{ |state| state[tup.at2] = tup.at1; state }
	}.injectFSig( (0..14).collect({ |x| [x.mod(3)] }) );
	//.enDebug("sources");

	//*** pad press logic ***

	//we can write the logic for each button presss separate from the other buttons
	//we just need to use a recursive definition on freqArraySig, the last value
	//of the frequencies for each pad
	//this is the logic for pad i
	var processOnePad = { |padES, shiftSigNot, sliderES, copyFromTo, sources, i|

		var padSig = padES.hold(false);
		var padWhenShiftOff = when( shiftSigNot, padES);

		//only take values from slider if pad is pressed and shift is not pressed
		//soft set prevents jumps from slider repositiong while pad not pressed.
		var sliderWhenPadOnAndShiftOff = q.softset.( [when( (_&&_).lift.(shiftSigNot, padSig ) , sliderES )] );

		//frequency can be determined by copying from another pad or by setting from slider

		//check from the output of copy logic if the last description was to copy into this pad
		//if so, copy frequencies from the indexes in first element of the copyFromTo tupple
		//only take the first frequency from each array
		//note the recursive dependency on freqArraySig
		var copy = copyFromTo.select{ |t| t.at2 == i}.collect{ |t|
			var currentFreqs = freqArraySig.now;
			currentFreqs[t.at1].collect(_.first)
		};

		//moving slider while pad playing causes all synths in the pad to use same frequency
		//take value from slider and make an array with enough values for the number of synths
		var fromSlider = { |xs, v| v.dup(xs[i].size) } <%> sources <@> sliderWhenPadOnAndShiftOff;
		//there are only two things changing frequency, the slider and the copying, so merge them:
		var freqs = copy | fromSlider;
		var freqsSig = freqs.hold([0.0]);

		//*** outputs: Perform actions ***

		//debug statements for convenience
		//padSig.enDebug("pad "++i);
		//sliderWhenPadOnAndShiftOff.enDebug("sl "++i);
		//freqsSig.enDebug("freq "++i);

		//freq changed set new freq to synth
		freqs.collect{ |fs| q.setFreq.(i,fs) }
		//.enDebug("freq "++i)
		.enOut;

		//pad on -> synth start
		({ |fs, sources, play| q.startSynths.(i, fs, sources) } <%> freqsSig <*> sources <@> padWhenShiftOff.select(I.d) )
		//.enDebug("start "++i)
		.enOut;

		//pad off -> synth stop
		padES.select(_.not).collect{ q.stopSynths.(i) }
		//.enDebug("stop "+i)
		.enOut;

		//return the freqSig to be used by other pads
		freqsSig
	};

	//create array of signals with frequencies for each pad by declaring the
	//event graph for each separate pad
	var freqSigs = allButtonsESs.collect{ |pad, i|
		processOnePad.(pad, shiftSigNot, sliderES, copyFromTo, sources, i)
	};

	//transform array of signals into signal of arrays
	//it's just automatic merging of signals with the
	//array function
	// [ FPSignal [Float] ] -> FPSignal [ [ Float ] ]
	// this is the signal that will be used recursivelly
	// to determine itself since freqArraySig depends on
	// each 'freqSig' of each pad, which depends on 'copy'
	// which depends again on 'freqArraySig', creating a cycle.
	freqArraySig = freqSigs.sequence;

}).start
)

//close gui and remove all actions
(
q.mpdwin.close;
ENdef(\x).clear;
)
descendants
«mpd18UseCase -- MFunc style» by LFSaw (private)
full graph
raw 10527 chars (focus & ctrl+a+c to copy)
reception
comments