├── CllParameterHandlers.sc
├── cl-manual-examples.scd
├── cl-manual.pdf
├── clLiveCode-ext.sc
├── ddwChucklib-livecode.quark
├── edit-gui.scd
├── helper-funcs.scd
├── manual
├── cl-manual.org
└── manual-supporting
│ ├── item-spans.odg
│ ├── item-spans.pdf
│ ├── rhythmic-notation-crop.pdf
│ ├── rhythmic-notation.ly
│ ├── rhythmic-notation.pdf
│ └── rhythmic-notation.svg
├── mobile-objects.scd
├── nanoktl-objects.scd
├── parsenodes.sc
├── preprocessor-generators.scd
├── preprocessor.scd
└── readme.org
/CllParameterHandlers.sc:
--------------------------------------------------------------------------------
1 | /**
2 | Chucklib-livecode: A framework for live-coding improvisation of electronic music
3 | Copyright (C) 2018 Henry James Harkins
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU General Public License as published by
7 | the Free Software Foundation, either version 3 of the License, or
8 | (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU General Public License for more details.
14 |
15 | You should have received a copy of the GNU General Public License
16 | along with this program. If not, see .
17 | **/
18 |
19 | // these assume to be run within a chucklib-livecode BP
20 | // the BP supplies ~valueForParm
21 |
22 |
23 | // parameter handlers
24 | CllParmHandlerFactory {
25 | // the logic to decode the sub-cases is still a bit ugly
26 | // but at least 1/ it's now concentrated in this one location
27 | // and 2/ refactored as nested ifs
28 | *new { |parm, bpKey, map|
29 | var bp;
30 |
31 | if(BP.exists(bpKey)) {
32 | bp = BP(bpKey);
33 | } {
34 | Error("Can't create parm handler because BP(%) doesn't exist"
35 | .format(bpKey.asCompileString)).throw;
36 | };
37 |
38 | if(map.isNil) {
39 | map = bp.parmMap[parm];
40 | };
41 | if(map.isNil and: { #[delta, dur].includes(parm).not }) {
42 | Error("BP(%): Can't create handler for nonexistent parm '%'"
43 | .format(bpKey.asCompileString, parm)).throw;
44 | };
45 |
46 | if(map.tryPerform(\at, \alias).isNil) {
47 | // non-aliased cases (2)
48 | if(bp.parmIsDefault(parm)) {
49 | ^CllDefaultNoAliasParm(parm, bpKey, map)
50 | } {
51 | ^CllParm(parm, bpKey, map)
52 | };
53 | } {
54 | // aliased cases
55 | if(bp.parmIsDefault(parm)) {
56 | // default parm with alias is always this type
57 | ^CllDefaultArrayAliasParm(parm, bpKey, map)
58 | } {
59 | if(map[\alias].size == 0) {
60 | // non-default, alias is not an array
61 | ^CllSimpleAliasParm(parm, bpKey, map)
62 | } {
63 | // non-default, alias is an array
64 | ^CllNonDefaultArrayAliasParm(parm, bpKey, map)
65 | }
66 | };
67 | }
68 | }
69 | }
70 |
71 | CllParm {
72 | var = result.size) {
220 | storeParm.do { |key, i|
221 | inEvent.put(key, result.wrapAt(i));
222 | };
223 | } {
224 | "Alias for % allows % values, but too many (%) were provided"
225 | .format(storeParm.asCompileString, storeParm, result.size)
226 | .warn;
227 | };
228 | // valueID[1] should be the dur value -- necessary to be second return value
229 | // if 'result' is one item, unbubble reverses the 'asArray' earlier
230 | ^[result.unbubble, valueID[1], valueID[2]]
231 | }
232 | }
233 |
234 | CllNonDefaultArrayAliasParm : CllParm {
235 | processValue { |valueID, inEvent|
236 | var result = this.valueForParm(valueID, inEvent);
237 | if(result.isNil) {
238 | result = [Rest(valueID)];
239 | } {
240 | result = result.asArray;
241 | };
242 | if(storeParm.size >= result.size) {
243 | storeParm.do { |key, i|
244 | inEvent.put(key, result.wrapAt(i));
245 | };
246 | } {
247 | "Alias for % allows % values, but too many (%) were provided"
248 | .format(storeParm.asCompileString, storeParm.size, result.size)
249 | .warn;
250 | };
251 | ^result
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/cl-manual-examples.scd:
--------------------------------------------------------------------------------
1 | /**************
2 | Listing 1. Launching chucklib-livecode.
3 | **************/
4 |
5 | \loadAllCl.eval;
6 |
7 | // optional
8 | \cllGui.eval;
9 |
10 |
11 | /**************
12 | Listing 2. A quick techno-ish drumset.
13 | **************/
14 |
15 | \loadAllCl.eval;
16 | TempoClock.tempo = 124/60;
17 |
18 | /hh.(\hardhh);
19 | /hhh = ".-.-.-.-";
20 | /hhh+
21 |
22 | /drum.(\tightsnr);
23 | /tsn = " - -";
24 | /tsn+
25 |
26 | /drum.(\deepkick);
27 | /dk = "o| o| _o |";
28 | /dk+
29 |
30 | // mixing board
31 | /makeEmptyMixer8.();
32 | /hhh => MCG(0);
33 | /tsn => MCG(1);
34 | /dk => MCG(2);
35 |
36 | /hhh/tsn/dk-
37 |
38 |
39 | /**************
40 | Listing 3. Generators for drums.
41 | **************/
42 |
43 | /hhh/tsn/dk+
44 |
45 | // A
46 | /tsn = "[ - -]::\ins(".", 2, 0.25)";
47 |
48 | // B
49 | /tsn = "[ - -]::\ins(".", 2, 0.5)";
50 |
51 | // C
52 | /tsn = "[ - -]::\ins(".", 2, 0.5)::\shift(".", 2, 0.25)";
53 |
54 | // D (empty source, so, omitted)
55 | /hhh = "\ins("-", 1, 0.5)::\ins(".", 7, 0.5)";
56 |
57 | // E (one closed HH to fill start of bar)
58 | /hhh = "[.]::\ins("-", 1, 0.5)::\ins(".", 6, 0.5)";
59 |
60 | // F
61 | /hhh = "[.]::\ins("-", 1, 0.5)::\ins(".", 6, 0.5)::\ins(".", 2, 0.25)";
62 |
63 | // G
64 | /hhh = "\fork("|\ins("-", 1, 0.5)||x")::\ins(".", 7, 0.5)::\ins(".", 2, 0.25)";
65 |
66 | /hhh/tsn/dk-
67 |
68 |
69 | /**************
70 | Listing 4. Adding sound effects to a simple beat.
71 | **************/
72 |
73 | \loadAllCl.eval; // If you haven't already done this
74 | TempoClock.tempo = 124/60;
75 |
76 | // The beat
77 | BP(#[tk, clp, hhh, tsn]).free; // Clean up first
78 | /drum.(\tightkick); /drum.(\clap); /hh.(\hardhh);
79 | /tk = "o|| o|";
80 | /clp = " - ";
81 | /hhh = "........";
82 |
83 | /tk/clp/hhh+
84 |
85 | /drum.(\tightsnr);
86 | /tsn = "|||.";
87 |
88 | /tsn+
89 |
90 | /clp = "|-|| . ";
91 | /tsn = "|||. .";
92 |
93 | // make the effects
94 | /make(bufBP:mch(set:\machine));
95 | /mch = "| -||";
96 | /mch+
97 |
98 | /mch = "| -| , ,|";
99 |
100 | /tk/clp/hhh/tsn/mch-;
101 |
102 |
103 | /**************
104 | Listing 5. Bassline template.
105 | **************/
106 |
107 | /changeKey.(\dmixo);
108 | /make(pbsVC:pbs/melBP:bs(octave:3));
109 |
110 | /bs = "1_| 1.| 7~4|x";
111 |
112 | /bs+
113 | /bs-
114 |
115 |
116 | /**************
117 | Listing 6. Chord-playing template.
118 | **************/
119 |
120 | /make(anapadVC:pad/chordBP:ch(chords:\one));
121 | /ch = "87~05";
122 | /ch+
123 |
124 | VC(\pad).gui
125 |
126 | MBM(0)[\two] => BP(\ch);
127 |
128 | MBM(0)[\smallch] => BP(\ch);
129 |
130 | /ch-
131 |
132 |
133 | /**************
134 | Listing 7. Example of arpeggiator usage.
135 | **************/
136 |
137 | /make(fmclavVC:fmc/arpegBP:arp(chords:\bigch));
138 |
139 | // These are indices, from the top down, into the current chord.
140 | /arp = "1234";
141 |
142 | /arp+
143 |
144 | // Add some lower notes as a second layer.
145 | // Accent articulates the start of the bar.
146 | /arp = "[1>234]::\ins("456", 6, 0.25)";
147 |
148 | // Extend the second layer higher.
149 | /arp = "[1>234]::\ins("23456", 7, 0.25)";
150 |
151 | // Use wildcards to substitute a sequential pattern.
152 | /arp = "[1>234]::\ins("*", 7, 0.25)::\seq("65432")";
153 |
154 | // Change the harmony's top note every bar.
155 | /arp..top = "[*]::\seq("5'6'3'2'")";
156 |
157 | // Skip: Play dyads instead of single notes.
158 | /arp..skip = "2";
159 |
160 | // Skip can also accent specific notes.
161 | /arp..skip = "20 |20 |20 |20 ";
162 |
163 | // same, but algorithmic
164 | /arp..skip = "[2222]::\choke(0.25, "0")";
165 |
166 | // Add a second process to change the chord root.
167 | // After this, you should hear tonic, dominant
168 | // and subdominant functions.
169 | // No instrument -- this is for data only.
170 | /make(melBP:root(bassID:\bass));
171 | /root = "[*]::\seq("154")";
172 | /root+
173 |
174 | /arp/root-
175 |
176 |
177 | /**************
178 | Listing 8. Phrase selection for drum fills.
179 | **************/
180 |
181 | TempoClock.tempo = 124/60;
182 |
183 | /drum.(\tightkick); /drum.(\tightsnr); /hh.(\thinhh);
184 |
185 | /tk = "oooo";
186 | /tsn = " - -";
187 | /thh = "[.]::\ins("-", 1, 0.5)::\ins(".", 6, 0.5)";
188 |
189 | /tk/tsn/thh+
190 |
191 | /tk.fill = "o|| _|o __";
192 |
193 | // mid-bar source string:
194 | // in this position, it fills 3 eighth-notes
195 | /tsn.fill = "|-| [ - ]::\ins(".", 4, 0.25)|";
196 |
197 | /tk = (main*3.fill); /tsn = (main*3.fill);
198 |
199 | /tk/tsn/thh-
200 |
201 |
202 | /**************
203 | Listing 9. Multi-bar bassline.
204 | **************/
205 |
206 | // If the bass doesn't exist, first do this:
207 | /make(pbsVC:pbs/melBP:bs(octave:3));
208 |
209 | /bars.(\bs, 2, \a);
210 |
211 | /bs.a0 = "1>|4~5~7 | 4~|3'~";
212 | /bs.a1 = " 5>~|6| 4~| 3";
213 |
214 | /setupbars.(\bs, 2, \b);
215 |
216 | /bs.b0 = "9>.9.9 | 4'~| 3'|8~7~8~ ";
217 | /bs.b1 = " 33.| 4.5~ | 431.|6.6. 6.";
218 |
219 | // short form of /setm.(\bs, 2, \b)
220 | /bs = (b**2);
221 |
222 | /bs+
223 | /bs-
224 |
225 |
226 | /**************
227 | Listing 10. Defining a simple cll process as a factory.
228 | **************/
229 |
230 | (
231 | (
232 | defaultName: \beep,
233 | make: { |name|
234 | PR(\abstractLiveCode).chuck(BP(name), nil, (
235 | userprep: {
236 | ~buf = Buffer.read(
237 | s, Platform.resourceDir +/+ "sounds/a11wlk01.wav",
238 | 4982, 10320
239 | );
240 | ~defaults[\bufnum] = ~buf;
241 | SynthDef(\buf1, { |out, bufnum, pan, amp, time = 1|
242 | var sig = PlayBuf.ar(1, bufnum),
243 | eg = EnvGen.kr(
244 | Env.linen(0.02,
245 | min(time, BufDur.ir(bufnum) - 0.04), 0.02),
246 | doneAction: 2
247 | );
248 | Out.ar(out, Pan2.ar(sig, pan, amp * eg));
249 | }).add;
250 | },
251 | userfree: {
252 | ~buf.free;
253 | },
254 | defaultParm: \amp,
255 | parmMap: (
256 | amp: ($.: 0.1, $-: 0.4, $^: 0.8),
257 | pan: (
258 | $<: -0.9, $>: 0.9,
259 | $(: -0.4, $): 0.4,
260 | $-: 0
261 | )
262 | ),
263 | defaults: (instrument: \buf1),
264 | postDefaults: Pbind(
265 | \time, (Pkey(\dur) * 0.6 / Pfunc { ~clock.tempo }).clip(0.04, 0.2)
266 | )
267 | ));
268 | }, type: \bp) => Fact(\beepBP);
269 | )
270 |
271 |
272 | /**************
273 | Listing 11. Using the cll process factory in a performance.
274 | **************/
275 |
276 | \loadCl.eval; // or \loadAllCl.eval;
277 | TempoClock.tempo = 2;
278 |
279 | /make(beepBP); // defaultName is 'beep' so you get BP(\beep)
280 | /beep = "^|.. .| .- | . "; // "Set pattern"
281 | /beep+; // start it
282 |
283 | /beep..pan = "<><><><>"; // change something
284 |
285 | /make(beepBP:beep2); // ':' overrides defaultName
286 | /beep2 = " ..-| .^ |. ..| .";
287 | /beep2+
288 |
289 | /beep..pan = "<";
290 | /beep2..pan = ">";
291 |
292 | /beep/beep2-;
293 |
294 | /beep(free); /beep2(free);
295 |
296 |
297 | /**************
298 | Listing 12. Template for the parameter map.
299 | **************/
300 |
301 | parmMap: (
302 | parmName: (
303 | char: value,
304 | char: value,
305 | char: value...
306 | ),
307 | parmName: (...)
308 | )
309 |
310 |
311 | /**************
312 | Listing 13. How to write arrays in the parameter map.
313 | **************/
314 |
315 | parmMap: (
316 | freqs: (
317 | $2: [200, 300, 400],
318 | ),
319 | parmName: (...)
320 | )
321 |
322 |
323 | /**************
324 | Listing 14. Arrays for multiple-parameter setting using one cll parameter.
325 | **************/
326 |
327 | parmMap: (
328 | filt: (
329 | alias: [\ffreq, \rq],
330 | $x: [2000, 0.05]
331 | )
332 | )
333 |
334 |
335 | /**************
336 | Listing 15. Cll statements, one by one or as a batch.
337 | **************/
338 |
339 | // run one at a time
340 | /kick.fotf = "----";
341 | /snare.bt24 = " - -";
342 |
343 | // or as a batch
344 | /kick.fotf = "----"; /snare.bt24 = " - -";
345 |
346 |
347 | /**************
348 | Listing 16. Syntax template for the Set pattern statement.
349 | **************/
350 |
351 | /proc.phrase.parm = quant"string";
352 |
353 |
354 | /**************
355 | Listing 17. Multiple parameters with different timing.
356 | **************/
357 |
358 | /x = "--";
359 | /x.filt = "ab c"; // "c" is not heard
360 |
361 | /x = "-|- -"; // now "c" is heard on beat 4.5
362 |
363 |
364 | /**************
365 | Listing 18. A retro acid-house bassline, demonstrating pitch notation.
366 | **************/
367 |
368 | // Initialization code
369 | (
370 | SynthDef(\sqrbass, { |out, freq = 110, gate = 1,
371 | freqMul = 1.006, amp = 0.1,
372 | filtMul = 3, filtDecay = 0.12, ffreq = 2000, rq = 0.1,
373 | lagTime = 0.1|
374 | var sig = Mix(
375 | Pulse.ar(Lag.kr(freq, lagTime) * [1, freqMul], 0.5)
376 | ) * amp,
377 | filtEg = EnvGen.kr(
378 | Env([filtMul, filtMul, 1], [0.005, filtDecay], \exp),
379 | gate
380 | ),
381 | ampEg = EnvGen.kr(Env.adsr(0.01, 0.08, 0.5, 0.1),
382 | gate, doneAction: 2);
383 | sig = RLPF.ar(sig, (ffreq * filtEg).clip(20, 20000), rq);
384 | Out.ar(out, (sig * ampEg).dup);
385 | }).add;
386 |
387 | (
388 | keys: #[master], // ~master comes from \loadAllCl.eval
389 | defaultName: \sqrbs,
390 | initLevel: -12.dbamp,
391 | argPairs: #[amp, 0.5],
392 | make: { |name|
393 | var out;
394 | ~target = MixerChannel(name, s, 2, 2, ~initLevel, outbus: ~master);
395 | out = Voicer(5, \sqrbass, target: ~target);
396 | out.mapGlobal(\ffreq, nil, 300, \freq);
397 | out.mapGlobal(\rq, nil, 0.2, \myrq);
398 | out.mapGlobal(\filtMul, nil, 8, [1, 12]);
399 | out
400 | },
401 | free: { ~target.free },
402 | type: \vc) => Fact(\sqrbsVC);
403 | )
404 |
405 | // Performance code:
406 | \loadAllCl.eval;
407 | TempoClock.tempo = 132/60;
408 | Mode(\fsloc) => Mode(\default);
409 |
410 | /make(sqrbsVC/melBP:bs(octave:3));
411 | /bs = "1_ 1.|5~3_9.4.|7.2~4_5'.|5_8~2_4.";
412 | /bs+;
413 |
414 | /bs-;
415 |
416 |
417 | /**************
418 | Listing 19. Pitch notation in PR(\abstractLiveCode); not generally recommended.
419 | **************/
420 |
421 | // Use the same SynthDef as in the previous example
422 |
423 | BP(\acid).free;
424 | PR(\abstractLiveCode).chuck(BP(\acid), nil, (
425 | event: (eventKey: \default),
426 | alwaysReset: true,
427 | defaultParm: \degree,
428 | parmMap: (
429 | degree: (isPitch: true),
430 | ),
431 | defaults: (
432 | ffreq: 300, filtMul: 8, rq: 0.2,
433 | octave: 3, root: 6, scale: Scale.locrian.semitones
434 | ),
435 | postDefaults: PmonoArtic(\sqrbass,
436 | \dummy, 1
437 | )
438 | ));
439 |
440 | TempoClock.tempo = 132/60;
441 | )
442 |
443 | /acid = "1_ 1.|5~3_9.4.|7.2~4_5'.|5_8~2_4.";
444 |
445 | /acid+;
446 | /acid-;
447 |
448 |
449 | /**************
450 | Listing 20. Syntax template for "Set pattern" phrase selection.
451 | **************/
452 |
453 | /proc = (group...);
454 |
455 |
456 | /**************
457 | Listing 21. Nested phrase-selection groups.
458 | **************/
459 |
460 | ((a%4|b)*4.(a|b%4)*2)
461 |
462 |
463 | /**************
464 | Listing 22. Syntax template for make statements.
465 | **************/
466 |
467 | /make(factory0:targetName0(parameters0)/factory1:targetName1(parameters1)/...);
468 |
469 | // Or, with autoGui
470 | /make*(factory0:targetName0(parameters0)/factory1:targetName1(parameters1)/...);
471 |
472 |
473 | /**************
474 | Listing 23. Example of the make statement.
475 | **************/
476 |
477 | (
478 | // THIS PART IN THE INIT FILE
479 | (
480 | defaultName: \demo,
481 | octave: 5, // a default octave
482 | make: { |name|
483 | PR(\abstractLiveCode).chuck(BP(name), nil, (
484 | event: (eventKey: \default),
485 | defaultParm: \degree,
486 | parmMap: (degree: (isPitch: true)),
487 | // Here, the Factory transmits ~octave
488 | defaults: (octave: ~octave)
489 | ));
490 | }, type: \bp) => Fact(\demoBP);
491 | )
492 |
493 | // DO THIS IN PERFORMANCE
494 | /make(demoBP:dm(octave:6)); // :dm overrides defaultName
495 |
496 | /dm = "1353427,5,";
497 | /dm+;
498 | /dm-;
499 |
500 | /dm(free);
501 |
502 |
503 | /**************
504 | Listing 24. Syntax template for passthrough statements.
505 | **************/
506 |
507 | // This...
508 | /snr(clock = ~myTempoClock);
509 |
510 | // ... is the same as running:
511 | BP(\snr).clock = ~myTempoClock;
512 |
513 | // Or...
514 | /VC.bass(releaseAll); // VC(\bass).releaseAll;
515 |
516 |
517 | /**************
518 | Listing 25. Syntax template for Chuck statements.
519 | **************/
520 |
521 | // This...
522 | /snr => MCG(0);
523 |
524 | // ... is the same as running:
525 | BP(\snr) => MCG(0);
526 |
527 | // Or...
528 | /VC.keys => MCG(0); // VC(\keys) => MCG(0);
529 |
530 |
531 | /**************
532 | Listing 26. Syntax template for func-call statements.
533 | **************/
534 |
535 | /func.(arguments);
536 |
537 | // e.g.:
538 | /bars.(\proc, 2, \a);
539 |
540 |
541 | /**************
542 | Listing 27. Syntax template for copy/transfer statements.
543 | **************/
544 |
545 | /proc.phrase*n -> newPhrase; // copy
546 |
547 | /proc.phrase*n ->> newPhrase; // transfer
548 |
549 |
550 | /**************
551 | Listing 28. Demonstration of "Show pattern" statements.
552 | **************/
553 |
554 | /snr.a = " - -";
555 |
556 | /snr.a -> b;
557 |
558 | /snr.b // now hit ctrl-return at the end of this line
559 |
560 | // the line magically changes to
561 | /snr.b = " - -";
562 |
563 |
564 | /**************
565 | Listing 29. Common initialization sequence, using helper functions.
566 | **************/
567 |
568 | /make(kick);
569 | /bars.(\kick, 2, \a);
570 |
571 | // the following lines are automatically inserted
572 | /kick.a0 = "";
573 | /kick.a1 = "";
574 |
575 |
576 | /**************
577 | Listing 30. Isorhythmic cycles with generators.
578 | **************/
579 |
580 | (
581 | BP(\y).free;
582 | PR(\abstractLiveCode).chuck(BP(\y), nil, (
583 | event: (eventKey: \default),
584 | defaultParm: \degree,
585 | parmMap: (degree: (isPitch: true))
586 | ));
587 | )
588 |
589 | TempoClock.tempo = 140/60;
590 |
591 | /y = "12 4| 5 6| 12 |45"; // A
592 |
593 | /y+;
594 |
595 | /y = "[** *| * *| ** |**]::\seq("12456", "*")"; // B
596 |
597 | /y = "[** *| * *| ** |**]::\seq("12456", "*")::\ins("*", 7, 0.25)"; // C
598 |
599 | /y = "[** *| * *| ** |**]::\seq("12456", "*")::\ins("*", 7, 0.25)::\seq("6,214", "*")"; // D
600 |
601 | /y-;
602 |
603 |
604 | /**************
605 | Listing 31. Interaction between generator syntax and "set pattern" rhythmic notation.
606 | **************/
607 |
608 | // 1. Chain starts on the downbeat and occupies the whole bar.
609 | /y = "[1,]::\ins("*", 3, 0.5)::\rand("13467", "*")";
610 |
611 | /y+;
612 |
613 | // 2. Chain starts on beat 2
614 | // Note that a generator source can appear
615 | // anywhere within the bar!
616 | /y = "1,|[6,]::\ins("*", 3, 0.5)::\rand("13467", "*")||";
617 |
618 | // 3. Chain starts on the 2nd 16th-note of beat 2
619 | // Here, '6,' occupies time and is not a generator source.
620 | // So it is not bracketed.
621 | /y = "1,|6,\ins("*", 3, 0.5)::\rand("13467", "*") ||";
622 |
623 | // 4. Chain starts on the 2nd 16th-note of beat 2
624 | // and stop on the 'and' of 4
625 | /y = "1,|6,\ins("*", 3, 0.5)::\rand("13467", "*") || x";
626 |
627 | /y-;
628 |
629 |
630 | /**************
631 | Listing 32. Usage of \rot() generator.
632 | **************/
633 |
634 | // Reich, "Piano Phase"-ish
635 |
636 | (
637 | BP(\y).free;
638 | PR(\abstractLiveCode).chuck(BP(\y), nil, (
639 | event: (eventKey: \default, pan: -0.6),
640 | defaultParm: \degree,
641 | parmMap: (degree: (isPitch: true))
642 | ));
643 |
644 | BP(\z).free;
645 | PR(\abstractLiveCode).chuck(BP(\z), nil, (
646 | event: (eventKey: \default, pan: 0.6),
647 | defaultParm: \degree,
648 | parmMap: (degree: (isPitch: true))
649 | ));
650 | )
651 |
652 | TempoClock.setMeterAtBeat(3, TempoClock.nextBar);
653 | TempoClock.tempo = 112/60;
654 |
655 | /y = "[*^*^*^*^*^*^]::\seq("268", "*")::\seq("37", "^")";
656 |
657 | /z = "[*^*^*^*^*^*^]::\seq("268", "*")::\seq("37", "^")";
658 |
659 | /y/z+;
660 |
661 | /z = "[*^*^*^*^*^*^]::\seq("268", "*")::\seq("37", "^")::\rot(-0.25)";
662 |
663 | /z = "[*^*^*^*^*^*^]::\seq("268", "*")::\seq("37", "^")::\rot(-0.5)";
664 |
665 | /z = "[*^*^*^*^*^*^]::\seq("268", "*")::\seq("37", "^")::\rot(-0.75)";
666 |
667 | /y/z-;
668 |
669 |
670 | /**************
671 | Listing 33. Usage of \rDelta() generator.
672 | **************/
673 |
674 | // equal distribution
675 | // but more total time spent on longer notes
676 | /y = "\rDelta("*", 1, 8, , , `lin)::\wrand("\xrand("12345")\xrand("3'4'5'6'")", 2, 1)";
677 |
678 | /y+
679 |
680 | // equal total time spent on longer notes vs shorter
681 | /y = "\rDelta("*", 1, 8, , , `exp)::\wrand("\xrand("12345")\xrand("3'4'5'6'")", 2, 1)";
682 |
683 | // Phprand
684 | /y = "\rDelta("*", 1, 8, , , `hp)::\wrand("\xrand("12345")\xrand("3'4'5'6'")", 2, 1)";
685 |
686 | // Plprand
687 | /y = "\rDelta("*", 1, 8, , , `lp)::\wrand("\xrand("12345")\xrand("3'4'5'6'")", 2, 1)";
688 |
689 | // beta distribution: similar problem as `lin
690 | /y = "\rDelta("*", 1, 8, , , `beta, 0.2, 0.2)::\wrand("\xrand("12345")\xrand("3'4'5'6'")", 2, 1)";
691 |
692 | // expb: "exponentialized" beta distribution
693 | /y = "\rDelta("*", 1, 8, , , `expb, 0.2)::\wrand("\xrand("12345")\xrand("3'4'5'6'")", 2, 1)";
694 |
695 | /y-
696 |
697 |
698 | /**************
699 | Listing 34. Usage of \ramp() generator.
700 | **************/
701 |
702 | /y = "\ramp("*", 1, 0.2, 1, `exp)::\pitch("*", "2", 0, 0)";
703 | /y+
704 |
705 | // randomly choose accel or decel
706 | /y = "\ramp("*", 1, 0.2, {2.rand}, `exp)::\pitch("*", "2", 0, 0)";
707 |
708 | // alternate between accel and decel
709 | /y = "\ramp("*", 1, 0.2, {Pseq([0, 1], inf)}, `exp)::\pitch("*", "2", 0, 0)";
710 |
711 | /y = "\ramp("*", 1, 0.2, 0.5, `exp)::\pitch("*", "2", 0, 0)";
712 |
713 | // pulls 1 -> 0.2 curve to the right, biasing long durations
714 | /y = "\ramp("*", 1, 0.2, 0.5, 2)::\pitch("*", "2", 0, 0)";
715 |
716 | // pulls 1 -> 0.2 curve to the left, biasing short durations
717 | /y = "\ramp("*", 1, 0.2, 0.5, -2)::\pitch("*", "2", 0, 0)";
718 |
719 | /y = "\ramp("*", 1, 0.2, 0.5, -4)::\pitch("*", "2", 0, 0)";
720 |
721 | /y-
722 |
723 |
724 | /**************
725 | Listing 35. Using the \unis() generator to coordinate two harmony processes.
726 | **************/
727 |
728 | TempoClock.tempo = 124/60;
729 | /make(anapadVC:pad/chordBP:ch(chords:\bigch));
730 | /ch(leadTime = 0.01);
731 | /ch = "\delta("*", 0.5, 0, 1, 2, 2)::\shuf("0976")";
732 | /ch+
733 |
734 | /make(pulseLeadVC:pl/arpegBP:arp(chords:\bigch));
735 | /arp = "\unis(`note, "1", `ch)::\ins("*", 16, 0.25)::\seq("23456")::\artic(".")";
736 | /arp..top = "\unis(`note, , `ch)";
737 | /arp..acc = "\unis(`top, ">", `arp)::\choke(0.25, "-")";
738 | /arp+
739 |
740 | VC(\pad).v.gui; VC(\pl).v.gui; // adjust filters
741 |
742 |
743 | /**************
744 | Listing 36. Usage of \ser() generator.
745 | **************/
746 |
747 | /y = "\ins("*", 8, 0.5)::\seq("\seq*2..4("1234")\seq("875")")";
748 | /y+
749 |
750 | /y = "\ins("*", 8, 0.5)::\seq("\ser*2..4("1234")\seq("875")")";
751 |
752 | /y-
753 |
754 |
755 | /**************
756 | Listing 37. Usage of \pdefn() generator.
757 | **************/
758 |
759 | Pdefn(\y, Pn(Pseries(0, 1, 8), inf).collect { |d| SequenceNote(d, nil, 0.9) });
760 |
761 | /y.a0 = "[*]::\ins("*", 2, 0.5)::\pdefn(`y, "*")";
762 | /y.a1 = "\ins("*", 3, 0.5)::\pdefn(`y, "*")";
763 | /y = (a**2);
764 |
765 |
766 | /**************
767 | Listing 38. Usage of \gdefn() generator.
768 | **************/
769 |
770 | (
771 | BP(\x).free;
772 | PR(\abstractLiveCode).chuck(BP(\x), nil, (
773 | event: (play: { ~x.debug(~collIndex) }),
774 | defaultParm: \x,
775 | parmMap: (
776 | x: ($0: 0, $1: 1, $2: 2, $3: 3, $4: 4)
777 | ),
778 | ));
779 |
780 | BP(\y).free;
781 | PR(\abstractLiveCode).chuck(BP(\y), nil, (
782 | event: (play: { ~x.debug(~collIndex) }),
783 | defaultParm: \x,
784 | parmMap: (
785 | x: ($0: 0, $1: 1, $2: 2, $3: 3, $4: 4)
786 | ),
787 | ));
788 |
789 | BP(\x).leadTime = 0.01;
790 | Pdefn(\x, Pseq("01234", inf).trace(prefix: "pdefn: "));
791 | )
792 |
793 | /x = "[****]::\gdefn(`x, "*", 1)";
794 | /y = "[****]::\gdefn(`x, "*", 1)";
795 | /x/y+
796 | /x/y-
797 |
798 |
799 | /**************
800 | Listing 39. Usage of \pitch() and \pitch2 generators.
801 | **************/
802 |
803 | TempoClock.tempo = 140/60;
804 | Mode(\cphr) => Mode(\default);
805 |
806 | /make(pulseLeadVC:pl/melBP:pl);
807 |
808 | // Every "6,7," descending pattern starts at the previous given note
809 | /pl = "[5'>| 3'>|4'> 2'>|]::\ins("*", 5..10, 0.25)::\pitch("*", "6,7,", 8, 10, ".")";
810 | /pl+
811 |
812 | // The first descent starts from "5'" and keeps going down through the bar
813 | /pl = "[5'>| 3'>|4'> 2'>|]::\ins("*", 5..10, 0.25)::\pitch2("*", "6,7,", 8, 10, ".")";
814 |
815 | // One \pitch() stream;
816 | // another interlocking \pitch2() stream in a higher register
817 | /pl = "[5'>| 3'>|4'> 2'>|]::\ins("*", 3..7, 0.25)::\ins("@", 3..8, 0.25)::\pitch("*", "6,7,", 8, 10, ".")::\pitch2("@", "6,7,23", 14, 18, ".", 0)";
818 |
819 | /pl-
820 |
821 |
822 | /**************
823 | Listing 40. Complex sequencing with sub-generators.
824 | **************/
825 |
826 | (
827 | BP(\y).free;
828 | PR(\abstractLiveCode).chuck(BP(\y), nil, (
829 | event: (eventKey: \default),
830 | defaultParm: \degree,
831 | parmMap: (degree: (isPitch: true))
832 | ));
833 | )
834 |
835 | TempoClock.tempo = 140/60;
836 |
837 | /y = "\ins("*", 8, 0.5)::\seq("123")";
838 |
839 | /y+
840 |
841 | // can repeat (like Prand)
842 | /y = "\ins("*", 8, 0.5)::\rand("\seq("123")\seq("7854")\seq("26,")")";
843 |
844 | // no repeats (like Pxrand)
845 | /y = "\ins("*", 8, 0.5)::\xrand("\seq("123")\seq("7854")\seq("26,")")";
846 |
847 | // 2-5random notes after 6,
848 | // note here that articulation/transposition applies to sub-choices
849 | /y = "\ins("*", 8, 0.5)::\xrand("\seq("123")\seq("7854")\seq("26,\xrand*2..5("34567")::\xpose("1'")::\artic(".")")")";
850 |
851 | // also, articulations and transposition can be streamed this way
852 | /y = "\ins("*", 8, 0.5)::\seq("123")::\artic(\seq("\seq*3("_")\seq*7(".")"))";
853 |
854 | /y-
855 |
856 |
857 | /**************
858 | Listing 41. Usage of \artic() generator.
859 | **************/
860 |
861 | TempoClock.tempo = 140/60;
862 | Mode(\cphr) => Mode(\default);
863 | /make(pulseLeadVC:pl/melBP:pl);
864 |
865 | // No wildcards, all notes match: All are staccato
866 | /pl = "[1574]::\artic(".")";
867 | /pl+
868 |
869 | // Wildcard, but none of the notes came from a
870 | // wildcard operation, so none of them match.
871 | /pl = "[1574]::\artic(".", "*")";
872 |
873 | // Notes were inserted by \seq operating on "*";
874 | // all notes match.
875 | /pl = "[****]::\seq("1574")::\artic(".", "*")";
876 |
877 | // Two layers of notes with different articulations
878 | /pl = "[****]::\seq("1574")::\ins("@", 4..8, 0.25)::\shuf("1'2'3'4'5'6'", "@")::\artic(">.", "*")::\artic("~", "@")";
879 |
880 | /pl-
881 |
882 |
883 | /**************
884 | Listing 42. Usage of \xpose() generator.
885 | **************/
886 |
887 | TempoClock.tempo = 140/60;
888 | Mode(\cphr) => Mode(\default);
889 | /make(pulseLeadVC:pl/melBP:pl);
890 |
891 | /pl = "\ins("*", 16, 0.25)::\seq("12345432")";
892 | /pl+
893 |
894 | // Easy octave displacement.
895 | /pl = "\ins("*", 16, 0.25)::\seq("12345432")::\xpose("1111,1'1''")";
896 | /pl-
897 |
898 |
899 | /**************
900 | Listing 43. Usage of \fork() generator.
901 | **************/
902 |
903 | /y = "\ins("*", 10, 0.25)::\fork("\seq("13", "*")\seq("14", "*")")";
904 |
905 | /y = "\ins("1,", 10, 0.25)::\fork(" \seq("13", "1,")x\seq("14", "1,")")";
906 |
907 |
908 | /**************
909 | Listing 44. Cll statement regular expression templates.
910 | **************/
911 |
912 | ~statements = [
913 | \clMake -> "^ *make\\(.*\\)",
914 | \clFuncCall -> "^ *`id\\.\\(.*\\)",
915 | \clPassThru -> "^ *([A-Z][A-Za-z0-9_]*\\.)?`id\\(.*\\)",
916 | \clChuck -> "^ *([A-Z][A-Za-z0-9_]*\\.)?`id *=>.*",
917 | \clPatternSet -> "^ *`id(\\.|`id|`id\\*[0-9]+)* = .*",
918 | \clGenerator -> "^ *`id(\\.|`id)* \\*.*",
919 | // harder match should come first
920 | \clXferPattern -> "^ *`id(\\.`id)?(\\*`int)? ->>",
921 | \clCopyPattern -> "^ *`id(\\.`id)?(\\*`int)? ->",
922 | \clStartStop -> "^([/`spc]*`id)+[`spc]*[+-]",
923 | \clPatternToDoc -> "^ *`id(\\.|`id)*[`spc]*$"
924 | ];
925 |
926 |
927 | /**************
928 | Listing 45. Regular expression macros for SC language tokens.
929 | **************/
930 |
931 | ~tokens = (
932 | al: "A-Za-z",
933 | dig: "0-9",
934 | id: "[A-Za-z][A-Za-z0-9_]*",
935 | int: "(-[0-9]+|[0-9]+)",
936 | // http://www.regular-expressions.info/floatingpoint.html
937 | float: "[\\-+]?[0-9]*\\.?[0-9]+([eE][\\-+]?[0-9]+)?",
938 | spc: " " // space, tab, return
939 | );
940 |
941 |
942 | /**************
943 | Listing 46. Template for cll statement handlers.
944 | **************/
945 |
946 | Proto {
947 | ~process = { |code|
948 | // parse 'code' and build the SC language statement(s)...
949 | translatedStatement // return value
950 | };
951 | } => PR(\clMyNewStatement);
952 |
953 |
954 | /**************
955 | Listing 47. Adding a function into PR(\chucklibLiveCode).
956 | **************/
957 |
958 | PR(\chucklibLiveCode).clMyNewStatement = { |code|
959 | // parse 'code' and build the SC language statement(s)...
960 | translatedStatement // return value
961 | };
962 |
--------------------------------------------------------------------------------
/cl-manual.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamshark70/ddwChucklib-livecode/fb9153c1cd478800fde396dddcdb735667b47941/cl-manual.pdf
--------------------------------------------------------------------------------
/clLiveCode-ext.sc:
--------------------------------------------------------------------------------
1 | /**
2 | Chucklib-livecode: A framework for live-coding improvisation of electronic music
3 | Copyright (C) 2018 Henry James Harkins
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU General Public License as published by
7 | the Free Software Foundation, either version 3 of the License, or
8 | (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU General Public License for more details.
14 |
15 | You should have received a copy of the GNU General Public License
16 | along with this program. If not, see .
17 | **/
18 |
19 | // "decodes" pairs of [value, delta] into step sequences, for non-default parameters
20 |
21 | PstepDurPair : Pstep {
22 | var <>tolerance;
23 |
24 | *new { |pairs, repeats = 1, tolerance = 0.001|
25 | ^super.newCopyArgs(pairs, nil, repeats).init.tolerance_(tolerance);
26 | }
27 |
28 | embedInStream { |inval|
29 | var itemStream, durStream, pair, item, dur, nextChange = 0, elapsed = 0;
30 | repeats.value(inval).do {
31 | itemStream = list.asStream;
32 | while {
33 | pair = itemStream.next(inval);
34 | if(pair.notNil) {
35 | #item, dur = pair;
36 | item.notNil and: { dur.notNil }
37 | } { false } // terminate if stream was nil
38 | } {
39 | nextChange = nextChange + dur;
40 | // 'elapsed' increments, so nextChange - elapsed will get smaller
41 | // when this drops below 'tolerance' it's time to move on
42 | while { (nextChange - elapsed) >= tolerance } {
43 | elapsed = elapsed + inval.delta;
44 | inval = item.embedInStream(inval);
45 | };
46 | };
47 | };
48 | ^inval
49 | }
50 |
51 | // not properly part of a pattern, but I need to install hooks to load the environment
52 | // then you can do e.g. \loadCl.eval
53 | *initClass {
54 | Class.initClassTree(AbstractChuckArray);
55 | Class.initClassTree(Library);
56 | Library.put(\cl, \path, this.filenameSymbol.asString.dirname);
57 | Library.put(\cl, \files, [
58 | "preprocessor.scd", "preprocessor-generators.scd",
59 | "helper-funcs.scd"
60 | ]);
61 | Library.put(\cl, \extras, ["edit-gui.scd", "mobile-objects.scd", "nanoktl-objects.scd"]);
62 | if(File.exists(Quarks.folder +/+ "ddwLivecodeInstruments")) {
63 | Library.put(\cl, \instr, (Quarks.folder +/+ "ddwLivecodeInstruments/*.scd").pathMatch);
64 | {
65 | (Quarks.folder +/+ "ddwLivecodeInstruments/*.scd").pathMatch.do { |path|
66 | path.load
67 | };
68 | } => Func(\loadClInstr);
69 | };
70 |
71 | { |files|
72 | var dir = Library.at(\cl, \path);
73 | files.do { |name| (dir +/+ name).load };
74 | } => Func(\loadClFiles);
75 |
76 | { \loadClFiles.eval(Library.at(\cl, \files)) } => Func(\loadCl);
77 | { \loadClFiles.eval(Library.at(\cl, \extras)) } => Func(\loadClExtras);
78 | { #[loadCl, loadClExtras, loadClInstr].do(_.eval) } => Func(\loadAllCl);
79 | }
80 | }
81 |
82 |
83 | // used in clGenSeq for matching items
84 |
85 | + Rest {
86 | == { |that|
87 | if(that.isKindOf(Rest).not) { ^false };
88 | ^(this.value == that.value)
89 | }
90 | }
91 |
92 | + Object {
93 | processRest {}
94 | }
95 |
96 |
97 | // preset support
98 | + Fact {
99 | addPreset { |key, presetDef|
100 | // if you add to an empty Factory, later checks will die with error
101 | // so, prevent that
102 | if(this.class.exists(collIndex)) {
103 | if(presetDef.isKindOf(Dictionary).not) {
104 | Error(
105 | "Presetdef for '%' should be a dictionary but isn't"
106 | .format(key)
107 | ).throw;
108 | };
109 | Library.put(\cl, \presets, this.collIndex, key, presetDef);
110 | Fact.changed(\addPreset, this.collIndex, key);
111 | } {
112 | Error("Cannot add preset to empty Fact(%)".format(collIndex.asCompileString))
113 | .throw;
114 | };
115 | }
116 |
117 | presets {
118 | ^Library.at(\cl, \presets, this.collIndex)
119 | }
120 |
121 | presetAt { |key|
122 | ^Library.at(\cl, \presets, this.collIndex, key)
123 | }
124 |
125 | *savePresets { |force = false|
126 | var write = {
127 | Library.at(\cl, \presets).writeArchive(Platform.userConfigDir +/+ "chucklibPresets.txarch");
128 | };
129 | if(force) {
130 | write.value
131 | } {
132 | if(Library.at(\cl, \presetsLoaded) == true) {
133 | write.value
134 | } {
135 | "Presets not previously loaded from disk. Load first to avoid data loss".warn;
136 | }
137 | }
138 | }
139 |
140 | *loadPresets {
141 | var allPresets = Object.readArchive(Platform.userConfigDir +/+ "chucklibPresets.txarch");
142 | var curPresets = Library.at(\cl, \presets) ?? {
143 | IdentityDictionary.new
144 | };
145 | var conflicts = IdentityDictionary.new;
146 | allPresets.keysValuesDo { |factName, presets|
147 | if(curPresets[factName].isNil) {
148 | curPresets[factName] = IdentityDictionary.new;
149 | };
150 | presets.keysValuesDo { |presetName, values|
151 | if(values.isKindOf(Dictionary).not) {
152 | "Invalid preset '%' for '%': %"
153 | .format(presetName, factName, values.asCompileString)
154 | .warn;
155 | };
156 | if(curPresets[factName][presetName].isNil) {
157 | curPresets[factName][presetName] = values;
158 | } {
159 | conflicts[factName] = conflicts[factName].add(presetName);
160 | };
161 | };
162 | };
163 | if(conflicts.notEmpty) {
164 | "The following presets loaded from disk already existed in memory.
165 | The memory version is retained; the disk version was not loaded.".warn;
166 | conflicts.keysValuesDo { |factName, presetKeys|
167 | "Fact(%): %\n".postf(factName.asCompileString, presetKeys);
168 | };
169 | };
170 | Library.put(\cl, \presets, allPresets);
171 | Library.put(\cl, \presetsLoaded, true);
172 | }
173 | }
174 |
175 | // local only, not preserved with Factories
176 | + VC {
177 | addPreset { |key, presetDef|
178 | if(env[\presets].isNil) {
179 | env[\presets] = IdentityDictionary.new;
180 | };
181 | env[\presets].put(key, presetDef);
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/ddwChucklib-livecode.quark:
--------------------------------------------------------------------------------
1 | /**
2 | Chucklib-livecode: A framework for live-coding improvisation of electronic music
3 | Copyright (C) 2018 Henry James Harkins
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU General Public License as published by
7 | the Free Software Foundation, either version 3 of the License, or
8 | (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU General Public License for more details.
14 |
15 | You should have received a copy of the GNU General Public License
16 | along with this program. If not, see .
17 | **/
18 |
19 | (
20 | name: "ddwChucklib-livecode",
21 | summary: "Mini-language to supply live-coded patterns to chucklib BP processes.",
22 | author: "James Harkins",
23 | version: 0.3,
24 | // helpdoc: "Help/ChuckOverview.html",
25 | url: "http://www.dewdrop-world.net/display.php?src=sc.md",
26 | dependencies: ["ddwChucklib", "TabbedView"]
27 | )
28 |
--------------------------------------------------------------------------------
/edit-gui.scd:
--------------------------------------------------------------------------------
1 | var getTextProperties = {
2 | var result = IdentityDictionary.with(\textProperties -> ~textProperties);
3 | ~textProperties.do { |key|
4 | result.put(key, key.envirGet);
5 | };
6 | result
7 | };
8 |
9 | /**
10 | Chucklib-livecode: A framework for live-coding improvisation of electronic music
11 | Copyright (C) 2018 Henry James Harkins
12 |
13 | This program is free software: you can redistribute it and/or modify
14 | it under the terms of the GNU General Public License as published by
15 | the Free Software Foundation, either version 3 of the License, or
16 | (at your option) any later version.
17 |
18 | This program is distributed in the hope that it will be useful,
19 | but WITHOUT ANY WARRANTY; without even the implied warranty of
20 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 | GNU General Public License for more details.
22 |
23 | You should have received a copy of the GNU General Public License
24 | along with this program. If not, see .
25 | **/
26 |
27 | // function to jump across a bracketed group
28 | (
29 | Proto {
30 | ~delimiters = IdentityDictionary[$( -> $), $) -> $(,
31 | $[ -> $], $] -> $[,
32 | ${ -> $}, $} -> ${,
33 | $\" -> $\",
34 | // $' -> $'
35 | ];
36 | ~pairs = ["()", "[]", "{}", "\"\""/*, "''"*/];
37 |
38 | ~skipToDelimiter = { |str, i, step = 1|
39 | var ch;
40 | while {
41 | ch = str[i];
42 | ch.notNil and: { ~delimiters[ch].isNil }
43 | } {
44 | i = i + step;
45 | };
46 | i
47 | };
48 |
49 | // at entry: i is on or before a bracket
50 | // return 'i' is one after the closing bracket; Ref(i) if a bracket mismatch
51 | ~scanDelimiters = { |str, i, step = 1, lastMatchIndex(i), level = 0, assumeClosing, end = "\n"|
52 | var return = nil, ch, pair, matchPair, hitEnd = false,
53 | startI = (step < 0).binaryValue;
54 | var saveI = i;
55 | // [i, level, assumeClosing].debug(">> scanDelimiters");
56 | // skip up to the next delimiter
57 | if(~delimiters[str[i]].isNil) {
58 | i = ~skipToDelimiter.(str, i + step, step);
59 | };
60 | // look for the matching delimiter
61 | // must be a loop b/c of cases like ((abc)def(ghi))
62 | matchPair = ~pairs.detect { |p| p.includes(str[i]) };
63 | // [str, i, matchPair].debug("found delimiter");
64 | if(assumeClosing.isNil and: { "\"'".includes(str[i]) }) {
65 | assumeClosing = str[i];
66 | };
67 | i = i + step;
68 | while { return.isNil and: {
69 | (ch = str[i]).notNil and: { (hitEnd = end.includes(ch)).not }
70 | } } {
71 | if(~delimiters[ch].isNil) {
72 | // ch.debug("ignoring");
73 | i = i + step; // misc characters inside the delimiter
74 | } {
75 | pair = ~pairs.detect { |p| p.includes(ch) };
76 | // opening or closing?
77 | case
78 | { ch == assumeClosing } {
79 | return = (i + step); //.debug("return (assumeClosing)");
80 | }
81 | { pair[startI] == ch } {
82 | // ch.debug("opening bracket");
83 | i = ~scanDelimiters.(str, i, step, lastMatchIndex, level + 1,
84 | if(pair[0] == pair[1]) { pair[0] } { nil } // assumeClosing
85 | );
86 | if(i.isNumber) {
87 | lastMatchIndex = i.value; // .debug("set lastMatch (recursion)");
88 | } {
89 | return = i; // .debug("return (recursion/mismatch)");
90 | };
91 | }
92 | // done this level?
93 | { matchPair[1 - startI] == ch} {
94 | return = (i + step); // .debug("return (closing)");
95 | } {
96 | // save last matched bracket and keep scanning
97 | // mismatch: stop here
98 | return = Ref(i + step); // .debug("return (mismatch)");
99 | };
100 | };
101 | };
102 | if(hitEnd or: { ch.isNil }) {
103 | // end of string, treat as mismatch
104 | return = Ref(lastMatchIndex); // .debug("return (end of string)");
105 | };
106 | // [saveI, level, return].debug("<< scanDelimiters");
107 | return
108 | };
109 |
110 | ~jumpInTextView = { |view, step = 1|
111 | var str = view.string,
112 | i = view.selectionStart,
113 | newI;
114 | if(step < 0) { i = i - 1 };
115 | newI = ~scanDelimiters.(str, i, step, end: "\n");
116 | if(newI.notNil) {
117 | newI = newI.value; // Ref(index) means mismatch, but I don't care
118 | if(step < 0) { newI = newI + 1 };
119 | if(newI.inclusivelyBetween(0, str.size)) {
120 | view.select(newI, 0);
121 | };
122 | };
123 | currentEnvironment
124 | };
125 | } => PR(\clNavJumpBrackets);
126 | );
127 |
128 | // first: TEXTVIEW WRAPPERS for content change tracking
129 |
130 | Proto {
131 | var ddwSnippets = 'DDWSnippets'.asClass;
132 |
133 | ~background = Color.white;
134 | ~stringColor = Color.black;
135 | ~markColor = Color(0.8, 0, 0);
136 | ~font = Font("Inconsolata", 14);
137 | ~markFont = ~font.boldVariant;
138 | ~textProperties = #[font, background, stringColor];
139 | ~str = "";
140 | ~displayBookmarks = false;
141 | ~autoSave = true;
142 | ~autoSavePath = Platform.userAppSupportDir +/+ "cll-sketches";
143 |
144 | ~prep = { |parentView(~parentView), bounds(~bounds)|
145 | var currentEnv = currentEnvironment,
146 | shutdownFunc = inEnvir {
147 | if(~autoSave == true) { ~doAutoSave.() };
148 | };
149 | if(File.exists(~autoSavePath).not) {
150 | File.mkdir(~autoSavePath);
151 | };
152 | ~parentView = parentView;
153 | ~bounds = bounds;
154 | ~view = TextView(parentView, bounds)
155 | .string_(~str)
156 | .keyUpAction_(inEnvir { |... args| ~prKeyUpAction.(*args) })
157 | .keyDownAction_(inEnvir { |... args| ~prKeyDownAction.(*args) })
158 | .mouseUpAction_(inEnvir { |... args| ~prMouseUpAction.(*args) })
159 | .focusGainedAction_({ currentEnv.changed(\focused, true) })
160 | .focusLostAction_({ currentEnv.changed(\focused, false) })
161 | .onClose_({
162 | ShutDown.remove(shutdownFunc);
163 | shutdownFunc.value;
164 | currentEnv.changed(\didFree)
165 | });
166 | ShutDown.add(shutdownFunc);
167 | ~textProperties.().do { |key|
168 | ~view.tryPerform(key.asSetter, key.envirGet);
169 | };
170 | ~view.palette = QPalette.system.baseText_(~stringColor).base_(~background);
171 | // save time for platform lookup
172 | // this is to detect ctrl-C and ctrl-v
173 | // but in OSX, it's cmd-c and cmd-v
174 | ~ctrlMod = if(thisProcess.platform.name == \osx) { 1048576 } { 262144 };
175 | ~bookmarks = List.new;
176 | currentEnvironment
177 | };
178 |
179 | ~addBookmark = { |pos|
180 | var i = ~bookmarks.detectIndex { |mark| mark >= pos };
181 | case
182 | { i.isNil } { ~bookmarks.add(pos) }
183 | { ~bookmarks[i] != pos } {
184 | ~bookmarks.insert(i, pos);
185 | };
186 | ~drawBookmarks.();
187 | };
188 | ~removeBookmark = { |pos|
189 | ~bookmarks.remove(pos);
190 | ~drawBookmarks.();
191 | };
192 |
193 | ~background_ = { |color|
194 | ~background = color;
195 | currentEnvironment
196 | };
197 | ~drawBookmarks = {
198 | ~view.background_(~background).stringColor_(~stringColor);
199 | ~bookmarks.do { |mark|
200 | ~view.setStringColor(~markColor, mark, 1);
201 | };
202 | currentEnvironment
203 | };
204 |
205 | ~setString = { |string, start, length, stringObj|
206 | var end = start + length;
207 | ~view.setString(string, start, length);
208 | ~str = ~view.string;
209 | if(stringObj.notNil) {
210 | ~updateBookmarks.(start, string.size - length, ~str, (length > 0).asInteger, stringObj);
211 | };
212 | };
213 | ~string = { ~view.string };
214 | ~focus = { |bool| ~view.focus(bool) };
215 |
216 | ~highlight = { |highlightOn(false), start, size|
217 | defer(inEnvir {
218 | ~view.setStringColor(
219 | if(highlightOn) { ~markColor } { ~stringColor },
220 | start, size
221 | );
222 | ~view.setFont(
223 | if(highlightOn) { ~markFont } { ~font },
224 | start, size
225 | );
226 | });
227 | currentEnvironment
228 | };
229 | if(ddwSnippets.notNil) {
230 | ~keyDownAction = { |view, char, mod, unicode, keycode, key|
231 | if(keycode == ddwSnippets.hotkeyCode and: {
232 | mod bitAnd: ddwSnippets.hotkeyMods == ddwSnippets.hotkeyMods
233 | }) {
234 | ddwSnippets.makeGui(view); // changes focus to GUI window
235 | }
236 | };
237 | };
238 | ~doAutoSave = {
239 | var path = ~autoSavePath +/+ "cll_" ++ Date.getDate.stamp ++ ".scd";
240 | var file;
241 | if(~str.size > 0) {
242 | file = File(path, "w");
243 | if(file.isOpen) {
244 | protect {
245 | file << ~str;
246 | } {
247 | file.close;
248 | }
249 | } {
250 | "Cll editor autosave could not write to %".format(path).warn;
251 | };
252 | };
253 | };
254 | // DO NOT OVERRIDE
255 | ~prKeyUpAction = { |view, char, mod, unicode, keycode, key|
256 | var newString, ascii;
257 | if(char.notNil) {
258 | newString = view.string;
259 | ~updateBookmarks.(~selectionStart, newString.size - ~str.size, newString);
260 | };
261 | if(newString.notNil) {
262 | ~str = newString;
263 | };
264 | ~getSelection.();
265 | ~keyUpAction.(view, char, mod, unicode, keycode, key);
266 | };
267 | ~prKeyDownAction = { |view, char, mod, unicode, keycode, key|
268 | var return = ~keyDownAction.(view, char, mod, unicode, keycode, key);
269 | if(return.isArray) {
270 | if(return.any(_ == true)) { true } { nil }
271 | } { return };
272 | };
273 | ~prMouseUpAction = { |view, x, y, mod|
274 | ~getSelection.();
275 | ~mouseUpAction.(view, x, y, mod);
276 | };
277 | ~getSelection = {
278 | ~selectionStart = ~view.selectionStart;
279 | ~selectionSize = ~view.selectionSize;
280 | [~selectionStart, ~selectionSize]
281 | };
282 | ~updateBookmarks = { |start, delta = 0, string, startOffset(0), stringObj|
283 | var oldMark, newMark;
284 | if(start.notNil and: { delta != 0 }) {
285 | currentEnvironment.changed(\contentChanged, start, delta, string, stringObj);
286 | start = start + startOffset;
287 | block { |break|
288 | ~bookmarks.reverseDo { |mark, i|
289 | if(mark < start) {
290 | break.()
291 | } {
292 | oldMark = ~bookmarks[~bookmarks.size - i - 1];
293 | newMark = mark + delta;
294 | ~bookmarks[~bookmarks.size - i - 1] = newMark;
295 | };
296 | };
297 | };
298 | if(~displayBookmarks) { ~drawBookmarks.() };
299 | };
300 | };
301 | } => PR(\bookmarkTextView);
302 |
303 | PR(\bookmarkTextView).clone {
304 | ~addBookmark = { |start, end|
305 | var i = ~bookmarks.detectIndex { |mark| mark >= start };
306 | case
307 | { i.isNil } { ~bookmarks.add(start).add(end) }
308 | { ~bookmarks[i] != start } {
309 | if(i.even and: { end <= ~bookmarks[i+1] }) {
310 | ~bookmarks.insert(i, end).insert(i, start);
311 | } {
312 | "Range (% .. %) overlaps with another range".format(start, end).warn;
313 | };
314 | } {
315 | "Range (% .. %) overlaps with another range".format(start, end).warn;
316 | };
317 | ~drawBookmarks.();
318 | };
319 | ~removeBookmark = { |start|
320 | var i = ~bookmarks.detectIndex { |mark| mark >= start };
321 | if(i.notNil) {
322 | ~bookmarks.removeAt(i);
323 | ~bookmarks.removeAt(i);
324 | };
325 | ~drawBookmarks.();
326 | };
327 | ~drawBookmarks = {
328 | ~view.background_(~background).stringColor_(~stringColor);
329 | ~visibleBookmarks.pairsDo { |start, end|
330 | ~view.setStringColor(~markColor, start, end - start + 1);
331 | };
332 | currentEnvironment
333 | };
334 | } => PR(\rangeTextView);
335 |
336 |
337 | // SUPPORT OBJECTS
338 |
339 | Proto {
340 | ~string = "";
341 | ~pos = 0;
342 | ~existing = false;
343 | ~doHighlighting = true; // PR(\clGuiString).doHighlighting = false before making the GUI
344 |
345 | ~prep = {
346 | if(~doHighlighting and: { ~objKey.notNil and: { ~phrase.notNil } }) {
347 | NotificationCenter.register(
348 | ~objKey.asSymbol, ~phrase.asSymbol, currentEnvironment,
349 | inEnvir { |highlight(false)|
350 | currentEnvironment.changed(\highlight, highlight)
351 | }
352 | )
353 | };
354 | currentEnvironment
355 | };
356 | ~free = {
357 | if(~objKey.notNil and: { ~phrase.notNil }) {
358 | NotificationCenter.unregister(~objKey.asSymbol, ~phrase.asSymbol, currentEnvironment);
359 | };
360 | currentEnvironment
361 | };
362 |
363 | ~length = { ~string.size };
364 | ~end = { ~pos + ~string.size };
365 |
366 | ~string_ = { |newStr|
367 | var oldSize = ~string.size;
368 | ~string = newStr;
369 | currentEnvironment.changed(\string, newStr, oldSize, currentEnvironment);
370 | currentEnvironment
371 | };
372 | ~silentString_ = { |newStr|
373 | ~string = newStr;
374 | currentEnvironment
375 | };
376 | } => PR(\clGuiString);
377 |
378 | // temporary object to parse the current statement and allow syntax-aware navigation
379 | Proto {
380 | ~bgColors = [QtGUI.palette.color(\base), QtGUI.palette.color(\base).copy.green_(0.25)];
381 | ~statementMap = IdentityDictionary[
382 | \clPatternSet -> { |stream| ClPatternSetNode(stream) }
383 | ];
384 |
385 | ~prep = { |viewWrapper|
386 | var match;
387 | ~wrapper = viewWrapper;
388 | ~view = viewWrapper.view;
389 | ~string = ~view.string;
390 | ~pos = ~view.selectionStart;
391 | match = ~findStatement.();
392 | if(match.notNil) {
393 | ~stmtPos = match[0];
394 | ~endPos = match[1];
395 | ~stmtKey = match[2];
396 | ~statement = ~getStatement.();
397 | if(~statement.notNil) {
398 | ~selectUnit.();
399 | ~view.background = ~bgColors[1];
400 | ~keyFunc = inEnvir { |view, char, mods, unicode, keycode|
401 | ~doKey.(view, char, mods, unicode, keycode);
402 | true // while this is active, stop key event propagation (and no c++ response)
403 | };
404 | ~wrapper.keyDownAction = ~wrapper[\keyDownAction].addFunc(~keyFunc);
405 | currentEnvironment
406 | }
407 | }; // else nil
408 | };
409 | ~free = {
410 | ~wrapper.keyDownAction = ~wrapper[\keyDownAction].removeFunc(~keyFunc);
411 | ~view.background = ~bgColors[0];
412 | currentEnvironment.changed(\didFree);
413 | };
414 | ~findStatement = { |str(~string), pos(~pos)|
415 | var looking = true, cl = PR(\chucklibLiveCode), substr, match, endMatch;
416 | var startPos = pos, endPos = pos;
417 | while { looking and: { startPos >= 0 } } {
418 | if(str[startPos] == $/) {
419 | match = cl.statements.detect { |assn|
420 | str.findRegexpAt(cl.replaceRegexpMacros(assn.value), startPos + 1).notNil
421 | };
422 | if(match.notNil) {
423 | looking = false;
424 | } {
425 | startPos = startPos - 1;
426 | };
427 | } {
428 | startPos = startPos - 1;
429 | }
430 | };
431 | if(match.notNil) {
432 | looking = true;
433 | while { looking and: { endPos < str.size } } {
434 | if(str[endPos] == $/) {
435 | endMatch = cl.statements.detect { |assn|
436 | str.findRegexpAt(cl.replaceRegexpMacros(assn.value), startPos + 1).notNil
437 | };
438 | if(endMatch.notNil) {
439 | looking = false;
440 | } {
441 | endPos = endPos + 1;
442 | };
443 | } {
444 | endPos = endPos + 1;
445 | }
446 | };
447 | if(match.notNil) { [startPos, endPos - 1, match.key] } { nil }
448 | }
449 | };
450 | ~getStatement = { |pos(~stmtPos), endPos(~endPos), key(~stmtKey)|
451 | var stream, statement;
452 | if(~statementMap[key].notNil) {
453 | stream = CollStream(~view.string[ .. endPos]);
454 | stream.pos = pos;
455 | // try {
456 | statement = ~statementMap[key].value(stream);
457 | // } { |error| error.reportError; nil };
458 | }; // else nil
459 | };
460 | ~selectUnit = { |pos(~pos)|
461 | var current, child, keepLooking;
462 | current = ~statement;
463 | keepLooking = true;
464 | while { keepLooking and: { current.children.size > 0 } } {
465 | child = current.children.detect { |ch|
466 | // [~view.selectionStart, ch.begin, ch.end].debug("checking");
467 | pos.inclusivelyBetween(ch.begin, ch.end);
468 | };
469 | if(child.notNil) {
470 | // [~view.selectionStart, child.begin, child.end].debug("found");
471 | current = child;
472 | } {
473 | keepLooking = false;
474 | };
475 | };
476 | // [current.begin, current.selectionSize].debug("selecting");
477 | ~current = current;
478 | ~view.select(current.begin, current.selectionSize);
479 | current.setLastSelected;
480 | };
481 | ~doKey = { |view, char, mods, unicode, keycode|
482 | var next, nextParent, sibs;
483 | if(mods bitAnd: 0x000A0000 == 0x000A0000) {
484 | ~free.();
485 | } {
486 | case
487 | { keycode == 65362 } {
488 | // ~current.listVars;
489 | if(~current.parentNode.notNil) {
490 | ~current = ~current/*.parentSkipOne;*/ .parentNode;
491 | };
492 | // ~current.listVars;
493 | }
494 | { keycode == 65364 } {
495 | if(~current.children.size > 0) {
496 | if(~current.lastSelected.notNil) {
497 | ~current = ~current.children[~current.lastSelected];
498 | } {
499 | ~current = ~current.children.middle;
500 | }
501 | }
502 | }
503 | { keycode == 65363 } {
504 | if(~current.parentNode.notNil) {
505 | next = ~current.nearbySib(1);
506 | if(next.notNil) {
507 | ~current = next;
508 | } {
509 | i = 0; // count levels we had to go up to find a next-sibling
510 | while {
511 | i = i + 1;
512 | nextParent = ~current.parentNode;
513 | if(nextParent.notNil) {
514 | next = nextParent.tryPerform(\nearbySib, 1);
515 | } {
516 | next = nil;
517 | };
518 | nextParent.notNil and: { next.isNil }
519 | } {
520 | ~current = nextParent;
521 | };
522 | if(next.notNil) {
523 | // now, go down no more than that many levels to find a leftmost leaf node
524 | while {
525 | i > 0 and: { next.children.size > 0 }
526 | } {
527 | i = i - 1;
528 | next = next.children.first;
529 | };
530 | if(next.notNil) { ~current = next };
531 | };
532 | };
533 | };
534 | }
535 | { keycode == 65361 } {
536 | if(~current.parentNode.notNil) {
537 | next = ~current.nearbySib(-1);
538 | if(next.notNil) {
539 | ~current = next;
540 | } {
541 | i = 0; // count levels we had to go up to find a previous-sibling
542 | while {
543 | i = i + 1;
544 | nextParent = ~current.parentNode;
545 | if(nextParent.notNil) {
546 | next = nextParent.tryPerform(\nearbySib, -1);
547 | } {
548 | next = nil;
549 | };
550 | nextParent.notNil and: { next.isNil }
551 | } {
552 | ~current = nextParent;
553 | };
554 | if(next.notNil) {
555 | // now, go down no more than that many levels to find a rightmost leaf node
556 | while {
557 | i > 0 and: { next.children.size > 0 }
558 | } {
559 | i = i - 1;
560 | next = next.children.last;
561 | };
562 | if(next.notNil) { ~current = next };
563 | };
564 | };
565 | };
566 | }
567 | { char.isDecDigit } {
568 | sibs = ~current.siblings;
569 | if(sibs.notNil) {
570 | i = (char.digit - 1).wrap(0, 9);
571 | if(sibs[i].notNil) { ~current = sibs[i] };
572 | };
573 | }
574 | { "!@#$%^&*()".includes(char) } {
575 | if(~current.children.notNil) {
576 | i = "!@#$%^&*()".indexOf(char);
577 | if(~current.children[i].notNil) {
578 | ~current = ~current.children[i];
579 | };
580 | };
581 | };
582 | if(~current.notNil) {
583 | // [~current.begin, ~current.selectionSize].debug("selecting");
584 | view.select(~current.begin, ~current.selectionSize);
585 | ~current.setLastSelected;
586 | } {
587 | "OOPS! Current is nil".warn;
588 | };
589 | };
590 | };
591 | } => PR(\clGuiStringNav);
592 |
593 | Proto {
594 | ~name = "";
595 | ~defaultString = "\n\n//--- scratch\n";
596 | ~prep = {
597 | ~strings = List.new;
598 | currentEnvironment
599 | };
600 | ~free = {
601 | ~strings.do(_.free);
602 | currentEnvironment.changed(\modelWasFreed);
603 | };
604 |
605 | ~addString = { |str, objKey, phrase|
606 | var newStr, i;
607 | if(str.last == $\n) {
608 | str = str.drop(-1);
609 | };
610 | newStr = PR(\clGuiString).copy
611 | .objKey_(objKey).phrase_(phrase)
612 | .prep
613 | .string_(str);
614 | i = ~strings.detectIndex { |item| item.string > str };
615 | if(i.isNil) {
616 | newStr.pos = ~strings.sum(_.length) + ~strings.size;
617 | ~strings.add(newStr);
618 | } {
619 | newStr.pos = ~strings[i].pos;
620 | ~strings.insert(i, newStr);
621 | };
622 | currentEnvironment.changed(\addString, newStr);
623 | };
624 | ~removeStrings = { |match|
625 | var c = ~strings.size - 1;
626 | ~strings.reverseDo { |str, i|
627 | if(str.string.contains(match)) {
628 | str.free;
629 | ~strings.removeAt(c - i);
630 | currentEnvironment.changed(\removeString, str);
631 | };
632 | };
633 | currentEnvironment
634 | };
635 |
636 | ~stringForPhrase = { |match|
637 | ~strings.detect { |item| item.string.contains(match) };
638 | };
639 |
640 | ~sortStrings = {
641 | ~strings.sort { |a, b| a.pos < b.pos };
642 | currentEnvironment.changed(\reset, ~strings);
643 | };
644 |
645 | ~contentChanged = { |start, delta, string, stringObj|
646 | block { |break|
647 | ~strings.reverseDo { |str, i|
648 | if(str !== stringObj) {
649 | if(str.pos < start) {
650 | if(str.end >= start) {
651 | // silentString_ b/c the change is coming from the GUI
652 | str.silentString_(string[str.pos .. str.end - 1 + delta]);
653 | };
654 | break.();
655 | } {
656 | if(str.existing) {
657 | str.pos = str.pos + delta;
658 | } {
659 | str.existing = true
660 | };
661 | };
662 | };
663 | };
664 | };
665 | currentEnvironment
666 | };
667 | ~getTextProperties = getTextProperties;
668 | } => PR(\clGuiSection);
669 |
670 | Proto {
671 | ~textViewKey = \rangeTextView;
672 | ~minHeight = 250;
673 | ~prep = { |model, layout, insertIndex|
674 | var textview;
675 | ~model = model;
676 | // model.getTextProperties.keysValuesDo { |key, value|
677 | // key.envirPut(value);
678 | // };
679 | ~view = PR(~textViewKey).copy
680 | .putAll(model.getTextProperties)
681 | .str_(model.defaultString ? "")
682 | .prep(nil, nil);
683 | ~view.view.minHeight_(~minHeight);
684 | if(insertIndex.notNil) {
685 | layout.insert(~view.view, insertIndex);
686 | } {
687 | layout.add(~view.view);
688 | };
689 | model.addDependant(currentEnvironment);
690 | ~view.addDependant(currentEnvironment);
691 | ~view.keyUpAction = inEnvir { |view, char, mods, unicode, keycode|
692 | if(char.notNil and: { char.ascii == 27 }) {
693 | ~model.changed(\escKey);
694 | };
695 | };
696 | ~view.keyDownAction = ~view[\keyDownAction].addFunc(inEnvir { |view, char, mods, unicode, keycode|
697 | case
698 | { mods == 524288 and: { #[65361, 65363].includes(keycode) } } {
699 | PR(\clNavJumpBrackets).jumpInTextView(view, keycode - 65362);
700 | true
701 | }
702 | { mods bitAnd: 0x000A0000 == 0x000A0000 } {
703 | if(~madSelector.isNil) {
704 | ~madSelector = PR(\clGuiStringNav).copy.prep(~view);
705 | ~madSelectorUpd = SimpleController(~madSelector)
706 | .put(\didFree, inEnvir {
707 | ~madSelectorUpd.remove;
708 | ~madSelectorUpd = nil;
709 | ~madSelector = nil;
710 | });
711 | true
712 | };
713 | };
714 | });
715 | ~view.view.receiveDragHandler_(inEnvir({ |view|
716 | view.setString(View.currentDrag.asString, view.selectionStart, 0);
717 | // potentially nice idea but what a mess
718 | ~dragAction.();
719 | }, ~view))
720 | .canReceiveDragHandler_({ View.currentDrag.isString });
721 | currentEnvironment
722 | };
723 | ~free = {
724 | ~view.removeDependant(currentEnvironment);
725 | ~view.remove;
726 | ~model.strings.do(_.removeDependant(currentEnvironment));
727 | ~model.removeDependant(currentEnvironment);
728 | currentEnvironment
729 | };
730 | ~update = { |obj, what ... args|
731 | // [obj, what, args].debug(">> clGuiSectionView:update");
732 | switch(what)
733 | { \contentChanged } {
734 | ~model.contentChanged(*args);
735 | }
736 | { \highlight } {
737 | ~view.highlight(args[0], obj.pos, obj.length);
738 | }
739 | { \string } { // obj is the \clGuiString
740 | ~view.setString(args[0], obj.pos, args[1], obj);
741 | }
742 | { \addString } {
743 | args = args[0];
744 | ~view.setString(args.string ++ $\n, args.pos, 0);
745 | args.addDependant(currentEnvironment); // receive changes from this \clGuiString
746 | }
747 | { \removeString } {
748 | args = args[0];
749 | ~view.setString("", args.pos, args.length + 1);
750 | args.removeDependant(currentEnvironment);
751 | }
752 | { \reset } {
753 | "\clGuiSectionView:reset, implement later".warn;
754 | }
755 | { \focus } {
756 | ~view.focus(args[0] ? true)
757 | }
758 | { \focused } {
759 | // I have to change the message name,
760 | // or ~model.changed recurs infinitely back to here
761 | ~model.changed(\focusedSection, args[0], ~model)
762 | }
763 | { \modelWasFreed } {
764 | ~free.();
765 | };
766 | // [obj, what, args].debug("<< clGuiSectionView:update");
767 | };
768 | // ~textProperties = { PR(~textViewKey).textProperties };
769 | // ~getTextProperties = getTextProperties;
770 | } => PR(\clGuiSectionView);
771 |
772 | Proto {
773 | ~prep = { |layout, insertIndex|
774 | ~view = ScrollView();
775 | if(insertIndex.notNil) {
776 | layout.insert(~view, insertIndex);
777 | } {
778 | layout.add(~view);
779 | };
780 | ~layout = VLayout();
781 | ~view.canvas_(View().background_(~scrollBackground).layout_(~layout));
782 | // ~view.background_(~scrollBackground);
783 | ~view.palette_(QPalette.new.base_(~scrollBackground));
784 | ~sections = List.new;
785 | currentEnvironment
786 | };
787 | ~free = {
788 | ~sections.do { |pair| pair[0].removeDependant(currentEnvironment).free };
789 | };
790 | ~sectionForPhraseIndex = { |phrIndex|
791 | var pair = ~sections.detect { |pair| pair[0].name == phrIndex };
792 | if(pair.notNil) { pair[0] } { nil };
793 | };
794 | ~labelForPhraseIndex = { |phrIndex|
795 | var i = ~sections.detectIndex { |pair| pair[0].name == phrIndex };
796 | if(i.notNil) {
797 | ~view.children[i * 2 + 1]
798 | };
799 | };
800 | ~newSection = { |name|
801 | var i = ~sections.detectIndex { |pair| pair[0].name > name },
802 | newSect = PR(\clGuiSection).copy
803 | .name_(name.asString)
804 | .putAll(~getTextProperties.())
805 | .prep,
806 | label = StaticText().string_(name).stringColor_(~stringColor),
807 | newGui = PR(\clGuiSectionView).copy.prep(newSect, ~layout, if(i.notNil) { i * 2 });
808 |
809 | if(i.notNil) {
810 | ~layout.insert(label, i * 2);
811 | ~sections.insert(i, [newSect, newGui]);
812 | } {
813 | ~layout.insert(label, ~sections.size * 2);
814 | ~sections.add([newSect, newGui]);
815 | };
816 | newSect.addDependant(currentEnvironment);
817 | newSect
818 | };
819 | ~textProperties = { ~textProperties ?? { PR(\clEditGui).textProperties } };
820 | ~getTextProperties = getTextProperties;
821 | ~updateForwards = IdentitySet.with(\escKey, \focusedSection);
822 | ~update = { |obj, what ... args|
823 | if(~updateForwards.includes(what)) {
824 | currentEnvironment.changed(what, *args);
825 | };
826 | };
827 | } => PR(\clGuiPage);
828 |
829 | Proto {
830 | ~maxWidth = 120;
831 | ~prep = { |model|
832 | ~oldPage = model.currentPage;
833 | ~oldVisible = model.pageVisibleOrigin(~oldPage);
834 | ~view = TextField().maxWidth_(~maxWidth)
835 | .keyUpAction_(inEnvir { |view, char|
836 | if(char.notNil) {
837 | case
838 | { char.isPrint } {
839 | currentEnvironment.changed(\setVisible, view.string);
840 | }
841 | { char.ascii == 27 } {
842 | currentEnvironment.changed(\resetVisible, ~oldPage, ~oldVisible);
843 | ~remove.();
844 | };
845 | };
846 | })
847 | .action_(inEnvir { |view|
848 | currentEnvironment.changed(\setVisible, view.string, \done);
849 | ~remove.();
850 | });
851 | model.menuLayout.add(~view);
852 | currentEnvironment
853 | };
854 | ~remove = {
855 | ~view.remove;
856 | currentEnvironment.changed(\popUpRemoved);
857 | };
858 | ~focus = { |bool|
859 | ~view.focus(bool);
860 | currentEnvironment
861 | };
862 | } => PR(\clGuiPopUpSelector);
863 |
864 | // MAIN GUI
865 |
866 | Proto {
867 | ~bounds = Window.screenBounds;
868 | ~windowTitle = "Pattern editor";
869 | ~backColor = Color.gray(0.078);
870 | ~windowBackground = Color.gray(0.15);
871 | ~scrollBackground = Color.gray(0.3);
872 | // ~stringColor = Color.white;
873 | // ~textFont = Font("Inconsolata", 24);
874 | ~updateDefer = 0.1;
875 | ~defaultPhrIndex = " - Unnumbered - ";
876 |
877 | // textview properties, to pass down
878 | ~background = Color.gray(0.15);
879 | ~stringColor = Color.white;
880 | ~markColor = Color(0.5, 1, 0.5); // Color(0.8, 0, 0);
881 | ~font = Font("Inconsolata",
882 | if(Window.screenBounds.height > 1000) { 28 } { 18 }
883 | );
884 | ~markFont = ~font.boldVariant;
885 | ~textProperties = #[background, scrollBackground, stringColor, markColor, font, markFont];
886 | ~getTextProperties = getTextProperties;
887 |
888 | ~prep = {
889 | ~makeGui.();
890 | ~scanBPs.();
891 | ~notifier = NotificationCenter.register(\clLiveCode, \phraseString, ~collIndex, inEnvir {
892 | |objKey, phrase, parm, string|
893 | if(parm != \dur and: { string.size > 0 }) {
894 | (inEnvir { ~updatePatString.(objKey, phrase, parm, string) }).defer;
895 | };
896 | });
897 | };
898 | ~freeCleanup = {
899 | ~notifier.remove;
900 | if(~selectField.notNil) { ~selectField.removeDependant(currentEnvironment) };
901 | ~pages.do { |page| page.removeDependant(currentEnvironment).free };
902 | ~win.onClose_(nil).close;
903 | "freed".debug;
904 | };
905 |
906 | ~makeGui = {
907 | ~win = Window(~windowTitle, ~bounds.value).background_(~windowBackground);
908 | ~pages = Array.new;
909 | ~pageMenu = PopUpMenu()
910 | .background_(~backColor).stringColor_(~stringColor)
911 | .action_(inEnvir { |view| ~setPage.(view.value) });
912 | ~win.layout = VLayout(
913 | ~menuLayout = HLayout(~pageMenu),
914 | ~stackLayout = StackLayout()
915 | );
916 | ~newPage.(\main);
917 | ~pages[0].sections[0][0].dependants;
918 | ~pages[0].sections[0][0].changed(\focus, true);
919 | ~setPage.(0);
920 | ~win.onClose_(inEnvir {
921 | ~win.onClose = nil;
922 | BP(~collIndex).free;
923 | })
924 | // when GUI window is front, /bars.(...) should NOT affect current document
925 | .toFrontAction_({
926 | Library.put(\clLivecode, \setupbars, \guiIsFront, true);
927 | })
928 | .endFrontAction_({
929 | Library.put(\clLivecode, \setupbars, \guiIsFront, false);
930 | });
931 | ~win.front;
932 | };
933 |
934 | ~newPage = { |key|
935 | var i = block { |break|
936 | ~pages.do { |page, i|
937 | var existingKey = page.name;
938 | case
939 | { key == existingKey } {
940 | Error("BP(%): Page at % already exists".format(
941 | ~collIndex.asCompileString,
942 | key.asCompileString
943 | )).throw;
944 | }
945 | { key < existingKey } {
946 | break.(i)
947 | };
948 | };
949 | nil
950 | };
951 | var origItem;
952 | var newPage = PR(\clGuiPage).copy
953 | .name_(key)
954 | .putAll(~getTextProperties.())
955 | .prep(~stackLayout, i);
956 | if(i.isNil) {
957 | i = ~pages.size;
958 | ~pages = ~pages.add(newPage);
959 | ~pageMenu.items = ~pageMenu.items.add(key.asString);
960 | } {
961 | origItem = ~pageMenu.items[~pageMenu.value];
962 | ~pages = ~pages.insert(i, newPage);
963 | ~pageMenu.items = ~pageMenu.items.insert(i, key.asString);
964 | ~currentPage = ~pageMenu.items.indexOfEqual(origItem);
965 | ~pageMenu.value = ~currentPage;
966 | };
967 | newPage.addDependant(currentEnvironment);
968 | newPage.newSection(~defaultPhrIndex);
969 | i
970 | };
971 |
972 | ~updatePatString = { |objKey, phrase, parm, string|
973 | var phrKey, phrIndex, pageI, page, fullPhrID, section, str, newStr;
974 | if(~isDefaultParm.(objKey, parm)) { parm = nil };
975 | #phrKey, phrIndex = ~phraseAndIndex.(phrase);
976 | pageI = ~pageIndexForPhrase.(phrKey);
977 | if(pageI.isNil) {
978 | pageI = ~newPage.(phrKey.asSymbol);
979 | };
980 | page = ~pages[pageI];
981 | if(phrIndex.isNil) { phrIndex = ~defaultPhrIndex };
982 | section = page.sectionForPhraseIndex(phrIndex);
983 | if(section.isNil) {
984 | section = page.newSection(phrIndex);
985 | };
986 | fullPhrID = "/%.%%".format(
987 | objKey,
988 | phrase,
989 | if(parm.notNil) { "." ++ parm } { "" }
990 | );
991 | str = section.stringForPhrase(fullPhrID);
992 | newStr = "% = %;".format(fullPhrID, string);
993 | if(str.isNil) {
994 | str = section.addString(newStr, objKey, phrase);
995 | } {
996 | str.string = newStr;
997 | };
998 | currentEnvironment
999 | };
1000 |
1001 | ~phraseAndIndex = { |phrase|
1002 | var j, phrIndex;
1003 | phrase = phrase.asString;
1004 | j = phrase.size - 1;
1005 | while { j >= 0 and: { phrase[j].isDecDigit } } {
1006 | j = j - 1;
1007 | };
1008 | if(j < 0) {
1009 | Error("BP(%): Phrase key % has no letters".format(
1010 | ~collIndex.asCompileString, phrase.asCompileString
1011 | )).throw;
1012 | };
1013 | if(j < (phrase.size - 1) and: { phrase[j+1].isDecDigit }) {
1014 | phrIndex = phrase[j+1 ..];
1015 | phrase = phrase[.. j];
1016 | };
1017 | [phrase, phrIndex]
1018 | };
1019 |
1020 | ~pageIndexForPhrase = { |phrase|
1021 | phrase = phrase.asString;
1022 | block { |break|
1023 | ~pages.do { |page, i|
1024 | if(page.name.asString == phrase) {
1025 | break.(i);
1026 | };
1027 | };
1028 | nil
1029 | };
1030 | };
1031 |
1032 | ~setPage = { |index|
1033 | ~currentPage = index;
1034 | ~pageMenu.value = index;
1035 | ~stackLayout.index = index;
1036 | };
1037 | ~pageVisibleOrigin = { |index|
1038 | ~pages[index].view.visibleOrigin;
1039 | };
1040 |
1041 | ~isDefaultParm = { |objKey, parm|
1042 | BP.exists(objKey) and: { parm == BP(objKey).defaultParm }
1043 | };
1044 |
1045 | ~compareStringAt = { |sourceStr, findStr, i|
1046 | block { |break|
1047 | findStr.do { |ch, j|
1048 | if(ch < sourceStr[i+j]) { break.(-1) } {
1049 | if(ch > sourceStr[i+j]) { break.(1) }
1050 | };
1051 | };
1052 | 0
1053 | };
1054 | };
1055 |
1056 | // typing navigation
1057 | ~popUpSelector = {
1058 | if(~selectField.isNil) {
1059 | ~selectField = PR(\clGuiPopUpSelector).copy.prep(currentEnvironment);
1060 | ~selectField.addDependant(currentEnvironment);
1061 | ~selectField.focus(true);
1062 | } {
1063 | ~selectField.focus(true);
1064 | };
1065 | };
1066 |
1067 | ~findVisible = { |str|
1068 | var regex, phrKey, phrIndex, pageIndex, label;
1069 | if(str.notNil) {
1070 | regex = str.findRegexp("([0-9]+)$");
1071 | if(regex.notEmpty) {
1072 | phrIndex = regex[1][1];
1073 | if(regex[1][0] > 0) {
1074 | phrKey = str[ .. regex[1][0] - 1];
1075 | } {
1076 | pageIndex = ~currentPage;
1077 | }
1078 | } {
1079 | phrKey = str;
1080 | };
1081 | [pageIndex, phrKey, phrIndex];
1082 | if(pageIndex.isNil) {
1083 | pageIndex = block { |break|
1084 | ~pages.do { |page, i|
1085 | if(page.name.asString.beginsWith(phrKey)) { break.(i) };
1086 | };
1087 | nil
1088 | };
1089 | };
1090 | if(pageIndex.notNil) {
1091 | ~setPage.(pageIndex);
1092 | ~label = ~pages[pageIndex].labelForPhraseIndex(phrIndex ?? { ~defaultPhrIndex });
1093 | if(~label.notNil) {
1094 | ~pages[pageIndex].view.visibleOrigin = Point(0, ~label.bounds.top);
1095 | };
1096 | };
1097 | };
1098 | [phrKey, phrIndex]
1099 | };
1100 |
1101 | ~update = { |obj, what ... args|
1102 | var phrKey, phrIndex;
1103 | switch(what)
1104 | { \escKey } {
1105 | ~popUpSelector.();
1106 | }
1107 | { \setVisible } {
1108 | #phrKey, phrIndex = ~findVisible.(args[0]);
1109 | if(args[1] == \done /*and: { phrIndex.notNil }*/) {
1110 | ~pages[~currentPage].sectionForPhraseIndex(phrIndex ?? { ~defaultPhrIndex })
1111 | .changed(\focus, true);
1112 | };
1113 | }
1114 | { \resetVisible } {
1115 | ~setPage.(args[0]);
1116 | ~pages[args[0]].view.visibleOrigin = args[1];
1117 | if(~currentSection.notNil) {
1118 | ~currentSection.changed(\focus, true)
1119 | };
1120 | }
1121 | { \popUpRemoved } {
1122 | ~selectField.removeDependant(currentEnvironment);
1123 | ~selectField = nil;
1124 | }
1125 | { \focusedSection } {
1126 | if(args[0]) {
1127 | ~currentSection = args[1];
1128 | };
1129 | };
1130 | };
1131 |
1132 | ~syncPages = { |keys|
1133 | if(keys.isNil) {
1134 | keys = Set.new;
1135 | BP.all.do { |bp|
1136 | // cl-livecode processes must implement this to work in the GUI
1137 | if(bp.v.respondsTo(\phrases)) {
1138 | bp.phrases.keysDo { |phr|
1139 | keys.add(~phraseAndIndex.(phr)[0]); // strip index
1140 | };
1141 | };
1142 | };
1143 | };
1144 | keys.do { |phr|
1145 | phr = phr.asString;
1146 | BP.all.do { |bp|
1147 | if(bp.v.respondsTo(\phrases)) {
1148 | bp.phrases.keysDo { |key|
1149 | if(key.asString.beginsWith(phr)) {
1150 | tryNoTrace {
1151 | bp.phraseStrings.at(key).tryPerform(\keysValuesDo, { |parm, string|
1152 | ~updatePatString.(bp.collIndex, key, parm, string)
1153 | });
1154 | }
1155 | };
1156 | };
1157 | };
1158 | };
1159 | };
1160 | };
1161 | } => PR(\clEditGui);
1162 |
--------------------------------------------------------------------------------
/helper-funcs.scd:
--------------------------------------------------------------------------------
1 |
2 | Library.put(\clLivecode, \setupbars, \addToDoc, Main.versionAtLeast(3, 7) and: { Platform.ideName == "scqt" });
3 | // common helper funcs
4 | // when GUI window is front, /bars.(...) should NOT affect current document
5 | Library.put(\clLivecode, \setupbars, \guiIsFront, false);
6 |
7 | /**
8 | Chucklib-livecode: A framework for live-coding improvisation of electronic music
9 | Copyright (C) 2018 Henry James Harkins
10 |
11 | This program is free software: you can redistribute it and/or modify
12 | it under the terms of the GNU General Public License as published by
13 | the Free Software Foundation, either version 3 of the License, or
14 | (at your option) any later version.
15 |
16 | This program is distributed in the hope that it will be useful,
17 | but WITHOUT ANY WARRANTY; without even the implied warranty of
18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 | GNU General Public License for more details.
20 |
21 | You should have received a copy of the GNU General Public License
22 | along with this program. If not, see .
23 | **/
24 |
25 |
26 | {
27 | // Library.at(\clLivecode, \setupbars, \addToDoc) ?? { false }
28 | // and: {
29 | // not(Library.at(\clLivecode, \setupbars, \guiIsFront) ?? { false })
30 | // }
31 | true
32 | } => Func(\clBarFuncMayAddToDoc);
33 |
34 | { |key, n(4), prefix("m"), meters|
35 | var phrases = [[prefix], (0 .. n-1)].flop.collect(_.join),
36 | strings, stepForward, doc, pos, method;
37 | meters = meters.asArray;
38 | strings = phrases.collect { |phrase, i|
39 | "/%.% = %\"\";".format(key, phrase, meters.wrapAt(i) ?? { "" })
40 | };
41 | phrases.do { |phrase, i|
42 | \chucklibLiveCode.eval(strings[i]).interpret;
43 | };
44 | if(\clBarFuncMayAddToDoc.eval) {
45 | stepForward = PR(\clPatternToDoc)[\stepForward];
46 | if(stepForward.isNil) {
47 | stepForward = PR(\clPatternToDoc).checkStepForward;
48 | };
49 |
50 | doc = PR(\clPatternToDoc).currentDoc;
51 | pos = doc.selectionStart + doc.selectionSize;
52 | method = if(doc.isKindOfByName('Document')) { 'string_' } { 'setString' };
53 | if(stepForward.not and: { doc.isKindOfByName('Document') }) { pos = pos + 1 };
54 | doc.perform(method,
55 | "\n" ++ strings.join("\n"),
56 | pos, 0
57 | );
58 | } {
59 | "Added " ++ phrases
60 | }
61 | } => Func(\setupbars);
62 |
63 | { |key, n(4), prefix("m"), phrases|
64 | var argPhrases = phrases, str;
65 | phrases ?? {
66 | phrases = [[prefix], (0 .. n-1)].flop.collect(_.join);
67 | };
68 | str = "/% = (%)".format(
69 | key,
70 | // '^prefix' supports easy variations:
71 | // /proc.m00 = ...
72 | // /proc.m01 = ... and so on
73 | phrases.collect { |phr| "'^%'".format(phr) }.join($.),
74 | );
75 | if(argPhrases.isNil) { str = "%(%)".format(str, n.neg) }; // set BP's quant
76 | \chucklibLiveCode.eval(str).interpret;
77 | } => Func(\setm);
78 |
79 | { |key, n(4), prefix("m")|
80 | \setm.eval(key, n, prefix);
81 | \setupbars.eval(key, n, prefix);
82 | } => Func(\bars);
83 |
84 | // setMeterAtBeat is unnecessarily hard to use
85 | // if last 2 are omitted, it will wait until the next barline to switch the meter
86 | { |beatsPerBar, clock(TempoClock.default), barBeat(clock.nextBar), newBaseBarBeat(barBeat)|
87 | clock.schedAbs(barBeat, {
88 | clock.setMeterAtBeat(beatsPerBar, newBaseBarBeat);
89 | nil
90 | });
91 | } => Func(\changeMeter);
92 |
93 | { |tempo, clock(TempoClock.default), barBeat(clock.nextBar)|
94 | clock.schedAbs(barBeat, {
95 | if(clock.isKindOfByName('BeaconClock')) {
96 | clock.setGlobalTempo(tempo, barBeat);
97 | } {
98 | clock.tempo = tempo;
99 | };
100 | nil
101 | });
102 | } => Func(\changeTempo);
103 |
104 | { |mode = \default|
105 | if(Mode.exists(mode)) {
106 | "changeKey: Mode(%) => Mode('default')\n".postf(mode.asCompileString);
107 | Mode(mode) => Mode(\default);
108 | } {
109 | "changeKey: Mode(%) does not exist".format(mode.asCompileString).warn;
110 | };
111 | } => Func(\changeKey);
112 |
113 | // assign-voicer functions
114 | { |vcKey, bpKey|
115 | if(bpKey.notNil) {
116 | if(BP.exists(bpKey)) {
117 | VC(vcKey) => BP(bpKey);
118 | } {
119 | "VC(%) should go into BP(%), but it doesn't exist. Remember to => manually"
120 | .format(
121 | vcKey.asCompileString,
122 | bpKey.asCompileString
123 | )
124 | .warn;
125 | };
126 | };
127 | } => Func(\vcToDefaultBP);
128 |
129 | // apply a voicer to the default event prototype
130 | { |ev|
131 | var mixer;
132 | if(ev[\voicer].notNil) {
133 | ev.put(\instrument, ev[\voicer].nodes[0].defname);
134 | mixer = ev[\voicer].bus.asMixer;
135 | if(mixer.notNil) {
136 | ev.put(\group, mixer.synthgroup)
137 | .put(\out, mixer.inbus.index);
138 | };
139 | ev[\voicer].nodes[0].initArgs.pairsDo { |key, value|
140 | // difficult. If the event has a default, the event's default takes precedence.
141 | if(ev[key].isNil) { ev.put(key, value) };
142 | };
143 | ev[\voicer].globalControls.keysValuesDo { |key, value|
144 | ev.put(key, value.asMap);
145 | };
146 | };
147 | ev
148 | } => Func(\voicer);
149 |
150 | // intended for use in \preset
151 | { |bp|
152 | if(BP.exists(bp)) {
153 | BP(bp).event[\voicer]
154 | } {
155 | Error("BP(%) doesn't exist, so, can't find voicer".format(bp.asCompileString)).throw;
156 | }
157 | } => Func(\bpAsVoicer);
158 |
159 | { |bp|
160 | var voicer = \bpAsVoicer.eval(bp);
161 | var vc;
162 | if(voicer.isNil) {
163 | Error("\\preset: BP(%) doesn't have a voicer".format(bp.asCompileString)).throw;
164 | };
165 | vc = VC.collection.detect { |vc| vc.value === voicer };
166 | if(vc.isNil) {
167 | Error("\\preset: BP(%)'s voicer can't be found in VC".format(bp.asCompileString)).throw;
168 | };
169 | vc
170 | } => Func(\bpAsVC);
171 |
172 | { |preset, bp|
173 | var vc, presetDef, proc;
174 | var gcs;
175 | var watcher;
176 | var applyPreset = {
177 | watcher.remove;
178 | "applying preset pairs".debug;
179 | gcs = vc.v.globalControls;
180 | // pairsDo supports both SeqColl and Dictionary
181 | presetDef.pairsDo { |key, value|
182 | switch(key)
183 | { \makeParms } { nil } // no-op
184 | { \postAction } {
185 | vc.use { value.value(vc) }
186 | }
187 | {
188 | // presets should be able to override globalcontrol defaults
189 | // but not with patterns
190 | if(gcs[key].notNil and: { value.isNumber }) {
191 | gcs[key].value = value
192 | } {
193 | proc.set(key, value);
194 | };
195 | };
196 | };
197 | };
198 | vc = \bpAsVC.eval(bp);
199 | presetDef = vc.env[\presets].tryPerform(\at, preset) ?? {
200 | // vc.env[\collIndex] is the Factory's collIndex
201 | Library.at(\cl, \presets, vc.env[\collIndex], preset)
202 | };
203 | if(presetDef.isNil) {
204 | Error("\\preset: VC(%) does not define preset %".format(
205 | vc.collIndex.asCompileString,
206 | preset.asCompileString
207 | )).throw;
208 | };
209 | proc = BP(bp); // already validated by \bpAsVC
210 | if(vc.env[\vcIsReady] == true) {
211 | applyPreset.value
212 | } {
213 | watcher = SimpleController(vc)
214 | .put(\vcReady, applyPreset);
215 | };
216 | proc
217 | } => Func(\preset);
218 |
219 | { |vcOrBP, array|
220 | var v;
221 | var out;
222 | var names;
223 | var printComma = false;
224 | var key2;
225 | var isControl = { |key|
226 | names.includes(key.asSymbol) or: {
227 | v.globalControls[key].notNil
228 | }
229 | };
230 |
231 | if(VC.exists(vcOrBP)) {
232 | v = VC(vcOrBP).v
233 | } {
234 | v = \bpAsVoicer.eval(vcOrBP);
235 | };
236 |
237 | if(v.notNil) {
238 | names = v.controlNames;
239 |
240 | out = Array(array.size);
241 | array.pairsDo { |key, value|
242 | if(isControl.(key) or: {
243 | key2 = key.asString;
244 | if(key2.endsWith("Plug")) {
245 | isControl.(key2.drop(-4).asSymbol)
246 | } { false }
247 | }) {
248 | out.add(key).add(value);
249 | if(printComma) { ", ".post } { printComma = true };
250 | "%: %".postf(key, value.asCompileString);
251 | };
252 | };
253 | "\n".post;
254 | out
255 | } {
256 | array
257 | }
258 | } => Func(\prunePreset);
259 |
260 | { |parent, bounds(Rect(800, 200, 500, 400))|
261 | var view = TreeView(parent, bounds);
262 | var watcher;
263 | var editor, closeFunc;
264 |
265 | view.columns_(["ID", "preset"]).setColumnWidth(0, 140);
266 | if(parent.isNil) { view.front };
267 |
268 | // populate
269 | Fact.keys.as(Array).sort.do { |key|
270 | var fact = Fact(key);
271 | var presets, factoryItem;
272 | if(fact.isVoicer) {
273 | presets = fact.presets;
274 | if(presets.size > 0) {
275 | factoryItem = view.addChild([fact.collIndex.asString]);
276 | presets.keys.as(Array).sort.do { |presetKey|
277 | var str = String.streamContentsLimit({ |stream|
278 | fact.presetAt(presetKey).printOn(stream)
279 | }, 50);
280 | factoryItem.addChild([presetKey.asString, str]);
281 | };
282 | };
283 | };
284 | };
285 |
286 | // this is absolutely not modularized in a good way
287 | watcher = SimpleController(Fact)
288 | .put(\addPreset, { |obj, what, factKey, presetKey|
289 | // queue these? or assume the clock will do it
290 | defer {
291 | var fIndex, pIndex, fItem, pItem;
292 | var fKeyStr = factKey.asString;
293 | var pKeyStr;
294 | var presetStr = String.streamContentsLimit({ |stream|
295 | Fact(factKey).presetAt(presetKey).printOn(stream)
296 | }, 50);
297 |
298 | fIndex = block { |break|
299 | view.numItems.do { |i|
300 | if(view.itemAt(i).strings[0] == fKeyStr) {
301 | break.(i);
302 | }
303 | };
304 | nil
305 | };
306 | if(fIndex.notNil) {
307 | pKeyStr = presetKey.asString;
308 | fItem = view.itemAt(fIndex);
309 | // super-oldskool while loop
310 | // because there's no 'numChildren' method for TreeViewItem
311 | pIndex = 0;
312 | while {
313 | fItem.childAt(pIndex).notNil and: {
314 | fItem.childAt(pIndex).strings[0] != pKeyStr
315 | }
316 | } {
317 | pIndex = pIndex + 1;
318 | };
319 | pItem = fItem.childAt(pIndex);
320 | if(pItem.notNil) {
321 | pItem.strings = [pKeyStr, presetStr]
322 | } {
323 | fItem.addChild([pKeyStr, presetStr]);
324 | }
325 | } {
326 | fItem = view.addChild([fKeyStr]);
327 | fItem.addChild([pKeyStr, presetStr]);
328 | };
329 | }
330 | });
331 |
332 | view.onClose = { watcher.remove };
333 | view.beginDragAction = { |view|
334 | var item = view.currentItem;
335 | var str, parent;
336 | if(item.notNil) {
337 | str = item.strings;
338 | // child item or parent item?
339 | if(str.size >= 2) {
340 | parent = item.parent;
341 | "/make(%:name(preset:%%)/".format(
342 | parent.strings[0],
343 | $\\,
344 | item.strings[0]
345 | );
346 | } { nil }
347 | } { nil }
348 | };
349 |
350 | // do not commit this! it assumes a specific GUI
351 | // also there's no way to do this for IDE documents
352 | editor = topEnvironment[\editor];
353 | if(editor.notNil) {
354 | closeFunc = {
355 | defer { view.close };
356 | editor.view[\dragAction].removeFunc(closeFunc);
357 | };
358 | editor.view[\dragAction] = editor.view[\dragAction].addFunc(closeFunc);
359 | };
360 |
361 | view
362 | } => Func(\presetBrowser);
363 |
364 | // record several mixers with sample-accurate onset
365 | // for simplicity/livecode use, create a folder in recordingsDir
366 | { |... mixers|
367 | var path, result, servers;
368 | {
369 | path = Platform.recordingsDir +/+ "tracks_" ++ Date.getDate.stamp;
370 | result = File.mkdir(path);
371 | if(result) {
372 | servers = mixers.collect(_.server).as(Set);
373 | if(servers.size > 1) {
374 | "Recording on multiple servers; sample-accurate sync is not guaranteed".warn;
375 | };
376 | mixers.do { |mixer|
377 | mixer.prepareRecord(path +/+ mixer.name ++ ".aiff", "AIFF");
378 | };
379 | servers.do(_.sync);
380 | servers.do { |server|
381 | server.makeBundle(0.2, {
382 | mixers.do { |mixer|
383 | if(mixer.server == server) {
384 | mixer.startRecord;
385 | };
386 | };
387 | });
388 | };
389 | } {
390 | "Directory '%' is not empty; not recording".format(path).warn;
391 | };
392 | }.fork(SystemClock);
393 | } => Func(\syncRecord);
394 |
395 | {
396 | MixerChannel.servers.do { |mixers|
397 | mixers.do { |mixer|
398 | if(mixer.isRecording) { mixer.stopRecord };
399 | };
400 | };
401 | } => Func(\stopRecord);
402 |
403 |
404 | // finally publish my GUI functions
405 | { |controller(\nanoTouch), midiDevice("The default search string should fail")| // or \mix16Touch
406 | var sBounds = Window.screenBounds, touchExtent, w, lay, touchParent, v, left,
407 | midiIndex,
408 | path = Platform.userAppSupportDir +/+ "cll-cheatsheet.txt",
409 | file, str;
410 |
411 | if(File.exists(path)) {
412 | file = File(path, "r");
413 | if(file.isOpen) {
414 | protect {
415 | str = file.readAllString;
416 | } { file.close }
417 | } {
418 | "can't open cheatsheet.txt".warn;
419 | };
420 | };
421 |
422 | if(MBM.exists(0).not) {
423 | MIDIClient.init;
424 | midiIndex = MIDIClient.sources.detectIndex { |endpt|
425 | endpt.device.containsi(midiDevice)
426 | };
427 | MIDIPort.init(midiIndex.asArray);
428 | MIDIBufManager(nil, 0) => MBM.prNew(0);
429 | };
430 |
431 | BP(#[touchGui, touch]).free;
432 | PR(controller).chuck(BP(\touch), nil, (pingDebug: false));
433 |
434 | touchExtent = PR(\abstractTouchGUI).calcExtent(BP(\touch).v);
435 | w = Window("control panel",
436 | Rect(sBounds.width - touchExtent.x, 0, touchExtent.x, sBounds.height)
437 | );
438 | lay = VLayout(
439 | touchParent = View().fixedSize_(touchExtent + Point(-4, 4)),
440 | StaticText().fixedHeight_(3), // spacer
441 | PR(\controlList) => BP(\clist),
442 | ).margins_(2).spacing_(4);
443 | if(str.notNil) {
444 | lay.add(StaticText().fixedHeight_(3)); // spacer
445 | v = TextView().string_(str)
446 | // for some stupid reason, I have to set the text before colors
447 | .background_(Color.black).stringColor_(Color.white);
448 | lay.add(v);
449 | lay.add(Button(/*w, Rect(2, 2, w.view.bounds.width - 4, 20)*/)
450 | .states_([["save"]])
451 | .action_({
452 | var file = File(path, "w");
453 | protect {
454 | if(file.isOpen) {
455 | file.putString(v.string);
456 | } {
457 | "can't open cheatsheet.txt for writing".warn;
458 | };
459 | } { file.close };
460 | })
461 | );
462 | };
463 |
464 | w.layout = lay;
465 |
466 | PR(\abstractTouchGUI).chuck(BP(\touchGui), nil, (
467 | model: BP(\touch).v,
468 | parentView: touchParent
469 | ));
470 | PR(\chuckOSC) => BP(\chuckOSC);
471 | t = BP(\chuckOSC).v; // must chuck into the proto, not the BP
472 | w.front;
473 |
474 | ~cleanup = {
475 | var dontFree = #[chuckOSC, touchGui, touch, clist];
476 | VC.all.free;
477 | BP.all.do { |bp|
478 | if(dontFree.includes(bp.collIndex).not) {
479 | bp.free;
480 | };
481 | };
482 | };
483 |
484 | NotificationCenter.notify(\clInterface, \ready);
485 | t
486 | } => Func(\makeController);
487 |
488 | {
489 | var sBounds = Window.screenBounds, touchExtent = PR(\abstractTouchGUI).calcExtent(BP(\touch).v),
490 | left;
491 |
492 | PR(\clGuiString).doHighlighting = false; // like setting a classvar
493 | left = max(180, 0.5 * (sBounds.width - 800));
494 | ~editWindow = Window("code editor",
495 | // Rect.aboutPoint(sBounds.center, min(400, sBounds.width * 0.5), sBounds.height * 0.5)
496 | // -8? Apparently border is extra
497 | Rect(left, 0, sBounds.width - touchExtent.x - left - 8, sBounds.height)
498 | );
499 | ~editWindow.layout = VLayout(
500 | HLayout(
501 | nil,
502 | Button().fixedSize_(Size(80, 20))
503 | .states_([
504 | ["autosave"],
505 | ["autosave", Color.white, Color.green(0.45)]
506 | ])
507 | .action_(inEnvir { |view|
508 | ~editor.view.autoSave = view.value > 0;
509 | }),
510 | Button().fixedSize_(Size(80, 20))
511 | .states_([["load"]])
512 | .action_(inEnvir {
513 | ~editor.view.doAutoSave; // if window is not currently empty
514 | FileDialog(inEnvir { |path|
515 | var file, str;
516 | file = File(path, "r");
517 | if(file.isOpen) {
518 | protect {
519 | str = file.readAllString;
520 | } {
521 | file.close;
522 | };
523 | ~editor.view.setString(str, 0, ~editor.view.str.size);
524 | } {
525 | "Error opening '%'".format(path).warn;
526 | };
527 | }, fileMode: 1, acceptMode: 0, stripResult: true,
528 | path: ~editor.view.autoSavePath
529 | );
530 | })
531 | )
532 | );
533 | // I don't really need a model, but the GUI needs it
534 | ~editModel = PR(\clGuiSection).copy
535 | .defaultString_("")
536 | .putAll(PR(\clEditGui).v.getTextProperties)
537 | .prep;
538 | ~editor = PR(\clGuiSectionView).copy.prep(~editModel, ~editWindow.layout, nil);
539 | ~editor.view.autoSave = false;
540 | // experimental hack: watch for focus -- must do this before .front!
541 | ~focusWatcher = SimpleController(~editor.view)
542 | // this should be OK for multiple views
543 | // because the sequence is always a/ lose old focus, then b/ gain new focus
544 | .put(\focused, { |view, what, bool|
545 | if(bool) {
546 | PR(\clPatternToDoc).activeView = view.view;
547 | } {
548 | PR(\clPatternToDoc).activeView = nil; // clear to revert to Document
549 | };
550 | })
551 | .put(\didFree, inEnvir { ~focusWatcher.remove });
552 | ~editWindow.onClose_(inEnvir { ~editModel.free }).front;
553 | Library.put(\clLivecode, \setupbars, \addToDoc, false);
554 | ~editor
555 | } => Func(\makeCodeWindow);
556 |
557 | { |controller(\nanoTouch), midiDevice|
558 | \makeController.eval(controller, midiDevice);
559 | \makeCodeWindow.eval;
560 | } => Func(\cllGui);
561 |
562 | { |vcKey, index = 0, exclude(#[]), overwrite = true|
563 | var t = thisProcess.interpreter.t;
564 | var i = index;
565 | if(VC.exists(vcKey)) {
566 | block { |break|
567 | VC(vcKey).globalControlsByCreation.do { |gc|
568 | // also check i < size
569 | if(i >= t.faderKeys.size) {
570 | "VC(%).globalControls[%]: Ran out of GUI slots"
571 | .format(vcKey.asCompileString, gc.name.asCompileString)
572 | .warn;
573 | break.(nil)
574 | };
575 | if(overwrite or: { t.maps[t.faderKeys[i]].isNil }) {
576 | if(exclude.includes(gc.name).not) {
577 | gc.chuck(t, i);
578 | i = i + 1;
579 | };
580 | };
581 | };
582 | };
583 | } {
584 | "gcChuck: VC(%) doesn't exist".format(vcKey.asCompileString).warn;
585 | };
586 | } => Func(\gcChuck);
587 |
588 |
589 | { |str, i|
590 | var match;
591 | var alphaNum = { |chr|
592 | chr.respondsTo(\isAlphaNum) and: { chr.isAlphaNum }
593 | };
594 | // allow cursor to trail the identifier
595 | if(alphaNum.(str[i]).not) {
596 | i = i - 1
597 | };
598 | while { i > 0 and: { alphaNum.(str[i]) } } {
599 | i = i - 1;
600 | };
601 | // now we should be one char before the symbol
602 | i = i + 1;
603 | match = str.findRegexpAt("([A-Za-z0-9]+)", i);
604 | [i, match]
605 | } => Func(\strGetIdentifier);
606 |
607 |
608 | // quicky make modulator synthdefs for Plug
609 | { |name, func|
610 | SynthDef(name, { |out|
611 | var sig = SynthDef.wrap(func);
612 | Out.perform(UGen.methodSelectorForRate(sig.rate), out, sig);
613 | }).add;
614 | } => Func(\plugDef);
615 |
--------------------------------------------------------------------------------
/manual/manual-supporting/item-spans.odg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamshark70/ddwChucklib-livecode/fb9153c1cd478800fde396dddcdb735667b47941/manual/manual-supporting/item-spans.odg
--------------------------------------------------------------------------------
/manual/manual-supporting/item-spans.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamshark70/ddwChucklib-livecode/fb9153c1cd478800fde396dddcdb735667b47941/manual/manual-supporting/item-spans.pdf
--------------------------------------------------------------------------------
/manual/manual-supporting/rhythmic-notation-crop.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamshark70/ddwChucklib-livecode/fb9153c1cd478800fde396dddcdb735667b47941/manual/manual-supporting/rhythmic-notation-crop.pdf
--------------------------------------------------------------------------------
/manual/manual-supporting/rhythmic-notation.ly:
--------------------------------------------------------------------------------
1 | \version "2.18.2"
2 | \language "english"
3 |
4 | global = { \numericTimeSignature \time 4/4 }
5 |
6 | \markup {
7 | \column {
8 | \line {
9 | \column {
10 | \line { 1. \typewriter "\"--\"" }
11 | \hspace #25
12 | }
13 | \column {
14 | \score {
15 | \new RhythmicStaff {
16 | \global b2 b2
17 | }
18 | \layout {}
19 | }
20 | }
21 | }
22 |
23 | \line {
24 | \column {
25 | \line { 2. \typewriter "\"----\"" }
26 | \hspace #25
27 | }
28 | \column {
29 | \score {
30 | \new RhythmicStaff {
31 | \global b4 b b b
32 | }
33 | \layout {}
34 | }
35 | }
36 | }
37 |
38 | \line {
39 | \column {
40 | \line { 3. \typewriter "\"- --\"" }
41 | \hspace #25
42 | }
43 | \column {
44 | \score {
45 | \new RhythmicStaff {
46 | \global b2 b4 b4
47 | }
48 | \layout {}
49 | }
50 | }
51 | }
52 |
53 | \line {
54 | \column {
55 | \line { 4. \typewriter "\"- --- -\"" }
56 | \hspace #25
57 | }
58 | \column {
59 | \score {
60 | \new RhythmicStaff {
61 | \global \tuplet 7/4 { b2 b4 b4 b2 b4 }
62 | }
63 | \layout {}
64 | }
65 | }
66 | }
67 |
68 | \line {
69 | \column {
70 | \line { 5. \typewriter "\"-|-|-|-\"" }
71 | \hspace #25
72 | }
73 | \column {
74 | \score {
75 | \new RhythmicStaff {
76 | \global b4 b b b
77 | }
78 | \layout {}
79 | }
80 | }
81 | }
82 |
83 | \line {
84 | \column {
85 | \line { 6. \typewriter "\"-|--|-|-\"" }
86 | \hspace #25
87 | }
88 | \column {
89 | \score {
90 | \new RhythmicStaff {
91 | \global b4 b8 b b4 b
92 | }
93 | \layout {}
94 | }
95 | }
96 | }
97 |
98 | \line {
99 | \column {
100 | % Note: There is a U+200B "zero-width space"
101 | % between the consecutive spaces in beat 2.
102 | % Otherwise SVG renders incorrectly.
103 | \line { 7. \typewriter "\"--| - |- --| -\"" }
104 | \hspace #25
105 | }
106 | \column {
107 | \score {
108 | \new RhythmicStaff {
109 | \global b8 b ~ b16 b8. b8 b16 b16 ~ b8 b8
110 | }
111 | \layout {}
112 | }
113 | }
114 | }
115 | }
116 | }
117 |
118 |
--------------------------------------------------------------------------------
/manual/manual-supporting/rhythmic-notation.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamshark70/ddwChucklib-livecode/fb9153c1cd478800fde396dddcdb735667b47941/manual/manual-supporting/rhythmic-notation.pdf
--------------------------------------------------------------------------------
/manual/manual-supporting/rhythmic-notation.svg:
--------------------------------------------------------------------------------
1 |
200 |
--------------------------------------------------------------------------------
/mobile-objects.scd:
--------------------------------------------------------------------------------
1 | // mobile control interface objects
2 |
3 | var saveSubtype = AbstractChuckArray.defaultSubType;
4 | var parentEnvir = currentEnvironment;
5 |
6 | /**
7 | Chucklib-livecode: A framework for live-coding improvisation of electronic music
8 | Copyright (C) 2018 Henry James Harkins
9 |
10 | This program is free software: you can redistribute it and/or modify
11 | it under the terms of the GNU General Public License as published by
12 | the Free Software Foundation, either version 3 of the License, or
13 | (at your option) any later version.
14 |
15 | This program is distributed in the hope that it will be useful,
16 | but WITHOUT ANY WARRANTY; without even the implied warranty of
17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 | GNU General Public License for more details.
19 |
20 | You should have received a copy of the GNU General Public License
21 | along with this program. If not, see .
22 | **/
23 |
24 | protect {
25 | AbstractChuckArray.defaultSubType = \mobile;
26 |
27 | Proto {
28 | ~addr = nil; // address to match
29 | ~pingSetsAddr = true; // auto filter on /ping receipt
30 | ~sendPort = 9000;
31 | ~pingDebug = true;
32 | ~keepAlive = true;
33 | ~prep = {
34 | ~respFunc = inEnvir { |msg, time, replyAddr, recvPort|
35 | if(replyAddr.matches(~addr) or: { msg[0] == '/ping' }) {
36 | // if(msg[0] != '/accxyz') { msg.debug("raw") };
37 | ~respond.(msg, time, replyAddr, recvPort)
38 | };
39 | };
40 | thisProcess.addOSCRecvFunc(~respFunc);
41 | ~data = IdentityDictionary.new; // save all incoming data by oscpath
42 | if(~labels.isNil) { ~labels = IdentityDictionary.new };
43 | ~setDataKeys.();
44 | if(~keepAlive) { ~startAliveThread.() };
45 | currentEnvironment
46 | };
47 | ~freeCleanup = {
48 | ~stopAliveThread.();
49 | thisProcess.removeOSCRecvFunc(~respFunc);
50 | NotificationCenter.notify(currentEnvironment, \modelWasFreed);
51 | };
52 | ~startAliveThread = {
53 | ~stopAliveThread.();
54 | ~aliveThread = Routine {
55 | loop {
56 | if(~addr.notNil) {
57 | ~addr.sendMsg("/alive");
58 | };
59 | 10.wait;
60 | };
61 | };
62 | currentEnvironment
63 | };
64 | ~stopAliveThread = { ~aliveThread.stop; };
65 |
66 | ~setLabel = { |oscpath, label|
67 | var str, num;
68 | if(label.isNil) { label = "" /*oscpath.asString.split($/).last*/ };
69 | ~labels[oscpath] = label;
70 | // open stage control can do this:
71 | str = oscpath.asString;
72 | // a bit of a dodge: buttons have labels, faders don't
73 | // but a globalcontrol =>.x t will assign to a fader;
74 | // we need to send the label message anyway
75 | if(~sendAddr.notNil) {
76 | num = str.detectIndex(_.isDecDigit);
77 | num = str[num..];
78 | ~sendAddr.sendMsg("/label_" ++ num, label)
79 | };
80 | NotificationCenter.notify(currentEnvironment, \any, [[\label, oscpath, label]]);
81 | currentEnvironment
82 | };
83 | // for single-value controls (common case)
84 | ~setValue = { |oscpath, value|
85 | if(~sendAddr.notNil) {
86 | ~sendAddr.sendMsg(*[oscpath, value]);
87 | };
88 | ~respond.([oscpath, value], SystemClock.beats, NetAddr.localAddr, NetAddr.langPort);
89 | };
90 | ~setGUIValue = { |oscpath, value|
91 | if(~sendAddr.notNil) {
92 | ~sendAddr.sendMsg(oscpath, value);
93 | };
94 | ~respond.([oscpath, value], SystemClock.beats, NetAddr.localAddr, NetAddr.langPort, true);
95 | };
96 | // for multiple-value controls (rare case)
97 | ~setValues = { |oscpath ... values|
98 | if(~sendAddr.notNil) {
99 | ~sendAddr.sendMsg(oscpath, *values);
100 | };
101 | ~respond.([oscpath] ++ values, SystemClock.beats, NetAddr.localAddr, NetAddr.langPort);
102 | };
103 | ~setGUIValues = { |oscpath ... values|
104 | if(~sendAddr.notNil) {
105 | ~sendAddr.sendMsg(oscpath, *values);
106 | };
107 | ~respond.([oscpath] ++ values, SystemClock.beats, NetAddr.localAddr, NetAddr.langPort, true);
108 | };
109 |
110 | ~respond = { |msg, time, replyAddr, recvPort, guiOnly(false)|
111 | var args = [msg, time, replyAddr, recvPort];
112 | if(~saveKeys.includes(msg[0])) {
113 | if(msg.size == 2) {
114 | ~data[msg[0]] = msg[1]
115 | } {
116 | ~data[msg[0]] = msg[1..];
117 | };
118 | };
119 | NotificationCenter.notify(currentEnvironment, \any, args);
120 | if(guiOnly.not) {
121 | NotificationCenter.notify(currentEnvironment, msg[0], args);
122 | };
123 | switch(msg[0])
124 | { '/ping' } {
125 | if(~pingDebug or: { ~addr != replyAddr }) {
126 | "/ping: set mobile IP%\n".postf(
127 | if(~pingDebug) {
128 | " = " ++ replyAddr
129 | } { "" }
130 | )
131 | };
132 | ~addr = replyAddr;
133 | ~sendAddr = NetAddr(~addr.ip, ~sendPort);
134 | parentEnvir[\pingStatus] = true;
135 | if(parentEnvir[\pingCond].notNil) {
136 | parentEnvir[\pingCond].signalAll;
137 | };
138 | }
139 | { '/openstage_connect' } {
140 | (inEnvir {
141 | NotificationCenter.registrationsFor(currentEnvironment)
142 | .keysValuesDo { |path, dict|
143 | dict.keysValuesDo { |obj, func|
144 | if(obj.isKindOf(Proto)) {
145 | obj.resendState;
146 | };
147 | };
148 | };
149 | NotificationCenter.notify(currentEnvironment, \openstage_connect);
150 | }).defer(0.5);
151 | };
152 | };
153 |
154 | ~setDataKeys = {
155 | var new = IdentitySet.new;
156 | new.addAll(~dataKeys);
157 | ~tabSpecs.pairsDo { |name, viewspecs|
158 | viewspecs.pairsDo { |oscpath, viewspec|
159 | new.add(oscpath)
160 | }
161 | };
162 | ~saveKeys = new;
163 | };
164 | ~viewChanged = { |path, value|
165 | if(value.size > 0) {
166 | ~setValues.(path, *value)
167 | } {
168 | ~setValue.(path, value)
169 | };
170 | currentEnvironment
171 | };
172 |
173 | ~tabMessage = { |i| [("/" ++ (i+1)).asSymbol] };
174 | ~tabOffset = 0;
175 | } => PR(\abstractTouch);
176 |
177 | // this has GUI stuff in it -- maybe revisit
178 | PR(\abstractTouch).clone {
179 | var lightness;
180 |
181 | ~presetOSCCmd = '/1/push5';
182 |
183 | ~white = Color.white;
184 | ~black = Color.black;
185 | ~tabBackground = Color.gray(0.3);
186 | ~sliderBG = Color.new255(38, 38, 38);
187 |
188 | ~yellow = Color(0.7, 0.7, 0.2); // Color.yellow(0.6);
189 | ~ltYellow = Color.yellow(0.6, 0.6, 0.2); // ~yellow.blend(~black, 0.7);
190 | ~yellowFor2D = Color(0.4, 0.4, 0.2);
191 |
192 | ~aqua = Color(0, 0.4, 1.0);
193 | ~ltAqua = ~aqua.blend(~black, 0.7);
194 |
195 | ~purple = Color(1.0, 0.2, 1.0);
196 | ~ltPurple = ~purple.blend(~black, 0.7);
197 |
198 | ~green = Color(0.3, 1.0, 0.3);
199 | ~ltGreen = ~green.blend(~black, 0.7);
200 |
201 | ~red = Color(0.8, 0, 0);
202 | ~ltRed = ~red.blend(~white, 0.7);
203 |
204 | ~buttonExtent = Point(25, 25);
205 | ~labelHeight = 16;
206 | ~labelFont = Font.default.copy.pixelSize_(11);
207 | ~sliderWidth = 200;
208 | ~xyExtent = Point(140, 145);
209 | ~gap = Point(5, 5);
210 |
211 | // this is defined in wslib but I don't want a dependency on it
212 | lightness = [~tabBackground.red, ~tabBackground.green, ~tabBackground.blue].mean;
213 | ~labelBG = Color.grey(1.0 - lightness.round, 0.06);
214 |
215 | ~sliders = { |n = 1, prefix = "/1", oneBound(Rect(0, 0, 400, 50)), buttonExtent(Point(50, 50)), gap = 10, bcolor, bltColor, scolor, sltColor, sBkColor, startI = 0, nameStartI = 1|
216 | var out = Array(n * 5),
217 | togStr = prefix ++ "/toggle",
218 | slStr = prefix ++ "/fader",
219 | labelStr = prefix ++ "/letter", // no OSC message for this
220 | origin = Point(gap, gap + oneBound.top);
221 | oneBound = oneBound.copy.top_(0);
222 | n.do { |i|
223 | out
224 | .add((labelStr ++ (i+nameStartI)).asSymbol)
225 | .add((
226 | bounds: Rect.fromPoints(origin, origin + buttonExtent - Point(gap, 0)),
227 | class: StaticText,
228 | init: { |view|
229 | view.string_((i + startI).asDigit.asString);
230 | }
231 | ))
232 | .add((togStr ++ (i+nameStartI)).asSymbol)
233 | .add((
234 | bounds: Rect.fromPoints(
235 | origin + Point(buttonExtent.x - gap, 0),
236 | origin + Point(buttonExtent.x * 2 - gap, buttonExtent.y)
237 | ),
238 | class: Button,
239 | init: { |view|
240 | // init func runs in the touchGui environment
241 | view.states_([[" ", nil, bltColor], ["", nil, bcolor]])
242 | .receiveDragHandler_(inEnvir { |view|
243 | // we don't have access to the 't' touch object
244 | // notification assumes there's just one (untested with multiple)
245 | NotificationCenter.notify(~model, \receiveDrag, [View.currentDrag, i+startI]);
246 | })
247 | },
248 | ))
249 | .add((slStr ++ (i+nameStartI)).asSymbol)
250 | .add((
251 | bounds: Rect.fromPoints(
252 | origin + Point(buttonExtent.x * 2, 0),
253 | origin + oneBound.extent
254 | ),
255 | class: Slider,
256 | // I considered implementing receive-drag on the slider too but it didn't work
257 | init: { |view| view.knobColor_(scolor).background_(sBkColor) },
258 | spec: [0, 1]
259 | ));
260 | origin.y = origin.y + oneBound.height + gap;
261 | };
262 | out
263 | };
264 |
265 | ~tabSpecs = [
266 | "Tab1", {
267 | var out = ~sliders.(1, "/1", oneBound: Rect(0, 0, ~sliderWidth, ~buttonExtent.y), buttonExtent: ~buttonExtent, gap: ~gap.x, bcolor: ~yellow, bltColor: ~ltYellow, scolor: ~yellow, sltColor: ~ltYellow, sBkColor: ~sliderBG, startI: 16),
268 | last = out.last;
269 | out = out ++ ~sliders.(2, "/1", oneBound: Rect(0, last.bounds.bottom, ~sliderWidth, ~buttonExtent.y), buttonExtent: ~buttonExtent, gap: ~gap.x, bcolor: ~aqua, bltColor: ~ltAqua, scolor: ~aqua, sltColor: ~ltAqua, sBkColor: ~sliderBG, nameStartI: 2, startI: 17);
270 | last = out.last;
271 | out = out.grow(out.size + (7*2));
272 | 5.do { |i|
273 | out.add(("/1/push" ++ (i+1)).asSymbol)
274 | .add((
275 | bounds: Rect(~gap.x, last.bounds.bottom + ~gap.y, ~buttonExtent.x, ~buttonExtent.y),
276 | class: Button,
277 | init: inEnvir { |view|
278 | view.states = [[" ", nil, ~ltPurple], ["", nil, ~purple]];
279 | },
280 | ));
281 | last = out.last;
282 | };
283 | // last = out.last;
284 | out.add('/1/fader4').add((
285 | bounds: Rect(last.bounds.right + ~gap.x, out[17].bounds.bottom + ~gap.y, ~buttonExtent.x, ~xyExtent.y),
286 | class: Slider,
287 | init: inEnvir { |view| view.knobColor_(~purple) },
288 | spec: [1, 0]
289 | ));
290 | last = out.last;
291 | out.add('/1/xy').add((
292 | bounds: Rect(last.bounds.right + ~gap.x, out[17].bounds.bottom + ~gap.y, ~xyExtent.x, ~xyExtent.y),
293 | class: Slider2D,
294 | init: inEnvir { |view| view.background_(~yellowFor2D).knobColor_(~yellow) },
295 | updater: { |view, x, y| view.setXY(x, 1.0 - y) }
296 | ));
297 | out
298 | }.value,
299 | "Tab2", ~sliders.(
300 | 8, "/2", oneBound: Rect(0, 0, ~sliderWidth, ~buttonExtent.y),
301 | buttonExtent: ~buttonExtent, gap: ~gap.y,
302 | bcolor: ~aqua, bltColor: ~ltAqua,
303 | scolor: ~green, sltColor: ~ltGreen, sBkColor: ~sliderBG,
304 | startI: 0
305 | ),
306 | "Tab3", ~sliders.(
307 | 8, "/3", oneBound: Rect(0, 0, ~sliderWidth, ~buttonExtent.y),
308 | buttonExtent: ~buttonExtent, gap: ~gap.y,
309 | bcolor: ~aqua, bltColor: ~ltAqua,
310 | scolor: ~red, sltColor: ~ltRed, sBkColor: ~sliderBG,
311 | startI: 8
312 | ),
313 | ];
314 | ~chuckOSCKeys = {
315 | var allKeys = /*BP(~oscKey).*/ ~saveKeys.collect(_.asString),
316 | keys, faderKeys, toggleKeys;
317 | keys = allKeys.select { |key| "23".includes(key[1]) }.as(Array).sort; // looking for /2... or /3...
318 | keys = keys ++ sort(allKeys.select({ |key|
319 | key[1] == $1 and: {
320 | "ft".includes(key[3]) and: { key.last != $4 }
321 | }
322 | }).as(Array));
323 | faderKeys = keys.select { |key| key[3] == $f }.collect(_.asSymbol);
324 | toggleKeys = keys.select { |key| key[3] == $t }.collect(_.asSymbol);
325 | (keys: keys, faderKeys: faderKeys, toggleKeys: toggleKeys)
326 | };
327 | } => PR(\mix16Touch);
328 |
329 | PR(\mix16Touch).clone {
330 | ~buttonExtent = Point(15, 19);
331 | ~labelHeight = 12;
332 | ~labelFont = Font.default.copy.pixelSize_(9);
333 | ~sliderWidth = 100;
334 | ~xyExtent = Point(140, 145);
335 | ~gap = Point(3, 3);
336 |
337 | ~presetOSCCmd = '/presetButton';
338 |
339 | ~sliders = { |n = 1, prefix = "/1", oneBound(Rect(0, 0, 400, 50)), buttonExtent(Point(50, 50)), gap = 10, bcolor, bltColor, scolor, sltColor, sBkColor, startI = 0, nameStartI = 1|
340 | var out = Array(n * 6),
341 | togStr = prefix ++ "/button_",
342 | slStr = prefix ++ "/fader_",
343 | labelStr = prefix ++ "/label_",
344 | origin = Point(gap + oneBound.left, gap + oneBound.top);
345 | oneBound = oneBound.copy.top_(0);
346 | n.do { |i|
347 | var num = (i + nameStartI).asString;
348 | if(num.size < 2) { num = "0" ++ num }; // special case, only 2
349 | out
350 | .add((labelStr ++ num).asSymbol)
351 | .add((
352 | bounds: Rect.fromPoints(origin, origin + buttonExtent - Point(gap, 0)),
353 | class: StaticText,
354 | init: { |view|
355 | view.string_(((i + startI % 24)).asDigit.asString);
356 | }
357 | ))
358 | .add((togStr ++ num).asSymbol)
359 | .add((
360 | bounds: Rect.fromPoints(
361 | origin + Point(buttonExtent.x - gap, 0),
362 | origin + Point(buttonExtent.x * 2 - gap, buttonExtent.y)
363 | ),
364 | class: Button,
365 | init: { |view|
366 | // init func runs in the touchGui environment
367 | view.states_([[" ", nil, bltColor], ["", nil, bcolor]])
368 | .receiveDragHandler_(inEnvir { |view|
369 | // we don't have access to the 't' touch object
370 | // notification assumes there's just one (untested with multiple)
371 | NotificationCenter.notify(~model, \receiveDrag, [View.currentDrag, i+startI]);
372 | })
373 | },
374 | ))
375 | .add((slStr ++ num).asSymbol)
376 | .add((
377 | bounds: Rect.fromPoints(
378 | origin + Point(buttonExtent.x * 2, 0),
379 | origin + oneBound.extent
380 | ),
381 | class: Slider,
382 | // I considered implementing receive-drag on the slider too but it didn't work
383 | init: { |view| view.knobColor_(scolor).background_(sBkColor) },
384 | spec: [0, 1]
385 | ));
386 | origin.y = origin.y + oneBound.height + gap;
387 | };
388 | out
389 | };
390 |
391 | ~tabSpecs = [
392 | "Tab1", ~sliders.(
393 | 12, "", oneBound: Rect(0, 0, ~sliderWidth, ~buttonExtent.y),
394 | buttonExtent: ~buttonExtent, gap: ~gap.y,
395 | bcolor: ~aqua, bltColor: ~ltAqua,
396 | scolor: ~green, sltColor: ~ltGreen, sBkColor: ~sliderBG,
397 | startI: 0
398 | ) ++ ~sliders.(
399 | 12, "", oneBound: Rect(106, 0, ~sliderWidth, ~buttonExtent.y),
400 | buttonExtent: ~buttonExtent, gap: ~gap.y,
401 | bcolor: ~aqua, bltColor: ~ltAqua,
402 | scolor: ~green, sltColor: ~ltGreen, sBkColor: ~sliderBG,
403 | startI: 12, nameStartI: 13
404 | ),
405 | "Tab2", ~sliders.(
406 | 12, "", oneBound: Rect(0, 0, ~sliderWidth, ~buttonExtent.y),
407 | buttonExtent: ~buttonExtent, gap: ~gap.y,
408 | bcolor: ~aqua, bltColor: ~ltAqua,
409 | scolor: ~green, sltColor: ~ltGreen, sBkColor: ~sliderBG,
410 | startI: 24, nameStartI: 25,
411 | ) ++ ~sliders.(
412 | 12, "", oneBound: Rect(106, 0, ~sliderWidth, ~buttonExtent.y),
413 | buttonExtent: ~buttonExtent, gap: ~gap.y,
414 | bcolor: ~aqua, bltColor: ~ltAqua,
415 | scolor: ~green, sltColor: ~ltGreen, sBkColor: ~sliderBG,
416 | startI: 36, nameStartI: 37
417 | ),
418 | "Tab3", ~sliders.(
419 | 3, "", oneBound: Rect(0, 0, ~sliderWidth, ~buttonExtent.y),
420 | buttonExtent: ~buttonExtent, gap: ~gap.y,
421 | bcolor: ~aqua, bltColor: ~ltAqua,
422 | scolor: ~green, sltColor: ~ltGreen, sBkColor: ~sliderBG,
423 | startI: 48, nameStartI: 49
424 | )
425 | ];
426 | ~chuckOSCKeys = {
427 | var allKeys = /*BP(~oscKey).*/ ~saveKeys.collect(_.asString),
428 | keys, faderKeys, toggleKeys;
429 | keys = allKeys.select { |key| "fb".includes(key[1]) }.as(Array).sort;
430 | // keys = keys ++ sort(allKeys.select({ |key|
431 | // key[1] == $1 and: {
432 | // "ft".includes(key[3]) and: { key.last != $4 }
433 | // }
434 | // }).as(Array));
435 | faderKeys = keys.select { |key| key[1] == $f }.collect(_.asSymbol);
436 | toggleKeys = keys.select { |key| key[1] == $b }.collect(_.asSymbol);
437 | (keys: keys, faderKeys: faderKeys, toggleKeys: toggleKeys)
438 | };
439 |
440 | ~tabOffset = { |activeTab| activeTab * 24 };
441 | ~tabMessage = { |i| ['/panel_1', i.asFloat] };
442 | } => PR(\openStageTouch);
443 |
444 | Proto {
445 | ~windowName = "TouchOSC";
446 | ~minExtent = Point(150, 0);
447 | ~font = { ~model[\labelFont] ?? { Font.default.copy.size_(14) } };
448 |
449 | ~prep = { |model/*, parentView*/|
450 | if(model.notNil) { ~model = model };
451 | // ~parentView = parentView;
452 | ~font = ~font.value;
453 | ~notification = NotificationCenter.register(~model, \any, currentEnvironment, inEnvir { |msg|
454 | ~respond.(~model, msg); // args[0] == msg
455 | });
456 | ~freeNotify = NotificationCenter.register(~model, \modelWasFreed, currentEnvironment, inEnvir {
457 | ~free.();
458 | });
459 | ~tabSpecs = ~model.tabSpecs.deepCopy;
460 | ~makeWindow.();
461 | ~makeTabs.();
462 | ~window.front;
463 |
464 | currentEnvironment;
465 | };
466 | ~free = {
467 | ~parentView.remove;
468 | if(~iMadeWindow and: { ~window.notNil and: { ~window.isClosed.not } }) {
469 | ~window.close;
470 | };
471 | ~window = nil;
472 | ~notification.remove;
473 | ~freeNotify.remove;
474 | };
475 | ~freeCleanup = { ~free.() };
476 |
477 | ~makeWindow = {
478 | var temp;
479 | ~maxExtent = ~calcExtent.();
480 | if(~parentView.isNil) {
481 | ~window = Window(~windowName, ~windowBoundsFromExtent.(~maxExtent));
482 | ~parentView = ~window.view;
483 | ~iMadeWindow = true;
484 | } {
485 | temp = ~parentView;
486 | while { temp.parent.notNil } { temp = temp.parent };
487 | // now temp should be a TopView
488 | ~window = temp.findWindow;
489 | ~iMadeWindow = false;
490 | };
491 | ~parentView.onClose = inEnvir {
492 | if(~window.notNil) { ~window.onClose = nil };
493 | ~free.();
494 | };
495 | };
496 | ~calcExtent = { |overrideModel|
497 | var maxPt = ~minExtent, specs;
498 | if(overrideModel.notNil) {
499 | specs = overrideModel.tabSpecs;
500 | } {
501 | specs = ~tabSpecs ?? { ~model.tabSpecs };
502 | };
503 | specs.pairsDo { |name, viewSpecs|
504 | viewSpecs.pairsDo { |oscpath, spec|
505 | maxPt = max(maxPt, spec.bounds.rightBottom);
506 | }
507 | };
508 | maxPt + 20
509 | };
510 | ~windowBoundsFromExtent = { |extent|
511 | var sb = Window.screenBounds;
512 | // was: Rect.aboutPoint(Window.screenBounds.center, extent.x / 2, extent.y / 2)
513 | Rect(sb.right - extent.x, sb.center.y - (extent.y / 2), extent.x, extent.y)
514 | };
515 | ~makeTabs = {
516 | ~tabs = TabbedView(~parentView, ~parentView.bounds.insetBy(2, 2), ~tabSpecs[0, 2 ..])
517 | .backgrounds_([~model.tabBackground]);
518 | ~tabSwitch = Array.fill(~tabs.views.size, { |i| ~model.tabMessage(i) });
519 | ~views = IdentityDictionary.new;
520 | ~labels = IdentityDictionary.new;
521 | ~tabSpecs.pairsDo { |name, viewSpecs, i|
522 | ~fillTab.(i div: 2, viewSpecs);
523 | };
524 |
525 | // I found it's too easy to drag-focus tab 1
526 | // but maybe useful to drag-select other tabs
527 | ~tabs.tabViews[0].canReceiveDragHandler = nil;
528 |
529 | // if you switch tab onscreen, it should switch on the phone too
530 | // where to get the address? no time now
531 | ~tabFocusActive = true;
532 | ~tabs.focusActions = Array.fill(~tabs.views.size, { |i|
533 | inEnvir {
534 | if(~tabFocusActive and: { ~model.addr.notNil }) {
535 | ~model.sendAddr.sendMsg(*(~tabSwitch[i]));
536 | };
537 | }
538 | });
539 | };
540 |
541 | ~fillTab = { |index, specs|
542 | var parent = ~tabs.views[index], view;
543 | specs.pairsDo { |oscpath, spec|
544 | view = spec.copy;
545 | view[\view] = view[\class].new(parent, view[\bounds]);
546 | view[\init].value(view.view);
547 | view[\spec] = view[\spec].asSpec;
548 | if(view[\class] == Slider2D) {
549 | view.view.action = inEnvir { |vw|
550 | // this is not exactly right
551 | ~model.tryPerform(\viewChanged, oscpath, view[\spec].map([vw.x, vw.y]));
552 | };
553 | } {
554 | view.view.action = inEnvir { |vw|
555 | ~model.tryPerform(\viewChanged, oscpath, view[\spec].map(vw.value));
556 | };
557 | };
558 | ~views[oscpath] = view;
559 | ~viewHook.(oscpath, view, parent); // currently, adds superimposed label view
560 | };
561 | };
562 |
563 | ~viewHook = { |oscpath, view, parent|
564 | var label;
565 | if(~model.labels[oscpath].notNil) {
566 | label = ~model.labels[oscpath]
567 | } {
568 | label = "" // oscpath.asString.split($/).last;
569 | };
570 | ~labels[oscpath] = StaticText(parent, view.bounds.setExtent(view.bounds.width, ~model.labelHeight))
571 | .background_(~model.tryPerform(\labelBG) ?? { Color.clear })
572 | .align_(\center)
573 | .font_(~font)
574 | .string_(label);
575 | };
576 |
577 | ~respond = { |obj, msg| // what, args
578 | var what = msg[0], view;
579 |
580 | // reserved: switch tabs
581 | case { (view = ~tabSwitch.indexOfEqual(msg)).notNil } {
582 | (inEnvir {
583 | ~tabFocusActive = false; // suppress focusAction
584 | ~tabs.focus(view);
585 | ~tabFocusActive = true;
586 | }).defer;
587 | }
588 | { msg[0] == \label } {
589 | inEnvir { ~labels[msg[1]].string = msg[2] }.defer;
590 | }
591 | // { what == \modelWasFreed } { ~free.() }
592 | // default: locate and update the view onscreen
593 | {
594 | view = ~views[what];
595 | if(view.notNil) {
596 | if(view[\updater].notNil) {
597 | { view[\updater].value(view.view, *msg[1..]) }.defer;
598 | } {
599 | { view.view.value = view.spec.unmap(msg[1]) }.defer;
600 | };
601 | };
602 | }
603 | };
604 |
605 | ~tabOffset = { ~model.tabOffset(~tabs.activeTab) };
606 | } => PR(\abstractTouchGUI);
607 |
608 | Proto {
609 | ~windowSize = 15;
610 | ~sendWait = 0.08;
611 | ~sourceBP = \touch;
612 | ~prep = {
613 | // why this? take advantage of address filtering in the BP
614 | ~resp = NotificationCenter.register(BP(~sourceBP).v, '/accxyz', currentEnvironment, inEnvir { |msg|
615 | ~smooth.(msg);
616 | });
617 | // ~resp = OSCFunc(inEnvir { |msg| ~smooth.(msg) }, '/accxyz');
618 | ~movingBuf = Array.fill(~windowSize, #[0, 0, 0]);
619 | ~sum = [0, 0, 0];
620 | ~avg = [0, 0, 0];
621 | ~index = 0;
622 | ~lastSendTime = SystemClock.beats;
623 | };
624 | ~freeCleanup = {
625 | ~resp.remove;
626 | // ~resp.free;
627 | };
628 | ~smooth = { |msg|
629 | var oldest = ~movingBuf.wrapAt(~index + 1);
630 | ~sum.do { |sum, i|
631 | ~sum[i] = sum - oldest[i] + msg[i+1];
632 | };
633 | ~avg = ~sum / ~windowSize;
634 | ~index = (~index + 1) % ~windowSize;
635 | ~movingBuf[~index] = msg[1..];
636 | if((SystemClock.beats - ~lastSendTime) >= ~sendWait) {
637 | NotificationCenter.notify(currentEnvironment, '/accxyz', [~avg]);
638 | ~lastSendTime = SystemClock.beats;
639 | };
640 | };
641 | } => PR(\accxyzSmoother);
642 |
643 |
644 |
645 | // musical action responders
646 | Proto {
647 | ~oscInKey = \touch;
648 | ~prep = { |path, specs|
649 | var oscin = BP(~oscInKey).v;
650 | ~path = path.asArray;
651 | ~specs = specs;
652 | ~rdepth = 0;
653 | ~resp = ~path.collect { |path|
654 | NotificationCenter.register(oscin, path, currentEnvironment, inEnvir {
655 | |msg, time, addr, recvPort|
656 | if(~rdepth < 50) {
657 | ~rdepth = ~rdepth + 1;
658 | ~prAction.(msg, time, addr, recvPort);
659 | ~rdepth = ~rdepth - 1;
660 | } {
661 | Error("OSC response: Recursion limit reached").throw;
662 | };
663 | });
664 | };
665 | ~userprep.(~path, specs);
666 | ~setLabels.();
667 | currentEnvironment
668 | };
669 | ~free = { |wasReassigned(false)|
670 | if(wasReassigned.not) {
671 | ~setLabels.([""] /*~path.collect { |p| p.asString.split($/).last }*/);
672 | };
673 | ~userfree.(wasReassigned);
674 | ~resp.do(_.remove);
675 | NotificationCenter.notify(currentEnvironment, \didFree);
676 | };
677 | ~setLabels = { |labels|
678 | var oscin = BP(~oscInKey).v;
679 | if(labels.isNil) { labels = ~specs[\label].asArray };
680 | if(labels.isString) { labels = [labels] };
681 | ~path.do { |p, i| oscin.setLabel(p, labels.wrapAt(i)) };
682 | };
683 | ~resendState = 0; // default no-op
684 | } => PR(\abstrMobileResp);
685 |
686 | PR(\abstrMobileResp).clone {
687 | ~userprep = { |path|
688 | var oscin = BP(~oscInKey).v, playing = 0;
689 | ~udepth = 0;
690 | BP(~specs[\bp]).do { |bp|
691 | bp.addDependant(currentEnvironment);
692 | // [path, bp, bp.isPlaying].debug("play check");
693 | if(bp.isPlaying) { playing = 1 };
694 | };
695 | path.do { |p| oscin.setGUIValue(p, playing) };
696 | ~prepHook.(path);
697 | };
698 | ~userfree = { |wasReassigned|
699 | var oscin = BP(~oscInKey).v;
700 | BP(~specs[\bp]).do { |bp|
701 | bp.removeDependant(currentEnvironment);
702 | };
703 | if(wasReassigned.not) {
704 | ~path.do { |p| oscin.setGUIValue(p, 0) };
705 | };
706 | };
707 | ~prAction = { |msg|
708 | if(msg[1] > 0) {
709 | if(~specs[\once] ? false) {
710 | BP(~specs[\bp]).do { |bp| bp.triggerOneEvent(~specs[\quant]) };
711 | } {
712 | BP(~specs[\bp]).play(~specs[\quant]);
713 | };
714 | } {
715 | BP(~specs[\bp]).stop(~specs[\quant]);
716 | };
717 | };
718 | ~update = { |obj, what|
719 | if(~udepth < 50) {
720 | ~udepth = ~udepth + 1;
721 | case
722 | { what == \free } { ~free.() }
723 | { #[schedFailed, stop, couldNotPrepare, couldNotStream, oneEventPlayed].includes(what) } {
724 | ~path.do { |p|
725 | BP(~oscInKey).setGUIValue(p, 0);
726 | }
727 | }
728 | { what == \play } {
729 | ~path.do { |p|
730 | BP(~oscInKey).setGUIValue(p, 1);
731 | }
732 | };
733 | ~udepth = ~udepth - 1;
734 | } {
735 | Error("OSC response: updater recursion limit reached").throw;
736 | };
737 | };
738 | ~resendState = {
739 | var addr = BP(~oscInKey)[\addr];
740 | if(addr.notNil) {
741 | ~path.do { |p|
742 | addr.sendMsg(p, BP(~specs[\bp]).isPlaying.binaryValue);
743 | };
744 | ~setLabels.([~specs[\bp]]);
745 | };
746 | };
747 | } => PR(\bptrig);
748 |
749 | PR(\abstrMobileResp).clone {
750 | ~userprep = { |path|
751 | ~gc = ~specs[\gc];
752 | if(~gc.isKindOf(GlobalControlBase).not) { ~gc = ~gc.value };
753 | ~cspec = (~specs[\spec] ?? { ~gc.spec }).asSpec;
754 | BP(~oscInKey).setGUIValue(path[0], ~cspec.unmap(~gc.value));
755 | ~gc.addDependant(currentEnvironment);
756 | };
757 | ~userfree = { |wasReassigned|
758 | if(wasReassigned.not) {
759 | ~path.do { |p|
760 | BP(~oscInKey).setGUIValue(p, 0);
761 | };
762 | };
763 | ~gc.removeDependant(currentEnvironment);
764 | };
765 | ~prAction = { |msg|
766 | ~gc.set(~cspec.map(msg[1]));
767 | };
768 | ~update = { |obj, what|
769 | switch(what.tryPerform(\at, \what))
770 | { \value } {
771 | BP(~oscInKey).setGUIValue(~path[0], ~cspec.unmap(~gc.value));
772 | }
773 | { \modelWasFreed } { ~free.() }
774 | };
775 | ~resendState = {
776 | var addr = BP(~oscInKey)[\addr];
777 | var v;
778 | if(addr.notNil) {
779 | v = ~getValue.().asArray;
780 | ~path.do { |p, i|
781 | addr.sendMsg(p, v.wrapAt(i));
782 | };
783 | ~setLabels.(~getLabels.());
784 | };
785 | };
786 | ~getValue = { ~gc.unmappedValue };
787 | ~getLabels = { nil };
788 | } => PR(\gcmap);
789 |
790 | PR(\gcmap).clone {
791 | ~gcDidRegister = false;
792 | ~userprep = { |path|
793 | ~mixer = ~specs[\mixer];
794 | if(~mixer.isKindOf(MixerChannel).not) { ~mixer = ~mixer.value };
795 | ~gc = ~mixer.controls[~specs[\ctl] ?? { \level }];
796 | if(~gc.notNil) {
797 | ~cspec = (~specs[\spec] ?? { ~gc.spec }).asSpec;
798 | BP(~oscInKey).setGUIValue(path[0], ~cspec.unmap(~gc.value));
799 | ~gc.addDependant(currentEnvironment);
800 | ~gc.bus.addDependant(currentEnvironment); // to sync with 'watch'... grr, bad hacks
801 | NotificationCenter.register(~gc, \setMixerGui, currentEnvironment, inEnvir { |mcgui|
802 | if(mcgui.notNil) {
803 | ~gc.register(~specs[\ctl], mcgui);
804 | ~gcDidRegister = true;
805 | } {
806 | ~gc.register(nil, nil, 1);
807 | ~gcDidRegister = false;
808 | };
809 | });
810 | if(~gc.mixerGui.notNil) {
811 | NotificationCenter.notify(~gc, \setMixerGui, [~gc.mixerGui]); // stupid ugly hack
812 | };
813 | };
814 | };
815 | ~userfree = { |wasReassigned|
816 | if(wasReassigned.not) {
817 | ~path.do { |p|
818 | BP(~oscInKey).setGUIValue(p, 0);
819 | };
820 | };
821 | if(~gcDidRegister) { ~gc.register(nil, nil, 1) };
822 | ~gc.bus.removeDependant(currentEnvironment);
823 | ~gc.removeDependant(currentEnvironment);
824 | NotificationCenter.unregister(~gc, \setMixerGui, currentEnvironment);
825 | };
826 | ~prAction = { |msg|
827 | var value = ~cspec.map(msg[1]);
828 | ~gc.set(value);
829 | ~gc.update(~gc.bus, [value]);
830 | };
831 | ~update = { |obj, what|
832 | case
833 | { what.isKindOf(Dictionary) } {
834 | switch(what.tryPerform(\at, \what))
835 | { \value } {
836 | BP(~oscInKey).setGUIValue(~path[0], ~cspec.unmap(~gc.value));
837 | }
838 | { \modelWasFreed } { ~free.() }
839 | }
840 | { obj.isKindOf(Bus) } {
841 | BP(~oscInKey).setGUIValue(~path[0], ~cspec.unmap(what[0]));
842 | }
843 | };
844 | ~getLabels = { nil /*[~specs[\mixer].name]*/ };
845 | } => PR(\mxmap);
846 |
847 | PR(\abstrMobileResp).clone {
848 | ~userprep = { |path|
849 | path.do { |p| BP(~oscInKey).setGUIValue(p, 0) };
850 | };
851 | ~prAction = { |msg, time, addr, recvPort|
852 | ~specs[\action].value(msg, time, addr, recvPort);
853 | if(~specs[\switchOff] == true) {
854 | inEnvir {
855 | ~path.do { |p| BP(~oscInKey).setGUIValue(p, 0) };
856 | }.defer(0.15);
857 | };
858 | };
859 | } => PR(\trigact);
860 |
861 | Proto {
862 | ~prep = {
863 | ~maps = IdentityDictionary.new;
864 | };
865 | ~freeCleanup = {
866 | ~maps.keysDo { |key| ~unmapMobile.(key) };
867 | };
868 |
869 | ~mapMobile = { |type, path, specs|
870 | var new;
871 | if(PR.exists(type)) {
872 | new = PR(type).copy.prep(path, specs);
873 | ~maps[path] = ~maps[path].add(new);
874 | };
875 | };
876 |
877 | ~unmapMobile = { |path|
878 | ~maps[path].do { |obj| obj.free };
879 | };
880 | } => PR(\mapStorage);
881 |
882 | BP(\osc).free;
883 | Proto {
884 | ~oscKey = \touch;
885 | ~event = (); // dummy, to prevent bindVC from breaking
886 |
887 | ~prep = {
888 | BP(~oscKey).chuckOSCKeys.keysValuesDo { |key, value|
889 | key.envirPut(value);
890 | };
891 | ~maps = IdentityDictionary.new;
892 |
893 | // drag-n-drop: the 'model' is BP(~oscKey)
894 | // the model doesn't know directly about me, but it sends notifications
895 | NotificationCenter.register(BP(~oscKey).v, \receiveDrag, ~collIndex, inEnvir(~receiveDrag));
896 | };
897 | ~freeCleanup = {
898 | NotificationCenter.unregister(BP(~oscKey).v, \receiveDrag, ~collIndex);
899 | ~maps.do(_.free);
900 | };
901 | ~empty = { |indices|
902 | if(indices.isNil) { indices = 16 };
903 | indices.do { |i| ~bindNil.(nil, i) };
904 | };
905 | ~receiveDrag = { |drag, i|
906 | var method = ("bind" ++ drag.bindClassName).asSymbol;
907 | if(method.envirGet.isFunction) {
908 | method.envirGet.value(drag, i.asString); // adverb should be a string
909 | } {
910 | "Dragging % is not allowed here".format(drag).warn;
911 | };
912 | currentEnvironment
913 | };
914 | // don't do mixers this way
915 | ~bindGenericGlobalControl = { |thing, adverb, parms|
916 | // prefer Tab3 for gcs
917 | var index = ~getIndexFromAdverb.(adverb, #[8, 0], \fader), newMap;
918 |
919 | // if last occupant was a mxmap, there may be a toggle map attached
920 | // which will be invalid, so, dump it before reassigning
921 | ~maps[~toggleKeys[index]].free;
922 |
923 | ~maps[~faderKeys[index]].free;
924 | newMap = PR(\gcmap).copy.prep(~faderKeys[index], (gc: thing, label: thing.name));
925 | ~maps[~faderKeys[index]] = newMap;
926 | ~setNotification.(newMap, ~faderKeys[index]);
927 | };
928 | ~bindVC = { |vc, adverb, parms|
929 | var gcs = parms.tryPerform(\at, \gcs) ?? { vc.v.globalControlsByCreation }, num = gcs.size;
930 | // need custom adverb logic
931 | adverb = adverb.asString;
932 | if(adverb.every(_.isDecDigit)) {
933 | adverb = adverb.asInteger
934 | } {
935 | if(parms.tryPerform(\at, \over) == true) {
936 | adverb = 8;
937 | } {
938 | Error("Searching not implemented yet").throw;
939 | }
940 | };
941 | if(adverb.notNil) {
942 | block { |break|
943 | gcs.do { |gc, i|
944 | if(gc.allowGUI) {
945 | if(adverb >= ~faderKeys.size) {
946 | "VC(%) has controls that couldn't be assigned".format(vc.collIndex).warn;
947 | break.(i);
948 | };
949 | ~bindGenericGlobalControl.(gc, adverb, parms);
950 | adverb = adverb + 1;
951 | };
952 | };
953 | };
954 | };
955 | };
956 | ~bindMixerChannel = { |mixer, adverb, parms|
957 | var index = ~getIndexFromAdverb.(adverb, 0, \fader),
958 | newMap;
959 | // volume fader
960 | ~maps[~faderKeys[index]].free;
961 | newMap = PR(\mxmap).copy.prep(~faderKeys[index],
962 | (mixer: mixer, ctl: \level, label: mixer.name.asString /*+ "level"*/));
963 | ~maps[~faderKeys[index]] = newMap;
964 | ~setNotification.(newMap, ~faderKeys[index]);
965 | // mute button -- some hackage here
966 | if(parms.tryPerform(\at, \setMute) != false) {
967 | ~maps[~toggleKeys[index]].free;
968 | newMap = PR(\abstrMobileResp).copy.prep(~toggleKeys[index],
969 | (mixer: mixer, label: mixer.name.asString));
970 | newMap.prAction = { |msg| mixer.mute(msg[1] > 0) };
971 | newMap.resendState = PR(\gcmap).v[\resendState];
972 | // newMap.getLabels = { "M" };
973 | newMap.getValue = { ~specs[\mixer].muted.binaryValue };
974 | newMap[\update] = newMap[\update].addFunc(inEnvir({ |obj, what, ag|
975 | if(what == \mixerFreed and: { ag === mixer }) {
976 | ~free.();
977 | MixerChannel.removeDependant(currentEnvironment);
978 | };
979 | }, newMap));
980 | newMap.userfree = {
981 | BP(~oscInKey).setGUIValue(~path[0], 0);
982 | };
983 | MixerChannel.addDependant(newMap);
984 | ~maps[~toggleKeys[index]] = newMap;
985 | // abstract responder doesn't set value; go to the model
986 | BP(~oscKey).setGUIValue(~toggleKeys[index], mixer.muted.asInteger);
987 | ~setNotification.(newMap, ~toggleKeys[index]);
988 | };
989 | };
990 | // both mixer and play/stop
991 | ~bindBP = { |bp, adverb, parms|
992 | var index = ~getIndexFromAdverb.(adverb, 0, \fader),
993 | mixer = bp.v.chan,
994 | newMap;
995 | if(mixer.isNil) {
996 | mixer = bp.event[\voicer]; // mixer as temp here
997 | if(mixer.notNil) { mixer = mixer.asMixer }; // mixer is really a Voicer at the start of this!
998 | };
999 | if(parms.isNil) {
1000 | parms = () // setMute: false
1001 | };
1002 | // may be multiple mixers
1003 | // note: 'do' handles [mixer, mixer...], and mixer, and nil
1004 | mixer.do { |mixer, i|
1005 | var localParms = parms;
1006 | if(i == 0) {
1007 | localParms = localParms.copy.put(\setMute, false);
1008 | };
1009 | if(index + i < ~faderKeys.size) {
1010 | ~bindMixerChannel.(mixer, index + i, localParms);
1011 | };
1012 | };
1013 | ~maps[~toggleKeys[index]].free;
1014 | newMap = PR(\bptrig).copy.prep(~toggleKeys[index],
1015 | (bp: bp.collIndex, label: bp.collIndex, once: parms[\once]));
1016 | ~maps[~toggleKeys[index]] = newMap;
1017 | ~setNotification.(newMap, ~toggleKeys[index]);
1018 | };
1019 | ~bindFunction = { |func, adverb, parms|
1020 | var index = ~getIndexFromAdverb.(adverb, 0, \toggle);
1021 | var newMap;
1022 | ~maps[~toggleKeys[index]].free;
1023 | newMap = PR(\trigact).copy.prep(~toggleKeys[index],
1024 | (action: func, switchOff: parms.tryPerform(\at, \switchOff) == true,
1025 | label: parms.tryPerform(\at, \label) ?? { "toggle" }
1026 | )
1027 | );
1028 | ~maps[~toggleKeys[index]] = newMap;
1029 | ~setNotification.(newMap, ~toggleKeys[index]);
1030 | };
1031 | ~bindNil = { |aNil, adverb|
1032 | var index;
1033 | try {
1034 | index = ~getIndexFromAdverb.(adverb, #[], \fader);
1035 | } { |err|
1036 | // catch and ignore 'bad index' errors
1037 | if(err.errorString.contains("bad index").not) { err.throw };
1038 | };
1039 | if(index.isInteger) {
1040 | ~maps[~toggleKeys[index]].free;
1041 | ~maps[~faderKeys[index]].free;
1042 | } {
1043 | "Nothing to remove, or bad index".warn
1044 | };
1045 | };
1046 | ~getIndexFromAdverb = { |adverb, offsetsToTry = #[0], type(\fader)|
1047 | var coll = (type ++ "Keys").asSymbol.envirGet;
1048 | if(coll.isNil) {
1049 | Error("BP(%): Wrong type %".format(~collIndex.asCompileString, type.asCompileString)).throw;
1050 | };
1051 | adverb = adverb.asString;
1052 | if(adverb.every(_.isDecDigit)) {
1053 | adverb = adverb.asInteger;
1054 | } {
1055 | adverb = block { |break|
1056 | offsetsToTry.asArray.do { |offset|
1057 | (offset .. coll.size - 1).do { |i|
1058 | if(~maps[coll[i]].isNil) { break.(i) };
1059 | };
1060 | };
1061 | nil
1062 | };
1063 | };
1064 | if(adverb.isNil or: { adverb.inclusivelyBetween(0, coll.size).not }) {
1065 | Error("BP(%): No available OSC %s or bad index".format(~collIndex.asCompileString, type)).throw;
1066 | } {
1067 | adverb
1068 | };
1069 | };
1070 | ~setNotification = { |mapObj, key|
1071 | NotificationCenter.register(mapObj, \didFree, currentEnvironment, inEnvir {
1072 | NotificationCenter.unregister(mapObj, \didFree, currentEnvironment);
1073 | ~maps[key] = nil;
1074 | });
1075 | };
1076 | } => PR(\chuckOSC);
1077 |
1078 | // not really "mobile" but helps you locate and guify controls
1079 | Proto {
1080 | ~classes = [BP, VC];
1081 | ~bounds = Rect(800, 200, 260, 175);
1082 | ~prep = {
1083 | ~bpSimpleCtls = IdentityDictionary.new;
1084 | ~vcSimpleCtls = IdentityDictionary.new;
1085 | ~deleteTimes = Dictionary.new;
1086 | ~expanded = Dictionary.new;
1087 | ~chucking = false;
1088 | ~chuckTargetChars = "0123456789ABCDEFGHIJKLMN";
1089 | if(~viewParent.isNil) {
1090 | // if user puts the BP into a layout
1091 | ~view = ListView.new;
1092 | } {
1093 | ~view = ListView(~viewParent, ~bounds)
1094 | };
1095 | ~view.beginDragAction_(inEnvir { |view|
1096 | if(~items.size > 0) {
1097 | ~items[~view.value].tryPerform(\at, \dragObject)
1098 | } { nil };
1099 | })
1100 | .keyDownAction_(inEnvir { |view, char, mod, unicode, keycode, key|
1101 | var item, i, return;
1102 | // if you hit a mod key, the function fires and char.ascii is 0
1103 | // don't want to clear $^ status in that case
1104 | if(~chucking and: { char.ascii > 0 }) {
1105 | item = ~items[view.value];
1106 | char = char.toUpper;
1107 | if(item.notNil and: { ~chuckTargetChars.includes(char) }) {
1108 | if(BP.exists(~touchKey)) {
1109 | i = BP(~touchKey).tabOffset;
1110 | } {
1111 | i = 0;
1112 | };
1113 | i = i + ~chuckTargetChars.indexOf(char);
1114 | // must chuck into the proto, not the BP
1115 | item[\dragObject].chuck(BP(\chuckOSC).v, i);
1116 | };
1117 | ~chucking = false;
1118 | return = true; // do not do default key action!
1119 | };
1120 | case
1121 | { char == $^ } {
1122 | ~chucking = return = true;
1123 | }
1124 | { #[8, 127].includes(char.ascii) } {
1125 | item = ~items[view.value];
1126 | if(item.notNil) {
1127 | ~doFree.(item);
1128 | };
1129 | }
1130 | { keycode == 65363 } {
1131 | item = ~items[view.value];
1132 | if(item.notNil and: { item[\object].isKindOf(BP) }) {
1133 | ~expanded[item[\id]] = true;
1134 | };
1135 | ~updateView.();
1136 | }
1137 | { keycode == 65361 } {
1138 | i = view.value;
1139 | item = ~items[i];
1140 | if(item[\object].isKindOf(GenericGlobalControl)) {
1141 | while {
1142 | i = i - 1;
1143 | i >= 0 and: { ~items[i][\object].isKindOf(BP).not }
1144 | };
1145 | if(i >= 0) {
1146 | ~view.value = i;
1147 | item = ~items[i];
1148 | } {
1149 | item = nil
1150 | };
1151 | };
1152 | if(item.notNil) {
1153 | ~expanded[item[\id]] = nil;
1154 | };
1155 | ~updateView.();
1156 | };
1157 | return
1158 | })
1159 | .onClose_(inEnvir { BP(~collIndex).free });
1160 | ~updateView.();
1161 | if(~viewParent.isNil) { ~view.front };
1162 | ~updateView = inEnvir(~updateView);
1163 | ~putHook = inEnvir { |key, obj|
1164 | if(obj.isKindOf(VC)) {
1165 | // defer b/c Fact=>VC does VC new first, then putHook, *then* populate
1166 | // so 'obj.value' is not available Right Now
1167 | defer(inEnvir {
1168 | ~vcSimpleCtls.put(key, SimpleController(obj.value)
1169 | .put(\addedGlobalControl, ~updateView)
1170 | .put(\removedGlobalControl, ~updateView)
1171 | );
1172 | }, 0.1);
1173 | };
1174 | if(obj.isKindOf(BP)) {
1175 | defer(inEnvir {
1176 | // creating a BP seems to call classHooks at least twice
1177 | // and '.exists' doesn't disambiguate
1178 | // so we can only check if we created a controller before
1179 | // but it gets worse... the two calls are for physically
1180 | // different BP objects. So we cannot use 'obj'
1181 | // as it is out of date by the time the defer{} wakes up.
1182 | // For now, the best I can think of is to keep the 'defer'
1183 | // to allow BP to juggle the objects, then look it up
1184 | // based on the key (use the latest available BP('st'),
1185 | // of which there should only ever be one).
1186 | // This is... horrid.
1187 | if(~bpSimpleCtls[key].isNil) {
1188 | // do not try to access BP(key)
1189 | // unless there's something there
1190 | // otherwise it will mistakenly create an empty
1191 | // BP and this will break future =>
1192 | if(BP.exists(key)) {
1193 | obj = BP(key); // this has changed, see above
1194 | ~bpSimpleCtls.put(key, SimpleController(obj)
1195 | .put(\play, inEnvir { defer(~updateView) })
1196 | .put(\stop, inEnvir { |obj, what, value|
1197 | if(value == \stopped) { defer(~updateView) };
1198 | })
1199 | .put(\voicer_, inEnvir { defer(~updateView) })
1200 | );
1201 | };
1202 | };
1203 | }, 0.1);
1204 | };
1205 | ~updateView.defer(0.3); // updateView is already linked to this envir
1206 | };
1207 | ~freeHook = inEnvir { |key, obj|
1208 | var item = ~items.detect { |it| it[\object] === obj };
1209 | if(item.notNil) {
1210 | ~deleteTimes.removeAt(item[\id]);
1211 | ~expanded.removeAt(item[\id]);
1212 | };
1213 | if(obj.isKindOf(VC)) {
1214 | ~vcSimpleCtls[key].remove;
1215 | ~vcSimpleCtls.removeAt(key);
1216 | };
1217 | if(obj.isKindOf(BP)) {
1218 | ~bpSimpleCtls[key].remove;
1219 | ~bpSimpleCtls.removeAt(key);
1220 | };
1221 | ~updateView.defer(0.05); // updateView is already linked to this envir
1222 | };
1223 | ~classes.do { |class|
1224 | class.addHook(\put, ~putHook)
1225 | .addHook(\free, ~freeHook);
1226 | };
1227 | currentEnvironment
1228 | };
1229 | ~freeCleanup = {
1230 | ~classes.do { |class|
1231 | class.removeHook(\put, ~updateHook)
1232 | .removeHook(\free, ~updateHook);
1233 | };
1234 | ~bpSimpleCtls.do(_.remove);
1235 | ~vcSimpleCtls.do(_.remove);
1236 | ~view.close;
1237 | };
1238 | ~asView = { ~view };
1239 | ~updateView = {
1240 | // tryPerform: ~items may be nil or empty
1241 | // also (FML) ~view.value may be nil
1242 | var currentID, i;
1243 | if(~items.notNil and: { ~view.value.notNil }) {
1244 | // and even if you have an items array, you might pull nil out of it
1245 | currentID = ~items[~view.value].tryPerform(\at, \id);
1246 | };
1247 | ~items = ~makeList.();
1248 | ~view.items = ~items.collect(_.string);
1249 | i = ~items.detectIndex { |item| item[\id] == currentID };
1250 | if(i.notNil) { ~view.value = i };
1251 | ~updateColors.();
1252 | };
1253 | ~updateColors = {
1254 | ~view.colors = ~getColors.();
1255 | };
1256 | ~makeList = {
1257 | // oopsy, need drags and strings
1258 | // gc drags need to point back to the parent -- data structure
1259 | // (uniqueID [for restoring view pos], string, dragObject, parent [only for VoicerGlobalControls])
1260 | ~makeBPList.() ++ ~makeVCList.()
1261 | };
1262 | ~makeBPList = {
1263 | var items = Array.new, item, event, ctls;
1264 | // it seems to be possible to have a symbol in 'keys' whose BP doesn't actually exist
1265 | // probably should prevent that but for now, filter such items out
1266 | BP.keys.select { |key| BP.exists(key) }.as(Array).sort.do { |key|
1267 | event = BP(key)[\event];
1268 | // if you have an eventKey, you're a playable process
1269 | // cll interface BPs do not have this
1270 | if(event.notNil and: { event[\eventKey].notNil }) {
1271 | item = (
1272 | id: "BP" ++ key,
1273 | string: "BP(%)".format(key.asCompileString),
1274 | object: BP(key),
1275 | dragObject: BP(key) // mixer
1276 | );
1277 | items = items.add(item);
1278 | ctls = ~getBPCtls.(BP(key), 0); // maybe nil, but nil ++ array is ok
1279 | if(event[\voicer].notNil) {
1280 | ctls = ctls ++ ~getVoicerCtls.(event[\voicer], ctls.size);
1281 | };
1282 | if(ctls.size > 0) {
1283 | if(~expanded[items.last[\id]] == true) {
1284 | item[\string] = "-- " ++ item[\string];
1285 | items = items ++ ctls;
1286 | } {
1287 | item[\string] = "+ " ++ item[\string];
1288 | };
1289 | };
1290 | };
1291 | };
1292 | items
1293 | };
1294 | ~getBPCtls = { |bp, startI|
1295 | if(bp.v[\globalControls].notNil) {
1296 | bp.globalControls.collect { |ctl, i|
1297 | if(ctl.isKindOf(GlobalControlBase)) {
1298 | ~itemForCtl.(ctl, bp.collIndex, startI + i)
1299 | } {
1300 | "BP(%).globalControls[%] is invalid: %"
1301 | .format(bp.collIndex.asCompileString, i, ctl)
1302 | .warn;
1303 | nil
1304 | };
1305 | }
1306 | .reject(_.isNil);
1307 | };
1308 | };
1309 | ~itemForCtl = { |gc, key, i| // 'key' and 'i' are for the id field
1310 | (
1311 | id: "VC%:%-%".format(key, i.asPaddedString(2, "0"), gc.name),
1312 | string: " - % (% .. %)".format(gc.name, gc.spec.minval, gc.spec.maxval),
1313 | object: gc,
1314 | dragObject: gc
1315 | )
1316 | };
1317 | ~getVoicerCtls = { |voicer, startI|
1318 | var items, ctls, key;
1319 | // 'globalControlsByCreation' keeps only 'allowGUI' controls
1320 | ctls = voicer.tryPerform(\globalControls);
1321 | if(ctls.size > 0) {
1322 | if(ctls.isKindOf(ValidatingDictionary)) {
1323 | ctls = ctls.values.as(Array);
1324 | };
1325 | key = VC.collection.detect { |vc| vc.v === voicer }
1326 | .tryPerform(\collIndex) ?? { "unknown" };
1327 | ctls.sort { |a, b| a.voicerIndex < b.voicerIndex }
1328 | .do { |gc, i|
1329 | items = items.add(~itemForCtl.(gc, key, startI + i))
1330 | };
1331 | };
1332 | items
1333 | };
1334 | ~makeVCList = {
1335 | var items = Array.new, ctls;
1336 | VC.keys.select { |key| VC.exists(key) }.as(Array).sort.do { |key|
1337 | items = items.add((
1338 | id: "VC" ++ key,
1339 | string: "VC(%)".format(key.asCompileString),
1340 | object: VC(key),
1341 | dragObject: VC(key).asMixer // maybe nil but shouldn't be
1342 | ));
1343 | // ctls will be shown with BPs instead
1344 | // ctls = ~getVoicerCtls.(VC(key).v);
1345 | // if(ctls.size > 0) { items = items ++ ctls };
1346 | };
1347 | items
1348 | };
1349 | ~deleteCancelTime = 0.7;
1350 | ~doFree = { |item|
1351 | var index;
1352 | if(~canFree.(item)) {
1353 | if(~deleteTimes[item[\id]].isNil) {
1354 | ~deleteTimes[item[\id]] = SystemClock.seconds;
1355 | ~updateColors.();
1356 | AppClock.sched(~deleteCancelTime, inEnvir {
1357 | // ~deleteTimes[item[\id]] is cleared if the user hit del a second time to cancel
1358 | if(~deleteTimes[item[\id]].notNil) {
1359 | index = ~view.value;
1360 | item[\object].free; // should delete from items too
1361 | defer(inEnvir {
1362 | if(index >= ~items.size) {
1363 | ~view.value = max(0, ~items.size - 1);
1364 | } {
1365 | ~view.value = index;
1366 | };
1367 | }, 0.06);
1368 | };
1369 | });
1370 | } {
1371 | if(SystemClock.seconds - ~deleteTimes[item[\id]] <= ~deleteCancelTime) {
1372 | ~deleteTimes[item[\id]] = nil;
1373 | };
1374 | };
1375 | } {
1376 | "Can't delete %".format(item[\object].asString).warn;
1377 | };
1378 | };
1379 | ~canFree = { |item|
1380 | var obj = item[\object];
1381 | switch(obj.class)
1382 | { BP } {
1383 | // free-able only if stopped (don't accidentally delete something that's playing)
1384 | obj.isPlaying.not
1385 | }
1386 | { VC } {
1387 | BP.collection.every { |bp|
1388 | var event = if(bp.exists) { bp[\event] };
1389 | event.isNil or: { event[\voicer] !== obj.v }
1390 | }
1391 | }
1392 | { false } // GenericGlobalControl, can't free this way
1393 | };
1394 | if(QPalette.new.color(\window).red < 0.5) {
1395 | // dark theme
1396 | ~deletePendingColor = Color.new255(116, 46, 0);
1397 | ~playingColor = Color.new255(5, 66, 0);
1398 | ~idleColor = Color.clear;
1399 | } {
1400 | ~deletePendingColor = Color.new255(255, 178, 78);
1401 | ~playingColor = Color.new255(140, 255, 131);
1402 | ~idleColor = Color.clear;
1403 | };
1404 | ~getColors = {
1405 | ~items.collect { |item|
1406 | case
1407 | { item[\deleteTime].notNil } { ~deletePendingColor }
1408 | // OK for VCs because it falls back to Object's implementation --> false
1409 | { item[\object].isPlaying } { ~playingColor }
1410 | { ~idleColor };
1411 | };
1412 | };
1413 | } => PR(\controlList);
1414 | } {
1415 | AbstractChuckArray.defaultSubType = saveSubtype;
1416 | };
1417 |
--------------------------------------------------------------------------------
/nanoktl-objects.scd:
--------------------------------------------------------------------------------
1 | // nanoKontrol interface objects
2 |
3 | var saveSubtype = AbstractChuckArray.defaultSubType;
4 | var parentEnvir = currentEnvironment;
5 |
6 | /**
7 | Chucklib-livecode: A framework for live-coding improvisation of electronic music
8 | Copyright (C) 2018 Henry James Harkins
9 |
10 | This program is free software: you can redistribute it and/or modify
11 | it under the terms of the GNU General Public License as published by
12 | the Free Software Foundation, either version 3 of the License, or
13 | (at your option) any later version.
14 |
15 | This program is distributed in the hope that it will be useful,
16 | but WITHOUT ANY WARRANTY; without even the implied warranty of
17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 | GNU General Public License for more details.
19 |
20 | You should have received a copy of the GNU General Public License
21 | along with this program. If not, see .
22 | **/
23 |
24 | if(PR.exists(\abstractTouch).not) {
25 | (thisProcess.nowExecutingPath.dirname +/+ "mobile-objects.scd").load;
26 | };
27 |
28 | protect {
29 | PR(\mix16Touch).clone {
30 | ~uid = nil;
31 | ~chan = nil;
32 | // jeez. asPaddedString isn't core. OK. fine. whatever.
33 | ~padString = { |string, size, padWithChar($0)|
34 | string = string.asString; // if it's, say, a number as below
35 | if(string.size < size) {
36 | String.fill(size - string.size, padWithChar) ++ string
37 | } {
38 | string
39 | }
40 | };
41 | // flat array of pairs: ccnum, oscpath, ccnum, oscpath etc.
42 | // for GUI definition below, zero-padding the index is important!
43 | ~pathTable = [
44 | [(0..7), (16..23)].flat,
45 | [(64..71), (32..39)].flat
46 | ].collect { |row, i|
47 | var prefix = #["/1/fader", "/1/toggle"][i];
48 | row.collect { |ccnum, j| [ccnum, (prefix ++ ~padString.(j+1, 2)).asSymbol] }
49 | }.flat;
50 | ~keepAlive = false;
51 | ~prep = {
52 | ~pathsForCC = Array.newClear(128);
53 | ~pathTable.pairsDo { |ccnum, path|
54 | ~pathsForCC[ccnum] = path;
55 | };
56 | // special case for presetbrowser
57 | ~pathsForCC[45] = '/1/push5';
58 | // if we do this now, it delays server init messages
59 | NotificationCenter.registerOneShot(\clInterface, \ready, \nanoKtl, {
60 | if(MIDIClient.initialized.not) { MIDIIn.connectAll };
61 | });
62 | ~resp = MIDIFunc.cc(inEnvir { |value, ccnum, chan|
63 | var path;
64 | path = ~pathsForCC[ccnum];
65 | if(path.notNil) {
66 | // downstream may use 'nil' to detect that it's coming from MIDI
67 | ~respond.([path, value / 127.0], SystemClock.seconds, nil, nil)
68 | };
69 | }, chan: ~chan, srcID: ~uid);
70 | ~data = IdentityDictionary.new; // save all incoming data by oscpath
71 | ~syncSigns = IdentityDictionary.new;
72 | if(~labels.isNil) { ~labels = IdentityDictionary.new };
73 | ~setDataKeys.();
74 | // touch interface needs to wait for ping; MIDI doesn't
75 | // so signal that it's OK to move ahead
76 | parentEnvir[\pingStatus] = true;
77 | if(parentEnvir[\pingCond].notNil) {
78 | parentEnvir[\pingCond].signalAll;
79 | };
80 | currentEnvironment
81 | };
82 | ~freeCleanup = {
83 | ~resp.free;
84 | NotificationCenter.notify(currentEnvironment, \modelWasFreed);
85 | };
86 |
87 | ~setLabel = { |oscpath, label|
88 | if(label.isNil) { label = "" /*oscpath.asString.split($/).last*/ };
89 | ~labels[oscpath] = label;
90 | NotificationCenter.notify(currentEnvironment, \any, [[\label, oscpath, label]]);
91 | currentEnvironment
92 | };
93 |
94 | // setValue and related methods supply local address
95 | // so, if there's an address, we know we should override saved value
96 | // if replyAddr is nil, the new value comes from MIDI
97 | // and we should sync up if there's a saved value
98 |
99 | ~respond = { |msg, time, replyAddr, recvPort, guiOnly(false)|
100 | if(msg[0].asString[3] == $f) {
101 | ~respondFader.(msg, time, replyAddr, recvPort, guiOnly);
102 | } {
103 | ~respondToggle.(msg, time, replyAddr, recvPort, guiOnly);
104 | };
105 | };
106 |
107 | // syncSigns:
108 | // nil = not synced, check before changing real value
109 | // 0 = synced, pass value through
110 | // -1 = MIDI value is less than old value
111 | ~respondFader = { |msg, time, replyAddr, recvPort, guiOnly(false)|
112 | var args = [msg, time, replyAddr, recvPort];
113 | var sign, data;
114 | // [msg, time, replyAddr, recvPort, guiOnly].debug("respond");
115 | if(replyAddr.notNil) {
116 | if(
117 | (msg[1] ?? { -2 }) absdif: (~data[msg[0]] ?? { -3 }) > 0.007 // 1/127 ~= 0.00787
118 | ) {
119 | ~syncSigns[msg[0]] = nil;
120 | };
121 | ~prUpdateControl.(msg, args, guiOnly);
122 | } {
123 | // MIDI: check sync
124 | sign = ~syncSigns[msg[0]];
125 | case
126 | { sign == 0 } {
127 | ~prUpdateControl.(msg, args, guiOnly);
128 | }
129 | { sign.isNil } {
130 | data = ~data[msg[0]];
131 | if(data.notNil) {
132 | ~syncSigns[msg[0]] = sign(msg[1] - data);
133 | } {
134 | // no existing data, MIDI is first to set
135 | ~syncSigns[msg[0]] = 0;
136 | ~prUpdateControl.(msg, args, guiOnly);
137 | }
138 | }
139 | {
140 | // sync should get close or cross over
141 | data = ~data[msg[0]];
142 | if(data absdif: msg[1] <= 0.016 or: { // 2/127 ~= 0.016
143 | sign(msg[1] - data) != sign
144 | }) {
145 | ~syncSigns[msg[0]] = 0;
146 | ~prUpdateControl.(msg, args, guiOnly);
147 | };
148 | };
149 | };
150 | };
151 |
152 | // toggle:
153 | // if never used before (nothing in ~data), assume we are switching on
154 | // otherwise read value and flip
155 | ~respondToggle = { |msg, time, replyAddr, recvPort, guiOnly(false)|
156 | var args = [msg, time, replyAddr, recvPort];
157 | var old;
158 | if(replyAddr.notNil) { // again, if setting from client (not MIDI)
159 | ~prUpdateControl.(msg, args, guiOnly);
160 | } {
161 | if(msg[1] > 0) {
162 | old = ~data[msg[0]];
163 | if(old.isNil) {
164 | msg[1] = 1; // assume flipping on
165 | } {
166 | msg[1] = (old == 0).asInteger;
167 | };
168 | ~prUpdateControl.(msg, args, guiOnly);
169 | };
170 | };
171 | };
172 |
173 | ~prUpdateControl = { |msg, args, guiOnly(false)|
174 | if(~saveKeys.includes(msg[0])) {
175 | ~saveValueFromMsg.(msg);
176 | };
177 | NotificationCenter.notify(currentEnvironment, \any, args);
178 | if(guiOnly.not) {
179 | NotificationCenter.notify(currentEnvironment, msg[0], args);
180 | };
181 | };
182 |
183 | ~saveValueFromMsg = { |msg|
184 | if(msg.size == 2) {
185 | // "saveValueFromMsg".debug;
186 | ~data[msg[0]] = msg[1] //.dump
187 | } {
188 | ~data[msg[0]] = msg[1..];
189 | };
190 | };
191 |
192 | ~fullWidth = 200;
193 | ~sliderWidth = ~fullWidth - (2 * (~buttonExtent.x + ~gap.x));
194 | ~tabSpecs = [
195 | "Tab1", {
196 | var out = Array(64), // 8 rows: button, slider, button, knob with names
197 | origin = ~gap.copy,
198 | oneBound = Rect(0, 0, ~sliderWidth, ~buttonExtent.y),
199 | buttonExtent = ~buttonExtent,
200 | gap = ~gap.y,
201 | // must save colors because GUI object init runs in another proto
202 | ltAqua = ~ltAqua, aqua = ~aqua,
203 | green = ~green, ltGreen = ~ltGreen,
204 | sBkColor = ~sliderBG;
205 | 8.do { |i|
206 | out.add("/1/toggle%".format(~padString.(i+1, 2)).asSymbol).add((
207 | bounds: Rect.fromPoints(
208 | origin + Point(gap.neg, 0),
209 | origin + Point(buttonExtent.x - gap, buttonExtent.y)
210 | ),
211 | class: Button,
212 | init: { |view|
213 | // init func runs in the touchGui environment
214 | view.states_([[" ", nil, ltAqua], ["", nil, aqua]])
215 | .receiveDragHandler_(inEnvir { |view|
216 | // we don't have access to the 't' touch object
217 | // notification assumes there's just one (untested with multiple)
218 | NotificationCenter.notify(~model, \receiveDrag, [View.currentDrag, i]);
219 | })
220 | },
221 | ))
222 | .add("/1/fader%".format(~padString.(i+1, 2)).asSymbol).add((
223 | bounds: Rect.fromPoints(
224 | origin + Point(buttonExtent.x, 0),
225 | origin + oneBound.extent
226 | ),
227 | class: Slider,
228 | // I considered implementing receive-drag on the slider too but it didn't work
229 | init: { |view| view.knobColor_(green).background_(sBkColor) },
230 | spec: [0, 1]
231 | ))
232 | .add("/1/toggle%".format(~padString.(i+9, 2)).asSymbol).add((
233 | bounds: Rect.fromPoints(
234 | origin + Point(gap + ~sliderWidth, 0),
235 | origin + Point(buttonExtent.x + ~sliderWidth + gap, buttonExtent.y)
236 | ),
237 | class: Button,
238 | init: { |view|
239 | // init func runs in the touchGui environment
240 | view.states_([[" ", nil, ltAqua], ["", nil, aqua]])
241 | .receiveDragHandler_(inEnvir { |view|
242 | // we don't have access to the 't' touch object
243 | // notification assumes there's just one (untested with multiple)
244 | NotificationCenter.notify(~model, \receiveDrag, [View.currentDrag, i+8]);
245 | })
246 | },
247 | ))
248 | .add("/1/fader%".format(~padString.(i+9, 2)).asSymbol).add((
249 | bounds: Rect.fromPoints(
250 | origin + Point(~fullWidth - buttonExtent.x, 0),
251 | origin + Point(~fullWidth, buttonExtent.y)
252 | ),
253 | class: Knob,
254 | // I considered implementing receive-drag on the slider too but it didn't work
255 | init: { |view|
256 | view.color_([ltGreen, green, ltGreen, green])
257 | .mode_(\vert)
258 | },
259 | spec: [0, 1]
260 | ));
261 | origin.y = origin.y + oneBound.height + gap;
262 | };
263 | out
264 | }.value
265 | ];
266 |
267 | ~chuckOSCKeys = {
268 | var allKeys = ~saveKeys.collect(_.asString),
269 | keys, faderKeys, toggleKeys;
270 | keys = sort(allKeys.select({ |key|
271 | key[1] == $1 and: { "ft".includes(key[3]) }
272 | }).as(Array));
273 | faderKeys = keys.select { |key| key[3] == $f }.collect(_.asSymbol);
274 | toggleKeys = keys.select { |key| key[3] == $t }.collect(_.asSymbol);
275 | (keys: keys, faderKeys: faderKeys, toggleKeys: toggleKeys)
276 | };
277 | } => PR(\nanoTouch); // 'touch' is not accurate for MIDI, but reflects object design
278 | } {
279 | AbstractChuckArray.defaultSubType = saveSubtype;
280 | };
281 |
--------------------------------------------------------------------------------
/parsenodes.sc:
--------------------------------------------------------------------------------
1 | /**
2 | Chucklib-livecode: A framework for live-coding improvisation of electronic music
3 | Copyright (C) 2018 Henry James Harkins
4 |
5 | This program is free software: you can redistribute it and/or modify
6 | it under the terms of the GNU General Public License as published by
7 | the Free Software Foundation, either version 3 of the License, or
8 | (at your option) any later version.
9 |
10 | This program is distributed in the hope that it will be useful,
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 | GNU General Public License for more details.
14 |
15 | You should have received a copy of the GNU General Public License
16 | along with this program. If not, see .
17 | **/
18 |
19 | ClNumProxy {
20 | var lo, begin, <>end, <>string, <>parentNode, <>children;
72 | var