├── cl-manual.pdf
├── manual
└── manual-supporting
│ ├── item-spans.odg
│ ├── item-spans.pdf
│ ├── rhythmic-notation.pdf
│ ├── rhythmic-notation-crop.pdf
│ ├── rhythmic-notation.ly
│ └── rhythmic-notation.svg
├── ddwChucklib-livecode.quark
├── readme.org
├── clLiveCode-ext.sc
├── CllParameterHandlers.sc
├── nanoktl-objects.scd
├── helper-funcs.scd
├── cl-manual-examples.scd
├── edit-gui.scd
├── parsenodes.sc
└── mobile-objects.scd
/cl-manual.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamshark70/ddwChucklib-livecode/HEAD/cl-manual.pdf
--------------------------------------------------------------------------------
/manual/manual-supporting/item-spans.odg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamshark70/ddwChucklib-livecode/HEAD/manual/manual-supporting/item-spans.odg
--------------------------------------------------------------------------------
/manual/manual-supporting/item-spans.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamshark70/ddwChucklib-livecode/HEAD/manual/manual-supporting/item-spans.pdf
--------------------------------------------------------------------------------
/manual/manual-supporting/rhythmic-notation.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamshark70/ddwChucklib-livecode/HEAD/manual/manual-supporting/rhythmic-notation.pdf
--------------------------------------------------------------------------------
/manual/manual-supporting/rhythmic-notation-crop.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamshark70/ddwChucklib-livecode/HEAD/manual/manual-supporting/rhythmic-notation-crop.pdf
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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([Dictionary, SequenceableCollection].every { |class|
104 | presetDef.isKindOf(class).not
105 | }) {
106 | Error(
107 | "Presetdef for '%' should be a dictionary/array but isn't"
108 | .format(key)
109 | ).throw;
110 | };
111 | Library.put(\cl, \presets, this.collIndex, key, presetDef);
112 | Fact.changed(\addPreset, this.collIndex, key);
113 | } {
114 | Error("Cannot add preset to empty Fact(%)".format(collIndex.asCompileString))
115 | .throw;
116 | };
117 | }
118 |
119 | presets {
120 | ^Library.at(\cl, \presets, this.collIndex)
121 | }
122 |
123 | presetAt { |key|
124 | ^Library.at(\cl, \presets, this.collIndex, key)
125 | }
126 |
127 | *savePresets { |force = false|
128 | var write = {
129 | Library.at(\cl, \presets).writeArchive(Platform.userConfigDir +/+ "chucklibPresets.txarch");
130 | };
131 | if(force) {
132 | write.value
133 | } {
134 | if(Library.at(\cl, \presetsLoaded) == true) {
135 | write.value
136 | } {
137 | "Presets not previously loaded from disk. Load first to avoid data loss".warn;
138 | }
139 | }
140 | }
141 |
142 | *loadPresets {
143 | var allPresets = Object.readArchive(Platform.userConfigDir +/+ "chucklibPresets.txarch");
144 | var curPresets = Library.at(\cl, \presets) ?? {
145 | IdentityDictionary.new
146 | };
147 | var conflicts = IdentityDictionary.new;
148 | allPresets.keysValuesDo { |factName, presets|
149 | if(curPresets[factName].isNil) {
150 | curPresets[factName] = IdentityDictionary.new;
151 | };
152 | presets.keysValuesDo { |presetName, values|
153 | if(values.isKindOf(Dictionary).not) {
154 | "Invalid preset '%' for '%': %"
155 | .format(presetName, factName, values.asCompileString)
156 | .warn;
157 | };
158 | if(curPresets[factName][presetName].isNil) {
159 | curPresets[factName][presetName] = values;
160 | } {
161 | conflicts[factName] = conflicts[factName].add(presetName);
162 | };
163 | };
164 | };
165 | if(conflicts.notEmpty) {
166 | "The following presets loaded from disk already existed in memory.
167 | The memory version is retained; the disk version was not loaded.".warn;
168 | conflicts.keysValuesDo { |factName, presetKeys|
169 | "Fact(%): %\n".postf(factName.asCompileString, presetKeys);
170 | };
171 | };
172 | Library.put(\cl, \presets, allPresets);
173 | Library.put(\cl, \presetsLoaded, true);
174 | }
175 | }
176 |
177 | // local only, not preserved with Factories
178 | + VC {
179 | addPreset { |key, presetDef|
180 | if(env[\presets].isNil) {
181 | env[\presets] = IdentityDictionary.new;
182 | };
183 | env[\presets].put(key, presetDef);
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | proc.changed(\presetReady);
198 | };
199 | vc = \bpAsVC.eval(bp);
200 | presetDef = vc.env[\presets].tryPerform(\at, preset) ?? {
201 | // vc.env[\collIndex] is the Factory's collIndex
202 | Library.at(\cl, \presets, vc.env[\collIndex], preset)
203 | };
204 | if(presetDef.isNil) {
205 | Error("\\preset: VC(%) does not define preset %".format(
206 | vc.collIndex.asCompileString,
207 | preset.asCompileString
208 | )).throw;
209 | };
210 | proc = BP(bp); // already validated by \bpAsVC
211 | if(vc.env[\vcIsReady] == true) {
212 | applyPreset.value
213 | } {
214 | watcher = SimpleController(vc)
215 | .put(\vcReady, applyPreset);
216 | };
217 | proc
218 | } => Func(\preset);
219 |
220 | { |vcOrBP, array|
221 | var v;
222 | var out;
223 | var names;
224 | var printComma = false;
225 | var key2;
226 | var isControl = { |key|
227 | names.includes(key.asSymbol) or: {
228 | v.globalControls[key].notNil
229 | }
230 | };
231 |
232 | if(VC.exists(vcOrBP)) {
233 | v = VC(vcOrBP).v
234 | } {
235 | v = \bpAsVoicer.eval(vcOrBP);
236 | };
237 |
238 | if(v.notNil) {
239 | names = v.controlNames;
240 |
241 | out = Array(array.size);
242 | array.pairsDo { |key, value|
243 | if(isControl.(key) or: {
244 | key2 = key.asString;
245 | if(key2.endsWith("Plug")) {
246 | isControl.(key2.drop(-4).asSymbol)
247 | } { false }
248 | }) {
249 | out.add(key).add(value);
250 | if(printComma) { ", ".post } { printComma = true };
251 | "%: %".postf(key, value.asCompileString);
252 | };
253 | };
254 | "\n".post;
255 | out
256 | } {
257 | array
258 | }
259 | } => Func(\prunePreset);
260 |
261 | { |parent, bounds(Rect(800, 200, 500, 400))|
262 | var view = TreeView(parent, bounds);
263 | var watcher;
264 | var editor, closeFunc;
265 |
266 | view.columns_(["ID", "preset"]).setColumnWidth(0, 140);
267 | if(parent.isNil) { view.front };
268 |
269 | // populate
270 | Fact.keys.as(Array).sort.do { |key|
271 | var fact = Fact(key);
272 | var presets, factoryItem;
273 | if(fact.isVoicer) {
274 | presets = fact.presets;
275 | if(presets.size > 0) {
276 | factoryItem = view.addChild([fact.collIndex.asString]);
277 | presets.keys.as(Array).sort.do { |presetKey|
278 | var str = String.streamContentsLimit({ |stream|
279 | fact.presetAt(presetKey).printOn(stream)
280 | }, 50);
281 | factoryItem.addChild([presetKey.asString, str]);
282 | };
283 | };
284 | };
285 | };
286 |
287 | // this is absolutely not modularized in a good way
288 | watcher = SimpleController(Fact)
289 | .put(\addPreset, { |obj, what, factKey, presetKey|
290 | // queue these? or assume the clock will do it
291 | defer {
292 | var fIndex, pIndex, fItem, pItem;
293 | var fKeyStr = factKey.asString;
294 | var pKeyStr;
295 | var presetStr = String.streamContentsLimit({ |stream|
296 | Fact(factKey).presetAt(presetKey).printOn(stream)
297 | }, 50);
298 |
299 | fIndex = block { |break|
300 | view.numItems.do { |i|
301 | if(view.itemAt(i).strings[0] == fKeyStr) {
302 | break.(i);
303 | }
304 | };
305 | nil
306 | };
307 | if(fIndex.notNil) {
308 | pKeyStr = presetKey.asString;
309 | fItem = view.itemAt(fIndex);
310 | // super-oldskool while loop
311 | // because there's no 'numChildren' method for TreeViewItem
312 | pIndex = 0;
313 | while {
314 | fItem.childAt(pIndex).notNil and: {
315 | fItem.childAt(pIndex).strings[0] != pKeyStr
316 | }
317 | } {
318 | pIndex = pIndex + 1;
319 | };
320 | pItem = fItem.childAt(pIndex);
321 | if(pItem.notNil) {
322 | pItem.strings = [pKeyStr, presetStr]
323 | } {
324 | fItem.addChild([pKeyStr, presetStr]);
325 | }
326 | } {
327 | fItem = view.addChild([fKeyStr]);
328 | fItem.addChild([pKeyStr, presetStr]);
329 | };
330 | }
331 | });
332 |
333 | view.onClose = { watcher.remove };
334 | view.beginDragAction = { |view|
335 | var item = view.currentItem;
336 | var str, parent;
337 | if(item.notNil) {
338 | str = item.strings;
339 | // child item or parent item?
340 | if(str.size >= 2) {
341 | parent = item.parent;
342 | "/make(%:name(preset:%%)/".format(
343 | parent.strings[0],
344 | $\\,
345 | item.strings[0]
346 | );
347 | } { nil }
348 | } { nil }
349 | };
350 |
351 | // do not commit this! it assumes a specific GUI
352 | // also there's no way to do this for IDE documents
353 | editor = topEnvironment[\editor];
354 | if(editor.notNil) {
355 | closeFunc = {
356 | defer { view.close };
357 | editor.view[\dragAction].removeFunc(closeFunc);
358 | };
359 | editor.view[\dragAction] = editor.view[\dragAction].addFunc(closeFunc);
360 | };
361 |
362 | view
363 | } => Func(\presetBrowser);
364 |
365 |
366 | // record several mixers with sample-accurate onset
367 | // for simplicity/livecode use, create a folder in recordingsDir
368 | { |... mixers|
369 | var path, result, servers;
370 | {
371 | path = Platform.recordingsDir +/+ "tracks_" ++ Date.getDate.stamp;
372 | result = File.mkdir(path);
373 | if(result) {
374 | servers = mixers.collect(_.server).as(Set);
375 | if(servers.size > 1) {
376 | "Recording on multiple servers; sample-accurate sync is not guaranteed".warn;
377 | };
378 | mixers.do { |mixer|
379 | mixer.prepareRecord(path +/+ mixer.name ++ ".aiff", "AIFF");
380 | };
381 | servers.do(_.sync);
382 | servers.do { |server|
383 | server.makeBundle(0.2, {
384 | mixers.do { |mixer|
385 | if(mixer.server == server) {
386 | mixer.startRecord;
387 | };
388 | };
389 | });
390 | };
391 | } {
392 | "Directory '%' is not empty; not recording".format(path).warn;
393 | };
394 | }.fork(SystemClock);
395 | } => Func(\syncRecord);
396 |
397 | {
398 | MixerChannel.servers.do { |mixers|
399 | mixers.do { |mixer|
400 | if(mixer.isRecording) { mixer.stopRecord };
401 | };
402 | };
403 | } => Func(\stopRecord);
404 |
405 |
406 | // finally publish my GUI functions
407 | { |controller(\nanoTouch), midiDevice("The default search string should fail")| // or \mix16Touch
408 | var sBounds = Window.screenBounds, touchExtent, w, lay, touchParent, v, left,
409 | midiIndex,
410 | path = Platform.userAppSupportDir +/+ "cll-cheatsheet.txt",
411 | file, str;
412 |
413 | if(File.exists(path)) {
414 | file = File(path, "r");
415 | if(file.isOpen) {
416 | protect {
417 | str = file.readAllString;
418 | } { file.close }
419 | } {
420 | "can't open cheatsheet.txt".warn;
421 | };
422 | };
423 |
424 | if(MBM.exists(0).not) {
425 | MIDIClient.init;
426 | midiIndex = MIDIClient.sources.detectIndex { |endpt|
427 | endpt.device.containsi(midiDevice)
428 | };
429 | MIDIPort.init(midiIndex.asArray);
430 | MIDIBufManager(nil, 0) => MBM.prNew(0);
431 | };
432 |
433 | BP(#[touchGui, touch]).free;
434 | PR(controller).chuck(BP(\touch), nil, (pingDebug: false));
435 |
436 | touchExtent = PR(\abstractTouchGUI).calcExtent(BP(\touch).v);
437 | w = Window("control panel",
438 | Rect(sBounds.width - touchExtent.x, 0, touchExtent.x, sBounds.height)
439 | );
440 | lay = VLayout(
441 | touchParent = View().fixedSize_(touchExtent + Point(-4, 4)),
442 | StaticText().fixedHeight_(3), // spacer
443 | PR(\controlList) => BP(\clist),
444 | ).margins_(2).spacing_(4);
445 | if(str.notNil) {
446 | lay.add(StaticText().fixedHeight_(3)); // spacer
447 | v = TextView().string_(str)
448 | // for some stupid reason, I have to set the text before colors
449 | .background_(Color.black).stringColor_(Color.white);
450 | lay.add(v);
451 | lay.add(Button(/*w, Rect(2, 2, w.view.bounds.width - 4, 20)*/)
452 | .states_([["save"]])
453 | .action_({
454 | var file = File(path, "w");
455 | protect {
456 | if(file.isOpen) {
457 | file.putString(v.string);
458 | } {
459 | "can't open cheatsheet.txt for writing".warn;
460 | };
461 | } { file.close };
462 | })
463 | );
464 | };
465 |
466 | w.layout = lay;
467 |
468 | PR(\abstractTouchGUI).chuck(BP(\touchGui), nil, (
469 | model: BP(\touch).v,
470 | parentView: touchParent
471 | ));
472 | PR(\chuckOSC) => BP(\chuckOSC);
473 | t = BP(\chuckOSC).v; // must chuck into the proto, not the BP
474 | w.front;
475 |
476 | ~cleanup = {
477 | var dontFree = #[chuckOSC, touchGui, touch, clist];
478 | VC.all.free;
479 | BP.all.do { |bp|
480 | if(dontFree.includes(bp.collIndex).not) {
481 | bp.free;
482 | };
483 | };
484 | };
485 |
486 | NotificationCenter.notify(\clInterface, \ready);
487 | t
488 | } => Func(\makeController);
489 |
490 | {
491 | var sBounds = Window.screenBounds, touchExtent = PR(\abstractTouchGUI).calcExtent(BP(\touch).v),
492 | left;
493 |
494 | PR(\clGuiString).doHighlighting = false; // like setting a classvar
495 | left = max(180, 0.5 * (sBounds.width - 800));
496 | ~editWindow = Window("code editor",
497 | // Rect.aboutPoint(sBounds.center, min(400, sBounds.width * 0.5), sBounds.height * 0.5)
498 | // -8? Apparently border is extra
499 | Rect(left, 0, sBounds.width - touchExtent.x - left - 8, sBounds.height)
500 | );
501 | ~editWindow.layout = VLayout(
502 | HLayout(
503 | nil,
504 | Button().fixedSize_(Size(80, 20))
505 | .states_([
506 | ["autosave"],
507 | ["autosave", Color.white, Color.green(0.45)]
508 | ])
509 | .action_(inEnvir { |view|
510 | ~editor.view.autoSave = view.value > 0;
511 | }),
512 | Button().fixedSize_(Size(80, 20))
513 | .states_([["load"]])
514 | .action_(inEnvir {
515 | ~editor.view.doAutoSave; // if window is not currently empty
516 | FileDialog(inEnvir { |path|
517 | var file, str;
518 | file = File(path, "r");
519 | if(file.isOpen) {
520 | protect {
521 | str = file.readAllString;
522 | } {
523 | file.close;
524 | };
525 | ~editor.view.setString(str, 0, ~editor.view.str.size);
526 | } {
527 | "Error opening '%'".format(path).warn;
528 | };
529 | }, fileMode: 1, acceptMode: 0, stripResult: true,
530 | path: ~editor.view.autoSavePath
531 | );
532 | })
533 | )
534 | );
535 | // I don't really need a model, but the GUI needs it
536 | ~editModel = PR(\clGuiSection).copy
537 | .defaultString_("")
538 | .putAll(PR(\clEditGui).v.getTextProperties)
539 | .prep;
540 | ~editor = PR(\clGuiSectionView).copy.prep(~editModel, ~editWindow.layout, nil);
541 | ~editor.view.autoSave = false;
542 | // experimental hack: watch for focus -- must do this before .front!
543 | ~focusWatcher = SimpleController(~editor.view)
544 | // this should be OK for multiple views
545 | // because the sequence is always a/ lose old focus, then b/ gain new focus
546 | .put(\focused, { |view, what, bool|
547 | if(bool) {
548 | PR(\clPatternToDoc).activeView = view.view;
549 | } {
550 | PR(\clPatternToDoc).activeView = nil; // clear to revert to Document
551 | };
552 | })
553 | .put(\didFree, inEnvir { ~focusWatcher.remove });
554 | ~editWindow.onClose_(inEnvir { ~editModel.free }).front;
555 | Library.put(\clLivecode, \setupbars, \addToDoc, false);
556 | ~editor
557 | } => Func(\makeCodeWindow);
558 |
559 | { |controller(\nanoTouch), midiDevice|
560 | \makeController.eval(controller, midiDevice);
561 | \makeCodeWindow.eval;
562 | } => Func(\cllGui);
563 |
564 | { |vcKey, index = 0, exclude(#[]), overwrite = true|
565 | var t = thisProcess.interpreter.t;
566 | var i = index;
567 | if(VC.exists(vcKey)) {
568 | block { |break|
569 | VC(vcKey).globalControlsByCreation.do { |gc|
570 | // also check i < size
571 | if(i >= t.faderKeys.size) {
572 | "VC(%).globalControls[%]: Ran out of GUI slots"
573 | .format(vcKey.asCompileString, gc.name.asCompileString)
574 | .warn;
575 | break.(nil)
576 | };
577 | if(overwrite or: { t.maps[t.faderKeys[i]].isNil }) {
578 | if(exclude.includes(gc.name).not) {
579 | gc.chuck(t, i);
580 | i = i + 1;
581 | };
582 | };
583 | };
584 | };
585 | } {
586 | "gcChuck: VC(%) doesn't exist".format(vcKey.asCompileString).warn;
587 | };
588 | } => Func(\gcChuck);
589 |
590 |
591 | { |str, i|
592 | var match;
593 | var alphaNum = { |chr|
594 | chr.respondsTo(\isAlphaNum) and: { chr.isAlphaNum }
595 | };
596 | // allow cursor to trail the identifier
597 | if(alphaNum.(str[i]).not) {
598 | i = i - 1
599 | };
600 | while { i > 0 and: { alphaNum.(str[i]) } } {
601 | i = i - 1;
602 | };
603 | // now we should be one char before the symbol
604 | i = i + 1;
605 | match = str.findRegexpAt("([A-Za-z0-9]+)", i);
606 | [i, match]
607 | } => Func(\strGetIdentifier);
608 |
609 |
610 | // quicky make modulator synthdefs for Plug
611 | { |name, func|
612 | SynthDef(name, { |out|
613 | var sig = SynthDef.wrap(func);
614 | Out.perform(UGen.methodSelectorForRate(sig.rate), out, sig);
615 | }).add;
616 | } => Func(\plugDef);
617 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/manual/manual-supporting/rhythmic-notation.svg:
--------------------------------------------------------------------------------
1 |
200 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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