├── .gitignore ├── LICENSE ├── Makefile ├── NOTES.txt ├── README.md ├── lib ├── learner-class.js ├── license.js └── synthetickeyboard.js ├── plugins ├── Makefile.inc ├── humandoubler │ └── humandoubler.js ├── pitchcaster │ ├── Makefile │ ├── pitchcaster-blurb.js │ └── pitchcaster.js └── pitchcaster2 │ ├── Makefile │ ├── PC2.txt │ ├── pitchcaster2-blurb.js │ └── pitchcaster2.js └── release ├── killersolos-humandoubler.txt └── killersolos-pitchcaster.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Karl Lehenbauer 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: 3 | cd plugins/pitchcaster; make 4 | 5 | clean: 6 | cd plugins/pitchcaster; make clean 7 | -------------------------------------------------------------------------------- /NOTES.txt: -------------------------------------------------------------------------------- 1 | for testing... 2 | 3 | js -f learner-class.txt -i 4 | 5 | -- 6 | 7 | setting up the IAC bus 8 | 9 | go to utilities > Audio MIDI Setup > Window > Show MIDI Window 10 | 11 | double click on IAC driver 12 | 13 | click on the checkbox Device is online 14 | 15 | Due to limitations in the scripter stuff, probably due to being the first release of the capability, it's hard to get the midi the scripter makes into a track. 16 | 17 | you need to inable the IAC driver 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | logic-pro-x-scripts 2 | =================== 3 | 4 | Scripter plug-in scripts for Logic Pro X 5 | 6 | Logic Pro X has an exciting new capability, the Scripter plug-in, which provides a way to write scripts that can manipulate and create MIDI data in realtime. 7 | 8 | There's a MIDI plugin, now, in the channel strip, so any or all MIDI channels can have scripts active that are doing things to the MIDI data streams. 9 | 10 | The technology is a little basic as far as developer infrastructure is concerned, like the save format that Logic uses has some binary doodads and it doesn't have a way to include files. 11 | 12 | I've already got like three class libraries that are gonna get used for a number of scripts I have in mind, so I went ahead and made the files reasonable and then use a makefile to cat them together to produce the file that can be copied and pasted into Logic's script editor and then saved as a project or a patch in the library or whatever. 13 | 14 | -------------------------------------------------------------------------------- /lib/learner-class.js: -------------------------------------------------------------------------------- 1 | // include file learner-class.js 2 | // 3 | // Copyright © 2013 Karl Lehenbauer. All Rights Reserved. 4 | // 5 | // Released to the public under the Berkeley copyright. 6 | // 7 | // Redistribution and use, with or without modification, is permitted 8 | // provided the copyright notice is maintained. 9 | // 10 | // see the LICENSE file for details 11 | // 12 | 13 | // 14 | // Learner class 15 | // 16 | // invoke learn method repeatedly with pitches and the Learner class will 17 | // count them, learning what notes were struck and how often 18 | // 19 | // Creating an instance of the Learner class also instances an instance of 20 | // the ProbabilityVector class, which Learner's genprobos method will load 21 | // up with an object for each note played during the learning period, with 22 | // the note numbers augmented by the count of that note and a ratio that 23 | // is used for randomly picking notes with the same distribution. 24 | // 25 | // the pick_note() method of the ProbabilityVector class, once the Learner 26 | // and learned and its knowledge passed to the ProbabilityVector, will return 27 | // random note numbers distributed at about the same rate as during the 28 | // learning period. 29 | // 30 | function Learner() { 31 | this.keyboard = new Array(128); 32 | this.probo = new ProbabilityVector(); 33 | 34 | this.reset(); 35 | } 36 | 37 | Learner.prototype.reset = function() { 38 | for (var i = 1; i <= 128; i++) { 39 | this.keyboard[i] = 0; 40 | } 41 | this.notesLearned = 0; 42 | this.uniqueNotesLearned = 0; 43 | this.probo.reset(); 44 | Trace('learner object reset: ' + this); 45 | } 46 | 47 | // learn - learn a note 48 | Learner.prototype.learn = function (pitch) { 49 | if (this.keyboard[pitch] == 0) { 50 | this.uniqueNotesLearned++; 51 | } 52 | this.keyboard[pitch]++; 53 | this.notesLearned++; 54 | } 55 | 56 | // sum - calculate the number of notes learned 57 | Learner.prototype.sum = function () { 58 | var sum = 0; 59 | for (var i = 1; i <= 128; i++) { 60 | sum += this.keyboard[i]; 61 | } 62 | return(sum); 63 | } 64 | 65 | // genprobos - generate data into the probability vector 66 | Learner.prototype.genprobos = function () { 67 | this.probo.reset(); 68 | 69 | // find all the keys that had notes played on them 70 | // and stick them into the probability vector object 71 | // we created when we were instantiated 72 | for (var i = 1; i <= 128; i++) { 73 | if (this.keyboard[i] == 0) { 74 | continue; 75 | } 76 | 77 | this.probo.addprobo(i, this.keyboard[i]); 78 | } 79 | 80 | this.probo.calc_ratios(); 81 | } 82 | 83 | // pick_note - helper function to simplify invoking the pick_note 84 | // function of the probability ector 85 | Learner.prototype.pick_note = function () { 86 | return this.probo.pick_note(); 87 | } 88 | 89 | // 90 | // ProbabilityVector class 91 | // 92 | // invoke addprobo method with note numbers and counts for each note 93 | // 94 | // invoke calc_ratios() to prep it 95 | // 96 | // invoke pick_note() to get note numbers 97 | // 98 | 99 | function ProbabilityVector() { 100 | this.reset(); 101 | } 102 | 103 | // reset - clear the probability vector 104 | ProbabilityVector.prototype.reset = function () { 105 | this.vector = []; 106 | this.sum = 0; 107 | } 108 | 109 | // sum - get a note count of learned notes from the probability vector 110 | ProbabilityVector.prototype.sum = function () { 111 | var sum = 0; 112 | for (var i = 0; i < this.vector.length; i++) { 113 | sum += this.vector[i].count; 114 | } 115 | return (sum); 116 | } 117 | 118 | // create an object with a note and count and push it onto the vector 119 | ProbabilityVector.prototype.addprobo = function (note, count) { 120 | var probo = new Object(); 121 | 122 | probo.note = note; 123 | probo.count = count; 124 | this.vector.push(probo); 125 | this.sum += count; 126 | } 127 | 128 | // calc_ratios - run through each note object in the probability vector 129 | // and add a ratio element that transits 0.0 to 1.0 130 | // for each note's probability of being chosen 131 | ProbabilityVector.prototype.calc_ratios = function () { 132 | var sofar = 0.0; 133 | 134 | for (var i = 0; i < this.vector.length; i++) { 135 | sofar += (this.vector[i].count / this.sum); 136 | this.vector[i].ratio = sofar; 137 | } 138 | } 139 | 140 | // pick_note - pick a note following the desired distribution 141 | ProbabilityVector.prototype.pick_note = function () { 142 | var wantRatio = Math.random(); 143 | 144 | // logger('wantRatio', wantRatio); 145 | 146 | for (var i = 0; i < this.vector.length; i++) { 147 | // logger('this ratio', this.vector[i].ratio); 148 | if (wantRatio < this.vector[i].ratio) { 149 | return this.vector[i].note; 150 | } 151 | } 152 | } 153 | 154 | logger = function (string1, string2) { 155 | return; 156 | var s = new String(); 157 | 158 | Trace(s.concat(string1, ":", string2)); 159 | } 160 | 161 | 162 | // vim: set ts=4 sw=4 sts=4 noet : 163 | 164 | if (0) { 165 | x = new ProbabilityVector(); 166 | 167 | x.addprobo(60,1); 168 | x.addprobo(61,3); 169 | x.addprobo(65,5); 170 | x.addprobo(71,1); 171 | x.addprobo(73,6); 172 | x.addprobo(79,2); 173 | 174 | x.calc_ratios(); 175 | } 176 | -------------------------------------------------------------------------------- /lib/license.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2013, Karl Lehenbauer 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | Redistributions in binary form must reproduce the above copyright notice, this 12 | list of conditions and the following disclaimer in the documentation and/or 13 | other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 19 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 22 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | -------------------------------------------------------------------------------- /lib/synthetickeyboard.js: -------------------------------------------------------------------------------- 1 | // include file synthetickeyboard.js 2 | // 3 | // SyntheticKeyboard class - has methods to turn on and off synthetic keys, 4 | // and to query to see if a synthetic key is on or not. 5 | // 6 | // Also the fetch and store methods provide a way for arbitrary stuff to 7 | // be associated with a pitch (like the pitch of another key that the key 8 | // was translated to) 9 | // 10 | // define a SyntheticKeyboard object. 11 | // 12 | // Reset it with the reset method. 13 | // 14 | // store a value for any pitch using store and fetch it using fetch. 15 | // 16 | // the convenience functions note_on, note_off, and is_note_on are 17 | // provided for common use. 18 | // 19 | function SyntheticKeyboard () { 20 | this.keyboard = new Array(128); 21 | 22 | this.reset(); 23 | } 24 | 25 | SyntheticKeyboard.prototype.reset = function () { 26 | for(var i = 1; i <= 128; i++) { 27 | this.keyboard[i] = 0; 28 | } 29 | } 30 | 31 | SyntheticKeyboard.prototype.store = function (pitch, value) { 32 | this.keyboard[pitch] = value; 33 | } 34 | 35 | SyntheticKeyboard.prototype.fetch = function (pitch) { 36 | return this.keyboard[pitch]; 37 | } 38 | 39 | 40 | SyntheticKeyboard.prototype.note_on = function (pitch) { 41 | this.store(pitch,1); 42 | } 43 | 44 | SyntheticKeyboard.prototype.note_off = function (pitch) { 45 | this.store(pitch,0); 46 | } 47 | 48 | SyntheticKeyboard.prototype.is_note_on = function (pitch) { 49 | return this.fetch(pitch); 50 | } 51 | 52 | 53 | -------------------------------------------------------------------------------- /plugins/Makefile.inc: -------------------------------------------------------------------------------- 1 | 2 | RELEASEDIR=../../release 3 | LIBDIR=../../lib 4 | 5 | LICENSE=$(LIBDIR)/license.js 6 | LEARNER_CLASS=$(LIBDIR)/learner-class.js 7 | SYNTHETICKEYBOARD_CLASS=$(LIBDIR)/synthetickeyboard.js 8 | RANDOM_CLASS=$(LIBDIR)/random.js 9 | 10 | -------------------------------------------------------------------------------- /plugins/humandoubler/humandoubler.js: -------------------------------------------------------------------------------- 1 | // main script humandoubler.js 2 | // 3 | // Killer Solos Human Doubler 4 | // 5 | // 6 | 7 | NeedsTimingInfo = true; 8 | 9 | function Reset() { 10 | } 11 | 12 | function HandleMIDI(event) { 13 | // if it's not a note, don't randomize it, this can end up with 14 | // pitches out of order, which leaves the last pitch not centered. 15 | if (!event instanceof Note) { 16 | event.send(); 17 | return; 18 | ] 19 | 20 | var delay = GetParameter('Delay'); 21 | var wobble = GetParameter('Wobble'); 22 | 23 | var thisDelay = delay + (Math.random() * 2 * wobble) - wobble; 24 | if (thisDelay < 0) { 25 | thisDelay = 0; 26 | } 27 | 28 | event.sendAfterMilliseconds(thisDelay); 29 | } 30 | 31 | var PluginParameters = [ 32 | {name:'Delay', type:'lin', unit:'ms', minValue:0, maxValue:100,numberOfSteps:100,defaultValue:10}, 33 | { name:'Wobble', type:'lin', unit:'ms', 34 | minValue:0, maxValue:50, numberOfSteps:50, defaultValue:5}, 35 | ]; 36 | 37 | 38 | -------------------------------------------------------------------------------- /plugins/pitchcaster/Makefile: -------------------------------------------------------------------------------- 1 | 2 | include ../Makefile.inc 3 | 4 | RELEASEFILE=$(RELEASEDIR)/killersolos-pitchcaster.txt 5 | 6 | BLURB=pitchcaster-blurb.js 7 | PLUGIN=pitchcaster.js 8 | 9 | all: $(RELEASEFILE) 10 | 11 | clean: 12 | rm -f $(RELEASEFILE) 13 | 14 | $(RELEASEFILE): $(BLURB) $(LICENSE) $(LEARNER_CLASS) $(SYNTHETICKEYBOARD_CLASS) $(PLUGIN) 15 | cat $(BLURB) $(LICENSE) $(LEARNER_CLASS) $(SYNTHETICKEYBOARD_CLASS) $(PLUGIN) >$(RELEASEFILE) 16 | 17 | open: 18 | open $(RELEASEFILE) 19 | 20 | -------------------------------------------------------------------------------- /plugins/pitchcaster/pitchcaster-blurb.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Killer Solos Pitchcaster v1.0 4 | 5 | Beware MIDI loops. Watch the video, YouTube user bitwonk. 6 | 7 | Copy this file by doing option-A, option-C in Text Editor. 8 | 9 | Then in Logic add a MIDI effect to a mixer channel and select scripter. 10 | 11 | Hit Save As... Killer Solos Pitchcaster. 12 | 13 | Then click on Open Script in Editor and in the editor, select all (option-A) and hit backspace to wipe out what's there, hit option-V to paste in the program, and off you go. 14 | 15 | Sorry this is clumsy but it's easier to deal with programs as text and the format Logic uses to save scripts into isn't text. 16 | 17 | Eventually there should be something better. 18 | 19 | */ 20 | 21 | -------------------------------------------------------------------------------- /plugins/pitchcaster/pitchcaster.js: -------------------------------------------------------------------------------- 1 | // main script pitchcaster.js 2 | // 3 | // Killer Solos Pitchcaster 4 | // 5 | // 6 | 7 | NeedsTimingInfo = true; 8 | 9 | var noteCounter = 0; 10 | 11 | var minNotesToLearn = 4; 12 | 13 | var learner = new Learner(); 14 | 15 | var syntheticKeyboard = new SyntheticKeyboard(); 16 | 17 | var keyTranslations = new SyntheticKeyboard(); 18 | 19 | function Reset() { 20 | Trace('big Reset invoked'); 21 | syntheticKeyboard.reset(); 22 | keyTranslations.reset(); 23 | } 24 | 25 | Reset(); 26 | 27 | function HandleMIDI(event) { 28 | var mode = GetParameter('Mode'); 29 | 30 | // if in learn mode and it's a note on, learn it. 31 | // also if it's not in learn mode but it has learned less 32 | // than 4 notes, learn it 33 | if (event instanceof NoteOn) { 34 | if (mode === 0 || learner.uniqueNotesLearned < minNotesToLearn) { 35 | learner.learn(event.pitch); 36 | 37 | // if it's not in learn mode and it now knows minNotesToLearn notes, 38 | // generate the probability vector 39 | if (mode != 0 && learner.uniqueNotesLearned == minNotesToLearn) { 40 | learner.genprobos(); 41 | Trace('generated probos due to minNotesToLearn'); 42 | } 43 | } 44 | } 45 | 46 | // if in Learn or Standby or it's in Run but hasn't learned at least 47 | // minNotesToLearn notes, send the event downstream and we're done 48 | if ((mode === 0) || (learner.uniqueNotesLearned < minNotesToLearn)) { 49 | event.send(); 50 | return; 51 | } 52 | 53 | // OK, we're in run mode. 54 | // if it's not a note on or note off, send it along and we're done 55 | if (!(event instanceof NoteOn) && !(event instanceof NoteOff)) { 56 | event.send(); 57 | return; 58 | } 59 | 60 | // ok, we're running and we know it's a note on or note off. 61 | 62 | if (event instanceof NoteOn) { 63 | // decide if we are going to change the note 64 | if (!ChangeIt()) { 65 | var wantNote = event.pitch; 66 | // logger('keep', wantNote); 67 | } else { 68 | var wantNote = learner.pick_note(); 69 | // logger('new', wantNote); 70 | } 71 | 72 | var success = 0; 73 | for (var i = 0; i < 20; i++) { 74 | if (!syntheticKeyboard.is_note_on(wantNote)) { 75 | success = 1; 76 | break; 77 | } 78 | wantNote = learner.pick_note(); 79 | // logger('repick', wantNote); 80 | } 81 | 82 | // if we tried and tried and never found a playable note, bail 83 | // NB not sure about this 84 | if (!success) { 85 | return; 86 | } 87 | 88 | // we picked the note, keep track of the translation 89 | syntheticKeyboard.note_on(wantNote); 90 | keyTranslations.store(event.pitch, wantNote); 91 | 92 | event.pitch = wantNote; 93 | event.send(); 94 | return; 95 | } 96 | 97 | // ok it's a note off, translate it to what we translated it to, 98 | // which may be the same as what we start with, send a note off 99 | // for that, and clear the note on the synthetic keyboard 100 | if (event instanceof NoteOff) { 101 | // logger('>off',event.pitch); 102 | var pitch = keyTranslations.fetch(event.pitch); 103 | keyTranslations.store(event.pitch,0); 104 | // logger('off',pitch); 105 | event.pitch = pitch; 106 | event.send(); 107 | syntheticKeyboard.note_off(pitch); 108 | } 109 | } 110 | 111 | function ChangeIt() { 112 | return (Math.random() < (GetParameter('Change') / 100.0)); 113 | } 114 | 115 | function ParameterChanged(param, value) { 116 | Trace('parameter ' + param + ' changed to ' + value); 117 | if (param === 0) { 118 | if (value === 0) { 119 | learner.reset(); 120 | } 121 | 122 | // if transitioned to standby or run, they might ended learn mode, 123 | // generate the probably vector 124 | if (value > 0) { 125 | // if they turned off learner mode (turned on run mode), 126 | // generate the probability vector 127 | learner.genprobos(); 128 | Trace('generated probos'); 129 | } 130 | } 131 | } 132 | 133 | var PluginParameters = [ 134 | {name:"Mode", type:'menu', valueStrings:["Learn", "Run"], defaultValue:0}, 135 | { name:'Change', type:'lin', unit:'percent', 136 | minValue:0, maxValue:100, numberOfSteps:100, defaultValue:50}, 137 | ]; 138 | 139 | 140 | -------------------------------------------------------------------------------- /plugins/pitchcaster2/Makefile: -------------------------------------------------------------------------------- 1 | 2 | include ../Makefile.inc 3 | 4 | BLURB=pitchcaster2-blurb.js 5 | PLUGIN=pitchcaster2.js 6 | RELEASEFILE=$(RELEASEDIR)/killersolos-pitchcaster2.txt 7 | 8 | all: $(RELEASEFILE) 9 | 10 | clean: 11 | rm -f $(RELEASEFILE) 12 | 13 | $(RELEASEFILE): $(BLURB) $(LICENSE) $(LEARNER_CLASS) $(SYNTHETICKEYBOARD_CLASS) $(PLUGIN) 14 | cat $(BLURB) $(LICENSE) $(LEARNER_CLASS) $(SYNTHETICKEYBOARD_CLASS) $(PLUGIN) >$(RELEASEFILE) 15 | 16 | open: 17 | open $(RELEASEFILE) 18 | -------------------------------------------------------------------------------- /plugins/pitchcaster2/PC2.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | normalize all input to a single octave 4 | 5 | above / below affinity 0 - 50 - 100 6 | 7 | octave range 1 2 3 8 | 9 | octave affinity 0 - 100 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /plugins/pitchcaster2/pitchcaster2-blurb.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Killer Solos Pitchcaster 2 v0.0 4 | 5 | This is not an upgrade to Pitchcaster but rather a variation that employs a modified approach to picking the pitch a changed note will receive. 6 | 7 | Logic Pro is an instrument. It's the one I play better than any other. If you're willing to go there, Pitchcaster and Pitchcaster 2 are themselves new kinds of instruments. 8 | 9 | Beware MIDI loops. Watch the video, YouTube user bitwonk. 10 | 11 | Copy this file by doing option-A, option-C in Text Editor. 12 | 13 | Then in Logic add a MIDI effect to a mixer channel and select scripter. 14 | 15 | Hit Save As... Killer Solos Pitchcaster 2. 16 | 17 | Then click on Open Script in Editor and in the editor, select all (option-A) and hit backspace to wipe out what's there, hit option-V to paste in the program, and off you go. 18 | 19 | Sorry this is clumsy but it's easier to deal with programs as text and the format Logic uses to save scripts into isn't text. 20 | 21 | Eventually there should be something better. 22 | 23 | */ 24 | 25 | -------------------------------------------------------------------------------- /plugins/pitchcaster2/pitchcaster2.js: -------------------------------------------------------------------------------- 1 | // main script pitchcaster.js 2 | // 3 | // Killer Solos Pitchcaster 2 4 | // 5 | // 6 | 7 | NeedsTimingInfo = true; 8 | 9 | var noteCounter = 0; 10 | 11 | var minNotesToLearn = 4; 12 | 13 | var learner = new Learner(); 14 | 15 | var syntheticKeyboard = new SyntheticKeyboard(); 16 | 17 | var keyTranslations = new SyntheticKeyboard(); 18 | 19 | function Reset() { 20 | Trace('big Reset invoked'); 21 | syntheticKeyboard.reset(); 22 | keyTranslations.reset(); 23 | } 24 | 25 | Reset(); 26 | 27 | function HandleMIDI(event) { 28 | var mode = GetParameter('Mode'); 29 | 30 | // if in learn mode and it's a note on, learn it. 31 | // also if it's not in learn mode but it has learned less 32 | // than 4 notes, learn it 33 | if (event instanceof NoteOn) { 34 | if (mode === 0 || learner.uniqueNotesLearned < minNotesToLearn) { 35 | // learn the pitch modulo 12, in other words strip the octave 36 | // and normalize all notes between 0 and 11. 37 | // 0 turns out to be C; the guys who invented MIDI were really smart. 38 | learner.learn(event.pitch % 12); 39 | 40 | // if it's not in learn mode and it now knows minNotesToLearn notes, 41 | // generate the probability vector 42 | if (mode != 0 && learner.uniqueNotesLearned == minNotesToLearn) { 43 | learner.genprobos(); 44 | Trace('generated probos due to minNotesToLearn'); 45 | } 46 | } 47 | } 48 | 49 | // if in Learn or Standby or it's in Run but hasn't learned at least 50 | // minNotesToLearn notes, send the event downstream and we're done 51 | if ((mode === 0) || (learner.uniqueNotesLearned < minNotesToLearn)) { 52 | event.send(); 53 | return; 54 | } 55 | 56 | // OK, we're in run mode. 57 | // if it's not a note on or note off, send it along and we're done 58 | if (!(event instanceof NoteOn) && !(event instanceof NoteOff)) { 59 | event.send(); 60 | return; 61 | } 62 | 63 | // ok, we're running and we know it's a note on or note off. 64 | 65 | if (event instanceof NoteOn) { 66 | var baseNote = event.pitch; 67 | // decide if we are going to change the note 68 | if (!ChangeIt()) { 69 | var wantNote = event.pitch; 70 | // logger('keep', wantNote); 71 | } else { 72 | var wantNote = PickNote(baseNote); 73 | // logger('new', wantNote); 74 | } 75 | 76 | var success = 0; 77 | for (var i = 0; i < 20; i++) { 78 | if (!syntheticKeyboard.is_note_on(wantNote)) { 79 | success = 1; 80 | break; 81 | } 82 | wantNote = PickNote(baseNote); 83 | // logger('repick', wantNote); 84 | } 85 | 86 | // if we tried and tried and never found a playable note, bail 87 | // NB not sure about this 88 | if (!success) { 89 | return; 90 | } 91 | 92 | // we picked the note, keep track of the translation 93 | syntheticKeyboard.note_on(wantNote); 94 | keyTranslations.store(event.pitch, wantNote); 95 | 96 | event.pitch = wantNote; 97 | event.send(); 98 | return; 99 | } 100 | 101 | // ok it's a note off, translate it to what we translated it to, 102 | // which may be the same as what we start with, send a note off 103 | // for that, and clear the note on the synthetic keyboard 104 | if (event instanceof NoteOff) { 105 | // logger('>off',event.pitch); 106 | var pitch = keyTranslations.fetch(event.pitch); 107 | keyTranslations.store(event.pitch,0); 108 | // logger('off',pitch); 109 | event.pitch = pitch; 110 | event.send(); 111 | syntheticKeyboard.note_off(pitch); 112 | } 113 | } 114 | 115 | // ChangeIt - based on a random number and the change parameter, tell 116 | // us if the note should change (1) or not change (0) 117 | function ChangeIt() { 118 | return (Math.random() < (GetParameter('Change') / 100.0)); 119 | } 120 | 121 | // AboveBelow - return 0 if note should be below the base note, 1 if above 122 | function AboveBelow() { 123 | return (Math.random() < (GetParameter('Below-Above') / 100.0)); 124 | } 125 | 126 | // PickNote - pick a note 127 | // 128 | // in this case learner.picknote() is only returning values from 0 to 11, so 129 | // we need to look at our above/below parameters and octave parameters to 130 | // come up with a relevant note 131 | function PickNote(baseNote) { 132 | var wantNote = learner.pick_note(); 133 | 134 | if (!AboveBelow()) { 135 | return baseNote - wantNote; 136 | } else { 137 | return baseNote + wantNote; 138 | } 139 | } 140 | 141 | function ParameterChanged(param, value) { 142 | Trace('parameter ' + param + ' changed to ' + value); 143 | if (param === 0) { 144 | if (value === 0) { 145 | learner.reset(); 146 | } 147 | 148 | // if transitioned to standby or run, they might ended learn mode, 149 | // generate the probably vector 150 | if (value > 0) { 151 | // if they turned off learner mode (turned on run mode), 152 | // generate the probability vector 153 | learner.genprobos(); 154 | Trace('generated probos'); 155 | } 156 | } 157 | } 158 | 159 | var PluginParameters = [ 160 | {name:"Mode", type:'menu', valueStrings:["Learn", "Run"], defaultValue:0}, 161 | { name:'Change', type:'lin', unit:'percent', 162 | minValue:0, maxValue:100, numberOfSteps:100, defaultValue:50}, 163 | { name:'Below-Above', type:'lin', unit:'percent', 164 | minValue:0, maxValue:100, numberOfSteps:100, defaultValue:50}, 165 | { name:'Octave-Affinity', type:'log', unit:'percent', 166 | minValue:0, maxValue:100, numberOfSteps:100, defaultValue:50}, 167 | ]; 168 | 169 | -------------------------------------------------------------------------------- /release/killersolos-humandoubler.txt: -------------------------------------------------------------------------------- 1 | // main script humandoubler.js 2 | // 3 | // Killer Solos Human Doubler 4 | // 5 | // 6 | 7 | NeedsTimingInfo = true; 8 | 9 | function Reset() { 10 | } 11 | 12 | function HandleMIDI(event) { 13 | // if it's not a note, don't randomize it, this can end up with 14 | // pitches out of order, which leaves the last pitch not centered. 15 | if (!event instanceof Note) { 16 | event.send(); 17 | return; 18 | ] 19 | 20 | var delay = GetParameter('Delay'); 21 | var wobble = GetParameter('Wobble'); 22 | 23 | var thisDelay = delay + (Math.random() * 2 * wobble) - wobble; 24 | if (thisDelay < 0) { 25 | thisDelay = 0; 26 | } 27 | 28 | event.sendAfterMilliseconds(thisDelay); 29 | } 30 | 31 | var PluginParameters = [ 32 | {name:'Delay', type:'lin', unit:'ms', minValue:0, maxValue:100,numberOfSteps:100,defaultValue:10}, 33 | { name:'Wobble', type:'lin', unit:'ms', 34 | minValue:0, maxValue:50, numberOfSteps:50, defaultValue:5}, 35 | ]; 36 | 37 | 38 | -------------------------------------------------------------------------------- /release/killersolos-pitchcaster.txt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Killer Solos Pitchcaster v1.0 4 | 5 | Beware MIDI loops. Watch the video, YouTube user bitwonk. 6 | 7 | Copy this file by doing option-A, option-C in Text Editor. 8 | 9 | Then in Logic add a MIDI effect to a mixer channel and select scripter. 10 | 11 | Hit Save As... Killer Solos Pitchcaster. 12 | 13 | Then click on Open Script in Editor and in the editor, select all (option-A) and hit backspace to wipe out what's there, hit option-V to paste in the program, and off you go. 14 | 15 | Sorry this is clumsy but it's easier to deal with programs as text and the format Logic uses to save scripts into isn't text. 16 | 17 | Eventually there should be something better. 18 | 19 | */ 20 | 21 | /* 22 | Copyright (c) 2013, Karl Lehenbauer 23 | All rights reserved. 24 | 25 | Redistribution and use in source and binary forms, with or without modification, 26 | are permitted provided that the following conditions are met: 27 | 28 | Redistributions of source code must retain the above copyright notice, this 29 | list of conditions and the following disclaimer. 30 | 31 | Redistributions in binary form must reproduce the above copyright notice, this 32 | list of conditions and the following disclaimer in the documentation and/or 33 | other materials provided with the distribution. 34 | 35 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 36 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 37 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 38 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 39 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 40 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 41 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 42 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 43 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 44 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 45 | */ 46 | // include file learner-class.js 47 | // 48 | // Copyright © 2013 Karl Lehenbauer. All Rights Reserved. 49 | // 50 | // Released to the public under the Berkeley copyright. 51 | // 52 | // Redistribution and use, with or without modification, is permitted 53 | // provided the copyright notice is maintained. 54 | // 55 | // see the LICENSE file for details 56 | // 57 | 58 | // 59 | // Learner class 60 | // 61 | // invoke learn method repeatedly with pitches and the Learner class will 62 | // count them, learning what notes were struck and how often 63 | // 64 | // Creating an instance of the Learner class also instances an instance of 65 | // the ProbabilityVector class, which Learner's genprobos method will load 66 | // up with an object for each note played during the learning period, with 67 | // the note numbers augmented by the count of that note and a ratio that 68 | // is used for randomly picking notes with the same distribution. 69 | // 70 | // the pick_note() method of the ProbabilityVector class, once the Learner 71 | // and learned and its knowledge passed to the ProbabilityVector, will return 72 | // random note numbers distributed at about the same rate as during the 73 | // learning period. 74 | // 75 | function Learner() { 76 | this.keyboard = new Array(128); 77 | this.probo = new ProbabilityVector(); 78 | 79 | this.reset(); 80 | } 81 | 82 | Learner.prototype.reset = function() { 83 | for (var i = 1; i <= 128; i++) { 84 | this.keyboard[i] = 0; 85 | } 86 | this.notesLearned = 0; 87 | this.uniqueNotesLearned = 0; 88 | this.probo.reset(); 89 | Trace('learner object reset: ' + this); 90 | } 91 | 92 | // learn - learn a note 93 | Learner.prototype.learn = function (pitch) { 94 | if (this.keyboard[pitch] == 0) { 95 | this.uniqueNotesLearned++; 96 | } 97 | this.keyboard[pitch]++; 98 | this.notesLearned++; 99 | } 100 | 101 | // sum - calculate the number of notes learned 102 | Learner.prototype.sum = function () { 103 | var sum = 0; 104 | for (var i = 1; i <= 128; i++) { 105 | sum += this.keyboard[i]; 106 | } 107 | return(sum); 108 | } 109 | 110 | // genprobos - generate data into the probability vector 111 | Learner.prototype.genprobos = function () { 112 | this.probo.reset(); 113 | 114 | // find all the keys that had notes played on them 115 | // and stick them into the probability vector object 116 | // we created when we were instantiated 117 | for (var i = 1; i <= 128; i++) { 118 | if (this.keyboard[i] == 0) { 119 | continue; 120 | } 121 | 122 | this.probo.addprobo(i, this.keyboard[i]); 123 | } 124 | 125 | this.probo.calc_ratios(); 126 | } 127 | 128 | // pick_note - helper function to simplify invoking the pick_note 129 | // function of the probability ector 130 | Learner.prototype.pick_note = function () { 131 | return this.probo.pick_note(); 132 | } 133 | 134 | // 135 | // ProbabilityVector class 136 | // 137 | // invoke addprobo method with note numbers and counts for each note 138 | // 139 | // invoke calc_ratios() to prep it 140 | // 141 | // invoke pick_note() to get note numbers 142 | // 143 | 144 | function ProbabilityVector() { 145 | this.reset(); 146 | } 147 | 148 | // reset - clear the probability vector 149 | ProbabilityVector.prototype.reset = function () { 150 | this.vector = []; 151 | this.sum = 0; 152 | } 153 | 154 | // sum - get a note count of learned notes from the probability vector 155 | ProbabilityVector.prototype.sum = function () { 156 | var sum = 0; 157 | for (var i = 0; i < this.vector.length; i++) { 158 | sum += this.vector[i].count; 159 | } 160 | return (sum); 161 | } 162 | 163 | // create an object with a note and count and push it onto the vector 164 | ProbabilityVector.prototype.addprobo = function (note, count) { 165 | var probo = new Object(); 166 | 167 | probo.note = note; 168 | probo.count = count; 169 | this.vector.push(probo); 170 | this.sum += count; 171 | } 172 | 173 | // calc_ratios - run through each note object in the probability vector 174 | // and add a ratio element that transits 0.0 to 1.0 175 | // for each note's probability of being chosen 176 | ProbabilityVector.prototype.calc_ratios = function () { 177 | var sofar = 0.0; 178 | 179 | for (var i = 0; i < this.vector.length; i++) { 180 | sofar += (this.vector[i].count / this.sum); 181 | this.vector[i].ratio = sofar; 182 | } 183 | } 184 | 185 | // pick_note - pick a note following the desired distribution 186 | ProbabilityVector.prototype.pick_note = function () { 187 | var wantRatio = Math.random(); 188 | 189 | // logger('wantRatio', wantRatio); 190 | 191 | for (var i = 0; i < this.vector.length; i++) { 192 | // logger('this ratio', this.vector[i].ratio); 193 | if (wantRatio < this.vector[i].ratio) { 194 | return this.vector[i].note; 195 | } 196 | } 197 | } 198 | 199 | logger = function (string1, string2) { 200 | return; 201 | var s = new String(); 202 | 203 | Trace(s.concat(string1, ":", string2)); 204 | } 205 | 206 | 207 | // vim: set ts=4 sw=4 sts=4 noet : 208 | 209 | if (0) { 210 | x = new ProbabilityVector(); 211 | 212 | x.addprobo(60,1); 213 | x.addprobo(61,3); 214 | x.addprobo(65,5); 215 | x.addprobo(71,1); 216 | x.addprobo(73,6); 217 | x.addprobo(79,2); 218 | 219 | x.calc_ratios(); 220 | } 221 | // include file synthetickeyboard.js 222 | // 223 | // SyntheticKeyboard class - has methods to turn on and off synthetic keys, 224 | // and to query to see if a synthetic key is on or not. 225 | // 226 | // Also the fetch and store methods provide a way for arbitrary stuff to 227 | // be associated with a pitch (like the pitch of another key that the key 228 | // was translated to) 229 | // 230 | // define a SyntheticKeyboard object. 231 | // 232 | // Reset it with the reset method. 233 | // 234 | // store a value for any pitch using store and fetch it using fetch. 235 | // 236 | // the convenience functions note_on, note_off, and is_note_on are 237 | // provided for common use. 238 | // 239 | function SyntheticKeyboard () { 240 | this.keyboard = new Array(128); 241 | 242 | this.reset(); 243 | } 244 | 245 | SyntheticKeyboard.prototype.reset = function () { 246 | for(var i = 1; i <= 128; i++) { 247 | this.keyboard[i] = 0; 248 | } 249 | } 250 | 251 | SyntheticKeyboard.prototype.store = function (pitch, value) { 252 | this.keyboard[pitch] = value; 253 | } 254 | 255 | SyntheticKeyboard.prototype.fetch = function (pitch) { 256 | return this.keyboard[pitch]; 257 | } 258 | 259 | 260 | SyntheticKeyboard.prototype.note_on = function (pitch) { 261 | this.store(pitch,1); 262 | } 263 | 264 | SyntheticKeyboard.prototype.note_off = function (pitch) { 265 | this.store(pitch,0); 266 | } 267 | 268 | SyntheticKeyboard.prototype.is_note_on = function (pitch) { 269 | return this.fetch(pitch); 270 | } 271 | 272 | 273 | // main script pitchcaster.js 274 | // 275 | // Killer Solos Pitchcaster 276 | // 277 | // 278 | 279 | NeedsTimingInfo = true; 280 | 281 | var noteCounter = 0; 282 | 283 | var minNotesToLearn = 4; 284 | 285 | var learner = new Learner(); 286 | 287 | var syntheticKeyboard = new SyntheticKeyboard(); 288 | 289 | var keyTranslations = new SyntheticKeyboard(); 290 | 291 | function Reset() { 292 | Trace('big Reset invoked'); 293 | syntheticKeyboard.reset(); 294 | keyTranslations.reset(); 295 | } 296 | 297 | Reset(); 298 | 299 | function HandleMIDI(event) { 300 | var mode = GetParameter('Mode'); 301 | 302 | // if in learn mode and it's a note on, learn it. 303 | // also if it's not in learn mode but it has learned less 304 | // than 4 notes, learn it 305 | if (event instanceof NoteOn) { 306 | if (mode === 0 || learner.uniqueNotesLearned < minNotesToLearn) { 307 | learner.learn(event.pitch); 308 | 309 | // if it's not in learn mode and it now knows minNotesToLearn notes, 310 | // generate the probability vector 311 | if (mode != 0 && learner.uniqueNotesLearned == minNotesToLearn) { 312 | learner.genprobos(); 313 | Trace('generated probos due to minNotesToLearn'); 314 | } 315 | } 316 | } 317 | 318 | // if in Learn or Standby or it's in Run but hasn't learned at least 319 | // minNotesToLearn notes, send the event downstream and we're done 320 | if ((mode === 0) || (learner.uniqueNotesLearned < minNotesToLearn)) { 321 | event.send(); 322 | return; 323 | } 324 | 325 | // OK, we're in run mode. 326 | // if it's not a note on or note off, send it along and we're done 327 | if (!(event instanceof NoteOn) && !(event instanceof NoteOff)) { 328 | event.send(); 329 | return; 330 | } 331 | 332 | // ok, we're running and we know it's a note on or note off. 333 | 334 | if (event instanceof NoteOn) { 335 | // decide if we are going to change the note 336 | if (!ChangeIt()) { 337 | var wantNote = event.pitch; 338 | // logger('keep', wantNote); 339 | } else { 340 | var wantNote = learner.pick_note(); 341 | // logger('new', wantNote); 342 | } 343 | 344 | var success = 0; 345 | for (var i = 0; i < 20; i++) { 346 | if (!syntheticKeyboard.is_note_on(wantNote)) { 347 | success = 1; 348 | break; 349 | } 350 | wantNote = learner.pick_note(); 351 | // logger('repick', wantNote); 352 | } 353 | 354 | // if we tried and tried and never found a playable note, bail 355 | // NB not sure about this 356 | if (!success) { 357 | return; 358 | } 359 | 360 | // we picked the note, keep track of the translation 361 | syntheticKeyboard.note_on(wantNote); 362 | keyTranslations.store(event.pitch, wantNote); 363 | 364 | event.pitch = wantNote; 365 | event.send(); 366 | return; 367 | } 368 | 369 | // ok it's a note off, translate it to what we translated it to, 370 | // which may be the same as what we start with, send a note off 371 | // for that, and clear the note on the synthetic keyboard 372 | if (event instanceof NoteOff) { 373 | // logger('>off',event.pitch); 374 | var pitch = keyTranslations.fetch(event.pitch); 375 | keyTranslations.store(event.pitch,0); 376 | // logger('off',pitch); 377 | event.pitch = pitch; 378 | event.send(); 379 | syntheticKeyboard.note_off(pitch); 380 | } 381 | } 382 | 383 | function ChangeIt() { 384 | return (Math.random() < (GetParameter('Change') / 100.0)); 385 | } 386 | 387 | function ParameterChanged(param, value) { 388 | Trace('parameter ' + param + ' changed to ' + value); 389 | if (param === 0) { 390 | if (value === 0) { 391 | learner.reset(); 392 | } 393 | 394 | // if transitioned to standby or run, they might ended learn mode, 395 | // generate the probably vector 396 | if (value > 0) { 397 | // if they turned off learner mode (turned on run mode), 398 | // generate the probability vector 399 | learner.genprobos(); 400 | Trace('generated probos'); 401 | } 402 | } 403 | } 404 | 405 | var PluginParameters = [ 406 | {name:"Mode", type:'menu', valueStrings:["Learn", "Run"], defaultValue:0}, 407 | { name:'Change', type:'lin', unit:'percent', 408 | minValue:0, maxValue:100, numberOfSteps:100, defaultValue:50}, 409 | ]; 410 | 411 | 412 | --------------------------------------------------------------------------------