├── 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 1. 10 | 11 | 12 | 13 | "--" 14 | 15 | 16 | 17 | 18 | 20 | 22 | 24 | 26 | 27 | 28 | 29 | 2. 30 | 31 | 32 | 33 | "----" 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | 46 | 47 | 48 | 49 | 50 | 51 | 3. 52 | 53 | 54 | 55 | "- --" 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 65 | 67 | 69 | 70 | 71 | 72 | 4. 73 | 74 | 75 | 76 | "- --- -" 77 | 78 | 79 | 80 | 81 | 82 | 84 | 85 | 86 | 87 | 88 | 89 | 7 90 | 91 | 92 | 93 | 94 | 95 | 97 | 99 | 101 | 102 | 103 | 104 | 105 | 106 | 5. 107 | 108 | 109 | 110 | "-|-|-|-" 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 121 | 123 | 124 | 125 | 126 | 127 | 128 | 6. 129 | 130 | 131 | 132 | "-|--|-|-" 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 146 | 148 | 149 | 150 | 151 | 152 | 153 | 7. 154 | 155 | 156 | 157 | "--| - ​ |- --| -" 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 182 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | Music engraving by LilyPond 2.18.2—www.lilypond.org 198 | 199 | 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 bpKey, <>isPitch = false, <>isMain = false, <>phrase, <>parm; 74 | var <>lastSelected; // for GUI, not used for parsing 75 | 76 | *new { |stream, parentNode, properties| 77 | var new = super.new; 78 | if(properties.notNil) { new.putAll(properties) }; 79 | ^new.init(stream, parentNode) 80 | } 81 | 82 | *newEmpty { ^super.new } 83 | 84 | init { |stream, argParent| 85 | parentNode = argParent; 86 | children = Array.new; 87 | begin = stream.pos; 88 | this.parse(stream); 89 | if(end.isNil) { end = stream.pos - 1 }; 90 | if(string.isNil) { 91 | if(end >= begin) { 92 | string = stream.collection[begin .. end] 93 | } { 94 | string = String.new; 95 | }; 96 | }; 97 | } 98 | 99 | // navigation 100 | selectionStart { ^begin } 101 | selectionSize { ^end - begin + 1 } 102 | siblings { 103 | ^if(parentNode.notNil) { parentNode.children }; // else nil 104 | } 105 | index { 106 | var sibs = this.siblings; 107 | ^if(sibs.notNil) { sibs.indexOf(this) }; // else nil 108 | } 109 | nearbySib { |incr = 1| 110 | var sibs = this.siblings, i; 111 | ^if(sibs.notNil) { 112 | i = sibs.indexOf(this); 113 | if(i.notNil) { 114 | sibs[i + incr] // may be nil 115 | } 116 | }; // else nil 117 | } 118 | setLastSelected { 119 | var i = this.index; 120 | if(i.notNil) { parentNode.tryPerform(\lastSelected_, i) }; 121 | } 122 | 123 | // set properties 124 | putAll { |dict| 125 | dict.keysValuesDo { |key, value| 126 | this.perform(key.asSetter, value) 127 | } 128 | } 129 | 130 | // utility function 131 | unquoteString { |str, pos = 0, delimiter = $", ignoreInParens(false)| 132 | var i = str.indexOf(delimiter), j, escaped = false, parenCount = 0; 133 | ^if(i.isNil) { 134 | str 135 | } { 136 | j = i; 137 | while { 138 | j = j + 1; 139 | j < str.size and: { 140 | escaped or: { str[j] != delimiter } 141 | } 142 | } { 143 | switch(str[j]) 144 | { $\\ } { escaped = escaped.not } 145 | { $( } { 146 | if(ignoreInParens) { 147 | parenCount = parenCount + 1; 148 | escaped = true; 149 | } { 150 | escaped = false; 151 | }; 152 | } 153 | { $) } { 154 | if(ignoreInParens) { 155 | parenCount = parenCount - 1; 156 | if(parenCount < 0) { 157 | "unquoteString: paren mismatch in '%'".format(str).warn; 158 | } { 159 | escaped = parenCount > 0; 160 | }; 161 | } { 162 | escaped = false; 163 | }; 164 | } 165 | { 166 | if(ignoreInParens.not or: { parenCount <= 0 }) { 167 | escaped = false; 168 | }; 169 | } 170 | }; 171 | if(j - i <= 1) { 172 | String.new // special case: two adjacent quotes = empty string 173 | } { 174 | str[i + 1 .. j - 1]; 175 | }; 176 | }; 177 | } 178 | 179 | idAllowed { ^"_" } 180 | 181 | getID { |stream, skipSpaces(true)| 182 | var str = String.new, ch, begin; 183 | if(skipSpaces) { this.skipSpaces(stream) }; 184 | begin = stream.pos; 185 | while { 186 | ch = stream.next; 187 | ch.notNil and: { ch.isAlphaNum or: { this.idAllowed.includes(ch) } } 188 | } { 189 | str = str.add(ch); 190 | }; 191 | if(ch.notNil) { stream.pos = stream.pos - 1 }; 192 | ^ClStringNode.newEmpty 193 | .parentNode_(this) 194 | .string_(str) 195 | .begin_(begin) 196 | .end_(begin + str.size - 1) 197 | } 198 | skipSpaces { |stream| 199 | var ch; 200 | while { 201 | ch = stream.next; 202 | ch.notNil and: { ch.isSpace } 203 | }; 204 | if(ch.notNil) { stream.pos = stream.pos - 1 }; 205 | } 206 | 207 | streamCode { |stream| stream << string } 208 | setTime { |onset(0), argDur(4)| 209 | time = onset; 210 | dur = argDur; 211 | } 212 | isSpacer { ^false } 213 | } 214 | 215 | ClStringNode : ClAbstractParseNode { 216 | var <>openDelimiter = "", <>closeDelimiter = "", <>endTest; 217 | // assumes you've skipped the opening delimiter 218 | parse { |stream| 219 | var str = String.new, ch; 220 | if(endTest.isNil) { endTest = { |ch| ch == $\" } }; 221 | while { 222 | ch = stream.next; 223 | ch.notNil and: { endTest.value(ch).not } 224 | } { 225 | str = str.add(ch); 226 | }; 227 | if(ch.notNil) { stream.pos = stream.pos - 1 }; 228 | } 229 | symbol { ^string.asSymbol } 230 | streamCode { |stream| 231 | stream << openDelimiter << string << closeDelimiter 232 | } 233 | isSpacer { ^string.every(_.isSpace) } 234 | } 235 | 236 | ClEventStringNode : ClStringNode { 237 | streamCode { |stream| 238 | stream << "(time: " << time << ", item: "; 239 | this.streamItem(stream); 240 | stream << ")"; 241 | } 242 | 243 | streamItem { |stream| 244 | if(isPitch ?? { false }) { 245 | stream <<< this.decodePitch(string); 246 | } { 247 | if(string.size == 1) { 248 | stream <<< string[0]; 249 | } { 250 | stream <<< string; 251 | }; 252 | }; 253 | } 254 | 255 | // whitespace placeholders and true rests end up being ClEventStringNodes 256 | // and you may have to call this to get a SequenceNote to render 257 | // so it has to be here, not in ClPitchEventNode 258 | decodePitch { |pitchStr| 259 | var degree, legato = 0.9, accent = false; 260 | ^case 261 | { isPitch and: { pitchStr.isString } } { 262 | pitchStr = pitchStr.asString; 263 | case 264 | { pitchStr[0].isDecDigit } { 265 | degree = (pitchStr[0].ascii - 48).wrap(1, 10) - 1; 266 | pitchStr.drop(1).do { |ch| 267 | switch(ch) 268 | { $- } { degree = degree - 0.1 } 269 | { $+ } { degree = degree + 0.1 } 270 | { $, } { degree = degree - 7 } 271 | { $' } { degree = degree + 7 } 272 | { $~ } { legato = inf /*1.01*/ } 273 | { $_ } { legato = 0.9 } 274 | { $. } { legato = 0.4 } 275 | { $> } { accent = true } 276 | }; 277 | // degree -> legato // Association identifies pitch above 278 | SequenceNote(degree, nil, legato, if(accent) { \accent }) 279 | } 280 | // { "~_.".includes(pitchStr[0]) } { pitchStr[0] } // for articulation pools? 281 | { "*@!".includes(pitchStr[0]) } { pitchStr[0] } // placeholders for clGens etc. 282 | { pitchStr[0] != $ } { 283 | // Rest(0) -> legato 284 | // also, now we need to distinguish between rests and replaceable slots 285 | SequenceNote(Rest(pitchStr[0].ascii), nil, legato) 286 | } 287 | { nil } 288 | } 289 | { pitchStr == $ } { nil } 290 | { pitchStr } 291 | } 292 | } 293 | 294 | ClPitchEventNode : ClEventStringNode { 295 | parse { |stream| 296 | // while loop assumes that the pitch number has already been eaten 297 | var ch = stream.next, new = String.with(ch); 298 | while { (ch = stream.next).notNil and: { "+-',~_.>".includes(ch) } } { 299 | new = new.add(ch); 300 | }; 301 | if(ch.notNil) { stream.pos = stream.pos - 1 }; 302 | } 303 | 304 | streamItem { |stream| 305 | stream <<< this.decodePitch(string); 306 | } 307 | } 308 | 309 | ClChordNode : ClEventStringNode { 310 | *new { |stream, parentNode, properties| 311 | if(properties.isNil) { 312 | properties = IdentityDictionary.new; 313 | } { 314 | properties = properties.copy; 315 | }; 316 | properties.put(\openDelimiter, $().put(\closeDelimiter, $)); 317 | ^super.new(stream, parentNode, properties) 318 | } 319 | // here, starts with first item (opening angle bracket is already skipped) 320 | // 'begin' is already set 321 | parse { |stream| 322 | var ch, new; 323 | var property = (isPitch: true); 324 | begin = begin - 1; 325 | while { 326 | ch = stream.peek; 327 | ch.notNil and: { ch != closeDelimiter } 328 | } { 329 | new = ClPitchEventNode(stream, this, property); 330 | children = children.add(new); 331 | }; 332 | if(ch != closeDelimiter) { 333 | Error("Improperly closed (chord node)").throw; 334 | } { 335 | stream.next; // used 'peek' above, have to advance past closing delimiter 336 | ch = stream.peek; 337 | if(".~>_".includes(ch)) { 338 | new = ClArticNode(stream, this); 339 | children = children.add(new); 340 | }; 341 | } 342 | } 343 | streamItem { |stream| 344 | var legato = 0.9, accent = false, 345 | i = 0, size = children.size, note; 346 | if(children.last.isMemberOf(ClArticNode)) { 347 | switch(children.last.char) 348 | { $~ } { legato = inf /*1.01*/ } 349 | { $_ } { legato = 0.9 } 350 | { $. } { legato = 0.4 }; 351 | accent = children.last.accented; 352 | size = size - 1; 353 | }; 354 | stream << "SequenceNote("; 355 | if(size > 1) { stream << "[" }; 356 | while { children[i].isMemberOf(ClPitchEventNode) } { 357 | if(i > 0) { stream << ", " }; 358 | note = children[i].decodePitch(children[i].string); 359 | stream << note.freq; 360 | i = i + 1; 361 | }; 362 | if(size > 1) { stream << "]" }; 363 | stream << ", nil, " << legato; 364 | if(accent) { stream << ", \\accent" }; 365 | stream << ")"; 366 | } 367 | } 368 | 369 | ClArticNode : ClStringNode { 370 | var artic = "_.~", accent = ">"; 371 | var <>char, <>accented = false; 372 | 373 | parse { |stream| 374 | var ch; 375 | ch = stream.next; 376 | if(accent.includes(ch)) { 377 | accented = true; 378 | ch = stream.next; 379 | this.checkArtic(ch, stream); 380 | } { 381 | this.checkArtic(ch, stream); 382 | }; 383 | } 384 | checkArtic { |ch, stream| 385 | case 386 | { artic.includes(ch) } { 387 | char = ch; 388 | ch 389 | } 390 | { accent.includes(ch) or: { ch == $" } } { 391 | char = nil; // special case (also valid for terminating quote) 392 | stream.pos = stream.pos - 1; // >~ is 2 chars; >> is two things of one char each 393 | } 394 | { 395 | Error("Articulation pool: Bad character $%".format(ch)).throw; 396 | } 397 | } 398 | streamCode { |stream| 399 | if(accented == true) { 400 | stream << "\\accent -> " <<< char; 401 | } { 402 | stream <<< char; 403 | }; 404 | } 405 | } 406 | 407 | ClArticPoolNode : ClStringNode { 408 | parse { |stream| 409 | var ch; 410 | if(endTest.isNil) { endTest = { |ch| ch == $\" } }; 411 | while { 412 | ch = stream.peek; 413 | ch.notNil and: { endTest.value(ch).not } 414 | } { 415 | children = children.add(ClArticNode(stream)); 416 | }; 417 | stream.next; // eat the last quote 418 | } 419 | streamCode { |stream| 420 | stream << "["; 421 | children.do { |item, i| 422 | if(i > 0) { stream << ", " }; 423 | item.streamCode(stream); 424 | }; 425 | stream << "]"; 426 | } 427 | } 428 | 429 | ClNumberNode : ClAbstractParseNode { 430 | classvar types; 431 | var <>value; 432 | 433 | *initClass { 434 | types = [ 435 | // fraction: integer/integer 436 | { |str| 437 | var slash = str.indexOf($/), // regexp guarantees "/" is there 438 | numerator = str[ .. slash-1].asInteger, 439 | denominator = str[slash+1 .. ].asInteger; 440 | Rational(numerator / denominator) 441 | } -> "[\\-+]?[0-9]+/[0-9]+", 442 | // tuplet: numNotes:noteValue 443 | { |str| 444 | var colon = str.indexOf($:), // regexp guarantees ":" is there 445 | numerator = str[ .. colon-1].asFloat, 446 | denominator = str[colon+1 .. ].asInteger; 447 | // 3:2 = triplet spanning half note = 0.66667 = 4 / 2 / 3 448 | // 3:4 = triplet spanning quarter note = 0.3333 = 4 / 4 / 3 449 | // 5:4 = quintuplet spanning quarter = 0.2 = 4 / 4 / 5 450 | // 4:1 = 4 in a whole-note = quarter note = 1 = 4 / 4 / 1 451 | Rational(4 / (numerator * denominator)) 452 | } -> "[\\-+]?[0-9]*\\.?[0-9]+([eE][\\-+]?[0-9]+)?:[0-9]+", 453 | _.asFloat -> "[\\-+]?[0-9]*\\.[0-9]+([eE][\\-+]?[0-9]+)?", 454 | _.asInteger -> "(-[0-9]+|[0-9]+)" 455 | ]; 456 | } 457 | parse { |stream| 458 | // hack into CollStream -- will fail with other IO streams 459 | var match, 460 | type = types.detect { |assn| 461 | match = stream.collection.findRegexpAt(assn.value, stream.pos); 462 | match.notNil 463 | }; 464 | if(match.notNil) { 465 | string = stream.nextN(match[1]); // match[1] = length of match 466 | value = type.key.value(string); 467 | }; // else leave state variables nil 468 | } 469 | streamCode { |stream| 470 | stream << "ClNumProxy("; 471 | if(value.isKindOf(Rational)) { 472 | stream <<< value.numerator << "/" <<< value.denominator; 473 | } { 474 | stream <<< value; 475 | }; 476 | stream << ")"; 477 | } 478 | // TODO delete? Proto:value redirects here 479 | next { ^value } 480 | } 481 | 482 | ClRangeNode : ClNumberNode { 483 | parse { |stream| 484 | var low, hi; 485 | low = ClNumberNode(stream, this); 486 | if(low.value.notNil) { 487 | this.skipSpaces(stream); 488 | 2.do { 489 | if(stream.next != $.) { 490 | Error("Invalid range separator").throw; 491 | } 492 | }; 493 | this.skipSpaces(stream); 494 | if(stream.peek == $n) { 495 | hi = \upperBound; 496 | stream.next; 497 | } { 498 | hi = ClNumberNode(stream, this); 499 | }; 500 | if(hi.value.isNil) { 501 | Error("No upper bound found in range").throw; 502 | }; 503 | } { 504 | Error("Invalid lower bound in range").throw; 505 | }; 506 | if(hi.isSymbol or: { low.value <= hi.value }) { 507 | children = [low, hi]; 508 | } { 509 | children = [hi, low]; 510 | }; 511 | } 512 | streamCode { |stream| 513 | stream << "ClRangeNumProxy(" 514 | <<< children[0].value << ", " <<< children[1].value 515 | << ")" 516 | // if(hi == \upperBound) { 517 | // 518 | // } { 519 | // stream << "ClNumProxy(Pwhite(" // "PR(\\clNumProxy).copy.prep(Pwhite(" 520 | // << ", inf))"; 521 | // } 522 | } 523 | } 524 | 525 | // \ins(, ".", {#[1, 4].choose}, 0.25) 526 | // inside curly braces, brackets and quotes are allowed; SC comments aren't 527 | ClPassthruNumberNode : ClStringNode { 528 | var Pfunc({ expr }); patterns --> no Pfunc. 560 | // we have to evaluate the expression to know which is which. 561 | // avoid code duplication in the generated string by using a function. 562 | // stream << "PR(\\clNumProxy).copy.prep(Plazy { var func = { " << ~value 563 | stream << "ClNumProxy(Plazy { var func = { " << value 564 | << " }, thing = func.value; if(thing.isPattern) { PnNilSafe(thing, inf, 10) } { Pfunc(func) } })"; 565 | } 566 | } 567 | 568 | ClDividerNode : ClAbstractParseNode { 569 | var <>endChars = "|\""; 570 | var items; 571 | parse { |stream| 572 | var ch, new; 573 | // stream.collection[stream.pos .. stream.pos + 10].debug(">> clDividerNode"); 574 | items = Array.new; 575 | while { 576 | ch = stream.next; 577 | ch.notNil and: { endChars.includes(ch).not } 578 | } { 579 | new = this.parseItem(stream, ch); 580 | if(new.notNil) { 581 | items = items.add(new); 582 | }; 583 | }; 584 | if(ch.notNil) { stream.pos = stream.pos - 1 }; 585 | // stream.collection[begin .. begin + 10].debug("<< clDividerNode"); 586 | } 587 | 588 | parseItem { |stream, ch| 589 | var new, begin, test; 590 | // [ch.asCompileString, stream.collection[stream.pos .. stream.pos + 10]].debug(">> parseItem"); 591 | if(ch.isNil) { ch = stream.next }; 592 | begin = stream.pos - 1; 593 | ^case 594 | { ch == $\\ } { 595 | stream.pos = stream.pos - 1; 596 | new = ClGeneratorNode(stream, this, 597 | (bpKey: bpKey, isPitch: isPitch, isMain: isMain, parm: parm) 598 | ); 599 | new = this.handleChain(stream, new); 600 | children = children.add(new); 601 | new//.debug("<< parseItem"); 602 | } 603 | { ch == $[ } { 604 | stream.pos = stream.pos - 1; 605 | new = ClSourceNode(stream, this, 606 | (bpKey: bpKey, isPitch: isPitch, isMain: isMain, parm: parm) 607 | ); 608 | new = this.handleChain(stream, new); 609 | children = children.add(new); 610 | new//.debug("<< parseItem"); 611 | } 612 | // code formatting: skip over CR and tab 613 | { #[$\n, $\t].includes(ch) } { nil } 614 | // legit chains should be handled in one of the above branches 615 | // but if it's the start of a divider, we might already have swallowed one colon 616 | // so match either :\ or ::\ 617 | // this does disallow : as a parmMap char in the following: "|::\ins() ||" 618 | { 619 | test = stream.collection.findRegexpAt(":*\\\\", stream.pos); 620 | // test[1] is length of match, for this case should be either :\ or ::\ 621 | test.notNil and: { test[1] >= 2 and: { test[1] <= 3 } } 622 | } { 623 | Error(":: chain syntax applies only to generators or [source]").throw; 624 | } 625 | { isPitch } { 626 | case { ch.isDecDigit } { 627 | stream.pos = stream.pos - 1; 628 | new = ClPitchEventNode(stream, this, (isPitch: true)); 629 | children = children.add(new); 630 | new 631 | /* 632 | new = String.with(ch); 633 | while { (ch = stream.next).notNil and: { "+-',~_.>".includes(ch) } } { 634 | new = new.add(ch); 635 | }; 636 | if(ch.notNil) { stream.pos = stream.pos - 1 }; 637 | children = children.add( 638 | ClEventStringNode.newEmpty 639 | .parentNode_(this) 640 | .string_(new) 641 | .isPitch_(isPitch) 642 | .begin_(begin) 643 | .end_(begin + new.size - 1); 644 | ); 645 | new//.debug("<< parseItem"); 646 | */ 647 | } 648 | { ch == $( } { 649 | // no, ClChordNode assumes '<' has been eaten already 650 | // stream.pos = stream.pos - 1; 651 | new = ClChordNode(stream, this/*, properties*/); 652 | children = children.add(new); 653 | new 654 | } 655 | { 656 | new = String.with(ch); 657 | children = children.add( 658 | ClEventStringNode.newEmpty 659 | .parentNode_(this) 660 | .string_(new) 661 | .isPitch_(isPitch) 662 | .begin_(begin) 663 | .end_(begin); 664 | ); 665 | new//.debug("<< parseItem"); 666 | }; 667 | } { 668 | new = String.with(ch); 669 | children = children.add( 670 | ClEventStringNode.newEmpty 671 | .parentNode_(this) 672 | .string_(new) 673 | .isPitch_(isPitch) 674 | .begin_(begin) 675 | .end_(begin); 676 | ); 677 | new//.debug("<< parseItem"); 678 | }; 679 | } 680 | 681 | handleChain { |stream, new| 682 | var begin, colonCount, rewindPos; 683 | // if(#[clGeneratorNode, clSourceNode].includes(new.nodeType.debug("nodeType")).not) { 684 | // Error(":: chain syntax applies only to generators or [source]").throw; 685 | // }; 686 | if(stream.peek == $:) { 687 | colonCount = 0; 688 | rewindPos = stream.pos; 689 | while { stream.next == $: } { colonCount = colonCount + 1 }; 690 | if(colonCount == 2) { 691 | stream.pos = stream.pos - 1; // need next to be the backslash 692 | new = ClChainNode(stream, this, 693 | (bpKey: bpKey, isPitch: isPitch, isMain: isMain, parm: parm), 694 | new // extra arg is first item in the chain 695 | ); 696 | } { 697 | Error("'::' syntax, wrong number of colons").throw; 698 | }; 699 | }; 700 | ^new 701 | } 702 | 703 | hasItems { ^children.any { |item| item.isSpacer.not } } 704 | streamCode { |stream| 705 | var needComma = false; 706 | if(this.hasItems) { 707 | if(children[0].time.isNil) { this.setTime(time, dur) }; 708 | // no array brackets: divider delimiters are for humans, not machines 709 | children.do { |item, i| 710 | if(item.isSpacer.not) { 711 | if(needComma) { stream << ", " }; 712 | item.streamCode(stream); 713 | needComma = true; 714 | }; 715 | }; 716 | }; 717 | } 718 | setTime { |onset(0), argDur(4), extraDur(0)| 719 | var itemDur = argDur / max(children.size, 1), durs, lastI; 720 | time = onset; 721 | dur = argDur; 722 | // children should be clStringNodes or clGeneratorNodes 723 | durs = Array(children.size); 724 | children.do { |item, i| 725 | if(item.isSpacer and: { lastI.notNil }) { 726 | durs[lastI] = durs[lastI] + itemDur; 727 | } { 728 | lastI = i; 729 | }; 730 | durs.add(itemDur); 731 | }; 732 | if(lastI.notNil) { 733 | durs[lastI] = durs[lastI] + extraDur; 734 | }; 735 | children.do { |item, i| 736 | item.setTime(onset + (i * itemDur), durs[i]); 737 | }; 738 | } 739 | } 740 | 741 | ClGeneratorNode : ClAbstractParseNode { 742 | classvar types, extras; 743 | var <>name, <>repeats, <>reset; 744 | 745 | *initClass { 746 | types = [ 747 | // pretty sure this is deprecated 748 | // (type: \clRhythmGenNode, regexp: "^:[a-zA-Z0-9_]+\\(.*\\)"), 749 | (type: ClGeneratorNode, regexp: "^\\\\[a-zA-Z0-9_]+"), 750 | (type: ClPassthruNumberNode, regexp: "^\{.*\}"), 751 | // more specific "" test must come first 752 | (type: ClArticPoolNode, regexp: "^\"[._~>]+\"", 753 | match: { |node| node.isPitch ?? { false } }, 754 | pre: { |stream| stream.next } // stringnodes assume you've already dropped the opening quote 755 | ), 756 | (type: ClPatStringNode, regexp: "^\".*\""), 757 | (type: ClRangeNode, regexp: "^-?[0-9.]+ *\\.\\. *(-?[0-9]+|n)"), 758 | (type: ClNumberNode, regexp: "^-?[0-9]"), 759 | (type: ClStringNode, regexp: "^`[A-Za-z0-9_]+", 760 | endTest: { |ch| not(ch.isAlphaNum or: { "_`".includes(ch) }) }, 761 | openDelimiter: $', closeDelimiter: $', 762 | pre: { |stream| stream.next } // drop ` intro 763 | ), 764 | (type: ClChainNode, regexp: "^::", 765 | pre: { |stream| stream.nextN(2) }, // drop :: 766 | // chain node needs to get the first sub-generator at prep time 767 | // also the chain replaces the last-parsed sub-generator 768 | parseSpecial: { |new, stream, node| 769 | if(node.children.last.isKindOf(ClGeneratorNode)) { 770 | new.init(stream, this, node.children.last); 771 | node.children = node.children.drop(-1); // last is subsumed into 'new' 772 | new 773 | } { 774 | Error("'::' syntax is valid between two generators only").throw; 775 | }; 776 | } 777 | ), 778 | (type: ClStringNode, regexp: "^,", // empty arg 779 | endTest: { |ch| ch == $, } 780 | ), 781 | ]; 782 | extras = #[endTest, openDelimiter, closeDelimiter]; 783 | } 784 | parse { |stream| 785 | var name, ch, newArg, testName; 786 | // stream.collection[stream.pos .. stream.pos + 10].debug(">> clGeneratorNode"); 787 | if(stream.peek == $\\) { stream.next }; 788 | name = this.getID(stream); 789 | testName = name.string.copy; 790 | testName[0] = testName[0].toUpper; 791 | if(PR.exists(("clGen" ++ testName).asSymbol).not) { 792 | Error("Incorrect generator name %".format(name.string)).throw; 793 | }; 794 | children = children.add(name); 795 | name = name.string; 796 | #repeats, reset, ch = this.parseModifiers(stream); 797 | if(ch == $() { 798 | while { 799 | ch = stream.next; 800 | ch.notNil and: { ch != $) } 801 | } { 802 | stream.pos = stream.pos - 1; 803 | // note: it is now not valid to collapse to ~children.add(this.parseArg(stream)) 804 | // because ~parseArg will modify ~children if it encounters a chain node 805 | // ~parseArg must finish before determining the receiver of 'add' 806 | newArg = this.parseArg(stream); 807 | children = children.add(newArg); 808 | }; 809 | // if(ch.notNil) { stream.pos = stream.pos - 1 }; 810 | } { 811 | // gen refs 812 | Error("Generator '%' has no argument list".format(name)).throw; 813 | }; 814 | // stream.collection[begin .. begin + 10].debug("<< clGeneratorNode"); 815 | } 816 | parseModifiers { |stream| 817 | var repeats, reset, ch; 818 | while { 819 | ch = stream.next; 820 | ch.notNil and: { "*!".includes(ch) } 821 | } { 822 | switch(ch) 823 | { $* } { 824 | if(repeats.notNil) { 825 | Error("\\%(): duplicate repeats".format(name)).throw; 826 | }; 827 | case 828 | { this.findType(ClPassthruNumberNode)[\regexp].matchRegexp(stream.collection, stream.pos) } { 829 | repeats = ClPassthruNumberNode(stream, this); 830 | } 831 | { this.findType(ClRangeNode)[\regexp].matchRegexp(stream.collection, stream.pos) } { 832 | repeats = ClRangeNode(stream, this); 833 | } 834 | { this.findType(ClNumberNode)[\regexp].matchRegexp(stream.collection, stream.pos) } { 835 | repeats = ClNumberNode(stream, this); 836 | }; 837 | } 838 | { $! } { 839 | if(reset.notNil) { 840 | Error("\\%(): duplicate reset flag".format(name)).throw; 841 | }; 842 | reset = true; 843 | }; 844 | }; 845 | if(ch != $() { 846 | Error("\\%(): invalid modifier character '%'".format(name, ch)).throw; 847 | }; 848 | ^[repeats, reset, ch] 849 | } 850 | parseArg { |stream| 851 | var type, ch, new; 852 | // [stream.pos, stream.collection[stream.pos .. stream.pos + 10]].debug(">> parseArg"); 853 | type = types.detect { |entry| 854 | (entry[\match].value(this) ?? { true }) and: { 855 | stream.collection.findRegexpAt(entry[\regexp], stream.pos).notNil 856 | } 857 | }; 858 | if(type/*.debug("type")*/.notNil) { 859 | type[\pre].value(stream); 860 | new = type[\type].newEmpty 861 | .putAll((bpKey: bpKey, isPitch: isPitch, isMain: isMain, parm: parm)); 862 | extras.do { |key| 863 | if(type[key].notNil) { 864 | new.perform(key.asSetter, type[key]); 865 | }; 866 | }; 867 | if(type[\parseSpecial].notNil) { 868 | type[\parseSpecial].value(new, stream, this); 869 | } { 870 | new.init(stream, this); 871 | }; 872 | } { 873 | Error("Syntax error in % arg list, at '%'".format(name, stream.collection[stream.pos .. stream.pos + 10])).throw; 874 | }; 875 | ch = stream.next; 876 | if(ch == $,) { this.skipSpaces(stream) } { 877 | if(ch.notNil) { stream.pos = stream.pos - 1 }; 878 | }; 879 | // "<< parseArg".debug; 880 | ^new 881 | } 882 | 883 | streamCode { |stream| 884 | var name = children[0].string; 885 | this.setTime(time, dur); 886 | stream << "PR(\\clGen"; 887 | if(name.size > 0) { 888 | stream << name[0].toUpper << name[1..]; 889 | }; 890 | stream << ").copy.putAll(("; 891 | if(repeats.notNil) { 892 | stream << "repeats: "; 893 | repeats.streamCode(stream); 894 | stream << ", "; 895 | }; 896 | if(reset.notNil) { 897 | stream << "resetFlag: true, "; 898 | }; 899 | stream << "bpKey: " <<< bpKey; 900 | stream << ", args: [ "; 901 | forBy(1, children.size - 1, 1) { |i| 902 | if(i > 1) { stream << ", " }; 903 | if(children[i].string == "") { 904 | stream << "nil" 905 | } { 906 | children[i].streamCode(stream); 907 | }; 908 | }; 909 | stream << " ], dur: " << dur << ", time: " << time; 910 | stream << ", isPitch: " << (isPitch ?? { false }); 911 | stream << ", isMain: " << (isMain ?? { false }); 912 | stream << ", parm: " <<< parm; 913 | stream << ")).prep"; 914 | } 915 | setTime { |onset(0), argDur(4)| 916 | var itemDur = argDur / max(children.size, 1); 917 | time = onset; 918 | dur = argDur; 919 | forBy(1, children.size - 1, 1) { |i| 920 | children[i].setTime(onset, dur); // meaningful for gens and patstrings 921 | }; 922 | } 923 | 924 | findType { |key| 925 | ^types.detect { |type| type[\type] == key } 926 | } 927 | } 928 | 929 | ClChainNode : ClAbstractParseNode { 930 | // special constructor -- you know what kind of node you're creating 931 | // assumes stream.next will be the second generator 932 | 933 | *new { |stream, parentNode, properties, leftNode| 934 | var new = super.newEmpty; 935 | if(properties.notNil) { new.putAll(properties) }; 936 | ^new.init(stream, parentNode, leftNode) 937 | } 938 | 939 | init { |stream, argParentNode, leftNode| 940 | if(stream.peek != $\\) { 941 | Error("'::' syntax is valid between two generators only").throw; 942 | }; 943 | parentNode = argParentNode; 944 | leftNode.parentNode = this; 945 | children = [leftNode]; 946 | // if(stream.peek == $/) { stream.next }; 947 | begin = leftNode.begin; // stream.pos; 948 | this.parse(stream); 949 | if(end.isNil) { end = stream.pos - 1 }; 950 | if(string.isNil) { 951 | if(end >= begin) { 952 | string = stream.collection[begin .. end] 953 | } { 954 | string = String.new; 955 | }; 956 | }; 957 | this 958 | } 959 | 960 | parse { |stream| 961 | var new, continue = true, rewindPos, colonCount; 962 | // stream.collection[stream.pos .. stream.pos + 10].debug(">> clChainNode"); 963 | while { continue } { 964 | if(stream.peek == $\\) { 965 | new = ClGeneratorNode(stream, this, 966 | (bpKey: bpKey, isPitch: isPitch, isMain: isMain, parm: parm) 967 | ); 968 | children = children.add(new); 969 | if(stream.peek == $:) { 970 | rewindPos = stream.pos; 971 | colonCount = 0; 972 | while { stream.next == $: } { colonCount = colonCount + 1 }; 973 | switch(colonCount) 974 | { 2 } { 975 | stream.pos = stream.pos - 1; 976 | } 977 | { 0 } { 978 | stream.pos = rewindPos; 979 | continue = false; 980 | } 981 | { Error("'::' syntax, wrong number of colons").throw }; 982 | if(stream.peek != $\\) { 983 | Error("'::' syntax is valid between two generators only").throw; 984 | }; 985 | } { 986 | continue = false; 987 | }; 988 | } { 989 | continue = false; 990 | } 991 | }; 992 | // stream.collection[stream.pos .. stream.pos + 10].debug("<< clChainNode"); 993 | } 994 | streamCode { |stream| 995 | if(children[0].time.isNil) { this.setTime(time, dur) }; 996 | stream << "PR(\\clGenChain).copy.putAll(("; 997 | stream << "bpKey: " <<< bpKey; 998 | stream << ", args: [ "; 999 | children.do { |child, i| 1000 | if(i > 0) { stream << ", " }; 1001 | child.streamCode(stream); 1002 | }; 1003 | stream << " ], dur: " << dur << ", time: " << time; 1004 | stream << ", isPitch: " << (isPitch ?? { false }); 1005 | stream << ", isMain: " << (isMain ?? { false }); 1006 | stream << ", parm: " <<< parm; 1007 | stream << ")).prep"; 1008 | } 1009 | setTime { |onset(0), argDur(4)| 1010 | time = onset; 1011 | dur = argDur; 1012 | children.do { |item| item.setTime(onset, dur) }; 1013 | } 1014 | } 1015 | 1016 | // this one is gone, right? 1017 | // PR(\clGeneratorNode).clone { 1018 | // ~superParse = ~parse; 1019 | // parse { |stream| 1020 | // if(stream.peek == $:) { stream.next }; 1021 | // ~superParse.(stream); 1022 | // }; 1023 | // } => PR(\clRhythmGenNode); 1024 | 1025 | ClPatStringNode : ClAbstractParseNode { 1026 | parse { |stream| 1027 | var str = String.new, ch, didOpenQuote = false; 1028 | // stream.collection[stream.pos .. stream.pos + 10].debug(">> clPatStringNode"); 1029 | while { 1030 | ch = stream.next; 1031 | ch.notNil and: { didOpenQuote.not or: { ch != $\" } } 1032 | } { 1033 | children = children.add( 1034 | ClDividerNode(stream, this, 1035 | (bpKey: bpKey, isPitch: isPitch, isMain: isMain, parm: parm) 1036 | ) 1037 | ); 1038 | didOpenQuote = true; 1039 | }; 1040 | // stream.collection[~begin .. ~begin + 10].debug("<< clPatStringNode"); 1041 | } 1042 | streamCode { |stream| 1043 | var needComma = false; 1044 | if(children[0].time.isNil) { this.setTime(time, dur) }; 1045 | stream << "[ "; 1046 | children.do { |item, i| 1047 | if(item.hasItems) { // all items should be divider nodes 1048 | if(needComma) { stream << ", " }; 1049 | item.streamCode(stream); 1050 | needComma = true; 1051 | }; 1052 | }; 1053 | stream << " ]"; 1054 | } 1055 | setTime { |onset(0), argDur(4)| 1056 | var itemDur = argDur / max(children.size, 1), i, extraDur = 0, first; 1057 | time = onset; 1058 | dur = argDur; 1059 | // all children should be divider nodes 1060 | // what about items spanning a division? reverse order 1061 | i = children.size - 1; 1062 | children.reverseDo { |item| 1063 | var itemOnset = onset + (i * itemDur); 1064 | item.setTime(itemOnset, itemDur, extraDur); 1065 | if(item.children.size == 0) { 1066 | extraDur = extraDur + itemDur; 1067 | } { 1068 | first = item.children.detect { |ch| ch.isSpacer.not }; 1069 | if(first.notNil) { 1070 | extraDur = first.time - itemOnset; 1071 | } { 1072 | extraDur = extraDur + itemDur; 1073 | } 1074 | }; 1075 | i = i - 1; 1076 | }; 1077 | } 1078 | } 1079 | 1080 | ClSourceNode : ClPatStringNode { 1081 | parse { |stream| 1082 | var str = String.new, ch; 1083 | // stream.collection[stream.pos .. stream.pos + 10].debug(">> clPatStringNode"); 1084 | if(stream.peek != $[) { 1085 | Error("Invalid bracketed source string").throw; 1086 | }; 1087 | while { 1088 | ch = stream.next; 1089 | ch.notNil and: { ch != $] } 1090 | } { 1091 | children = children.add(ClDividerNode(stream, this, 1092 | (bpKey: bpKey, isPitch: isPitch, isMain: isMain, parm: parm, endChars: "|]") 1093 | )); 1094 | }; 1095 | if(ch != $]) { 1096 | Error("Unterminated bracketed source string").throw; 1097 | }; 1098 | // stream.collection[~begin .. ~begin + 10].debug("<< clPatStringNode"); 1099 | } 1100 | streamCode { |stream| 1101 | var needComma = false; 1102 | this.setTime(time, dur); 1103 | stream << "PR(\\clGenSrc).copy.putAll(("; 1104 | stream << "bpKey: " <<< bpKey; 1105 | stream << ", args: [ [ "; 1106 | children.do { |child, i| 1107 | if(child.hasItems) /*{ 1108 | stream << "nil" 1109 | }*/ { 1110 | if(needComma) { stream << ", " }; 1111 | child.streamCode(stream); 1112 | needComma = true; 1113 | }; 1114 | }; 1115 | stream << " ] ], dur: " << dur << ", time: " << time; 1116 | stream << ", isPitch: " << (isPitch ?? { false }); 1117 | stream << ", isMain: " << (isMain ?? { false }); 1118 | stream << ", parm: " <<< parm; 1119 | stream << ")).prep"; 1120 | } 1121 | } 1122 | 1123 | ClIDNode : ClAbstractParseNode { 1124 | var <>clClass, <>objKey, <>objExists = false, /*<>phrase = \main,*/ <>numToApply; 1125 | // var <>parm = nil; // filled in in 'parse' 1126 | var <>idAllowed = "_*"; 1127 | 1128 | parse { |stream| 1129 | var i, ids, test, sym, temp, broke; 1130 | 1131 | broke = block { |break| 1132 | // class (I expect this won't be used often) 1133 | test = this.getID(stream); 1134 | children = children.add(test); 1135 | if(test.string.first.isUpper) { 1136 | clClass = test.symbol.asClass; 1137 | if(stream.next != $.) { break.(true) }; 1138 | test = this.getID(stream); 1139 | children = children.add(test); 1140 | } { 1141 | clClass = BP; 1142 | }; 1143 | 1144 | // chucklib object key 1145 | objKey = test.symbol; // really? what about array types? 1146 | if(clClass.exists(objKey)) { 1147 | objExists = true; 1148 | }; 1149 | if(stream.next != $.) { break.(true) }; 1150 | test = this.getID(stream); 1151 | children = children.add(test); 1152 | 1153 | // phrase name 1154 | test = test.string; 1155 | if(test.size == 0) { 1156 | phrase = \main; 1157 | } { 1158 | i = test.indexOf($*); 1159 | if(i.notNil) { 1160 | temp = test[i+1 .. ]; 1161 | if(temp.notEmpty and: temp.every(_.isDecDigit)) { 1162 | numToApply = temp.asInteger; 1163 | } { 1164 | "%: Invalid apply number".format(test).warn; 1165 | }; 1166 | phrase = test[ .. i-1].asSymbol; 1167 | } { 1168 | phrase = test.asSymbol; // really? what about array types? 1169 | }; 1170 | }; 1171 | if(stream.next != $.) { break.(true) }; 1172 | test = this.getID(stream); 1173 | children = children.add(test); 1174 | 1175 | // parameter name 1176 | parm = test.string; 1177 | false; 1178 | }; 1179 | if(phrase.isNil or: { phrase == '' }) { 1180 | phrase = \main; 1181 | }; 1182 | if(parm.size == 0) { 1183 | if(objExists) { 1184 | parm = clClass.new(objKey)[\defaultParm]; 1185 | }; 1186 | } { 1187 | parm = parm.asSymbol; 1188 | }; 1189 | if(broke) { stream.pos = stream.pos - 1 }; 1190 | } 1191 | 1192 | streamCode { |stream| 1193 | "clIDNode:streamCode not yet implemented".warn; 1194 | stream << "ClIDNode"; 1195 | } 1196 | } 1197 | 1198 | ClPatStringQuantNode : ClAbstractParseNode { 1199 | var <>additiveRhythm, <>quant; 1200 | parse { |stream| 1201 | var str = String.new, ch; 1202 | while { 1203 | ch = stream.next; 1204 | ch.notNil and: { ch != $" } 1205 | } { 1206 | str = str.add(ch); 1207 | }; 1208 | if(ch.notNil) { stream.pos = stream.pos - 1 }; 1209 | string = str; 1210 | additiveRhythm = str[0] == $+; 1211 | quant = str[additiveRhythm.asInteger ..].interpret; 1212 | } 1213 | } 1214 | 1215 | ClPatternSetNode : ClAbstractParseNode { 1216 | var <>hasQuant = false; 1217 | // getters, because ~children positions may vary 1218 | idNode { ^children[0] } 1219 | quantNode { 1220 | ^if(hasQuant) { children[1] } { nil }; 1221 | } 1222 | patStringNode { 1223 | ^if(hasQuant) { children[2] } { children[1] }; 1224 | } 1225 | // caller needs to wrap the outermost patStringNode in a generatorNode: need setter 1226 | patStringNode_ { |node| 1227 | if(hasQuant) { 1228 | children[2] = node; 1229 | } { 1230 | children[1] = node; 1231 | }; 1232 | } 1233 | parse { |stream| 1234 | var id, obj; 1235 | if(stream.peek == $/) { stream.next }; 1236 | id = ClIDNode(stream, this); 1237 | children = children.add(id); 1238 | this.skipSpaces(stream); 1239 | if(stream.peek == $=) { 1240 | stream.next; 1241 | } { 1242 | Error("clPatternSet must have '='").throw; 1243 | }; 1244 | this.skipSpaces(stream); 1245 | if(stream.peek == $() { 1246 | Error("Composite patterns not refactored yet").throw; 1247 | }; 1248 | if(stream.peek.isDecDigit or: { stream.peek == $+ }) { 1249 | children = children.add(ClPatStringQuantNode(stream, this)); 1250 | hasQuant = true; 1251 | }; 1252 | if(stream.peek == $\") { 1253 | if(id.clClass.exists(id.objKey)) { 1254 | obj = id.clClass.new(id.objKey); 1255 | try { 1256 | isPitch = obj.parmIsPitch(id.parm) ?? { false }; 1257 | isMain = id.parm == obj.defaultParm ?? { false }; 1258 | }; 1259 | }; 1260 | children = children.add( 1261 | ClPatStringNode(stream, this, 1262 | (bpKey: id.objKey, isPitch: isPitch, isMain: isMain, parm: id.parm) 1263 | ) 1264 | ); 1265 | } 1266 | } 1267 | 1268 | // setPattern { |phrase, parm, inParm, pattern, inString, newQuant| }; 1269 | 1270 | // maybe caller should be responsible for this 1271 | streamCode { |stream| 1272 | // var id = ~children[0]; 1273 | // stream << id.clClass.name << "(" <<< id.name << ").setPattern("; 1274 | // stream <<< id.phrase << ", " 1275 | } 1276 | setTime { |onset(0), argDur(4)| 1277 | time = onset; 1278 | dur = argDur; 1279 | this.patStringNode.setTime(onset, dur); 1280 | } 1281 | } 1282 | -------------------------------------------------------------------------------- /readme.org: -------------------------------------------------------------------------------- 1 | * chucklib-livecode 2 | 3 | An framework for live-coding using ddwChucklib objects in the 4 | SuperCollider programming language. 5 | 6 | ** Design overview 7 | 8 | In /chucklib/, =BP= objects play patterns to make sounds. Using this 9 | framework, =BPs= inherit from a specific process prototype, 10 | =PR(\abstractLiveCode)=, which provides hooks that accept new patterns 11 | from chucklib-livecode. Instances of this process can play any 12 | SuperCollider SynthDefs or Voicers, with a flexible default system. 13 | 14 | Chucklib-livecode installs a =preProcessor= into the SuperCollider 15 | interpreter, which translates compact livecoding commands into full SC 16 | syntax. The most important of these commands divides a bar's worth of 17 | musical time into events, generally indicated by single characters, 18 | which may also be grouped into subdivisions. This style of notation is 19 | inspired by http://www.ixi-audio.net/ixilang/ and is fairly 20 | straightforward to correlate to the sounding rhythm. /Generator/ 21 | functions may produce new content in every bar. 22 | 23 | #+begin_example 24 | /kik = "xxxx"; // 4otf 25 | 26 | /kik.fill1 = "x|x|x|x x"; // trailing 16th-note 27 | 28 | /kik.triple = "xxx"; // 3 divided over the bar 29 | 30 | /hh = ".-.-.-.-"; // normal offbeats 31 | 32 | /hh = ".-|. -^| ^- |.-"; // extra emphasis on 2-a and 3-e 33 | 34 | /kik/hh/snr+ // play 35 | /kik/hh/snr+4 // play on next quant = 4 36 | /kik/hh/snr- // stop 37 | #+end_example 38 | 39 | Full documentation is in PDF form: https://github.com/jamshark70/chucklib-livecode/blob/master/cl-manual.pdf 40 | 41 | * +Pitch support and Voicer+ 42 | There was a note in this space about using the =topic/rearticulation= branch of the ddwVoicer quark. That's no longer necessary; I've merged that branch into the quark's master branch. 43 | 44 | You might need to update ddwVoicer. 45 | 46 | * License 47 | 48 | chucklib-livecode is licensed this under Creative Commons CC-BY-NC-SA 49 | 4.0. You may create a derivative project, provided you don't use the 50 | code commercially and, if you release your code, you should credit me 51 | and license it under CC-BY-NC-SA or a more permissive license. 52 | 53 | [[http://creativecommons.org/licenses/by-nc-sa/4.0/]] 54 | --------------------------------------------------------------------------------