├── .gitignore ├── LICENSE ├── README.md ├── code_snippets ├── lpxs_active_notes.js ├── lpxs_calculate_timing.js ├── lpxs_capture_time_intervals.js ├── lpxs_events.js ├── lpxs_example_script_template.js ├── lpxs_idle_function.js ├── lpxs_parameter_changes.js ├── lpxs_parameter_controls.js ├── lpxs_random_values.js ├── lpxs_reset_function.js ├── lpxs_target_menu_1.js └── lpxs_target_menu_2.js ├── design_patterns ├── oc_building_chords.js ├── oc_custom_cc_messages.js ├── oc_managing_note_lengths.js ├── oc_managing_scales.js ├── oc_parameter_change_preventing_recursion.js ├── oc_tracking_playhead_across_process_blocks.js └── oc_weighted_random_selection.js ├── example_scripts ├── oc_beat_based_probability_gate.js ├── oc_live_humanize.js ├── oc_live_velocity_transform.js ├── oc_scripter_adsr.js ├── oc_scripter_modulator.js ├── oc_transposer.js └── random_devices.js ├── generative_music ├── oc_eno_bloom.js ├── oc_euclid.js ├── oc_live_chord_generator.js ├── oc_random_devices.js ├── oc_weighted_random_chord_progression_generation.js └── oc_weighted_random_melody_generator.js └── simple_scripter_tricks ├── controls_full_keyboard.js ├── global_reverse_pitches.js ├── simple_transposition.js ├── transpose_by_pitch.js ├── transpose_to_range.js └── voice_limiter.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | .vscode/launch.json 4 | untitled.bbprojectd/philipregan.bbprojectsettings 5 | untitled.bbprojectd/project.bbprojectdata 6 | untitled.bbprojectd/Scratchpad.txt 7 | untitled.bbprojectd/Unix Worksheet.worksheet 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This repository is released under the MIT License. 2 | 3 | Permissions 4 | * Commercial use 5 | * Modification 6 | * Distribution 7 | * Private use 8 | 9 | Limitations 10 | * x Liability 11 | * x Warranty 12 | 13 | Conditions 14 | * License and copyright notice 15 | 16 | MIT License 17 | 18 | Copyright (c) 2022 Philip Regan and Pilcrow Records 19 | 20 | Permission is hereby granted, free of charge, to any person obtaining a copy 21 | of this software and associated documentation files (the "Software"), to deal 22 | in the Software without restriction, including without limitation the rights 23 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 24 | copies of the Software, and to permit persons to whom the Software is 25 | furnished to do so, subject to the following conditions: 26 | 27 | The above copyright notice and this permission notice shall be included in all 28 | copies or substantial portions of the Software. 29 | 30 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 31 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 32 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 33 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 34 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 35 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 36 | SOFTWARE. 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LogicProScripter 2 | 3 | This repository contains code examples and standalone scripts for Logic Pro's Scripter MIDI plugin. This repository is offered as a free supplement to [The Complete Guide to Logic Pro's Scripter by Philip Regan](http://books.apple.com/us/book/id1611546793). 4 | 5 | Files with the "lpxs" prefix are examples scripts specific to certain aspects of Scripter, and tend to be more code snippets. 6 | 7 | Files with the "oc" prefix are complete scripts which can... 8 | * recreate in Scripter stock MIDI functionality in Logic Pro 9 | * expands upon the Logic Pro supplied scripts in Logic Pro 10 | * Leverages principle taught in [The Complete Guide to Logic Pro's Scripter by Philip Regan](http://books.apple.com/us/book/id1611546793) to show expanded musical possibilities with Scripter. 11 | 12 | If you used any of these scripts to make your music, please let me know! 13 | 14 | Pull requests are welcome. 15 | 16 | Other Scripter repos found on Github (licenses for reuse may vary): 17 | 18 | * https://github.com/lehenbauer/logic-pro-x-scripts 19 | * https://github.com/f-pimentel-work/Logic-Pro-X-JS-Scripts 20 | * https://github.com/joe-robot/Logic-Scripter---Random-Melody-Generator/ 21 | * https://github.com/kabirnagral/LPXscripts 22 | * https://github.com/djtech42/Logic-Pro-X-MIDI-Scripts 23 | 24 | This repository is released under the MIT License. 25 | 26 | Permissions 27 | * ✓ Commercial use 28 | * ✓ Modification 29 | * ✓ Distribution 30 | * ✓ Private use 31 | 32 | Limitations 33 | * x Liability 34 | * x Warranty 35 | 36 | Conditions 37 | * License and copyright notice 38 | 39 | MIT License 40 | 41 | Copyright (c) 2022 Philip Regan and Pilcrow Records 42 | 43 | Permission is hereby granted, free of charge, to any person obtaining a copy 44 | of this software and associated documentation files (the "Software"), to deal 45 | in the Software without restriction, including without limitation the rights 46 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 47 | copies of the Software, and to permit persons to whom the Software is 48 | furnished to do so, subject to the following conditions: 49 | 50 | The above copyright notice and this permission notice shall be included in all 51 | copies or substantial portions of the Software. 52 | 53 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 54 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 55 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 56 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 57 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 58 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 59 | SOFTWARE. 60 | -------------------------------------------------------------------------------- /code_snippets/lpxs_active_notes.js: -------------------------------------------------------------------------------- 1 | var activeNotes = []; 2 | 3 | function HandleMIDI( event ) { 4 | if ( event instanceof NoteOn ) { 5 | activeNotes.push( event ); 6 | } 7 | else if ( event instanceof NoteOff ) { 8 | for ( i = 0; i < activeNotes.length ; i++ ) { 9 | if ( activeNotes[i].pitch == event.pitch ) { 10 | activeNotes.splice(i, 1); 11 | break; 12 | } 13 | } 14 | } 15 | event.send(); 16 | } 17 | 18 | var ACTIVE_NOTES = {}; 19 | 20 | function HandleMIDI( event ) { 21 | const pitch = event.pitch; 22 | if ( event instanceof NoteOn ) { 23 | var notes = ACTIVE_NOTES[ pitch ]; 24 | if ( !notes ) { 25 | notes = []; 26 | } 27 | notes.push( event ); 28 | ACTIVE_NOTES[ pitch ] = notes; 29 | } else if ( event instanceof NoteOff ) { 30 | var notes = ACTIVE_NOTES[ pitch ]; 31 | if ( notes ) { 32 | var note = notes.pop(); 33 | ACTIVE_NOTES[ pitch ] = notes; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /code_snippets/lpxs_calculate_timing.js: -------------------------------------------------------------------------------- 1 | var NeedsTimingInfo = true; 2 | 3 | function ProcessMIDI() { 4 | 5 | var timingInfo = GetTimingInfo(); 6 | 7 | // convert beat position to ms 8 | var ms = Math.round(timingInfo.blockStartBeat * (60000. / timingInfo.tempo)); 9 | 10 | // get the current Beat 11 | var Beat = Math.floor(timingInfo.blockStartBeat); 12 | 13 | // know when in a new Bar 14 | var Bar = timingInfo.currBlockStart % timingInfo.meterNumerator 15 | } 16 | 17 | function convertMsToBeat ( ms , tempo ) { 18 | // 60000 = 60 seconds in milliseconds 19 | return ms / ( 60000 / tempo ); 20 | } 21 | 22 | function convertBeatToMs( beat , tempo ) { 23 | return Math.round( beat * (60000. / tempo)); 24 | } 25 | -------------------------------------------------------------------------------- /code_snippets/lpxs_capture_time_intervals.js: -------------------------------------------------------------------------------- 1 | var NeedsTimingInfo = true; 2 | var lastBlockStart = -1; 3 | 4 | function ProcessMIDI() { 5 | 6 | var timingInfo = GetTimingInfo(); 7 | if ( timingInfo.playing ) { 8 | var currBlockStart = Math.floor(timingInfo.blockStartBeat); 9 | if ( currBlockStart != lastBlockStart && ( currBlockStart <= 1 || currBlockStart % timingInfo.meterNumerator == 0 ) ) { 10 | // do something 11 | lastBlockStart = currBlockStart; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /code_snippets/lpxs_events.js: -------------------------------------------------------------------------------- 1 | // creating NoteOn and NoteOff 2 | var noteOn = new NoteOn; 3 | noteOn.pitch = 60; 4 | noteOn.velocity = 100; 5 | noteOn.sendAfterMilliseconds(100); 6 | var noteOff = new NoteOff(noteOn); 7 | noteOff.sendAfterMilliseconds(200); 8 | 9 | // creating sustain pedal events 10 | var sustainPedal = new ControlChange; 11 | sustainPedal.number=64; 12 | sustainPedal.value=0; // release the sustain pedal 13 | sustainPedal.send(); 14 | sustainPedal.value= 64; // press the sustain pedal 15 | sustainPedal.sendAfterMilliseconds(5); 16 | -------------------------------------------------------------------------------- /code_snippets/lpxs_example_script_template.js: -------------------------------------------------------------------------------- 1 | /* 2 | Name: 3 | Author(s): 4 | Purpose: 5 | * This template is provided to show how Scripter's monolithic scripts can be 6 | best organized for easy maintenance. 7 | Information: 8 | Change History: 9 | YY_MM_DD_V_##_##_##: Started script 10 | */ 11 | 12 | /* 13 | SCRIPTER GLOBAL VARIABLES 14 | */ 15 | 16 | var NeedsTimingInfo = true; 17 | var PluginParameters = []; 18 | 19 | /* 20 | CUSTOM GLOBAL VARIABLES 21 | */ 22 | 23 | /* 24 | SCRIPTER FUNCTIONS 25 | */ 26 | 27 | function HandleMIDI(event) { 28 | event.send(); 29 | } 30 | 31 | function ProcessMIDI() { 32 | var timingInfo = GetTimingInfo(); 33 | } 34 | 35 | function ParameterChanged(param, value) { 36 | switch ( param ) { 37 | case value: 38 | 39 | break; 40 | 41 | default: 42 | break; 43 | } 44 | } 45 | 46 | function Reset() { 47 | /* 48 | Convenience function which can help with maintaining settings in a script. 49 | Cwalled at these events: 50 | - When the transport is started. 51 | - When the plug-in is bypassed in the MIDI Effects chain. 52 | - When called directly elsewhere in the code. 53 | */ 54 | } 55 | 56 | function Idle() { 57 | /* 58 | Primarily used to handle Parameter Control changes during playing so that 59 | the changes do not interrupt performance. 60 | Called every few seconds regardless of tempo or time signature 61 | */ 62 | } 63 | 64 | /* 65 | CUSTOM FUNCTIONS 66 | */ 67 | 68 | /* TESTING: comment out before running in Scripter */ 69 | test(); 70 | function test() { 71 | 72 | } 73 | 74 | /* 75 | PARAMETER CONTROL MANAGEMENT 76 | 77 | -> Remember to update ParameterChanged() 78 | */ -------------------------------------------------------------------------------- /code_snippets/lpxs_idle_function.js: -------------------------------------------------------------------------------- 1 | var PluginParameters = []; 2 | 3 | PluginParameters.push({ 4 | name:"Add Slider", 5 | type:"checkbox", 6 | defaultValue:0 7 | }); 8 | 9 | var newSlider = { 10 | name:"Linear Slider", 11 | type:"lin", 12 | minValue:0, 13 | maxValue:100, 14 | numberOfSteps:100, 15 | defaultValue:50 16 | }; 17 | 18 | function Idle() { 19 | if ( GetParameter("Add Slider") == true && PluginParameters.length < 2 ) { 20 | PluginParameters.push(newSlider); 21 | UpdatePluginParameters(); 22 | } else if ( GetParameter("Add Slider") == false && PluginParameters.length > 1 ) { 23 | PluginParameters.pop(); 24 | UpdatePluginParameters(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /code_snippets/lpxs_parameter_changes.js: -------------------------------------------------------------------------------- 1 | var PluginParameters = []; 2 | 3 | PluginParameters.push({ 4 | name:"Add Slider", 5 | type:"checkbox", 6 | defaultValue:0 7 | }); 8 | 9 | var newSlider = { 10 | name:"Linear Slider", 11 | type:"lin", 12 | minValue:0, 13 | maxValue:100, 14 | numberOfSteps:100, 15 | defaultValue:50 16 | }; 17 | 18 | function HandleMIDI(event) { 19 | var value = GetParameter("Linear Slider"); // returns 0 20 | SetParameter(1, 0.5); 21 | } 22 | 23 | function ParameterChanged( param , value ) { 24 | switch (param) { 25 | case 0: 26 | if ( value == true && PluginParameters.length < 2 ) { 27 | PluginParameters.push(newSlider); 28 | } else if ( value == false && PluginParameters.length > 1 ) { 29 | PluginParameters.pop(); 30 | } 31 | UpdatePluginParameters(); 32 | break; 33 | case 1: 34 | Trace(value); 35 | break; 36 | default: 37 | Trace("ParameterChanged Error"); 38 | } 39 | } 40 | 41 | 42 | -------------------------------------------------------------------------------- /code_snippets/lpxs_parameter_controls.js: -------------------------------------------------------------------------------- 1 | // Percent slider set to 50% 2 | 3 | PluginParameters.push({ 4 | name:"Linear Slider", 5 | type:"lin", unit:"\%", 6 | minValue:0, 7 | maxValue:100, 8 | numberOfSteps:100, 9 | defaultValue:50 10 | }); 11 | 12 | // Slider which selects between a range of -12–12, set to 0 13 | 14 | PluginParameters.push({ 15 | name:"Linear Slider", 16 | type:"lin", 17 | minValue:-12, 18 | maxValue:12, 19 | numberOfSteps:24, 20 | defaultValue:0 21 | }); 22 | 23 | // Logarithmic slider with a similar range to a track fader 24 | 25 | PluginParameters.push({ 26 | name:"Logarithmic Slider", 27 | type:"log", 28 | minValue:0, 29 | maxValue:95, 30 | numberOfSteps:950, 31 | defaultValue:6.0 32 | }); 33 | 34 | // Pulldown menu with 3 items, and the last item set as the default 35 | 36 | PluginParameters.push({ 37 | name:"Pulldown Menu", 38 | type:"menu", 39 | valueStrings:["Item 0", "Item 1", "Item 2"], 40 | defaultValue:2 41 | }); 42 | 43 | // Radio button with ``On'' and ``Off'' labels 44 | 45 | PluginParameters.push({ 46 | name:"Radio Buttons", 47 | type:"menu", 48 | valueStrings:["On", "Off"] 49 | }); 50 | 51 | // Checkbox with the box set to be checked 52 | 53 | PluginParameters.push({ 54 | name:"Checkbox", 55 | type:"checkbox", 56 | defaultValue:1 57 | }); 58 | 59 | // Momentary Button 60 | 61 | PluginParameters.push({ 62 | name: "Momentary Button", 63 | type: "momentary", 64 | disableAutomation: false 65 | }); 66 | 67 | // Text Control 68 | 69 | PluginParameters.push({ 70 | name: "A Label for Control Group", 71 | type: "text" 72 | }); 73 | 74 | // Target Menu 75 | 76 | PluginParameters.push({ 77 | name: "ModWheel Target Menu", 78 | type: "target" 79 | }); 80 | -------------------------------------------------------------------------------- /code_snippets/lpxs_random_values.js: -------------------------------------------------------------------------------- 1 | // Simple probability gate 2 | 3 | function probabilityGate( threshold ) { 4 | var r = Math.ceil( Math.random() * 100 ); 5 | if ( r <= threshold ) { 6 | return true; 7 | } 8 | return false; 9 | } 10 | 11 | // Generate a random number within a specific range 12 | 13 | var r = rInt(1, 100); 14 | function rInt(min, max) { 15 | return Math.floor(Math.random() * (max - min + 1)) + min; 16 | } 17 | 18 | // Select a random value from an array 19 | 20 | var array = [60, 62, 64, 65, 67, 69, 71]; 21 | var rIndex = rInt(0, array.length - 1]; 22 | 23 | // Select multiple unique values from an array 24 | 25 | var array = [60, 62, 64, 65, 67, 69, 71]; 26 | var values = getRandomUniqueValuesFromArray( array, 3 ); 27 | 28 | function getRandomUniqueValuesFromArray( array, numValues = 1 ) { 29 | // copy the array so we don't destroy the original 30 | var src = array; 31 | var cache = []; 32 | for ( n = 0 ; n < numValues ; n++ ) { 33 | var r = rInt( 0 , src.length - 1 ); 34 | var value = src.splice( r , 1 ); 35 | cache.push( value[0] ); 36 | } 37 | return cache; 38 | } 39 | -------------------------------------------------------------------------------- /code_snippets/lpxs_reset_function.js: -------------------------------------------------------------------------------- 1 | var PluginParameters = []; 2 | 3 | PluginParameters.push({ 4 | name: "Reset", 5 | type: "momentary", 6 | }); 7 | 8 | PluginParameters.push({ 9 | name:"Linear Slider", 10 | type:"lin" 11 | }); 12 | 13 | function Reset() { 14 | SetParameter(1, 0.0); 15 | } 16 | 17 | function ParameterChanged( param , value ) { 18 | switch ( param ) { 19 | case 0: 20 | Reset(); 21 | break; 22 | case 1: 23 | // do nothing 24 | break 25 | default: 26 | // do nothing 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /code_snippets/lpxs_target_menu_1.js: -------------------------------------------------------------------------------- 1 | var PluginParameters = []; 2 | 3 | PluginParameters.push({ 4 | name:"Modwheel", 5 | type:"target" 6 | }); 7 | 8 | function HandleMIDI(event) { 9 | if ((event instanceof ControlChange) && (event.number == 1)) { 10 | var targetEvent = new TargetEvent(); 11 | targetEvent.target = "Modwheel"; 12 | targetEvent.value = event.value / 127; 13 | targetEvent.send(); 14 | } else { 15 | event.send(); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /code_snippets/lpxs_target_menu_2.js: -------------------------------------------------------------------------------- 1 | var PluginParameters = []; 2 | 3 | PluginParameters.push({ 4 | name:"Linear Slider", 5 | type:"lin", 6 | minValue:0, 7 | maxValue:100, 8 | defaultValue:50, 9 | numberOfSteps:100 10 | }); 11 | 12 | function HandleMIDI(event) { 13 | if ( event instanceof NoteOn ) { 14 | event.detune += GetParameter("Linear Slider"); 15 | } 16 | event.send(); 17 | } 18 | -------------------------------------------------------------------------------- /design_patterns/oc_building_chords.js: -------------------------------------------------------------------------------- 1 | const INTERVALS_LIB = { 2 | INTERVAL_KEY_PER_UNI : 0, 3 | INTERVAL_KEY_MIN_2ND : 1, 4 | INTERVAL_KEY_MAJ_2ND : 2, 5 | INTERVAL_KEY_MIN_3RD : 3, 6 | INTERVAL_KEY_MAJ_3RD : 4, 7 | INTERVAL_KEY_PER_4TH : 5, 8 | INTERVAL_KEY_AU4_DI5 : 6, 9 | INTERVAL_KEY_PER_5TH : 7, 10 | INTERVAL_KEY_MIN_6TH : 8, 11 | INTERVAL_KEY_MAJ_6TH : 9, 12 | INTERVAL_KEY_MIN_7TH : 10, 13 | INTERVAL_KEY_MAJ_7TH : 11, 14 | INTERVAL_KEY_PER_8TH : 12 15 | }; 16 | 17 | const CHORD_TEMPLATES_LIB = { 18 | "None" : [], 19 | "Major: 1 3 5" : [ INTERVALS_LIB.INTERVAL_KEY_PER_UNI, INTERVALS_LIB.INTERVAL_KEY_MAJ_3RD, INTERVALS_LIB.INTERVAL_KEY_PER_5TH ], 20 | "Minor: 1 b3 5" : [ INTERVALS_LIB.INTERVAL_KEY_PER_UNI, INTERVALS_LIB.INTERVAL_KEY_MIN_3RD, INTERVALS_LIB.INTERVAL_KEY_PER_5TH ], 21 | "Augmented: 1 3 #5" : [ INTERVALS_LIB.INTERVAL_KEY_PER_UNI, INTERVALS_LIB.INTERVAL_KEY_MAJ_3RD, INTERVALS_LIB.INTERVAL_KEY_MIN_6TH ], 22 | "Diminished: 1 b3 b5" : [ INTERVALS_LIB.INTERVAL_KEY_PER_UNI, INTERVALS_LIB.INTERVAL_KEY_MIN_3RD, INTERVALS_LIB.INTERVAL_KEY_AU4_DI5 ] 23 | }; 24 | 25 | const CHORD_PULLDOWN_LABELS = Object.keys( CHORD_TEMPLATES_LIB ); 26 | 27 | function calculate_chord_pitches ( root , chord_type ) { 28 | if ( chord_type == 0 ) { 29 | return; 30 | } 31 | 32 | var voices = {}; 33 | const template = CHORD_TEMPLATES_LIB[ CHORD_PULLDOWN_LABELS[ chord_type ] ]; 34 | // calculate the chord pitches 35 | template.forEach( function ( interval ) { 36 | var pitch = root + interval; 37 | if ( pitch >= 12 ) { 38 | pitch -= 12; 39 | } 40 | if ( half_steps == 0 ) { 41 | voices[ pitch ] = { "type" : "root" }; 42 | } else { 43 | voices[ pitch ] = { "type" : "harmonic" }; 44 | } 45 | }); 46 | 47 | var cache = {}; 48 | for (let pitch = 0; pitch < 12; pitch++) { 49 | const pitch_record = voices[pitch]; 50 | if ( pitch_record ) { 51 | cache[ pitch ] = pitch_record; 52 | } else { 53 | cache[ pitch ] = { "type" : "non-harmonic" }; 54 | } 55 | } 56 | 57 | // do something with cache 58 | } 59 | -------------------------------------------------------------------------------- /design_patterns/oc_custom_cc_messages.js: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | Name: Sending Custom Messages Across Scripts 3 | Author(s): Philip Regan 4 | Purpose: 5 | * This script documents how to take advantage of the undefined control bytes in 6 | the MIDI Spec with the ControlChange object. 7 | * Using these control changes assumes nothing else in the signal chain is using 8 | them as well. Check the documentation which came with MIDI controllers or 9 | synths to ensure there are no collisions. The most common functions are 10 | already defined in the specification, but accounting for every possible 11 | feature is not possible. 12 | 13 | Undefined control bytes --> ControlChange.number 14 | Source: https://www.midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2 15 | 16 | 85 01010101 55 Undefined --- --- 17 | 86 01010110 56 Undefined --- --- 18 | 87 01010111 57 Undefined --- --- 19 | 20 | 89 01011001 59 Undefined --- --- 21 | 90 01011010 5A Undefined --- --- 22 | 23 | 102 01100110 66 Undefined --- --- 24 | 103 01100111 67 Undefined --- --- 25 | 104 01101000 68 Undefined --- --- 26 | 105 01101001 69 Undefined --- --- 27 | 106 01101010 6A Undefined --- --- 28 | 107 01101011 6B Undefined --- --- 29 | 108 01101100 6C Undefined --- --- 30 | 109 01101101 6D Undefined --- --- 31 | 110 01101110 6E Undefined --- --- 32 | 111 01101111 6F Undefined --- --- 33 | 112 01110000 70 Undefined --- --- 34 | 113 01110001 71 Undefined --- --- 35 | 114 01110010 72 Undefined --- --- 36 | 115 01110011 73 Undefined --- --- 37 | 116 01110100 74 Undefined --- --- 38 | 117 01110101 75 Undefined --- --- 39 | 118 01110110 76 Undefined --- --- 40 | 119 01110111 77 Undefined --- --- 41 | 42 | This script is released under the MIT License. 43 | 44 | Permissions 45 | * Commercial use 46 | * Modification 47 | * Distribution 48 | * Private use 49 | 50 | Limitations 51 | x Liability 52 | x Warranty 53 | 54 | Conditions 55 | ! License and copyright notice 56 | 57 | Copyright Philip Regan and Pilcrow Records 58 | 59 | Permission is hereby granted, free of charge, to any person obtaining 60 | a copy of this software and associated documentation files (the 61 | "Software"), to deal in the Software without restriction, including 62 | without limitation the rights to use, copy, modify, merge, publish, 63 | distribute, sublicense, and/or sell copies of the Software, and to 64 | permit persons to whom the Software is furnished to do so, subject to 65 | the following conditions: 66 | 67 | The above copyright notice and this permission notice shall be 68 | included in all copies or substantial portions of the Software. 69 | 70 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 71 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 72 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 73 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 74 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 75 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 76 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 77 | 78 | ****************************************************************************/ 79 | 80 | /* 81 | Core Transmitter and Receiver Scripts 82 | */ 83 | 84 | // transmitter script 85 | let cc_event = new ControlChange(); 86 | cc_event.number = 85; 87 | cc_event.value = 42; 88 | Trace(cc_event); 89 | cc_event.send(); 90 | 91 | // Receiver Script 92 | function HandleMIDI(event) { 93 | if ( event instanceof ControlChange && event.number == 85 ) { 94 | // do something with the event. 95 | } 96 | } 97 | 98 | /****************************************************************************** 99 | Transmitting and receiving events are the easy part of the script. It's a 100 | simple send and capture of a MIDI event. The hard part is determing how best to 101 | manage custom data. For single data points, this is fully illustrated in the 102 | above example. For multiple data points, these need to be handled in a way that 103 | is reliably consistent. 104 | 105 | The following should be considered for transmitting multiple custom data 106 | points: 107 | * Sending multiple data points as an atomic task wherever possible. In other 108 | words, when sending two or more data points, do them in the same block of code, 109 | and not across multiple iterations or functions. 110 | * Sending messages for when types of data have begun being sent and when it has 111 | stopped. This is to ensure data is being captured as expected. This is 112 | particulary useful in situations where a set of data cannot be set atomically. 113 | * Establish JSON object libraries for numbers and values as necessary. Similar 114 | to the music theory libraries, a custom library which can be easily included 115 | in a script ensures data is implemented consistently across the signal chain. 116 | 117 | Essentially, the MIDI spec is representing musical data as pure numbers, and 118 | the same is true here. Custom data can be represented with a little planning. 119 | With 23 undefined fields and each field accepting a value o 0-127, there is 120 | the potential of 2,944 data points. There are two fundamental ways to approach 121 | this: 122 | * Sending data on a specific ControlChange number for each data point. 123 | * Sending data in a specific ControlChange for all data points and the value 124 | is broken into specific blocks. 125 | 126 | In either approach, economy of data management is critical because of the 127 | potential of limited ControlChanges numbers, the limited memory of the Scripter 128 | plug-in, and the need for fast calculations during playing. 129 | 130 | An example of multiple data points would be transmitting all of the information 131 | needed to re-build a chord. 132 | * scale root 133 | * scale type 134 | * chord root 135 | * chord type 136 | * chord voice modifiers 137 | * chord voicings/inversions 138 | * chord alternate bass 139 | * target octave 140 | 141 | Sending information within a single ControlChange number: 142 | 143 | Within a given field, the 0-127 value does not have to be treated as a single 144 | value within the range of 0-127. The 0-127 can be broken into blocks: 145 | * 0-11: status messages 146 | * 0: custom data start 147 | * 11: custom data end 148 | * 12-23: scale root, represented in the chromatic scale 149 | * 24-35: scale type 150 | * 24: Major/Ionian 151 | * 25: Dorian 152 | * 26: Phrygian 153 | * 27: Lydian 154 | * 28: Mixolydian 155 | * 29: Minor/Aeolian 156 | * 30: Locrian 157 | * 31-35: ... 158 | * 36-47: chord root, represented in the chromatic scale 159 | * 48-59: chord type 160 | * 48: major 161 | * 49: minor 162 | * 50: augmented 163 | * 51: diminished 164 | * 52-59: ... 165 | * 60-71 : chord voice modifiers 166 | * 60: 2nd flat 167 | * 61: 2nd sharp 168 | * 62: 3rd flat 169 | * 63: 3rd sharp 170 | * 64: 4th flat 171 | * 65: 4th sharp 172 | * 66: 5th flat 173 | * 67: 5th sharp 174 | * 68: 6th flat 175 | * 69: 6th sharp 176 | * 70: 7th flat 177 | * 71: 7th sharp 178 | * 72-83: chord voicings and inversions 179 | * 72: no inversion (included a previous version needs to be undone) 180 | * 73: 1st inversion 181 | * 74: 2nd inversion 182 | * 75-83: undefined, but more inversions can be noted for chords with more 183 | than four voices 184 | * 84-95: chord alternate bass, represented in chromatic scale 185 | * 96-107: target octave 186 | * -2 187 | * -1 188 | * 0 189 | * 1 190 | * 2 191 | * 3 (Middle C) 192 | * 4 193 | * 5 194 | * 6 195 | * 7 196 | * 8 197 | 108-127: undefined 198 | 199 | To define the root chord of C Major as a minor chord, the following numbers 200 | would be sent given the above values: 201 | 202 | * 0: custom data start 203 | * 12: Scale root: C 204 | * 24: Scale type: Major/Ionian 205 | * 36: Chord root: C 206 | * 48: Chord type: Major 207 | * 62: Chord voice modifier: 3rd flat 208 | * 11: custom data end 209 | 210 | The following example puts the scale and chord data into an object for easy 211 | reuse and shows how the data is sent as an atomic task. 212 | ******************************************************************************/ 213 | 214 | const CHORD_DATA_SEND_LIB = { 215 | 'custom_data_start' : 0, // Status messages 216 | 'undefined' : 1, // Status messages 217 | 'undefined' : 2, // Status messages 218 | 'undefined' : 3, // Status messages 219 | 'undefined' : 4, // Status messages 220 | 'undefined' : 5, // Status messages 221 | 'undefined' : 6, // Status messages 222 | 'undefined' : 7, // Status messages 223 | 'undefined' : 8, // Status messages 224 | 'undefined' : 9, // Status messages 225 | 'undefined' : 10, // Status messages 226 | 'custom_data_end' : 11, // Status messages 227 | 'scale_root_c' : 12, // Scale root 228 | 'scale_root_c#_db' : 13, // Scale root 229 | 'scale_root_d' : 14, // Scale root 230 | 'scale_root_d#_eb' : 15, // Scale root 231 | 'scale_root_e' : 16, // Scale root 232 | 'scale_root_f' : 17, // Scale root 233 | 'scale_root_f#_gb' : 18, // Scale root 234 | 'scale_root_g' : 19, // Scale root 235 | 'scale_root_g#_ab' : 20, // Scale root 236 | 'scale_root_a' : 21, // Scale root 237 | 'scale_root_a#_bb' : 22, // Scale root 238 | 'scale_root_b' : 23, // Scale root 239 | 'major_ionian' : 24, // Scale type 240 | 'dorian' : 25, // Scale type 241 | 'phrygian' : 26, // Scale type 242 | 'lydian' : 27, // Scale type 243 | 'mixolydian' : 28, // Scale type 244 | 'minor_aeolian' : 29, // Scale type 245 | 'locrian' : 30, // Scale type 246 | 'undefined' : 31, // Scale type 247 | 'undefined' : 32, // Scale type 248 | 'undefined' : 33, // Scale type 249 | 'undefined' : 34, // Scale type 250 | 'undefined' : 35, // Scale type 251 | 'chord_root_c' : 36, // Chord root 252 | 'chord_root_c#_db' : 37, // Chord root 253 | 'chord_root_d' : 38, // Chord root 254 | 'chord_root_d#_eb' : 39, // Chord root 255 | 'chord_root_e' : 40, // Chord root 256 | 'chord_root_f' : 41, // Chord root 257 | 'chord_root_f#_gb' : 42, // Chord root 258 | 'chord_root_g' : 43, // Chord root 259 | 'chord_root_g#_ab' : 44, // Chord root 260 | 'chord_root_a' : 45, // Chord root 261 | 'chord_root_a#_bb' : 46, // Chord root 262 | 'chord_root_b' : 47, // Chord root 263 | 'major' : 48, // Chord type 264 | 'minor' : 49, // Chord type 265 | 'augmented' : 50, // Chord type 266 | 'diminished' : 51, // Chord type 267 | 'undefined' : 52, // Chord type 268 | 'undefined' : 53, // Chord type 269 | 'undefined' : 54, // Chord type 270 | 'undefined' : 55, // Chord type 271 | 'undefined' : 56, // Chord type 272 | 'undefined' : 57, // Chord type 273 | 'undefined' : 58, // Chord type 274 | 'undefined' : 59, // Chord type 275 | '2nd_flat' : 60, // Chord voice modifier 276 | '2nd_sharp' : 61, // Chord voice modifier 277 | '3rd_flat' : 62, // Chord voice modifier 278 | '3rd_sharp' : 63, // Chord voice modifier 279 | '4th_flat' : 64, // Chord voice modifier 280 | '4th_sharp' : 65, // Chord voice modifier 281 | '5th_flat' : 66, // Chord voice modifier 282 | '5th_sharp' : 67, // Chord voice modifier 283 | '6th_flat' : 68, // Chord voice modifier 284 | '6th_sharp' : 69, // Chord voice modifier 285 | '7th_flat' : 70, // Chord voice modifier 286 | '7th_sharp' : 71, // Chord voice modifier 287 | 'no_inversion' : 72, // Chord voicing and inversions 288 | '1st_inversion' : 73, // Chord voicing and inversions 289 | '2nd_inversion' : 74, // Chord voicing and inversions 290 | '3rd_inversion' : 75, // Chord voicing and inversions 291 | 'undefined' : 76, // Chord voicing and inversions 292 | 'undefined' : 77, // Chord voicing and inversions 293 | 'undefined' : 78, // Chord voicing and inversions 294 | 'undefined' : 79, // Chord voicing and inversions 295 | 'undefined' : 80, // Chord voicing and inversions 296 | 'undefined' : 81, // Chord voicing and inversions 297 | 'undefined' : 82, // Chord voicing and inversions 298 | 'undefined' : 83, // Chord voicing and inversions 299 | 'chord_bass_c' : 84, // Chord alternate bass 300 | 'chord_bass_c#_db' : 85, // Chord alternate bass 301 | 'chord_bass_d' : 86, // Chord alternate bass 302 | 'chord_bass_d#_eb' : 87, // Chord alternate bass 303 | 'chord_bass_e' : 88, // Chord alternate bass 304 | 'chord_bass_f' : 89, // Chord alternate bass 305 | 'chord_bass_f#_gb' : 90, // Chord alternate bass 306 | 'chord_bass_g' : 91, // Chord alternate bass 307 | 'chord_bass_g#_ab' : 92, // Chord alternate bass 308 | 'chord_bass_a' : 93, // Chord alternate bass 309 | 'chord_bass_a#_bb' : 94, // Chord alternate bass 310 | 'chord_bass_b' : 95, // Chord alternate bass 311 | 'octave_-2' : 96, // Chord target octave 312 | 'octave_-1' : 97, // Chord target octave 313 | 'octave_0' : 98, // Chord target octave 314 | 'octave_1' : 99, // Chord target octave 315 | 'octave_2' : 100, // Chord target octave 316 | 'octave_3_(middle_c)' : 101, // Chord target octave 317 | 'octave_4' : 102, // Chord target octave 318 | 'octave_5' : 103, // Chord target octave 319 | 'octave_6' : 104, // Chord target octave 320 | 'octave_7' : 105, // Chord target octave 321 | 'octave_8' : 106, // Chord target octave 322 | 'undefined' : 107, // Undefined 323 | 'undefined' : 108, // Undefined 324 | 'undefined' : 109, // Undefined 325 | 'undefined' : 110, // Undefined 326 | 'undefined' : 111, // Undefined 327 | 'undefined' : 112, // Undefined 328 | 'undefined' : 113, // Undefined 329 | 'undefined' : 114, // Undefined 330 | 'undefined' : 115, // Undefined 331 | 'undefined' : 116, // Undefined 332 | 'undefined' : 117, // Undefined 333 | 'undefined' : 118, // Undefined 334 | 'undefined' : 119, // Undefined 335 | 'undefined' : 120, // Undefined 336 | 'undefined' : 121, // Undefined 337 | 'undefined' : 122, // Undefined 338 | 'undefined' : 123, // Undefined 339 | 'undefined' : 124, // Undefined 340 | 'undefined' : 125, // Undefined 341 | 'undefined' : 126, // Undefined 342 | 'undefined' : 127 // Undefined 343 | } 344 | 345 | const CHORD_DATA_RECEIVE_LIB = { 346 | 0 : 'custom_data_start', // Status messages 347 | 1 : 'undefined', // Status messages 348 | 2 : 'undefined', // Status messages 349 | 3 : 'undefined', // Status messages 350 | 4 : 'undefined', // Status messages 351 | 5 : 'undefined', // Status messages 352 | 6 : 'undefined', // Status messages 353 | 7 : 'undefined', // Status messages 354 | 8 : 'undefined', // Status messages 355 | 9 : 'undefined', // Status messages 356 | 10 : 'undefined', // Status messages 357 | 11 : 'custom_data_end', // Status messages 358 | 12 : 'scale_root_c', // Scale root 359 | 13 : 'scale_root_c#_db', // Scale root 360 | 14 : 'scale_root_d', // Scale root 361 | 15 : 'scale_root_d#_eb', // Scale root 362 | 16 : 'scale_root_e', // Scale root 363 | 17 : 'scale_root_f', // Scale root 364 | 18 : 'scale_root_f#_gb', // Scale root 365 | 19 : 'scale_root_g', // Scale root 366 | 20 : 'scale_root_g#_ab', // Scale root 367 | 21 : 'scale_root_a', // Scale root 368 | 22 : 'scale_root_a#_bb', // Scale root 369 | 23 : 'scale_root_b', // Scale root 370 | 24 : 'major_ionian', // Scale type 371 | 25 : 'dorian', // Scale type 372 | 26 : 'phrygian', // Scale type 373 | 27 : 'lydian', // Scale type 374 | 28 : 'mixolydian', // Scale type 375 | 29 : 'minor_aeolian', // Scale type 376 | 30 : 'locrian', // Scale type 377 | 31 : 'undefined', // Scale type 378 | 32 : 'undefined', // Scale type 379 | 33 : 'undefined', // Scale type 380 | 34 : 'undefined', // Scale type 381 | 35 : 'undefined', // Scale type 382 | 36 : 'chord_root_c', // Chord root 383 | 37 : 'chord_root_c#_db', // Chord root 384 | 38 : 'chord_root_d', // Chord root 385 | 39 : 'chord_root_d#_eb', // Chord root 386 | 40 : 'chord_root_e', // Chord root 387 | 41 : 'chord_root_f', // Chord root 388 | 42 : 'chord_root_f#_gb', // Chord root 389 | 43 : 'chord_root_g', // Chord root 390 | 44 : 'chord_root_g#_ab', // Chord root 391 | 45 : 'chord_root_a', // Chord root 392 | 46 : 'chord_root_a#_bb', // Chord root 393 | 47 : 'chord_root_b', // Chord root 394 | 48 : 'major', // Chord type 395 | 49 : 'minor', // Chord type 396 | 50 : 'augmented', // Chord type 397 | 51 : 'diminished', // Chord type 398 | 52 : 'undefined', // Chord type 399 | 53 : 'undefined', // Chord type 400 | 54 : 'undefined', // Chord type 401 | 55 : 'undefined', // Chord type 402 | 56 : 'undefined', // Chord type 403 | 57 : 'undefined', // Chord type 404 | 58 : 'undefined', // Chord type 405 | 59 : 'undefined', // Chord type 406 | 60 : '2nd_flat', // Chord voice modifier 407 | 61 : '2nd_sharp', // Chord voice modifier 408 | 62 : '3rd_flat', // Chord voice modifier 409 | 63 : '3rd_sharp', // Chord voice modifier 410 | 64 : '4th_flat', // Chord voice modifier 411 | 65 : '4th_sharp', // Chord voice modifier 412 | 66 : '5th_flat', // Chord voice modifier 413 | 67 : '5th_sharp', // Chord voice modifier 414 | 68 : '6th_flat', // Chord voice modifier 415 | 69 : '6th_sharp', // Chord voice modifier 416 | 70 : '7th_flat', // Chord voice modifier 417 | 71 : '7th_sharp', // Chord voice modifier 418 | 72 : 'no_inversion', // Chord voicing and inversions 419 | 73 : '1st_inversion', // Chord voicing and inversions 420 | 74 : '2nd_inversion', // Chord voicing and inversions 421 | 75 : '3rd_inversion', // Chord voicing and inversions 422 | 76 : 'undefined', // Chord voicing and inversions 423 | 77 : 'undefined', // Chord voicing and inversions 424 | 78 : 'undefined', // Chord voicing and inversions 425 | 79 : 'undefined', // Chord voicing and inversions 426 | 80 : 'undefined', // Chord voicing and inversions 427 | 81 : 'undefined', // Chord voicing and inversions 428 | 82 : 'undefined', // Chord voicing and inversions 429 | 83 : 'undefined', // Chord voicing and inversions 430 | 84 : 'chord_bass_c', // Chord alternate bass 431 | 85 : 'chord_bass_c#_db', // Chord alternate bass 432 | 86 : 'chord_bass_d', // Chord alternate bass 433 | 87 : 'chord_bass_d#_eb', // Chord alternate bass 434 | 88 : 'chord_bass_e', // Chord alternate bass 435 | 89 : 'chord_bass_f', // Chord alternate bass 436 | 90 : 'chord_bass_f#_gb', // Chord alternate bass 437 | 91 : 'chord_bass_g', // Chord alternate bass 438 | 92 : 'chord_bass_g#_ab', // Chord alternate bass 439 | 93 : 'chord_bass_a', // Chord alternate bass 440 | 94 : 'chord_bass_a#_bb', // Chord alternate bass 441 | 95 : 'chord_bass_b', // Chord alternate bass 442 | 96 : 'octave_-2', // Chord target octave 443 | 97 : 'octave_-1', // Chord target octave 444 | 98 : 'octave_0', // Chord target octave 445 | 99 : 'octave_1', // Chord target octave 446 | 100 : 'octave_2', // Chord target octave 447 | 101 : 'octave_3_(middle_c)', // Chord target octave 448 | 102 : 'octave_4', // Chord target octave 449 | 103 : 'octave_5', // Chord target octave 450 | 104 : 'octave_6', // Chord target octave 451 | 105 : 'octave_7', // Chord target octave 452 | 106 : 'octave_8', // Chord target octave 453 | 107 : 'undefined', // Undefined 454 | 108 : 'undefined', // Undefined 455 | 109 : 'undefined', // Undefined 456 | 110 : 'undefined', // Undefined 457 | 111 : 'undefined', // Undefined 458 | 112 : 'undefined', // Undefined 459 | 113 : 'undefined', // Undefined 460 | 114 : 'undefined', // Undefined 461 | 115 : 'undefined', // Undefined 462 | 116 : 'undefined', // Undefined 463 | 117 : 'undefined', // Undefined 464 | 118 : 'undefined', // Undefined 465 | 119 : 'undefined', // Undefined 466 | 120 : 'undefined', // Undefined 467 | 121 : 'undefined', // Undefined 468 | 122 : 'undefined', // Undefined 469 | 123 : 'undefined', // Undefined 470 | 124 : 'undefined', // Undefined 471 | 125 : 'undefined', // Undefined 472 | 126 : 'undefined', // Undefined 473 | 127 : 'undefined' // Undefined 474 | } 475 | 476 | const CHORD_CC_NUMBER = 55; 477 | 478 | // Sending the data 479 | 480 | // create and send a ControlChange object 481 | function send_control_change( number, value ) { 482 | let chord_cc_event = new ControlChange(); 483 | chord_cc_event.number = number; 484 | chord_cc_event.value = value; 485 | chord_cc_event.send(); 486 | } 487 | 488 | // sending everything atomically; no other tasks will happen until this block 489 | // of code has completed. 490 | send_control_change( CHORD_CC_NUMBER , CHORD_DATA_SEND_LIB.custom_data_start ); 491 | send_control_change( CHORD_CC_NUMBER , CHORD_DATA_SEND_LIB.scale_root_c ); 492 | send_control_change( CHORD_CC_NUMBER , CHORD_DATA_SEND_LIB.major_ionian ); 493 | send_control_change( CHORD_CC_NUMBER , CHORD_DATA_SEND_LIB.chord_root_c ); 494 | send_control_change( CHORD_CC_NUMBER , CHORD_DATA_SEND_LIB.major ); 495 | send_control_change( CHORD_CC_NUMBER , CHORD_DATA_SEND_LIB["3rd_flat"] ); 496 | send_control_change( CHORD_CC_NUMBER , CHORD_DATA_SEND_LIB.custom_data_end ); 497 | 498 | // receiving the data 499 | 500 | const CHROMATIC_SCALE_STRINGS = ["C", "C♯/D♭", "D", "D♯/E♭", "E", "F", "F♯/G♭", "G", "G♯/A♭", "A", "A♯/B♭", "B"]; 501 | 502 | var CHORD_DATA_CACHE = {}; 503 | var CHORD_DATA_RECEIVING = false; 504 | 505 | function HandleMIDI( event ) { 506 | if ( event instanceof ControlChange && event.number == CHORD_CC_NUMBER ) { 507 | handle_chord_control_change( event ); 508 | } 509 | } 510 | 511 | function handle_chord_control_change( chord_cc_event ) { 512 | switch ( chord_cc_event.value ) { 513 | case 0: 514 | CHORD_DATA_RECEIVING = true; 515 | break; 516 | case 11: 517 | CHORD_DATA_RECEIVING = false; 518 | default: 519 | break; 520 | } 521 | if ( !CHORD_DATA_RECEIVING ) { 522 | return; 523 | } 524 | 525 | /* 526 | collect all of the chord data in the cache 527 | */ 528 | switch ( chord_cc_event.value ) { 529 | case 12: 530 | case 13: 531 | case 14: 532 | case 15: 533 | case 16: 534 | case 17: 535 | case 18: 536 | case 19: 537 | case 20: 538 | case 21: 539 | case 22: 540 | case 23: 541 | CHORD_DATA_CACHE["scale_root"] = CHROMATIC_SCALE_STRINGS[ chord_cc_event.value - 12 ]; 542 | break; 543 | default: 544 | break; 545 | } 546 | 547 | /* 548 | build the chord 549 | */ 550 | 551 | } -------------------------------------------------------------------------------- /design_patterns/oc_managing_note_lengths.js: -------------------------------------------------------------------------------- 1 | const NOTE_LENGTHS_LIB = { 2 | "1/128" : 0.0105, 3 | "1/128d" : 0.04725, 4 | "1/128t" : 41.600, 5 | "1/64" : 0.063, 6 | "1/64d" : 0.094, 7 | "1/64t" : 0.021, 8 | "1/32" : 0.125, 9 | "1/32d" : 0.188, 10 | "1/32t" : 0.041, 11 | "1/16" : 0.250, 12 | "1/16d" : 0.375, 13 | "1/16t" : 0.333, 14 | "1/8" : 0.500, 15 | "1/8d" : 0.750, 16 | "1/8t" : 0.1667, 17 | "1/4" : 1.000, 18 | "1/4d" : 1.500, 19 | "1/4t" : 0.300, 20 | "1/2" : 2.000, 21 | "1/2d" : 3.000, 22 | "1/2t" : 0.667, 23 | "1 bar" : 4.000 24 | }; 25 | 26 | var NOTE_LENGTH_KEYS = Object.keys( NOTE_LENGTHS_LIB ); 27 | 28 | var whole_note = NOTE_LENGTH_KEYS.shift(); 29 | var whole_triplet = NOTE_LENGTH_KEYS.pop(); 30 | NOTE_LENGTH_KEYS.push( whole_note ); 31 | NOTE_LENGTH_KEYS.push( whole_triplet ); -------------------------------------------------------------------------------- /design_patterns/oc_managing_scales.js: -------------------------------------------------------------------------------- 1 | const CHROMATIC_SCALE_STRINGS = ["C", "C#/Db", "D", "D#/Eb", "E", "F", "F#/Gb", "G", "G#/Ab", "A", "A #/Bb", "B"]; 2 | const PITCH_RECORD_KEY_TYPE = "t"; 3 | const PITCH_TYPE_ROOT = "root"; 4 | const PITCH_TYPE_DIATONIC = "diatonic"; 5 | const PITCH_TYPE_NONDIATONIC = "non-diatonic"; 6 | 7 | const SCALE_TEMPLATES = { 8 | "Ionian" : [2, 2, 1, 2, 2, 2, 1], 9 | "Dorian" : [2, 1, 2, 2, 2, 1, 2], 10 | "Phrygian" : [1, 2, 2, 2, 1, 2, 2], 11 | "Lydian" : [2, 2, 2, 1, 2, 2, 1], 12 | "Mixolydian" : [2, 2, 1, 2, 2, 1, 2], 13 | "Aeolian" : [2, 1, 2, 2, 1, 2, 2], 14 | "Locrian" : [1, 2, 2, 1, 2, 2, 2] 15 | } 16 | const SCALE_KEYS = Object.keys( SCALE_TEMPLATES ); 17 | 18 | function updateNotePitchPool( root, templateIndex ) { 19 | 20 | var template = SCALE_TEMPLATES[ SCALE_KEYS[ templateIndex ] ]; 21 | var lastPitch = root; 22 | var pitch_map = {}; 23 | pitch_map[ lastPitch ] = { PITCH_RECORD_KEY_TYPE:PITCH_TYPE_ROOT }; 24 | 25 | for ( var index = 0 ; index <= template.length - 1 ; index++ ) { 26 | var steps = template[ index ]; 27 | var pitch = lastPitch + steps; 28 | if ( steps > 1 ) { 29 | while ( steps > 0 ) { 30 | pitch_map[ pitch ] = { PITCH_RECORD_KEY_TYPE:PITCH_TYPE_NONDIATONIC }; 31 | steps--; 32 | } 33 | } 34 | pitch_map[ pitch ] = { PITCH_RECORD_KEY_TYPE:PITCH_TYPE_DIATONIC }; 35 | lastPitch = pitch; 36 | } 37 | 38 | var origPitchKeys = Object.keys( pitch_map ); 39 | var cache = {}; 40 | for ( var index = 0 ; index < origPitchKeys.length ; index++ ) { 41 | var key = origPitchKeys[ index ]; 42 | var pitch = parseInt( key ); 43 | var pitchRecord = pitch_map[ key ] 44 | if ( pitch >= 12 ) { 45 | pitch = pitch - 12; 46 | } 47 | cache[ pitch ] = pitchRecord; 48 | } 49 | 50 | // cache is the completed scale within a single octave 51 | // do something with cache 52 | } 53 | -------------------------------------------------------------------------------- /design_patterns/oc_parameter_change_preventing_recursion.js: -------------------------------------------------------------------------------- 1 | var UPDATING_CONTROLS = false; 2 | 3 | function ParameterChanged( param, value ) { 4 | if ( UPDATING_CONTROLS == true ) { 5 | return; 6 | } 7 | switch( param ) { 8 | case 0: 9 | updateControlsToNewValue(); 10 | break; 11 | default: 12 | // do something 13 | break 14 | } 15 | } 16 | 17 | function updateControlsToNewValue( value ) { 18 | UPDATING_CONTROLS = true; 19 | for ( var index = 0 ; index < 3 ; index++ ) { 20 | SetParameter(index, value); 21 | } 22 | UPDATING_CONTROLS = false; 23 | updateScriptParametersToUpdatedControlSettings(); 24 | } 25 | -------------------------------------------------------------------------------- /design_patterns/oc_tracking_playhead_across_process_blocks.js: -------------------------------------------------------------------------------- 1 | /* 2 | This design pattern is based on the pattern found in the demo 3 | Drum Probability Sequencer script. This accomplishes two things: 4 | * accurately tracks the next beat to be scheduled based on the current 5 | process block 6 | * Manages a trigger value so that events can be created as needed 7 | */ 8 | 9 | //needed to call GetTimingInfo() 10 | var NeedsTimingInfo = true; 11 | ResetParameterDefaults = true; 12 | 13 | // Used by beatToSchedule and TRIGGER to align musically 14 | // determines how many notes are in the time siqnature denominator 15 | // 0.25 = 1/1 note, 1 = 1/4 note, 4 = 1/16, 8 = 1/32 16 | const TIME_SIG_DENOM_DIVISION = 16; // beatToSchedule results in 1/64 notes 17 | const CURSOR_INCREMENT = 0.0001; 18 | 19 | var NOTE_LENGTHS = { 20 | "1/64" : 0.0625, 21 | "1/32" : 0.125, 22 | "1/16" : 0.250, 23 | "1/8" : 0.500, 24 | "1/4" : 1.000, // in beatToSchedule 25 | "1/2" : 2.000, 26 | "1/1" : 4.000 27 | }; 28 | var NOTE_LENGTH = NOTE_LENGTHS["1/4"]; 29 | 30 | const RESET_VALUE = -1.0; 31 | var TRIGGER = RESET_VALUE; 32 | 33 | function ProcessMIDI() { 34 | 35 | var timing_info = GetTimingInfo(); 36 | 37 | if ( timing_info.playing ) { 38 | 39 | // init the values to calculate beats 40 | var beatToSchedule = align_beat_to_bar_division( timing_info.blockStartBeat, TIME_SIG_DENOM_DIVISION ); 41 | 42 | if ( TRIGGER == RESET_VALUE ) { 43 | TRIGGER = beatToSchedule; 44 | } 45 | 46 | // loop through the beats that fall within this buffer 47 | while ( beats_fall_within_buffer( beatToSchedule, timing_info ) ) { 48 | // adjust for cycle 49 | beatToSchedule = handle_beat_wraparound( beatToSchedule, timing_info ); 50 | TRIGGER = handle_beat_wraparound( TRIGGER, timing_info ); 51 | 52 | if ( beatToSchedule == TRIGGER ) { 53 | 54 | // DO SOMETHING 55 | var on = new NoteOn; 56 | on.pitch = 60 + beatToSchedule; 57 | on.velocity = 100; 58 | on.sendAtBeat(beatToSchedule); 59 | var off = new NoteOff(on); 60 | off.sendAtBeat(beatToSchedule + NOTE_LENGTH); 61 | 62 | // advance the trigger 63 | TRIGGER += NOTE_LENGTH; 64 | } 65 | 66 | 67 | // advance to next beat 68 | beatToSchedule += CURSOR_INCREMENT; 69 | beatToSchedule = align_beat_to_bar_division( beatToSchedule, TIME_SIG_DENOM_DIVISION ); 70 | } 71 | } else { 72 | // .playing == false; continuous loop with no way to stop 73 | 74 | // ensure the trigger aligns with the playhead on the next play 75 | TRIGGER = RESET_VALUE; 76 | } 77 | } 78 | 79 | 80 | // aligns any float value to the beats 81 | // ceiling used because all recordable beats are >= 1.000 82 | function align_beat_to_bar_division( value, division ) { 83 | return Math.ceil( value * division ) / division; 84 | } 85 | 86 | // when the intended beat falls outside the cycle, wrap it proportionally 87 | // from the cycle start 88 | function handle_beat_wraparound( value, timing_info ) { 89 | if ( timing_info.cycling && value >= timing_info.rightCycleBeat ) { 90 | value -= ( timing_info.rightCycleBeat - timing_info.leftCycleBeat ); 91 | } 92 | return value; 93 | } 94 | 95 | // loop through the beats that fall within this buffer 96 | // including beats that wrap around the cycle point 97 | // return false by default 98 | function beats_fall_within_buffer ( beatToSchedule, timing_info ) { 99 | let lookAheadEnd = timing_info.blockEndBeat; 100 | let cycleBeats = timing_info.rightCycleBeat - timing_info.leftCycleBeat; 101 | let cycleEnd = lookAheadEnd - cycleBeats; 102 | if ( beatToSchedule >= timing_info.blockStartBeat && beatToSchedule < lookAheadEnd || (timing_info.cycling && beatToSchedule < cycleEnd)) { 103 | return true; 104 | } 105 | return false; 106 | } -------------------------------------------------------------------------------- /design_patterns/oc_weighted_random_selection.js: -------------------------------------------------------------------------------- 1 | var WEIGHT_POOL = { 2 | "75":"0", 3 | "100":"2", 4 | "125":"4", 5 | "150":"5", 6 | "175":"7", 7 | "200":"9", 8 | "225":"11", 9 | "total":225 10 | } 11 | 12 | var result = getRandomValueFromWeightPool( WEIGHT_POOL ); 13 | 14 | function getRandomValueFromWeightPool( weightPool ) { 15 | 16 | var total = weightPool["total"]; 17 | 18 | var r = rInt( 1, total ); 19 | var weights = Object.keys(weightPool); 20 | 21 | if ( weights.length == 2 ) { 22 | return weightPool[weights[0]]; 23 | } 24 | 25 | weights.pop(); 26 | var last_weight = total; 27 | for ( let index = weights.length - 1 ; index > -1 ; index-- ) { 28 | const weight = parseInt(weights[index]); 29 | if ( r > weight ) { 30 | return weightPool[last_weight]; 31 | } 32 | last_weight = weight; 33 | } 34 | 35 | return weightPool[weights[0]]; 36 | 37 | } 38 | 39 | function rInt(min, max) { 40 | return Math.floor(Math.random() * (max - min + 1)) + min; 41 | } 42 | -------------------------------------------------------------------------------- /example_scripts/oc_beat_based_probability_gate.js: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | Name: Beat Based Probability Gate 3 | Author(s): Philip Regan 4 | Purpose: 5 | * Changes probability of an event being sent based on the beat, like whether 6 | the note should play on a strong or weak beat. 7 | * Useful for finding new rhythms or phrases for any MIDI track 8 | 9 | Instructions: 10 | * The beat probabilities are represented in the 16 probability sliders. The 11 | other controls are used to control characteristics of the probability 12 | * Probabilities are contained in 16-element arrays. Several options are 13 | provided to show how presets can be managed as well as creative possibilities 14 | * The frequency of probability changes can be changed with the "Division" 15 | control 16 | * The number of beats to be used can be changed with the Start and Length 17 | controls. 18 | * Setting this to a small odd number leads to some interesting possibilities 19 | * Increasing or decreasing the overall probability can be changed with the 20 | "Skew" control. 21 | 22 | This script is released under the MIT License. 23 | 24 | Permissions 25 | * Commercial use 26 | * Modification 27 | * Distribution 28 | * Private use 29 | 30 | Limitations 31 | x Liability 32 | x Warranty 33 | 34 | Conditions 35 | ! License and copyright notice 36 | 37 | Copyright Philip Regan and Pilcrow Records 38 | 39 | Permission is hereby granted, free of charge, to any person obtaining 40 | a copy of this software and associated documentation files (the 41 | "Software"), to deal in the Software without restriction, including 42 | without limitation the rights to use, copy, modify, merge, publish, 43 | distribute, sublicense, and/or sell copies of the Software, and to 44 | permit persons to whom the Software is furnished to do so, subject to 45 | the following conditions: 46 | 47 | The above copyright notice and this permission notice shall be 48 | included in all copies or substantial portions of the Software. 49 | 50 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 51 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 52 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 53 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 54 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 55 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 56 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 57 | 58 | ****************************************************************************/ 59 | 60 | var NeedsTimingInfo = true; 61 | var PluginParameters = []; 62 | 63 | const TIME_SIG_DENOM_DIVISION = 16; // beatToSchedule results in 1/64 notes 64 | 65 | const PROBS_LIB = { 66 | "Original" : [96, 90, 84, 78, 72, 66, 60, 54, 48, 42, 36, 30, 24, 18, 12, 6], 67 | "Reverse" : [6, 12, 18, 24, 30, 36, 42, 48, 54, 60, 66, 72, 78, 84, 90, 96], 68 | "Straight" : [96, 48, 72, 24, 84, 36, 60, 12, 90, 42, 66, 30, 78, 18, 54, 6], 69 | "Lead In on 4" : [96, 24, 48, 72, 84, 12, 36, 60, 90, 30, 42, 66, 78, 6, 18, 54], 70 | "Synco1" : [48, 96, 24, 72, 36, 84, 12, 60, 42, 90, 30, 66, 18, 78, 6, 54] 71 | } 72 | const PROBS_LIB_KEYS = Object.keys( PROBS_LIB ); 73 | // track where we are in the probability 74 | var PROBS_INDEX = 0; 75 | var CURRENT_PROB = 100; 76 | 77 | const NOTE_LENGTHS_LIB = { 78 | "1/32" : 0.125, 79 | "1/16" : 0.25, 80 | "1/8" : 0.5, 81 | "1/4" : 1.0, 82 | "1/2" : 2.0, 83 | "1" : 4.0, 84 | }; 85 | const NOTE_LENGTH_KEYS = Object.keys( NOTE_LENGTHS_LIB ); 86 | 87 | /* event tracking params */ 88 | 89 | const RESET_VALUE = -1.0; 90 | var TRIGGER = RESET_VALUE; 91 | const CURSOR_INCREMENT = 0.001; 92 | 93 | var ACTIVE_NOTES = {}; 94 | 95 | // user settings 96 | var PROBS_ARR = PROBS_LIB["Original"]; 97 | var PATTERN_START = 1; 98 | var PATTERN_LENGTH = 15; 99 | var PROB_SKEW = 0; 100 | var DIVISION = NOTE_LENGTHS_LIB["1/16"]; 101 | 102 | // controls 103 | const BEAT_PARAM_OFFSET = 5; 104 | const BEAT_PARAM_LENGTH = 16; 105 | var UPDATING_CONTROLS = false; 106 | 107 | function HandleMIDI( event ) { 108 | if ( PATTERN_LENGTH == 0 ) { 109 | event.send(); 110 | return; 111 | } 112 | 113 | const pitch = event.pitch; 114 | if ( event instanceof NoteOn ) { 115 | 116 | // do we play the note? 117 | let r = rInt( 1, 100 ); 118 | let gate = CURRENT_PROB + PROB_SKEW; 119 | if ( r <= gate ) { 120 | var notes = ACTIVE_NOTES[ pitch ]; 121 | if ( !notes ) { 122 | notes = []; 123 | } 124 | notes.push( event ); 125 | ACTIVE_NOTES[ pitch ] = notes; 126 | 127 | event.send(); 128 | } 129 | 130 | } else if ( event instanceof NoteOff ) { 131 | // handle all note off regardless of note on 132 | var notes = ACTIVE_NOTES[ pitch ]; 133 | if ( notes ) { 134 | var note = notes.pop(); 135 | ACTIVE_NOTES[ pitch ] = notes; 136 | } 137 | event.send(); 138 | } else { 139 | // pass through 140 | event.send(); 141 | } 142 | 143 | } 144 | 145 | function ProcessMIDI() { 146 | var timing_info = GetTimingInfo(); 147 | 148 | if ( timing_info.playing ) { 149 | 150 | // init the values to calculate beats 151 | var beatToSchedule = align_beat_to_bar_division( timing_info.blockStartBeat, TIME_SIG_DENOM_DIVISION ); 152 | 153 | if ( TRIGGER == RESET_VALUE ) { 154 | TRIGGER = beatToSchedule; 155 | } 156 | 157 | // loop through the beats that fall within this buffer 158 | while ( beats_fall_within_buffer( beatToSchedule, timing_info ) ) { 159 | // adjust for cycle 160 | beatToSchedule = handle_beat_wraparound( beatToSchedule, timing_info ); 161 | // TRIGGER = handle_beat_wraparound( TRIGGER, timing_info ); 162 | 163 | // if ( beatToSchedule == TRIGGER ) { 164 | // update the probability for this beat 165 | // used in HandleMIDI 166 | PROBS_INDEX = ( beatToSchedule * TIME_SIG_DENOM_DIVISION ) % PROBS_ARR.length; 167 | Trace(JSON.stringify({beatToSchedule:beatToSchedule,PROBS_INDEX:PROBS_INDEX})); 168 | CURRENT_PROB = PROBS_ARR[ PROBS_INDEX ]; 169 | // advance the trigger 170 | // TRIGGER += DIVISION; 171 | // } 172 | 173 | // advance to next beat 174 | beatToSchedule += CURSOR_INCREMENT; 175 | beatToSchedule = align_beat_to_bar_division( beatToSchedule, TIME_SIG_DENOM_DIVISION ); 176 | 177 | } 178 | 179 | 180 | } else { 181 | // .playing == false; continuous loop with no way to stop 182 | 183 | // ensure the trigger aligns with the playhead on the next play 184 | TRIGGER = RESET_VALUE; 185 | } 186 | } 187 | 188 | // aligns any float value to the beats 189 | // ceiling used because all recordable beats are >= 1.000 190 | function align_beat_to_bar_division( value, division ) { 191 | return Math.ceil( value * division ) / division; 192 | } 193 | 194 | // when the intended beat falls outside the cycle, wrap it proportionally 195 | // from the cycle start 196 | function handle_beat_wraparound( value, timing_info ) { 197 | if ( timing_info.cycling && value >= timing_info.rightCycleBeat ) { 198 | value -= ( timing_info.rightCycleBeat - timing_info.leftCycleBeat ); 199 | } 200 | return value; 201 | } 202 | 203 | // loop through the beats that fall within this buffer 204 | // including beats that wrap around the cycle point 205 | // return false by default 206 | function beats_fall_within_buffer ( beatToSchedule, timing_info ) { 207 | let lookAheadEnd = timing_info.blockEndBeat; 208 | let cycleBeats = timing_info.rightCycleBeat - timing_info.leftCycleBeat; 209 | let cycleEnd = lookAheadEnd - cycleBeats; 210 | if ( beatToSchedule >= timing_info.blockStartBeat && beatToSchedule < lookAheadEnd || (timing_info.cycling && beatToSchedule < cycleEnd)) { 211 | return true; 212 | } 213 | return false; 214 | } 215 | 216 | function ParameterChanged( param , value ) { 217 | if ( UPDATING_CONTROLS == true ) { 218 | return; 219 | } 220 | switch ( param ) { 221 | case 0: 222 | // pattern 223 | update_to_preset( value ); 224 | break; 225 | case 1: 226 | // start 227 | PATTERN_START = value + BEAT_PARAM_OFFSET; 228 | break; 229 | case 2: 230 | // length 231 | PATTERN_LENGTH = value - 1; 232 | break; 233 | case 3: 234 | // skew 235 | PROB_SKEW = value; 236 | break; 237 | case 4: 238 | // division 239 | DIVISION = NOTE_LENGTHS_LIB[ NOTE_LENGTH_KEYS[ value ] ]; 240 | break; 241 | case 5: 242 | case 6: 243 | case 7: 244 | case 8: 245 | case 9: 246 | case 10: 247 | case 11: 248 | case 12: 249 | case 13: 250 | case 14: 251 | case 15: 252 | case 16: 253 | case 17: 254 | case 18: 255 | case 19: 256 | case 20: 257 | case 21: 258 | update_probs_arr( param, value ); 259 | default: 260 | Trace("Error: ParameterChanged: " + param + " , " + value); 261 | break; 262 | } 263 | } 264 | 265 | function update_to_preset( value ) { 266 | UPDATING_CONTROLS = true; 267 | 268 | PROBS_ARR = PROBS_LIB[ PROBS_LIB_KEYS[ value ]]; 269 | 270 | for (let index = 0; index < BEAT_PARAM_LENGTH; index++) { 271 | SetParameter( index + BEAT_PARAM_OFFSET , PROBS_ARR[index] ); 272 | } 273 | 274 | UPDATING_CONTROLS = false; 275 | } 276 | 277 | function update_probs_arr( param, value ) { 278 | UPDATING_CONTROLS = true; 279 | 280 | PROBS_ARR[ param - BEAT_PARAM_OFFSET ] = value; 281 | 282 | for (let index = 0; index < BEAT_PARAM_LENGTH; index++) { 283 | SetParameter( index + BEAT_PARAM_OFFSET , PROBS_ARR[index] ); 284 | } 285 | 286 | UPDATING_CONTROLS = false; 287 | } 288 | 289 | function rInt( min, max ) { 290 | if (min == max ) {return min;} 291 | return Math.floor(min + Math.random()*(max + 1 - min)); 292 | } 293 | 294 | // 0 295 | PluginParameters.push({ 296 | name:"Pattern", 297 | type:"menu", 298 | valueStrings:PROBS_LIB_KEYS, 299 | defaultValue:2 300 | }); 301 | 302 | // 1 303 | PluginParameters.push({ 304 | name:"Start", 305 | type:"lin", 306 | minValue:1, 307 | maxValue:16, 308 | numberOfSteps:15, 309 | defaultValue:1 310 | }); 311 | 312 | // 2 313 | PluginParameters.push({ 314 | name:"Length", 315 | type:"lin", 316 | minValue:0, 317 | maxValue:16, 318 | numberOfSteps:16, 319 | defaultValue:16 320 | }); 321 | 322 | // 3 323 | PluginParameters.push({ 324 | name:"Skew", 325 | type:"lin", 326 | minValue:-100, 327 | maxValue:100, 328 | numberOfSteps:200, 329 | defaultValue:0 330 | }); 331 | 332 | // 4 333 | PluginParameters.push({ 334 | name:"Division", 335 | type:"menu", 336 | valueStrings:NOTE_LENGTH_KEYS, 337 | defaultValue:4 338 | }); 339 | 340 | // 5 - 21 341 | for (let index = 0; index < 16 ; index++) { 342 | PluginParameters.push({ 343 | name: "Beat " + (index + 1), 344 | type:"lin", 345 | minValue:0, 346 | maxValue:100, 347 | numberOfSteps:100, 348 | defaultValue:50 349 | }); 350 | } -------------------------------------------------------------------------------- /example_scripts/oc_live_humanize.js: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | Name: Live Humanize 3 | Author(s): Philip Regan 4 | Purpose: Recreates the Humanize MIDI transformation found in the piano roll. By 5 | having this functionality in Scripter, that means it can be used during live 6 | play. 7 | 8 | Transforms the following for any MIDI event which has these properties: 9 | * Shift the played beat. Limited to within the bounds of a process block, 10 | approximately 1/64th note. 11 | * Velocity 12 | * Detune (for Apple-supplied synths ONLY) 13 | * Pitch bend (PR: this may not actually be complete) 14 | 15 | This script is released under the MIT License. 16 | 17 | Permissions 18 | * Commercial use 19 | * Modification 20 | * Distribution 21 | * Private use 22 | 23 | Limitations 24 | x Liability 25 | x Warranty 26 | 27 | Conditions 28 | ! License and copyright notice 29 | 30 | Copyright Philip Regan and Pilcrow Records 31 | 32 | Permission is hereby granted, free of charge, to any person obtaining 33 | a copy of this software and associated documentation files (the 34 | "Software"), to deal in the Software without restriction, including 35 | without limitation the rights to use, copy, modify, merge, publish, 36 | distribute, sublicense, and/or sell copies of the Software, and to 37 | permit persons to whom the Software is furnished to do so, subject to 38 | the following conditions: 39 | 40 | The above copyright notice and this permission notice shall be 41 | included in all copies or substantial portions of the Software. 42 | 43 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 44 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 45 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 46 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 47 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 48 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 49 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 50 | 51 | ****************************************************************************/ 52 | 53 | /* 54 | SCRIPTER GLOBAL VARIABLES 55 | */ 56 | 57 | var NeedsTimingInfo = true; 58 | var PluginParameters = []; 59 | 60 | /* 61 | CUSTOM GLOBAL VARIABLES 62 | */ 63 | 64 | const TICKS_IN_BLOCK = 66; 65 | const TIMING_LENGTH = 0.03289494514465319; // assumes I/O Buffer 1024 66 | const BEATS_IN_TICK = TIMING_LENGTH / TICKS_IN_BLOCK; // 0.0004984082597674725 67 | 68 | const PROBABILITY_SLIDER_MIN = 0; 69 | const PROBABILITY_SLIDER_MAX = 100; 70 | const PROBABILITY_SLIDER_DEF = PROBABILITY_SLIDER_MIN; 71 | const PROBABILITY_SLIDER_STEPS = PROBABILITY_SLIDER_MAX; 72 | const PROBABILITY_MIN = 1; 73 | const PROBABILITY_MAX = 100; 74 | 75 | /* Note.beatPos */ 76 | 77 | // probability 78 | 79 | const BEATSHIFT_PROB_PCNAME = "beatPos Shift % Chance"; 80 | var beatShiftProb = PROBABILITY_SLIDER_DEF; 81 | 82 | // shift 83 | 84 | const BEATSHIFT_DEF = 0; 85 | const BEATSHIFT_STEPS = 240; 86 | 87 | const BEATSHIFT_MIN = 0; 88 | const BEATSHIFT_MIN_DEF = 0; 89 | const BEATSHIFT_MIN_PCNAME = "beatPos Shift Min"; 90 | var beatShiftMin = BEATSHIFT_MIN; 91 | 92 | const BEATSHIFT_MAX = 240; 93 | const BEATSHIFT_MAX_DEF = 0; 94 | const BEATSHIFT_MAX_PCNAME = "beatPos Shift Max"; 95 | var beatShiftMax = BEATSHIFT_MAX; 96 | 97 | /* Note.velocity */ 98 | 99 | // probability 100 | 101 | const VELOCITY_PROB_PCNAME = "Velocity Shift % Chance"; 102 | var velocityProb = PROBABILITY_SLIDER_DEF; 103 | 104 | // shift 105 | 106 | const VELOCITY_SHIFT_DEF = 0; 107 | const VELOCITY_SHIFT_STEPS = 254; 108 | 109 | const VELOCITY_SHIFT_MIN = -127; 110 | const VELOCITY_SHIFT_MIN_PCNAME = "Velocity Shift Min"; 111 | var velocityShiftMin = VELOCITY_SHIFT_MIN; 112 | 113 | const VELOCITY_SHIFT_MAX = 127; 114 | const VELOCITY_SHIFT_MAX_PCNAME = "Velocity Shift Max"; 115 | var velocityShiftMax = VELOCITY_SHIFT_MAX; 116 | 117 | // detune 118 | 119 | const DETUNE_SHIFT_DEF = 0; 120 | const DETUNE_SHIFT_STEPS = 25400; 121 | 122 | const DETUNE_SHIFT_MIN = -12700; 123 | const DETUNE_SHIFT_MIN_PCNAME = "Detune Shift Min"; 124 | var detuneShiftMin = DETUNE_SHIFT_DEF; 125 | 126 | const DETUNE_SHIFT_MAX = 12700; 127 | const DETUNE_SHIFT_MAX_PCNAME = "Detune Shift Max"; 128 | var detuneShiftMax = DETUNE_SHIFT_DEF; 129 | 130 | const DETUNE_PROB_PCNAME = "Detune Shift % Chance"; 131 | var detuneProb = PROBABILITY_SLIDER_DEF; 132 | 133 | // pitch bend 134 | 135 | const PITCHBEND_SHIFT_DEF = 0; 136 | const PITCHBEND_SHIFT_STEPS = 16383; 137 | 138 | const PITCHBEND_SHIFT_MIN = -8192; 139 | const PITCHBEND_SHIFT_MIN_PCNAME = "PitchBend Min"; 140 | var pitchBendMin = PITCHBEND_SHIFT_DEF; 141 | 142 | const PITCHBEND_SHIFT_MAX = 8191; 143 | const PITCHBEND_SHIFT_MAX_PCNAME = "PitchBend Max"; 144 | var pitchBendMax = PITCHBEND_SHIFT_DEF; 145 | 146 | const PITCHBEND_EVENT_TRIGGER_VALUES = ["One New NoteOn", "On New Process Block", "On Both New NoteOn and ProcessBlock"]; 147 | const PITCHBEND_EVENT_TRIGGER_PCNAME = "PitchBend Event Triggers"; 148 | var pitchBendEventValue = 0; 149 | 150 | const PITCHBEND_PROB_PCNAME = "PitchBend % Chance"; 151 | var pitchBendProb = PROBABILITY_SLIDER_DEF; 152 | 153 | /* 154 | SCRIPTER FUNCTIONS 155 | */ 156 | 157 | function HandleMIDI( event ) { 158 | 159 | if ( event instanceof NoteOn ) { 160 | 161 | // all probabilities are handled in the functions 162 | event.velocity = getRandomVelocityShift( event.velocity ); 163 | var beatShift = getRandomBeatShift(); 164 | event.sendAtBeat( event.beatPos + beatShift ); 165 | event.detune = getRandomDetuneShift( event.detune ); 166 | 167 | } 168 | 169 | if ( pitchBendEventValue == 0 || pitchBendEventValue == 2 ) { 170 | var pitchBendValue = getRandomPitchBend(); 171 | if ( pitchBendValue != 0 ) { 172 | var pitchBend = new PitchBend(); 173 | pitchBend.value = pitchBendValue; 174 | pitchBend.send(); 175 | } 176 | } 177 | 178 | event.send(); 179 | 180 | 181 | } 182 | 183 | function ProcessMIDI() { 184 | var timingInfo = GetTimingInfo(); 185 | if ( timingInfo.playing ) { 186 | 187 | if ( pitchBendEventValue == 1 || pitchBendEventValue == 2 ) { 188 | var pitchBendValue = getRandomPitchBend(); 189 | if ( pitchBendValue != 0 ) { 190 | var pitchBend = new PitchBend(); 191 | pitchBend.value = pitchBendValue; 192 | pitchBend.send(); 193 | } 194 | } 195 | } else { 196 | MIDI.allNotesOff(); 197 | } 198 | } 199 | 200 | function ParameterChanged( param, value ) { 201 | switch( param ) { 202 | case 0: 203 | beatShiftProb = value; 204 | break; 205 | case 1: 206 | beatShiftMin = value; 207 | break; 208 | case 2: 209 | beatShiftMax = value; 210 | break; 211 | case 3: 212 | velocityProb = value; 213 | break; 214 | case 4: 215 | velocityShiftMin = value; 216 | break; 217 | case 5: 218 | velocityShiftMax = value; 219 | break; 220 | case 6: 221 | detuneProb = value; 222 | break; 223 | case 7: 224 | detuneShiftMin = value; 225 | break; 226 | case 8: 227 | detuneShiftMax = value; 228 | break; 229 | case 9: 230 | detuneProb = value; 231 | break; 232 | case 10: 233 | detuneShiftMin = value; 234 | break; 235 | case 11: 236 | detuneShiftMax = value; 237 | break; 238 | case 12: 239 | pitchBendEventValue = value; 240 | default: 241 | Trace("ERROR: ParameterChanged: (" + param + ", " + value + ")"); 242 | } 243 | } 244 | 245 | /* 246 | CUSTOM FUNCTIONS 247 | */ 248 | 249 | function getRandomBeatShift() { 250 | var prob = rInt( PROBABILITY_MIN, PROBABILITY_MAX ); 251 | if ( prob <= beatShiftProb ) { 252 | var beatPosShift = rInt( beatShiftMin, beatShiftMax ); 253 | return beatPosShift * BEATS_IN_TICK; 254 | } 255 | return 0; 256 | } 257 | 258 | function getRandomVelocityShift( originalVelocity ) { 259 | var prob = rInt( PROBABILITY_MIN, PROBABILITY_MAX ); 260 | if ( prob <= velocityProb ) { 261 | var r = rInt( velocityShiftMin, velocityShiftMax ); 262 | return MIDI.normalizeData( originalVelocity + r ); 263 | } 264 | return originalVelocity; 265 | } 266 | 267 | function getRandomDetuneShift( originalDetune ) { 268 | var prob = rInt( PROBABILITY_MIN, PROBABILITY_MAX ); 269 | if ( prob <= detuneProb ) { 270 | var detuneShift = rInt( detuneShiftMin, detuneShiftMax ); 271 | var newDetune = originalDetune += detuneShift; 272 | var normalizedDetune = normalizeDetune( newDetune ); 273 | return normalizedDetune; 274 | } 275 | return originalDetune; 276 | } 277 | 278 | // pitch bend is a simple value application 279 | function getRandomPitchBend() { 280 | var prob = rInt( PROBABILITY_MIN, PROBABILITY_MAX ); 281 | if ( prob <= pitchBendProb ) { 282 | var r = rInt( pitchBendMin, pitchBendMax ); 283 | return r; 284 | } 285 | return 0; 286 | } 287 | 288 | 289 | function normalizeDetune( detune ) { 290 | if ( detune > DETUNE_SHIFT_MAX ) { 291 | return DETUNE_SHIFT_MAX; 292 | } else if ( detune < DETUNE_SHIFT_MIN ) { 293 | return DETUNE_SHIFT_MIN; 294 | } else { 295 | return detune; 296 | } 297 | } 298 | 299 | function rInt( min, max ) { 300 | if (min == max ) {return min;} 301 | return Math.floor(min + Math.random()*(max + 1 - min)); 302 | 303 | } 304 | 305 | /* 306 | PARAMETER CONTROL MANAGEMENT 307 | 308 | -> Remember to update ParameterChanged() 309 | */ 310 | // 0 311 | PluginParameters.push({ 312 | name: BEATSHIFT_PROB_PCNAME, 313 | type: "lin", 314 | minValue: PROBABILITY_SLIDER_MIN, 315 | maxValue: PROBABILITY_SLIDER_MAX, 316 | defaultValue: PROBABILITY_SLIDER_DEF, 317 | numberOfSteps: PROBABILITY_SLIDER_STEPS, 318 | unit:"%" 319 | }); 320 | // 1 321 | PluginParameters.push({ 322 | name: BEATSHIFT_MIN_PCNAME, 323 | type: "lin", 324 | minValue: BEATSHIFT_MIN, 325 | maxValue: BEATSHIFT_MAX, 326 | defaultValue: BEATSHIFT_MIN_DEF, 327 | numberOfSteps: BEATSHIFT_STEPS, 328 | unit:"Integer" 329 | }); 330 | // 2 331 | PluginParameters.push({ 332 | name: BEATSHIFT_MAX_PCNAME, 333 | type: "lin", 334 | minValue: BEATSHIFT_MIN, 335 | maxValue: BEATSHIFT_MAX, 336 | defaultValue: BEATSHIFT_MAX_DEF, 337 | numberOfSteps: BEATSHIFT_STEPS, 338 | unit:"Integer" 339 | }); 340 | 341 | PluginParameters.push({ 342 | name: VELOCITY_PROB_PCNAME, 343 | type: "lin", 344 | minValue: PROBABILITY_SLIDER_MIN, 345 | maxValue: PROBABILITY_SLIDER_MAX, 346 | defaultValue: PROBABILITY_SLIDER_DEF, 347 | numberOfSteps: PROBABILITY_SLIDER_STEPS, 348 | unit:"%" 349 | }); 350 | 351 | PluginParameters.push({ 352 | name: VELOCITY_SHIFT_MIN_PCNAME, 353 | type: "lin", 354 | minValue: VELOCITY_SHIFT_MIN, 355 | maxValue: VELOCITY_SHIFT_MAX, 356 | defaultValue: VELOCITY_SHIFT_DEF, 357 | numberOfSteps: VELOCITY_SHIFT_STEPS, 358 | unit:"Integer" 359 | }); 360 | 361 | PluginParameters.push({ 362 | name: VELOCITY_SHIFT_MAX_PCNAME, 363 | type: "lin", 364 | minValue: VELOCITY_SHIFT_MIN, 365 | maxValue: VELOCITY_SHIFT_MAX, 366 | defaultValue: VELOCITY_SHIFT_DEF, 367 | numberOfSteps: VELOCITY_SHIFT_STEPS, 368 | unit:"Integer" 369 | }); 370 | 371 | PluginParameters.push({ 372 | name: DETUNE_PROB_PCNAME, 373 | type: "lin", 374 | minValue: PROBABILITY_SLIDER_MIN, 375 | maxValue: PROBABILITY_SLIDER_MAX, 376 | defaultValue: PROBABILITY_SLIDER_DEF, 377 | numberOfSteps: PROBABILITY_SLIDER_STEPS, 378 | unit:"%" 379 | }); 380 | 381 | PluginParameters.push({ 382 | name: DETUNE_SHIFT_MIN_PCNAME, 383 | type: "lin", 384 | minValue: DETUNE_SHIFT_MIN, 385 | maxValue: DETUNE_SHIFT_MAX, 386 | defaultValue: DETUNE_SHIFT_DEF, 387 | numberOfSteps: DETUNE_SHIFT_STEPS, 388 | unit:"Integer" 389 | }); 390 | 391 | PluginParameters.push({ 392 | name: DETUNE_SHIFT_MAX_PCNAME, 393 | type: "lin", 394 | minValue: DETUNE_SHIFT_MIN, 395 | maxValue: DETUNE_SHIFT_MAX, 396 | defaultValue: DETUNE_SHIFT_DEF, 397 | numberOfSteps: DETUNE_SHIFT_STEPS, 398 | unit:"Integer" 399 | }); 400 | 401 | PluginParameters.push({ 402 | name: PITCHBEND_PROB_PCNAME, 403 | type: "lin", 404 | minValue: PROBABILITY_SLIDER_MIN, 405 | maxValue: PROBABILITY_SLIDER_MAX, 406 | defaultValue: PROBABILITY_SLIDER_DEF, 407 | numberOfSteps: PROBABILITY_SLIDER_STEPS, 408 | unit:"%" 409 | }); 410 | 411 | PluginParameters.push({ 412 | name: PITCHBEND_SHIFT_MIN_PCNAME, 413 | type: "lin", 414 | minValue: PITCHBEND_SHIFT_MIN, 415 | maxValue: PITCHBEND_SHIFT_MAX, 416 | defaultValue: PITCHBEND_SHIFT_DEF, 417 | numberOfSteps: PITCHBEND_SHIFT_STEPS, 418 | unit:"Integer" 419 | }); 420 | 421 | PluginParameters.push({ 422 | name: PITCHBEND_SHIFT_MAX_PCNAME, 423 | type: "lin", 424 | minValue: PITCHBEND_SHIFT_MIN, 425 | maxValue: PITCHBEND_SHIFT_MAX, 426 | defaultValue: PITCHBEND_SHIFT_DEF, 427 | numberOfSteps: PITCHBEND_SHIFT_STEPS, 428 | unit:"Integer" 429 | }); 430 | 431 | PluginParameters.push({ 432 | name: PITCHBEND_EVENT_TRIGGER_PCNAME, 433 | type: "menu", 434 | valueStrings: PITCHBEND_EVENT_TRIGGER_VALUES, 435 | defaultValue: 1 436 | }); 437 | -------------------------------------------------------------------------------- /example_scripts/oc_live_velocity_transform.js: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | Name: Velocity Limiter 3 | Author(s): Philip Regan 4 | Purpose: 5 | * Live versions of the `Fixed Velocity' and `Velocity Limiter' MIDI Transforms 6 | 7 | This script is released under the MIT License. 8 | 9 | Permissions 10 | * Commercial use 11 | * Modification 12 | * Distribution 13 | * Private use 14 | 15 | Limitations 16 | x Liability 17 | x Warranty 18 | 19 | Conditions 20 | ! License and copyright notice 21 | 22 | Copyright Philip Regan and Pilcrow Records 23 | 24 | Permission is hereby granted, free of charge, to any person obtaining 25 | a copy of this software and associated documentation files (the 26 | "Software"), to deal in the Software without restriction, including 27 | without limitation the rights to use, copy, modify, merge, publish, 28 | distribute, sublicense, and/or sell copies of the Software, and to 29 | permit persons to whom the Software is furnished to do so, subject to 30 | the following conditions: 31 | 32 | The above copyright notice and this permission notice shall be 33 | included in all copies or substantial portions of the Software. 34 | 35 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 36 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 37 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 38 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 39 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 40 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 41 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 42 | 43 | ****************************************************************************/ 44 | 45 | var PluginParameters = []; 46 | const PARAM_NAME_FIXED_VELOCITY_SWITCH = "Fixed Velocity Switch"; 47 | const PARAM_NAME_FIXED_VELOCITY_VALUE = "Fixed Velocity Value"; 48 | const PARAM_NAME_VELOCITY_LIMITER_SWITCH = "Velocity Limiter Switch"; 49 | const PARAM_NAME_VELOCITY_LIMITER_MIN = "Velocity Limiter Minimum"; 50 | const PARAM_NAME_VELOCITY_LIMITER_MAX = "Velocity Limiter Maximum"; 51 | 52 | function HandleMIDI(event) 53 | { 54 | if ( event instanceof NoteOn ) { 55 | if ( GetParameter( PARAM_NAME_FIXED_VELOCITY_SWITCH )) { 56 | event.velocity = GetParameter( PARAM_NAME_FIXED_VELOCITY_VALUE ); 57 | } 58 | 59 | if ( GetParameter( PARAM_NAME_VELOCITY_LIMITER_SWITCH )) { 60 | if ( event.velocity < GetParameter( PARAM_NAME_VELOCITY_LIMITER_MIN ) ) { 61 | event.velocity = GetParameter( PARAM_NAME_VELOCITY_LIMITER_MIN ); 62 | } 63 | if ( event.velocity > GetParameter( PARAM_NAME_VELOCITY_LIMITER_MAX ) ) { 64 | event.velocity = GetParameter( PARAM_NAME_VELOCITY_LIMITER_MAX ); 65 | } 66 | } 67 | } 68 | 69 | event.send(); 70 | } 71 | 72 | PluginParameters.push({ 73 | name:PARAM_NAME_FIXED_VELOCITY_SWITCH, 74 | type:"checkbox", 75 | defaultValue:0 76 | }); 77 | 78 | PluginParameters.push({ 79 | name:PARAM_NAME_FIXED_VELOCITY_VALUE, 80 | type:"lin", 81 | minValue:0, 82 | maxValue:128, 83 | numberOfSteps:128, 84 | defaultValue:90 85 | }); 86 | 87 | PluginParameters.push({ 88 | name:PARAM_NAME_VELOCITY_LIMITER_SWITCH, 89 | type:"checkbox", 90 | defaultValue:0 91 | }); 92 | 93 | PluginParameters.push({ 94 | name:PARAM_NAME_VELOCITY_LIMITER_MIN, 95 | type:"lin", 96 | minValue:0, 97 | maxValue:128, 98 | numberOfSteps:128, 99 | defaultValue:30 100 | }); 101 | 102 | PluginParameters.push({ 103 | name:PARAM_NAME_VELOCITY_LIMITER_MAX, 104 | type:"lin", 105 | minValue:0, 106 | maxValue:128, 107 | numberOfSteps:128, 108 | defaultValue:90 109 | }); -------------------------------------------------------------------------------- /example_scripts/oc_scripter_adsr.js: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | Name: Scripter ADSR 3 | Author(s): Philip Regan 4 | Purpose: 5 | * Recreates the ADSR envelope in the Modulator MIDI Effect Plug-In. The 6 | envelope outputs a number between 0.0-1.0, which can be used as a multiplier 7 | for any kind parameter or property. This script affects the velocity, length, 8 | and detune of NoteOn events. 9 | * Leverages the following design patterns 10 | * Tracking events across process blocks 11 | * Managing music theory 12 | * Managing active notes (in this case ADSR envelopes) 13 | * Contains the use of Objects to manage discrete envelopes for every NoteOn event. 14 | * BUG TO FIX: When looping, ensure any cycle is longer than the overall length of 15 | potentially running envelopes because one of two things is happening: 16 | * either the synth will become overwhelmed with NoteOn events. 17 | * Or the timing info isn't be calculated properly at it related to the 18 | envelope objects. 19 | 20 | This script is released under the MIT License. 21 | 22 | Permissions 23 | * Commercial use 24 | * Modification 25 | * Distribution 26 | * Private use 27 | 28 | Limitations 29 | x Liability 30 | x Warranty 31 | 32 | Conditions 33 | ! License and copyright notice 34 | 35 | Copyright Philip Regan and Pilcrow Records 36 | 37 | Permission is hereby granted, free of charge, to any person obtaining 38 | a copy of this software and associated documentation files (the 39 | "Software"), to deal in the Software without restriction, including 40 | without limitation the rights to use, copy, modify, merge, publish, 41 | distribute, sublicense, and/or sell copies of the Software, and to 42 | permit persons to whom the Software is furnished to do so, subject to 43 | the following conditions: 44 | 45 | The above copyright notice and this permission notice shall be 46 | included in all copies or substantial portions of the Software. 47 | 48 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 49 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 50 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 51 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 52 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 53 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 54 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 55 | 56 | ******************************************************************************/ 57 | 58 | /* 59 | SCRIPTER GLOBAL VARIABLES 60 | */ 61 | 62 | var NeedsTimingInfo = true; 63 | var PluginParameters = []; 64 | 65 | /* 66 | CUSTOM GLOBAL VARIABLES 67 | */ 68 | 69 | const NOTE_LENGTHS_LIB = { 70 | "No length" : 0.000, 71 | "1/64" : 0.063, 72 | "1/64d" : 0.094, 73 | "1/64t" : 0.021, 74 | "1/32" : 0.125, 75 | "1/32d" : 0.188, 76 | "1/32t" : 0.041, 77 | "1/16" : 0.250, 78 | "1/16d" : 0.375, 79 | "1/16t" : 0.333, 80 | "1/8" : 0.500, 81 | "1/8d" : 0.750, 82 | "1/8t" : 0.167, 83 | "1/4" : 1.000, 84 | "1/4d" : 1.500, 85 | "1/4t" : 0.300, 86 | "1/2" : 2.000, 87 | "1/2d" : 3.000, 88 | "1/2t" : 0.667, 89 | "1 bar" : 4.000, 90 | "1.5 bars" : 6.000, 91 | "2 bars" : 8.000, 92 | "3 bars" : 12.000, 93 | "4 bars" : 16.000, 94 | "6 bars" : 24.000, 95 | "8 bars" : 32.000, 96 | "10 bars" : 40.000, 97 | "12 bars" : 48.000, 98 | "16 bars" : 64.000, 99 | "20 bars" : 80.000, 100 | "24 bars" : 96.000, 101 | "28 bars" : 112.000, 102 | "32 bars" : 128.000, 103 | "36 bars" : 144.000, 104 | "40 bars" : 160.000 105 | }; 106 | var NOTE_LENGTH_KEYS = Object.keys( NOTE_LENGTHS_LIB ); 107 | 108 | /* adsr params */ 109 | 110 | const PARAM_NAME_ATTACK_LENGTH = "Attack Length"; 111 | const PARAM_NAME_DECAY_LENGTH = "Decay Length"; 112 | const PARAM_NAME_SUSTAIN_LENGTH = "Sustain Length"; 113 | const PARAM_NAME_SUSTAIN_LEVEL = "Sustain Level"; 114 | const PARAM_NAME_RELEASE_LENGTH = "Release Length"; 115 | 116 | /* adsr effect params */ 117 | /* velocity */ 118 | const PARAM_NAME_VELOCITY_SWITCH = "Velocity"; 119 | const PARAM_NAME_VELOCITY_MIN = "Velocity Min"; 120 | const PARAM_NAME_VELOCITY_MAX = "Velocity Max"; 121 | const PARAM_VELOCITY_MIN = 0; 122 | const PARAM_VELOCITY_MAX = 127; 123 | 124 | /* length */ 125 | const PARAM_NAME_LENGTH_SWITCH = "Length"; 126 | const PARAM_NAME_LENGTH_CONSTANT = "Length (Constant)"; 127 | const PARAM_NAME_LENGTH_ENV_MIN = "Length (Env) Min"; 128 | const PARAM_NAME_LENGTH_ENV_MAX = "Length (Env) Max"; 129 | 130 | /* detune */ 131 | const PARAM_NAME_DETUNE_SWITCH = "Detune"; 132 | const PARAM_NAME_DETUNE_MIN = "Detune Min"; 133 | const PARAM_NAME_DETUNE_MAX = "Detune Max"; 134 | const PARAM_DETUNE_MIN = -127; 135 | const PARAM_DETUNE_MAX = 127; 136 | 137 | /* managing played notes */ 138 | 139 | var ACTIVE_ADSRS = []; 140 | 141 | const RESET_VALUE = -1.0; 142 | var TRIGGER = RESET_VALUE; 143 | const CURSOR_INCREMENT = 0.001; // smallest note length = 0.125 144 | 145 | /* 146 | SCRIPTER FUNCTIONS 147 | */ 148 | 149 | function HandleMIDI( event ) { 150 | 151 | if ( event instanceof NoteOn ) { 152 | var adsr = new ADSR(); 153 | adsr.initialize( event ); 154 | ACTIVE_ADSRS.push( adsr ); 155 | } else if ( event instanceof NoteOff ) { 156 | // do nothing 157 | } else { 158 | event.send(); 159 | } 160 | } 161 | 162 | function ProcessMIDI() { 163 | var timing_info = GetTimingInfo(); 164 | 165 | // when the transport stops, stop any playing notes and track the cursor and trigger so play can begin uninterrupted 166 | if ( !timing_info.playing ){ 167 | cursor = timing_info.blockStartBeat; 168 | TRIGGER = RESET_VALUE; 169 | ACTIVE_ADSRS = []; 170 | return; 171 | } 172 | 173 | // calculate beat to schedule 174 | var lookAheadEnd = timing_info.blockEndBeat; 175 | var cursor = timing_info.blockStartBeat; 176 | if ( TRIGGER == RESET_VALUE ) { 177 | TRIGGER = timing_info.blockStartBeat; 178 | } 179 | 180 | // trigger can get stuck outside of cycle causing whole cycle loss of music 181 | if ( timing_info.cycling && ( !TRIGGER || TRIGGER > timing_info.rightCycleBeat ) ) { 182 | TRIGGER = ( timing_info.rightCycleBeat > timing_info.blockEndBeat ? timing_info.rightCycleBeat : timing_info.blockEndBeat ); 183 | // Assumes the cycle is on a whole number (quarter beat/bottom denominator in time sig); 184 | if ( TRIGGER == timing_info.rightCycleBeat && Math.trunc(cursor) == timing_info.leftCycleBeat ) { 185 | TRIGGER = timing_info.blockStartBeat; 186 | } 187 | 188 | } 189 | // cycling the playhead cretes buffers which need to be managed 190 | // the buffers are the edges of the cycle 191 | // process blocks do not line up with cycle bounds 192 | // when cycling, find the beats that wrap around the last buffer 193 | if ( timing_info.cycling && lookAheadEnd >= timing_info.rightCycleBeat ) { 194 | // is the end of the process block past the end of the cycle? 195 | if ( lookAheadEnd >= timing_info.rightCycleBeat ) { 196 | // get the length of the process block 197 | var cycleBeats = timing_info.rightCycleBeat - timing_info.leftCycleBeat; 198 | // get the difference between the end of the process block and the cycle length 199 | // this will be the relative shift back to the beginning of the cycle 200 | var cycleEnd = lookAheadEnd - cycleBeats; 201 | } 202 | } 203 | 204 | // increment the cursor through the beats that fall within this cycle's buffers 205 | while ((cursor >= timing_info.blockStartBeat && cursor < lookAheadEnd) 206 | // including beats that wrap around the cycle point 207 | || (timing_info.cycling && cursor < cycleEnd)) { 208 | // adjust the cursor and the trigger for the cycle 209 | if (timing_info.cycling && cursor >= timing_info.rightCycleBeat) { 210 | cursor -= (timing_info.rightCycleBeat - timing_info.leftCycleBeat); 211 | TRIGGER = cursor; 212 | } 213 | 214 | /* 215 | PROCESS ADSRs here 216 | */ 217 | 218 | // we need to manage the length of the active adsrs so we don't run out of memory 219 | var adsrs_to_remove = []; 220 | var i = 0; 221 | ACTIVE_ADSRS.forEach( function ( adsr ) { 222 | // assume the adsr is made active when created with the NoteOn event 223 | if ( adsr.state != adsr.states.idle ) { 224 | 225 | if ( adsr.state == adsr.states.init ) { 226 | adsr.calc_envelope( 227 | cursor, 228 | NOTE_LENGTHS_LIB[NOTE_LENGTH_KEYS[GetParameter(PARAM_NAME_ATTACK_LENGTH)]], 229 | NOTE_LENGTHS_LIB[NOTE_LENGTH_KEYS[GetParameter(PARAM_NAME_DECAY_LENGTH)]], 230 | NOTE_LENGTHS_LIB[NOTE_LENGTH_KEYS[GetParameter(PARAM_NAME_SUSTAIN_LENGTH)]], 231 | GetParameter(PARAM_NAME_SUSTAIN_LEVEL), 232 | NOTE_LENGTHS_LIB[NOTE_LENGTH_KEYS[GetParameter(PARAM_NAME_RELEASE_LENGTH)]] 233 | ); 234 | // TODO: Recalc based on cycling 235 | } 236 | 237 | adsr.process(); 238 | if ( adsr.env_trigger < adsr.env_cursor ) { 239 | adsr.env_trigger = adsr.env_cursor; 240 | } 241 | if ( adsr.env_trigger == adsr.env_cursor ) { 242 | 243 | // create a NoteOn with the adsr's note 244 | var note_on = new NoteOn(); 245 | note_on.pitch = adsr.note_obj.pitch; 246 | 247 | if ( GetParameter( PARAM_NAME_DETUNE_SWITCH )) { 248 | note_on.detune = calc_percent_delta( 249 | GetParameter( PARAM_NAME_DETUNE_MIN ), 250 | GetParameter( PARAM_NAME_DETUNE_MAX ), 251 | adsr.output_value 252 | ); 253 | } 254 | 255 | if ( GetParameter( PARAM_NAME_VELOCITY_SWITCH ) ) { 256 | note_on.velocity = calc_percent_delta( 257 | GetParameter(PARAM_NAME_VELOCITY_MIN), 258 | GetParameter(PARAM_NAME_VELOCITY_MAX), 259 | adsr.output_value 260 | ); 261 | } 262 | 263 | if ( GetParameter( PARAM_NAME_LENGTH_SWITCH ) ) { 264 | var note_length = calc_percent_delta( 265 | NOTE_LENGTHS_LIB[NOTE_LENGTH_KEYS[GetParameter(PARAM_NAME_LENGTH_ENV_MIN)]], 266 | NOTE_LENGTHS_LIB[NOTE_LENGTH_KEYS[GetParameter(PARAM_NAME_LENGTH_ENV_MAX)]], 267 | adsr.output_value 268 | ); 269 | } else { 270 | var note_length = NOTE_LENGTHS_LIB[NOTE_LENGTH_KEYS[GetParameter(PARAM_NAME_LENGTH_CONSTANT)]]; 271 | } 272 | 273 | var note_off = new NoteOff( note_on ); 274 | var note_off_beat = cursor + note_length; 275 | 276 | // adjust for the cycle buffers 277 | if ( timing_info.cycling && note_off_beat >= timing_info.rightCycleBeat ) { 278 | while ( note_off_beat >= timing_info.rightCycleBeat ) { 279 | note_off_beat -= cycleBeats; 280 | // ERROR: note_off_beat = null 281 | // ATTEMPT: chaning cycleBeats to actual calc crams events at the end of the cycle 282 | } 283 | } 284 | note_on.sendAtBeat( cursor ); 285 | note_off.sendAtBeat( note_off_beat ); 286 | 287 | // move the trigger a length from the cursor 288 | adsr.env_cursor = cursor; 289 | adsr.env_trigger = note_off_beat; 290 | 291 | } 292 | } else { 293 | adsrs_to_remove[i] = adsr; 294 | } 295 | i++; 296 | }); 297 | // remove the expended adsrs 298 | for ( let index = 0; index < adsrs_to_remove.length; index++ ) { 299 | const a = adsrs_to_remove[index]; 300 | if ( a ) { 301 | ACTIVE_ADSRS.splice(index, 1); 302 | } 303 | } 304 | 305 | // advance the cursor and trigger to the next beat 306 | cursor += CURSOR_INCREMENT; 307 | if ( TRIGGER < cursor ) { 308 | TRIGGER = cursor; 309 | } 310 | } 311 | } 312 | 313 | function ADSR () { 314 | 315 | this.states = { 316 | idle : 0, 317 | init : 1, 318 | attack : 2, 319 | decay : 3, 320 | sustain : 4, 321 | release : 5 322 | }; 323 | this.state = this.states.idle; 324 | 325 | this.note_obj = null; 326 | 327 | this.max_value = 1.0; 328 | this.min_value = 0.0; 329 | this.output_value = this.min_value; 330 | 331 | this.increment = 0.001; 332 | this.env_cursor = 1.0 - this.increment; 333 | 334 | this.attack_length = 0.0; 335 | this.attack_start = 0.0; 336 | this.attack_end = 0.0; 337 | this.attack_slope = 0.0; 338 | 339 | this.decay_length = 0.0; 340 | this.decay_start = 0.0; 341 | this.decay_end = 0.0; 342 | this.decay_slope = 0.0; 343 | 344 | this.sustain_length = 0.0; 345 | this.sustain_start = 0.0; 346 | this.sustain_end = 0.0; 347 | this.sustain_value = 0.0; 348 | 349 | this.release_length = 0.0; 350 | this.release_start = 0.0; 351 | this.release_end = 0.0; 352 | this.release_slope = 0.0; 353 | 354 | this.env_length = 0.0; 355 | 356 | // initialize prepares all of the values for calculating the envelope 357 | this.initialize = function ( note_obj ) { 358 | this.note_obj = note_obj; 359 | this.state = this.states.init; 360 | } 361 | 362 | this.calc_envelope = function ( cursor, a_len, d_len, s_len, s_val, r_len ) { 363 | // set the phase dimensions for the envelope 364 | this.attack_length = a_len; 365 | this.attack_start = cursor; 366 | this.attack_end = this.attack_start + this.attack_length; 367 | 368 | this.decay_length = d_len; 369 | this.decay_start = this.attack_end; 370 | this.decay_end = this.decay_start + this.decay_length; 371 | 372 | this.sustain_length = s_len; 373 | this.sustain_start = this.decay_end; 374 | this.sustain_end = this.sustain_start + this.sustain_length; 375 | 376 | this.release_length = r_len; 377 | this.release_start = this.sustain_end; 378 | this.release_end = this.release_start + this.release_length; 379 | 380 | this.env_length = this.release_end; 381 | 382 | this.sustain_value = s_val; 383 | 384 | this.attack_slope = this.calc_slope( this.attack_start, this.attack_end, this.min_value, this.max_value ); 385 | this.decay_slope = this.calc_slope( this.decay_start, this.decay_end, this.max_value, ( this.sustain_length > 0 ? this.sustain_value : this.min_value ) ); 386 | this.release_slope = this.calc_slope( this.release_start, this.release_end, ( this.sustain_length > 0 ? this.sustain_value : this.min_value ), this.min_value ); 387 | 388 | this.env_cursor = cursor - this.increment; 389 | 390 | this.state = this.states.attack; 391 | 392 | this.env_trigger = this.attack_start; 393 | } 394 | 395 | this.calc_slope = function ( x1, x2, y1, y2 ) { 396 | let rise = y2 - y1; 397 | let run = x2 - x1; 398 | let slope = rise / run; 399 | 400 | return slope; 401 | } 402 | 403 | // process moves through the envelope if the gate is open 404 | this.process = function () { 405 | // if the envelope is not being progressed for whatever reason 406 | // set the envelope to idle, reset the output value, and return 407 | if ( this.state == this.states.idle ) { 408 | return this.output_value; 409 | } 410 | 411 | if ( this.state == this.states.init ) { 412 | this.state = this.states.attack; 413 | } 414 | 415 | // calcuate the current value based on the position of the cursor 416 | switch ( this.state ) { 417 | case this.states.attack: 418 | 419 | this.output_value = this.attack_slope * ( this.env_cursor - this.attack_start ); 420 | this.output_value = this.truncate(this.output_value); 421 | 422 | // ensure the output is within the bounds of the envelope 423 | if ( this.output_value >= this.max_value ) { 424 | this.output_value = this.max_value; 425 | this.state = this.states.decay; 426 | } 427 | 428 | this.env_cursor += this.increment; 429 | return this.output_value; 430 | 431 | case this.states.decay: 432 | 433 | this.output_value = this.decay_slope * ( this.env_cursor - this.decay_start ); 434 | this.output_value += this.max_value; 435 | this.output_value = this.truncate(this.output_value); 436 | 437 | // ensure the output is within the bounds of the envelope 438 | // check where the cursor is in the envelope 439 | if ( this.sustain_length > 0 ) { 440 | if ( this.output_value <= this.sustain_value ) { 441 | this.output_value = this.sustain_value; 442 | this.state = this.states.sustain; 443 | } 444 | if ( this.output_value <= this.sustain_value ) { 445 | } 446 | } else { 447 | if ( this.output_value <= this.min_value ) { 448 | this.output_value = this.min_value; 449 | this.state = this.states.idle; 450 | } 451 | } 452 | 453 | this.env_cursor += this.increment; 454 | return this.output_value; 455 | 456 | case this.states.sustain: 457 | 458 | if ( this.env_cursor >= this.sustain_end - this.attack_start ) { 459 | this.state = this.states.release; 460 | } 461 | 462 | this.env_cursor += this.increment; 463 | return this.output_value; 464 | 465 | case this.states.release: 466 | 467 | this.output_value = this.release_slope * ( this.env_cursor - this.release_start ); 468 | this.output_value += this.sustain_value; 469 | this.output_value = this.truncate(this.output_value); 470 | 471 | // ensure the output is within the bounds of the envelope 472 | // check where the cursor is in the envelope 473 | if ( this.sustain_length > 0 ) { 474 | if ( this.output_value >= this.sustain_value ) { 475 | this.output_value = this.sustain_value; 476 | } 477 | } 478 | 479 | if ( this.output_value <= this.min_value ) { 480 | this.output_value = this.min_value; 481 | this.state = this.states.idle; 482 | } 483 | 484 | this.env_cursor += this.increment; 485 | return this.output_value; 486 | 487 | default: 488 | Trace("ADSR ERROR: process()"); 489 | // prevent endless processing 490 | break; 491 | } 492 | 493 | this.env_cursor += this.increment; 494 | 495 | } 496 | 497 | this.truncate = function ( n ) { 498 | // return Number.parseFloat(n).toFixed(3); 499 | let t = n.toFixed(3); 500 | let f = Number.parseFloat(t); 501 | return f; 502 | } 503 | } 504 | 505 | function calc_percent_delta( min, max, multiplier ) { 506 | return ( ( max - min ) * multiplier ) + min; 507 | } 508 | 509 | /* 510 | PARAMETER CONTROL MANAGEMENT 511 | 512 | -> Remember to update ParameterChanged() 513 | */ 514 | 515 | // 0 516 | PluginParameters.push({ 517 | name:PARAM_NAME_ATTACK_LENGTH, 518 | type:"menu", 519 | valueStrings:NOTE_LENGTH_KEYS, 520 | defaultValue:10 521 | }); 522 | 523 | // 1 524 | PluginParameters.push({ 525 | name:PARAM_NAME_DECAY_LENGTH, 526 | type:"menu", 527 | valueStrings:NOTE_LENGTH_KEYS, 528 | defaultValue:10 529 | }); 530 | 531 | // 2 532 | PluginParameters.push({ 533 | name:PARAM_NAME_SUSTAIN_LENGTH, 534 | type:"menu", 535 | valueStrings:NOTE_LENGTH_KEYS, 536 | defaultValue:10 537 | }); 538 | 539 | // 3 540 | PluginParameters.push({ 541 | name:PARAM_NAME_RELEASE_LENGTH, 542 | type:"menu", 543 | valueStrings:NOTE_LENGTH_KEYS, 544 | defaultValue:10 545 | }); 546 | 547 | // 4 548 | PluginParameters.push({ 549 | name:PARAM_NAME_SUSTAIN_LEVEL, 550 | type:"lin", 551 | minValue:0.0, 552 | maxValue:1.0, 553 | numberOfSteps:1000, 554 | defaultValue:0.5 555 | }); 556 | 557 | // 5 558 | PluginParameters.push({ 559 | name:PARAM_NAME_VELOCITY_SWITCH, 560 | type:"checkbox", 561 | defaultValue:1 562 | }); 563 | 564 | // 6 565 | PluginParameters.push({ 566 | name:PARAM_NAME_VELOCITY_MIN, 567 | type:"lin", 568 | minValue:PARAM_VELOCITY_MIN, 569 | maxValue:PARAM_VELOCITY_MAX, 570 | numberOfSteps:127, 571 | defaultValue:PARAM_VELOCITY_MIN 572 | }); 573 | 574 | // 7 575 | PluginParameters.push({ 576 | name:PARAM_NAME_VELOCITY_MAX, 577 | type:"lin", 578 | minValue:PARAM_VELOCITY_MIN, 579 | maxValue:PARAM_VELOCITY_MAX, 580 | numberOfSteps:127, 581 | defaultValue:PARAM_VELOCITY_MAX 582 | }); 583 | 584 | // 8 585 | PluginParameters.push({ 586 | name:PARAM_NAME_LENGTH_SWITCH, 587 | type:"checkbox", 588 | defaultValue:0 589 | }); 590 | 591 | // 9 592 | PluginParameters.push({ 593 | name:PARAM_NAME_LENGTH_CONSTANT, 594 | type:"menu", 595 | valueStrings:NOTE_LENGTH_KEYS, 596 | defaultValue:10 597 | }); 598 | 599 | // 9 600 | PluginParameters.push({ 601 | name:PARAM_NAME_LENGTH_ENV_MIN, 602 | type:"menu", 603 | valueStrings:NOTE_LENGTH_KEYS, 604 | defaultValue:4 605 | }); 606 | 607 | // 10 608 | PluginParameters.push({ 609 | name:PARAM_NAME_LENGTH_ENV_MAX, 610 | type:"menu", 611 | valueStrings:NOTE_LENGTH_KEYS, 612 | defaultValue:10 613 | }); 614 | 615 | // 11 616 | PluginParameters.push({ 617 | name:PARAM_NAME_DETUNE_SWITCH, 618 | type:"checkbox", 619 | defaultValue:1 620 | }); 621 | 622 | // 12 623 | PluginParameters.push({ 624 | name:PARAM_NAME_DETUNE_MIN, 625 | type:"lin", 626 | minValue:PARAM_DETUNE_MIN, 627 | maxValue:PARAM_DETUNE_MAX, 628 | numberOfSteps:256, 629 | defaultValue:PARAM_DETUNE_MIN 630 | }); 631 | 632 | // 13 633 | PluginParameters.push({ 634 | name:PARAM_NAME_DETUNE_MAX, 635 | type:"lin", 636 | minValue:PARAM_DETUNE_MIN, 637 | maxValue:PARAM_DETUNE_MAX, 638 | numberOfSteps:256, 639 | defaultValue:PARAM_DETUNE_MAX 640 | }); -------------------------------------------------------------------------------- /example_scripts/oc_scripter_modulator.js: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | 3 | Name: Scripter Modulator 4 | Author(s): Philip Regan 5 | Purpose: 6 | * Recreates the basic functionality of the Modulator MIDI Effect in Scripter. 7 | This makes available the possibility of customizing the functionality beyond 8 | what Modulator can do today (i.e. Shorter cycles than 1/128, longer cycles than 9 | 40 bars), and gives a sense of what it takes to program something like modulator. 10 | * This script along with the ADSR script handles almost all of the functionality 11 | of the Scripter plug-in. 12 | * Contains examples of the following: 13 | * Modeling note lengths and frequencies 14 | * Tracking playhead and beats locations across process blocks and loops 15 | * How to change output across multiple process blocks or within a cycle. 16 | 17 | Instructions for use: 18 | * All waveforms calculate a number between -1.000 to 1.000. This can be used as 19 | a multiplier for many uses. 20 | * There are example functions included for each of the different wave forms. All 21 | of them work basically the same. 22 | * "Waveform" selects the waveform to be used 23 | * "Cycle Length" is the note length of one full period of the waveform (peak 24 | to peak) 25 | * "Steps per Cycle" is used in HandleMIDI to limit the number of 26 | recalculations. 27 | * "Offset" moves the waveform higher or lower than the 0.000 amplitude. 28 | * "Output Level" compresses the waveform to conform to a smaller peak and 29 | trough distance than -1.000 to 1.000. This is also a multplier like the 30 | output. 31 | * "Symmetry" is best used with the Triangle waveform. This is a multiplier 32 | between 0.0 to 1.0. 33 | * A value of 0.0 creates a falling sawtooth wavefore. 34 | * A value of 0.5 creates a triangle waveform. 35 | * A value of 1.0 creates a rising sawtooth waveform. 36 | 37 | * Points of customization and known issues 38 | * The sine waveform does not have the symmetry value calculated like the 39 | Modulator MIDI Effect plug in. 40 | * Because in most cases the "resolution" of the waveform is limited to the 41 | NoteOn events or 1/128 notes, its effect is not as apparent as seeing the 42 | numbers in a bar chart. 43 | * The note lengths and cycle lengths could be extended further to more 44 | extreme values. 45 | 46 | This script is intended to automated by making scale and chord selection 47 | streamlined to two automation lanes, while still offering the ability to 48 | fine tune those selections as needed for blues, jazz, or any scale or 49 | chord variants. 50 | 51 | This script is released under the MIT License. 52 | 53 | Permissions 54 | * Commercial use 55 | * Modification 56 | * Distribution 57 | * Private use 58 | 59 | Limitations 60 | x Liability 61 | x Warranty 62 | 63 | Conditions 64 | ! License and copyright notice 65 | 66 | Copyright Philip Regan and Pilcrow Records 67 | 68 | Permission is hereby granted, free of charge, to any person obtaining 69 | a copy of this software and associated documentation files (the 70 | "Software"), to deal in the Software without restriction, including 71 | without limitation the rights to use, copy, modify, merge, publish, 72 | distribute, sublicense, and/or sell copies of the Software, and to 73 | permit persons to whom the Software is furnished to do so, subject to 74 | the following conditions: 75 | 76 | The above copyright notice and this permission notice shall be 77 | included in all copies or substantial portions of the Software. 78 | 79 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 80 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 81 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 82 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 83 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 84 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 85 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 86 | 87 | The sawtooth code was adapted from the Aquila open source project, Copyright (c) 2007-2014 Zbigniew 88 | Siciarz. Used per the MIT License included in the project. That project can be 89 | found at https://github.com/zsiciarz/aquila 90 | 91 | ******************************************************************************/ 92 | 93 | /* 94 | SCRIPTER GLOBAL VARIABLES 95 | */ 96 | 97 | var NeedsTimingInfo = true; 98 | var PluginParameters = []; 99 | 100 | /* 101 | CUSTOM GLOBAL VARIABLES 102 | */ 103 | // the trigger variable is where the next note (or rest) is to be played 104 | // trigger is global to track it across process blocks 105 | // the cursor is a simulated location of the transport/playhead in the track 106 | // cursor is handled locally because only the current process block matters while playing 107 | const RESET_VALUE = -1.0; 108 | var TRIGGER = RESET_VALUE; 109 | const CURSOR_INCREMENT = 0.001; // smallest note length = 0.125 110 | 111 | 112 | const PI = 3.142; 113 | 114 | const NOTE_LENGTHS_LIB = { 115 | "∞" : CURSOR_INCREMENT, 116 | "1/128" : 0.0105, 117 | "1/128d" : 0.04725, 118 | "1/128t" : 41.600, 119 | "1/64" : 0.063, 120 | "1/64d" : 0.094, 121 | "1/64t" : 0.021, 122 | "1/32" : 0.125, 123 | "1/32d" : 0.188, 124 | "1/32t" : 0.041, 125 | "1/16" : 0.250, 126 | "1/16d" : 0.375, 127 | "1/16t" : 0.333, 128 | "1/8" : 0.500, 129 | "1/8d" : 0.750, 130 | "1/8t" : 0.1667, 131 | "1/4" : 1.000, 132 | "1/4d" : 1.500, 133 | "1/4t" : 0.300, 134 | "1/2" : 2.000, 135 | "1/2d" : 3.000, 136 | "1/2t" : 0.667, 137 | "1 bar" : 4.000, 138 | "1.5 bars" : 6.000, 139 | "2 bars" : 8.000, 140 | "3 bars" : 12.000, 141 | "4 bars" : 16.000, 142 | "6 bars" : 24.000, 143 | "8 bars" : 32.000, 144 | "10 bars" : 40.000, 145 | "12 bars" : 48.000, 146 | "16 bars" : 64.000, 147 | "20 bars" : 80.000, 148 | "24 bars" : 96.000, 149 | "28 bars" : 112.000, 150 | "32 bars" : 128.000, 151 | "36 bars" : 144.000, 152 | "40 bars" : 160.000 153 | }; 154 | var NOTE_LENGTH_KEYS = Object.keys( NOTE_LENGTHS_LIB ); 155 | 156 | const CYCLE_LENGTH_LIB = { 157 | "1/128" : 32.00000000, 158 | "1/128d" : 24.00000000, 159 | "1/128t" : 41.60000000, 160 | "1/64" : 16.00000000, 161 | "1/64d" : 12.00000000, 162 | "1/64t" : 20.80000000, 163 | "1/32" : 8.00000000, 164 | "1/32d" : 6.00000000, 165 | "1/32t" : 10.40000000, 166 | "1/16" : 4.00000000, 167 | "1/16d" : 3.00000000, 168 | "1/16t" : 5.20000000, 169 | "1/8" : 2.00000000, 170 | "1/8d" : 1.50000000, 171 | "1/8t" : 2.60000000, 172 | "1/4" : 1.00000000, 173 | "1/4d" : 0.75000000, 174 | "1/4t" : 1.30000000, 175 | "1/2" : 0.50000000, 176 | "1/2d" : 0.37500000, 177 | "1/2t" : 0.65000000, 178 | "1 bar" : 0.02500000, 179 | "1.5 bars" : 0.01875000, 180 | "2 bars" : 0.01250000, 181 | "3 bars" : 0.00833333, 182 | "4 bars" : 0.00625000, 183 | "6 bars" : 0.00416667, 184 | "8 bars" : 0.00312500, 185 | "10 bars" : 0.00250000, 186 | "12 bars" : 0.00208333, 187 | "16 bars" : 0.00156250, 188 | "20 bars" : 0.00125000, 189 | "24 bars" : 0.00104167, 190 | "28 bars" : 0.00089286, 191 | "32 bars" : 0.00078125, 192 | "36 bars" : 0.00069444, 193 | "40 bars" : 0.00062500 194 | }; 195 | const CYCLE_LENGTH_KEYS = Object.keys( CYCLE_LENGTH_LIB ); 196 | 197 | const PARAM_LIST_WAVEFORMS = [ 198 | "Triangle (⋀)", 199 | "Sine (∿)", 200 | "Square (⊓)", 201 | ]; 202 | const PARAM_NAME_WAVEFORMS = "Waveform"; 203 | const PARAM_NAME_FREQUENCY = "Cycle Length"; 204 | const PARAM_NAME_STEPS = "Steps per Cycle"; 205 | const PARAM_NAME_OFFSET = "Offset"; 206 | const PARAM_NAME_OUTPUT_LEVEL = "Output Level"; 207 | const PARAM_NAME_SYMMETRY = "Symmetry (⋀,⊓ only)"; 208 | 209 | const AMPLITUDE_MIN = -1.0; 210 | const AMPLITUDE_MID = 0.0; 211 | const AMPLITUDE_MAX = 1.0; 212 | 213 | var SETTING_AMP = AMPLITUDE_MAX; 214 | var SETTING_FREQUENCY = CYCLE_LENGTH_LIB["1 bar"]; 215 | var SETTING_TIME = 1.0; 216 | var SETTING_PHASE = 0.0; 217 | var SETTING_STEPS = CYCLE_LENGTH_LIB["1/4"]; 218 | var SETTING_SYMMETRY = 0.0; 219 | var SETTING_OFFSET = 0.0; 220 | var SETTING_OUTPUT_LEVEL = 1.0; 221 | 222 | var CURRENT_WAVEFORM_OUTPUT = AMPLITUDE_MID; 223 | 224 | /* 225 | SCRIPTER FUNCTIONS 226 | */ 227 | 228 | function HandleMIDI(event) { 229 | 230 | 231 | if ( event instanceof NoteOn ) { 232 | 233 | // do something with CURRENT_WAVEFORM_OUTPUT 234 | 235 | // event = triangle_wave_example( event ); 236 | // event = sine_wave_example( event ); 237 | // event = square_wave_example( event ); 238 | } 239 | event.send(); 240 | } 241 | 242 | /* 243 | Examples showing how to effect note velocity with waveforms 244 | */ 245 | 246 | function triangle_wave_example( event ) { 247 | 248 | SetParameter( PARAM_NAME_WAVEFORMS, 0 ); 249 | SetParameter( PARAM_NAME_OFFSET, 0.6 ); 250 | SetParameter( PARAM_NAME_OUTPUT_LEVEL, 0.5 ); 251 | // symmetry 0 = sawtooth falling 252 | // symmetry 0.5 = triangle 253 | // symmetry 1 = sawtooth rising 254 | SetParameter( PARAM_NAME_SYMMETRY, 0.5 ); 255 | 256 | let vel = event.velocity; 257 | let factor = Math.abs( CURRENT_WAVEFORM_OUTPUT ); 258 | let velf = vel * factor; 259 | velf = Math.trunc( velf ); 260 | velf = MIDI.normalizeData(velf); 261 | event.velocity = velf; 262 | return event; 263 | } 264 | 265 | function sine_wave_example ( event ) { 266 | 267 | SetParameter( PARAM_NAME_WAVEFORMS, 1 ); 268 | SetParameter( PARAM_NAME_OFFSET, 0.8 ); 269 | SetParameter( PARAM_NAME_OUTPUT_LEVEL, 0.75 ); 270 | 271 | let vel = event.velocity; 272 | let factor = Math.abs( CURRENT_WAVEFORM_OUTPUT ); 273 | let velf = vel * factor; 274 | velf = Math.trunc( velf ); 275 | velf = MIDI.normalizeData(velf); 276 | event.velocity = velf; 277 | return event; 278 | } 279 | 280 | function square_wave_example( event ) { 281 | SetParameter( PARAM_NAME_WAVEFORMS, 2 ); 282 | SetParameter( PARAM_NAME_OFFSET, 0.8 ); 283 | SetParameter( PARAM_NAME_OUTPUT_LEVEL, 0.75 ); 284 | 285 | let vel = event.velocity; 286 | let factor = Math.abs( CURRENT_WAVEFORM_OUTPUT ); 287 | let velf = vel * factor; 288 | velf = Math.trunc( velf ); 289 | velf = MIDI.normalizeData(velf); 290 | event.velocity = velf; 291 | return event; 292 | } 293 | 294 | function ProcessMIDI() { 295 | var timing_info = GetTimingInfo(); 296 | 297 | // when the transport stops, stop any playing notes and track the cursor and trigger so play can begin uninterrupted 298 | if ( !timing_info.playing ){ 299 | 300 | SETTING_AMP = AMPLITUDE_MAX; 301 | SETTING_FREQUENCY = CYCLE_LENGTH_LIB[CYCLE_LENGTH_KEYS[GetParameter(PARAM_NAME_FREQUENCY)]]; 302 | SETTING_TIME = timing_info.blockStartBeat; 303 | SETTING_STEPS = NOTE_LENGTHS_LIB[NOTE_LENGTH_KEYS[GetParameter(PARAM_NAME_STEPS)]]; 304 | SETTING_OFFSET = GetParameter(PARAM_NAME_OFFSET); 305 | SETTING_OUTPUT_LEVEL = GetParameter(PARAM_NAME_OUTPUT_LEVEL); 306 | SETTING_SYMMETRY = GetParameter(PARAM_NAME_SYMMETRY); 307 | 308 | TRIGGER = RESET_VALUE; 309 | return; 310 | } 311 | 312 | // calculate beat to schedule 313 | var lookAheadEnd = timing_info.blockEndBeat; 314 | var cursor = timing_info.blockStartBeat; 315 | if ( TRIGGER == RESET_VALUE ) { 316 | TRIGGER = timing_info.blockStartBeat; 317 | } 318 | 319 | // trigger can get stuck outside of cycle causing whole cycle loss of music 320 | if ( timing_info.cycling && ( !TRIGGER || TRIGGER > timing_info.rightCycleBeat ) ) { 321 | TRIGGER = ( timing_info.rightCycleBeat > timing_info.blockEndBeat ? timing_info.rightCycleBeat : timing_info.blockEndBeat ); 322 | // Assumes the cycle is on a whole number (quarter beat/bottom denominator in time sig); 323 | if ( TRIGGER == timing_info.rightCycleBeat && Math.trunc(cursor) == timing_info.leftCycleBeat ) { 324 | TRIGGER = timing_info.blockStartBeat; 325 | } 326 | 327 | } 328 | 329 | // cycling the playhead cretes buffers which need to be managed 330 | // the buffers are the edges of the cycle 331 | // process blocks do not line up with cycle bounds 332 | // when cycling, find the beats that wrap around the last buffer 333 | if ( timing_info.cycling && lookAheadEnd >= timing_info.rightCycleBeat ) { 334 | // is the end of the process block past the end of the cycle? 335 | if ( lookAheadEnd >= timing_info.rightCycleBeat ) { 336 | // get the length of the process block 337 | var cycleBeats = timing_info.rightCycleBeat - timing_info.leftCycleBeat; 338 | // get the difference between the end of the process block and the cycle length 339 | // this will be the relative shift back to the beginning of the cycle 340 | var cycleEnd = lookAheadEnd - cycleBeats; 341 | } 342 | } 343 | 344 | // increment the cursor through the beats that fall within this cycle's buffers 345 | while ((cursor >= timing_info.blockStartBeat && cursor < lookAheadEnd) 346 | // including beats that wrap around the cycle point 347 | || (timing_info.cycling && cursor < cycleEnd)) { 348 | // adjust the cursor and the trigger for the cycle 349 | if (timing_info.cycling && cursor >= timing_info.rightCycleBeat) { 350 | cursor -= (timing_info.rightCycleBeat - timing_info.leftCycleBeat); 351 | TRIGGER = cursor; 352 | } 353 | 354 | // the cursor has come to the trigger 355 | if ( cursor == TRIGGER ) { 356 | 357 | SETTING_AMP = AMPLITUDE_MAX; 358 | SETTING_FREQUENCY = CYCLE_LENGTH_LIB[CYCLE_LENGTH_KEYS[GetParameter(PARAM_NAME_FREQUENCY)]]; 359 | SETTING_TIME = timing_info.blockStartBeat; 360 | SETTING_STEPS = NOTE_LENGTHS_LIB[NOTE_LENGTH_KEYS[GetParameter(PARAM_NAME_STEPS)]]; 361 | SETTING_OFFSET = GetParameter(PARAM_NAME_OFFSET); 362 | SETTING_OUTPUT_LEVEL = GetParameter(PARAM_NAME_OUTPUT_LEVEL); 363 | SETTING_SYMMETRY = GetParameter(PARAM_NAME_SYMMETRY); 364 | let waveform_selection = GetParameter( PARAM_NAME_WAVEFORMS ); 365 | switch ( waveform_selection ) { 366 | 367 | case 0: 368 | // "Triangle (⋀)" 369 | CURRENT_WAVEFORM_OUTPUT = calc_triangle( 370 | SETTING_AMP, 371 | SETTING_FREQUENCY, 372 | cursor, 373 | SETTING_PHASE, 374 | SETTING_OFFSET, 375 | SETTING_OUTPUT_LEVEL, 376 | SETTING_SYMMETRY 377 | ) 378 | break; 379 | case 1: 380 | // "Sine (∿)" 381 | CURRENT_WAVEFORM_OUTPUT = calc_sine( 382 | SETTING_AMP, 383 | SETTING_FREQUENCY, 384 | cursor, 385 | SETTING_PHASE, 386 | SETTING_OFFSET, 387 | SETTING_OUTPUT_LEVEL 388 | ) 389 | break; 390 | case 2: 391 | // "Square (⊓)" 392 | CURRENT_WAVEFORM_OUTPUT = calc_square( 393 | SETTING_AMP, 394 | SETTING_FREQUENCY, 395 | cursor, 396 | SETTING_PHASE, 397 | SETTING_OFFSET, 398 | SETTING_OUTPUT_LEVEL, 399 | SETTING_SYMMETRY 400 | ) 401 | break; 402 | default: 403 | Trace( "ERROR: Waveforms Parameter: " + waveform_selection ); 404 | break; 405 | } 406 | 407 | // do something with CURRENT_WAVEFORM_OUTPUT 408 | 409 | var trigger_cache = TRIGGER + GetParameter(PARAM_NAME_STEPS); 410 | 411 | // adjust for the cycle buffers 412 | if ( timing_info.cycling && trigger_cache >= timing_info.rightCycleBeat ) { 413 | while ( trigger_cache >= timing_info.rightCycleBeat ) { 414 | trigger_cache -= cycleBeats; 415 | } 416 | } 417 | 418 | TRIGGER = trigger_cache; 419 | 420 | } 421 | 422 | // advance the cursor and trigger to the next beat 423 | cursor += CURSOR_INCREMENT; 424 | if ( TRIGGER < cursor ) { 425 | TRIGGER = cursor; 426 | } 427 | } 428 | } 429 | 430 | /* 431 | CUSTOM FUNCTIONS 432 | */ 433 | 434 | 435 | function calc_triangle( amp, freq, time, phase, offset, output_level, symmetry ) { 436 | let period = 1.0 / freq; 437 | let r_len = symmetry * period; 438 | let f_len = period - r_len; 439 | let r_inc = ( ( r_len != 0 ) ? ( 2.0 * amp / r_len ) : 0 ); 440 | let f_dec = ( ( f_len != 0 ) ? ( 2.0 * amp / f_len ) : 0 ); 441 | let t = ( time % period ) ; 442 | let result = 0.0; 443 | if ( t < r_len ) { 444 | result = -amp + t * r_inc; 445 | } else { 446 | result = amp - ( t - r_len ) * f_dec; 447 | } 448 | result = (( result + offset ) * output_level).toFixed(3); 449 | return result; 450 | } 451 | 452 | function calc_sine( amp, freq, time, phase, offset, output_level ) { 453 | return limit_to_amp(( ( amp * Math.sin( 2 * PI * freq * time + phase ) ) + offset ) * output_level).toFixed(3); 454 | } 455 | 456 | function calc_square( amp, freq, time, phase, offset, output_level, symmetry ) { 457 | let tri = calc_triangle( amp, freq, time, phase, offset, output_level, symmetry ); 458 | if ( tri >= 0 ) { 459 | return amp; 460 | } else { 461 | return 0.0; 462 | } 463 | } 464 | 465 | function limit_to_amp( n ) { 466 | if ( n > AMPLITUDE_MAX ) { 467 | return AMPLITUDE_MAX; 468 | } 469 | 470 | if ( n < AMPLITUDE_MIN ) { 471 | return AMPLITUDE_MIN; 472 | } 473 | 474 | return n; 475 | } 476 | 477 | /* 478 | PARAMETER CONTROL MANAGEMENT 479 | 480 | -> Remember to update ParameterChanged() 481 | */ 482 | 483 | // 0 484 | PluginParameters.push({ 485 | name:PARAM_NAME_WAVEFORMS, 486 | type:"menu", 487 | valueStrings:PARAM_LIST_WAVEFORMS, 488 | defaultValue:10 489 | }); 490 | 491 | // 1 492 | PluginParameters.push({ 493 | name:PARAM_NAME_FREQUENCY, 494 | type:"menu", 495 | valueStrings:CYCLE_LENGTH_KEYS, 496 | defaultValue:0 497 | }); 498 | 499 | // 2 500 | PluginParameters.push({ 501 | name:PARAM_NAME_STEPS, 502 | type:"menu", 503 | valueStrings:NOTE_LENGTH_KEYS, 504 | defaultValue:0 505 | }); 506 | 507 | // 3 508 | PluginParameters.push({ 509 | name:PARAM_NAME_OFFSET, 510 | type:"lin", 511 | minValue:AMPLITUDE_MIN, 512 | maxValue:AMPLITUDE_MAX, 513 | unit:"+", 514 | numberOfSteps:200, 515 | defaultValue:AMPLITUDE_MID 516 | }); 517 | 518 | // 4 519 | PluginParameters.push({ 520 | name:PARAM_NAME_OUTPUT_LEVEL, 521 | type:"lin", 522 | minValue:AMPLITUDE_MID, 523 | maxValue:AMPLITUDE_MAX, 524 | unit:"×", 525 | numberOfSteps:100, 526 | defaultValue:AMPLITUDE_MAX 527 | }); 528 | 529 | // 5 530 | PluginParameters.push({ 531 | name:PARAM_NAME_SYMMETRY, 532 | type:"lin", 533 | minValue:0.0, 534 | maxValue:1.0, 535 | numberOfSteps:100, 536 | defaultValue:0.0 537 | }); -------------------------------------------------------------------------------- /example_scripts/oc_transposer.js: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | 3 | Name: Transposer JS 4 | Author(s): Philip Regan 5 | Purpose: Recreation of the Transposer MIDI Effects Plug-In 6 | Information: 7 | * Demonstrates 8 | * How to modify events in real time, particularly how to handle potential differences in NoteOn and NoteOff 9 | * How to model scales 10 | * parameter control indexes for pitches aligns with MIDI pitch values 0-11 11 | 12 | This script is released under the MIT License. 13 | 14 | Permissions 15 | * Commercial use 16 | * Modification 17 | * Distribution 18 | * Private use 19 | 20 | Limitations 21 | x Liability 22 | x Warranty 23 | 24 | Conditions 25 | ! License and copyright notice 26 | 27 | Copyright Philip Regan and Pilcrow Records 28 | 29 | Permission is hereby granted, free of charge, to any person obtaining 30 | a copy of this software and associated documentation files (the 31 | "Software"), to deal in the Software without restriction, including 32 | without limitation the rights to use, copy, modify, merge, publish, 33 | distribute, sublicense, and/or sell copies of the Software, and to 34 | permit persons to whom the Software is furnished to do so, subject to 35 | the following conditions: 36 | 37 | The above copyright notice and this permission notice shall be 38 | included in all copies or substantial portions of the Software. 39 | 40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 41 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 42 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 43 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 44 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 45 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 46 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 47 | 48 | ****************************************************************************/ 49 | 50 | /* 51 | SCRIPTER GLOBAL VARIABLES 52 | */ 53 | 54 | var NeedsTimingInfo = true; 55 | var PluginParameters = []; 56 | 57 | /* 58 | CUSTOM GLOBAL VARIABLES 59 | */ 60 | 61 | const CHROMATIC_SCALE_STRINGS = ["C", "C♯/D♭", "D", "D♯/E♭", "E", "F", "F♯/G♭", "G", "G♯/A♭", "A", "A♯/B♭", "B"]; 62 | const SCALE_TEMPLATES = { 63 | "Chromatic" : [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 64 | "Ionian" : [2, 2, 1, 2, 2, 2, 1], 65 | "Dorian" : [2, 1, 2, 2, 2, 1, 2], 66 | "Phrygian" : [1, 2, 2, 2, 1, 2, 2], 67 | "Lydian" : [2, 2, 2, 1, 2, 2, 1], 68 | "Mixolydian" : [2, 2, 1, 2, 2, 1, 2], 69 | "Aeolian" : [2, 1, 2, 2, 1, 2, 2], 70 | "Locrian" : [1, 2, 2, 1, 2, 2, 2] 71 | } 72 | const SCALE_KEYS = Object.keys(SCALE_TEMPLATES); 73 | 74 | /* used for calculating new scales and parameter control updates */ 75 | var CHROMATIC_MAP = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; 76 | var TRANSPOSE_MAP = CHROMATIC_MAP; 77 | 78 | /* used for live transposition */ 79 | var PITCH_SHIFT_TEMPLATE = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; 80 | var PITCH_SHIFT_MAP = PITCH_SHIFT_TEMPLATE; 81 | // 128-item array with pitch's base pitch in the map and the original octave 82 | const PITCH_INFO = [{ "basePitch" : 0 , "octave" : 0 }, { "basePitch" : 1 , "octave" : 0 }, { "basePitch" : 2 , "octave" : 0 }, { "basePitch" : 3 , "octave" : 0 }, { "basePitch" : 4 , "octave" : 0 }, { "basePitch" : 5 , "octave" : 0 }, { "basePitch" : 6 , "octave" : 0 }, { "basePitch" : 7 , "octave" : 0 }, { "basePitch" : 8 , "octave" : 0 }, { "basePitch" : 9 , "octave" : 0 }, { "basePitch" : 10 , "octave" : 0 }, { "basePitch" : 11 , "octave" : 0 }, { "basePitch" : 0 , "octave" : 1 }, { "basePitch" : 1 , "octave" : 1 }, { "basePitch" : 2 , "octave" : 1 }, { "basePitch" : 3 , "octave" : 1 }, { "basePitch" : 4 , "octave" : 1 }, { "basePitch" : 5 , "octave" : 1 }, { "basePitch" : 6 , "octave" : 1 }, { "basePitch" : 7 , "octave" : 1 }, { "basePitch" : 8 , "octave" : 1 }, { "basePitch" : 9 , "octave" : 1 }, { "basePitch" : 10 , "octave" : 1 }, { "basePitch" : 11 , "octave" : 1 }, { "basePitch" : 0 , "octave" : 2 }, { "basePitch" : 1 , "octave" : 2 }, { "basePitch" : 2 , "octave" : 2 }, { "basePitch" : 3 , "octave" : 2 }, { "basePitch" : 4 , "octave" : 2 }, { "basePitch" : 5 , "octave" : 2 }, { "basePitch" : 6 , "octave" : 2 }, { "basePitch" : 7 , "octave" : 2 }, { "basePitch" : 8 , "octave" : 2 }, { "basePitch" : 9 , "octave" : 2 }, { "basePitch" : 10 , "octave" : 2 }, { "basePitch" : 11 , "octave" : 2 }, { "basePitch" : 0 , "octave" : 3 }, { "basePitch" : 1 , "octave" : 3 }, { "basePitch" : 2 , "octave" : 3 }, { "basePitch" : 3 , "octave" : 3 }, { "basePitch" : 4 , "octave" : 3 }, { "basePitch" : 5 , "octave" : 3 }, { "basePitch" : 6 , "octave" : 3 }, { "basePitch" : 7 , "octave" : 3 }, { "basePitch" : 8 , "octave" : 3 }, { "basePitch" : 9 , "octave" : 3 }, { "basePitch" : 10 , "octave" : 3 }, { "basePitch" : 11 , "octave" : 3 }, { "basePitch" : 0 , "octave" : 4 }, { "basePitch" : 1 , "octave" : 4 }, { "basePitch" : 2 , "octave" : 4 }, { "basePitch" : 3 , "octave" : 4 }, { "basePitch" : 4 , "octave" : 4 }, { "basePitch" : 5 , "octave" : 4 }, { "basePitch" : 6 , "octave" : 4 }, { "basePitch" : 7 , "octave" : 4 }, { "basePitch" : 8 , "octave" : 4 }, { "basePitch" : 9 , "octave" : 4 }, { "basePitch" : 10 , "octave" : 4 }, { "basePitch" : 11 , "octave" : 4 }, { "basePitch" : 0 , "octave" : 5 }, { "basePitch" : 1 , "octave" : 5 }, { "basePitch" : 2 , "octave" : 5 }, { "basePitch" : 3 , "octave" : 5 }, { "basePitch" : 4 , "octave" : 5 }, { "basePitch" : 5 , "octave" : 5 }, { "basePitch" : 6 , "octave" : 5 }, { "basePitch" : 7 , "octave" : 5 }, { "basePitch" : 8 , "octave" : 5 }, { "basePitch" : 9 , "octave" : 5 }, { "basePitch" : 10 , "octave" : 5 }, { "basePitch" : 11 , "octave" : 5 }, { "basePitch" : 0 , "octave" : 6 }, { "basePitch" : 1 , "octave" : 6 }, { "basePitch" : 2 , "octave" : 6 }, { "basePitch" : 3 , "octave" : 6 }, { "basePitch" : 4 , "octave" : 6 }, { "basePitch" : 5 , "octave" : 6 }, { "basePitch" : 6 , "octave" : 6 }, { "basePitch" : 7 , "octave" : 6 }, { "basePitch" : 8 , "octave" : 6 }, { "basePitch" : 9 , "octave" : 6 }, { "basePitch" : 10 , "octave" : 6 }, { "basePitch" : 11 , "octave" : 6 }, { "basePitch" : 0 , "octave" : 7 }, { "basePitch" : 1 , "octave" : 7 }, { "basePitch" : 2 , "octave" : 7 }, { "basePitch" : 3 , "octave" : 7 }, { "basePitch" : 4 , "octave" : 7 }, { "basePitch" : 5 , "octave" : 7 }, { "basePitch" : 6 , "octave" : 7 }, { "basePitch" : 7 , "octave" : 7 }, { "basePitch" : 8 , "octave" : 7 }, { "basePitch" : 9 , "octave" : 7 }, { "basePitch" : 10 , "octave" : 7 }, { "basePitch" : 11 , "octave" : 7 }, { "basePitch" : 0 , "octave" : 8 }, { "basePitch" : 1 , "octave" : 8 }, { "basePitch" : 2 , "octave" : 8 }, { "basePitch" : 3 , "octave" : 8 }, { "basePitch" : 4 , "octave" : 8 }, { "basePitch" : 5 , "octave" : 8 }, { "basePitch" : 6 , "octave" : 8 }, { "basePitch" : 7 , "octave" : 8 }, { "basePitch" : 8 , "octave" : 8 }, { "basePitch" : 9 , "octave" : 8 }, { "basePitch" : 10 , "octave" : 8 }, { "basePitch" : 11 , "octave" : 8 }, { "basePitch" : 0 , "octave" : 9 }, { "basePitch" : 1 , "octave" : 9 }, { "basePitch" : 2 , "octave" : 9 }, { "basePitch" : 3 , "octave" : 9 }, { "basePitch" : 4 , "octave" : 9 }, { "basePitch" : 5 , "octave" : 9 }, { "basePitch" : 6 , "octave" : 9 }, { "basePitch" : 7 , "octave" : 9 }, { "basePitch" : 8 , "octave" : 9 }, { "basePitch" : 9 , "octave" : 9 }, { "basePitch" : 10 , "octave" : 9 }, { "basePitch" : 11 , "octave" : 9 }, { "basePitch" : 0 , "octave" : 10 }, { "basePitch" : 1 , "octave" : 10 }, { "basePitch" : 2 , "octave" : 10 }, { "basePitch" : 3 , "octave" : 10 }, { "basePitch" : 4 , "octave" : 10 }, { "basePitch" : 5 , "octave" : 10 }, { "basePitch" : 6 , "octave" : 10 }, { "basePitch" : 7 , "octave" : 10 }]; 83 | const BASE_PITCHES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5, 6, 7]; 84 | var NOTE_TRACKING = {}; 85 | 86 | // prevents endless loop of control and map changes 87 | var UPDATING_CONTROLS = false; 88 | 89 | /* 90 | SCRIPTER FUNCTIONS 91 | */ 92 | 93 | function HandleMIDI(event) { 94 | 95 | if ( event instanceof NoteOn ) { 96 | var originalPitch = event.pitch; 97 | 98 | // root and scale transposition 99 | var basePitch = BASE_PITCHES[ event.pitch ]; 100 | event.pitch += PITCH_SHIFT_MAP[ basePitch ]; 101 | 102 | // semitone transposition 103 | var semitones = GetParameter("Semitones"); 104 | event.pitch += semitones; 105 | 106 | NOTE_TRACKING[originalPitch] = event.pitch; 107 | 108 | } 109 | 110 | if ( event instanceof NoteOff ) { 111 | if ( event.pitch in NOTE_TRACKING ) { 112 | var temp = event.pitch; 113 | event.pitch = NOTE_TRACKING[event.pitch]; 114 | delete NOTE_TRACKING[temp]; 115 | } 116 | } 117 | 118 | event.send(); 119 | } 120 | 121 | function ProcessMIDI() { 122 | var timingInfo = GetTimingInfo(); 123 | } 124 | 125 | function ParameterChanged( param, value ) { 126 | if ( UPDATING_CONTROLS == true ) { 127 | return; 128 | } 129 | //Trace("ParameterChanged( " + param + " , " + value + " )"); 130 | switch( param ) { 131 | case 0: 132 | // root 133 | updateTranspositionMap(GetParameter("Root"), GetParameter("Scale")); 134 | break; 135 | case 1: 136 | // scale 137 | if ( value == 0 ) { 138 | updateToChromaticMap(); 139 | } else { 140 | updateTranspositionMap(GetParameter("Root"), GetParameter("Scale")); 141 | } 142 | break; 143 | case 2: 144 | case 3: 145 | case 4: 146 | case 5: 147 | case 6: 148 | case 7: 149 | case 8: 150 | case 9: 151 | case 10: 152 | case 11: 153 | case 12: 154 | case 13: 155 | updatePitchShiftMap( param , value ); 156 | break; 157 | case 14: 158 | // semitones, do nothing 159 | break; 160 | default: 161 | Trace("ERROR: ParameterChanged( " + param + " , " + value + " )"); 162 | } 163 | applyMapToControls( TRANSPOSE_MAP ); 164 | //Trace(TRANSPOSE_MAP); 165 | //Trace(PITCH_SHIFT_MAP); 166 | } 167 | 168 | /* 169 | CUSTOM FUNCTIONS 170 | */ 171 | 172 | // converts the half- and whole-step jumps into the transposition and pitch shift maps 173 | function updateTranspositionMap( root , templateIndex ) { 174 | 175 | var map = []; 176 | // root index maps directly to MIDI pitches 0-11 177 | var template = SCALE_TEMPLATES[SCALE_KEYS[templateIndex]]; 178 | var lastPitch = root; 179 | // init 180 | map.push(lastPitch); 181 | 182 | // build; length - 2 because we ignore the last value 183 | for ( var index = 0 ; index <= template.length - 2 ; index++ ) { 184 | var steps = template[index]; 185 | var pitch = lastPitch + steps; 186 | // add substitions for non-diatonic pitches 187 | if ( steps > 1 ) { 188 | for ( j = 0 ; j < steps - 1 ; j++ ) { 189 | // non-diatonics 190 | // sharp = pitch 191 | // flat = lastPitch 192 | map.push(pitch); 193 | } 194 | } 195 | map.push(pitch); 196 | lastPitch = pitch; 197 | } 198 | 199 | // normalize to octave C-2 (MIDI pitches 0-11) 200 | for ( var index = 0 ; index <= map.length - 1 ; index++ ) { 201 | var pitch = map[index]; 202 | if ( pitch >= 12 ) { 203 | map[index] = pitch - 12; 204 | } 205 | } 206 | 207 | map.sort(compareNumbers); 208 | 209 | TRANSPOSE_MAP = map; 210 | // update pitch shift map 211 | applyTransposeMapToPitchShiftMap(); 212 | } 213 | 214 | function updateToChromaticMap() { 215 | TRANSPOSE_MAP = CHROMATIC_MAP; 216 | PITCH_SHIFT_MAP = PITCH_SHIFT_TEMPLATE; 217 | } 218 | 219 | // updates the pitch shift map with individual changes 220 | function updatePitchShiftMap( param , value ) { 221 | // update the pitch shift map with the difference between the param index and the base pitch 222 | var index = param - 2; 223 | TRANSPOSE_MAP[ index ] = value; 224 | applyTransposeMapToPitchShiftMap(); 225 | } 226 | 227 | function applyTransposeMapToPitchShiftMap() { 228 | for ( var index = 0 ; index < TRANSPOSE_MAP.length ; index++ ) { 229 | var chromaticPitch = CHROMATIC_MAP[index]; 230 | var transposePitch = TRANSPOSE_MAP[index]; 231 | PITCH_SHIFT_MAP[ index ] = transposePitch - chromaticPitch; 232 | } 233 | } 234 | 235 | // required for sort() to view numbers as numbers 236 | function compareNumbers(a, b) { 237 | return a - b; 238 | } 239 | 240 | function applyMapToControls( map ) { 241 | UPDATING_CONTROLS = true; 242 | // apply the map to the controls 243 | for ( var index = 0 ; index < map.length ; index++ ) { 244 | var controlIndex = index + 2; 245 | SetParameter( controlIndex , map[index] ); 246 | } 247 | UPDATING_CONTROLS = false; 248 | } 249 | 250 | // transposes a pitch to its mapped value within its octave 251 | function transpose( pitch ) { 252 | var pitchInfo = PITCH_INFO[pitch]; 253 | var transposition = TRANSPOSE_MAP[ pitchInfo.basePitch ]; 254 | var transposedPitch = transposition + ( pitchInfo.octave * 12 ); 255 | //Trace( JSON.stringify(pitchInfo) + " " + transposition + " " + transposedPitch ); 256 | return transposedPitch; 257 | } 258 | 259 | PluginParameters.push({ 260 | name:"Root", 261 | type:"menu", 262 | valueStrings: CHROMATIC_SCALE_STRINGS, 263 | defaultValue:0 264 | }); 265 | 266 | PluginParameters.push({ 267 | name:"Scale", 268 | type:"menu", 269 | valueStrings: SCALE_KEYS, 270 | defaultValue:0 271 | }); 272 | 273 | var index = 0; 274 | CHROMATIC_SCALE_STRINGS.forEach(element => { 275 | PluginParameters.push({ 276 | name:CHROMATIC_SCALE_STRINGS[index], 277 | type:"menu", 278 | valueStrings:CHROMATIC_SCALE_STRINGS, 279 | defaultValue:index} 280 | ); 281 | index++; 282 | }); 283 | 284 | PluginParameters.push({ 285 | name:"Semitones", 286 | type:"lin", 287 | minValue:-24, 288 | maxValue:24, 289 | numberOfSteps:48, 290 | defaultValue:0 291 | }); -------------------------------------------------------------------------------- /example_scripts/random_devices.js: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | Name: Ableton Note and Chord 3 | Author(s): Philip Regan 4 | Purpose: Recreate devices in Ableton to generate chords and melodies. Works best 5 | with a transposition plug-in 6 | 7 | Roadmap: 8 | * Add default chord selections 9 | 10 | This script is released under the MIT License. 11 | 12 | Permissions 13 | * Commercial use 14 | * Modification 15 | * Distribution 16 | * Private use 17 | 18 | Limitations 19 | x Liability 20 | x Warranty 21 | 22 | Conditions 23 | ! License and copyright notice 24 | 25 | Copyright Philip Regan and Pilcrow Records 26 | 27 | Permission is hereby granted, free of charge, to any person obtaining 28 | a copy of this software and associated documentation files (the 29 | "Software"), to deal in the Software without restriction, including 30 | without limitation the rights to use, copy, modify, merge, publish, 31 | distribute, sublicense, and/or sell copies of the Software, and to 32 | permit persons to whom the Software is furnished to do so, subject to 33 | the following conditions: 34 | 35 | The above copyright notice and this permission notice shall be 36 | included in all copies or substantial portions of the Software. 37 | 38 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 39 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 40 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 41 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 42 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 43 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 44 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 45 | 46 | ******************************************************************************/ 47 | 48 | /* 49 | SCRIPTER GLOBAL VARIABLES 50 | */ 51 | 52 | var NeedsTimingInfo = true; 53 | var PluginParameters = []; 54 | 55 | /* 56 | CUSTOM GLOBAL VARIABLES 57 | */ 58 | 59 | const CHANCE_MIN = 0; 60 | const CHANCE_DEF = 50; 61 | const CHANCE_MAX = 100; 62 | const CHOICES_MIN = 1; 63 | const CHOICES_MAX = 24; 64 | const CHOICES_DEF = 3; 65 | const DISTANCE_MIN = 1; 66 | const DISTANCE_MAX = 12; 67 | const DISTANCE_DEF = 3; 68 | const SIGN_DOWN = -1; 69 | const SIGN_BI = 0; 70 | const SIGN_UP = 1; 71 | const SIGN_DOWN_TEXT = "Down"; 72 | const SIGN_BI_TEXT = "Bi-directional"; 73 | const SIGN_UP_TEXT = "Up"; 74 | const SIGN_DEF = 0; 75 | const HALFSTEPS_MIN = -36; 76 | const HALFSTEPS_MAX = 36; 77 | const HALFSTEPS_ITERATIONS = 72; 78 | const HALFSTEPS_DEF = 0; 79 | const ROUTING = [ "Randomize > Chordify", "Chordify > Randomize" ]; 80 | const NOTE_LENGTH_TRIGGERS = ["NoteOn", "NoteOff"]; 81 | const NOTE_LENGTH_TRIGGER_DEF = 0; 82 | const NOTE_LENGTH_MODES = ["sync", "ms"]; 83 | const NOTE_LENGTH_MODES_DEF = 0; 84 | 85 | // 0 86 | let RANDOMIZE_VAL = 0; // 1 87 | let CHORDIFY_VAL = 1; // 2 88 | let RELENGTH_VAL = 0; // 3 89 | let ROUTING_VAL = 2; // 4 90 | // 5 91 | let CHANCE_VAL = CHANCE_DEF; // 6 92 | let CHOICES_VAL = CHOICES_DEF; // 7 93 | let DISTANCE_VAL = DISTANCE_DEF; // 8 94 | let DIRECTION_VAL = SIGN_DEF; // 9 95 | // 10 96 | let VOICE_1_VAL = 0; // 11 97 | let VOICE_2_VAL = 0; // 12 98 | let VOICE_3_VAL = 0; // 13 99 | let VOICE_4_VAL = 0; // 14 100 | let VOICE_5_VAL = 0; // 15 101 | let VOICE_6_VAL = 0; // 16 102 | 103 | let NOTE_TRACKING = []; 104 | 105 | function HandleMIDI( event ) { 106 | if ( event instanceof NoteOn ) { 107 | if ( RANDOMIZE_VAL || CHORDIFY_VAL || RELENGTH_VAL ) { 108 | let cache = []; 109 | if ( ROUTING_VAL == 0 ) { 110 | // Trace("HandleMIDI: " + ROUTING[ROUTING_VAL]); 111 | if ( RANDOMIZE_VAL ) { 112 | let randomized_pitch = randomize( event.pitch, CHANCE_VAL, CHOICES_VAL, DISTANCE_VAL, DIRECTION_VAL ); 113 | cache.push(randomized_pitch); 114 | } else { 115 | cache.push( event.pitch ); 116 | } 117 | if ( CHORDIFY_VAL ) { 118 | cache = chordify( cache[0], VOICE_1_VAL, VOICE_2_VAL, VOICE_3_VAL, VOICE_4_VAL, VOICE_5_VAL, VOICE_6_VAL ); 119 | } 120 | } else { 121 | if ( CHORDIFY_VAL ) { 122 | cache = chordify( event.pitch, VOICE_1_VAL, VOICE_2_VAL, VOICE_3_VAL, VOICE_4_VAL, VOICE_5_VAL, VOICE_6_VAL ); 123 | } 124 | if ( RANDOMIZE_VAL ) { 125 | for (let index = 0; index < cache.length; index++) { 126 | const pitch = cache[index]; 127 | let randomized_pitch = randomize( pitch, CHANCE_VAL, CHOICES_VAL, DISTANCE_VAL, DIRECTION_VAL ); 128 | cache[index] = randomized_pitch; 129 | } 130 | } 131 | } 132 | add_events_to_active_notes( event, cache ); 133 | cache.forEach( function ( pitch ) { 134 | let note_on = new NoteOn(); 135 | note_on.pitch = pitch; 136 | note_on.velocity = event.velocity; 137 | note_on.send(); 138 | }); 139 | } else { 140 | event.send(); 141 | } 142 | } else if ( event instanceof NoteOff ) { 143 | let cache = remove_events_from_active_notes( event ); 144 | cache.forEach( function( pitch ) { 145 | let note_off = new NoteOff; 146 | note_off.pitch = pitch; 147 | note_off.send(); 148 | }); 149 | } else { 150 | event.send() 151 | } 152 | } 153 | 154 | function ParameterChanged( param, value ) { 155 | 156 | switch( param ) { 157 | // 0 158 | case 0: 159 | case 1: 160 | RANDOMIZE_VAL = value; // 1 161 | Trace(JSON.stringify({RANDOMIZE_VAL:RANDOMIZE_VAL})); 162 | break; 163 | case 2: 164 | CHORDIFY_VAL = value; // 2 165 | Trace(JSON.stringify({RANDOMIZE_VAL:CHORDIFY_VAL})); 166 | break; 167 | case 3: 168 | ROUTING_VAL = value; // 4 169 | Trace(JSON.stringify({ROUTING_VAL:ROUTING[ROUTING_VAL]})); 170 | break; 171 | case 4: 172 | case 5: 173 | CHANCE_VAL = value; // 6 174 | break; 175 | case 6: 176 | CHOICES_VAL = value; // 7 177 | break; 178 | case 7: 179 | DISTANCE_VAL = value; // 8 180 | break; 181 | case 8: 182 | DIRECTION_VAL = value; // 9 183 | break; 184 | case 9: 185 | case 10: 186 | VOICE_1_VAL = value; // 11 187 | break; 188 | case 11: 189 | VOICE_2_VAL = value; // 12 190 | break; 191 | case 12: 192 | VOICE_3_VAL = value; // 13 193 | break; 194 | case 13: 195 | VOICE_4_VAL = value; // 14 196 | break; 197 | case 14: 198 | VOICE_5_VAL = value; // 15 199 | break; 200 | case 15: 201 | VOICE_6_VAL = value; // 16 202 | break; 203 | default: 204 | // ERROR 205 | } 206 | } 207 | 208 | /* 209 | src_pitch: integer 0-127; MIDI pitch 210 | chance: integer 0-100; percentage 211 | choices: integer 1-24; half-steps 212 | scale: integer 1-12; half steps 213 | sign: integer {-1, 0, 1}; direction: down, bi, up 214 | returns integer 0-127; MIDI pitch 215 | */ 216 | function randomize( src_pitch, chance, choices, distance, direction ) { 217 | // console.log([src_pitch, chance, choices, distance, direction]); 218 | // are we randomizing this pitch? 219 | if ( rInt(0, 100) > chance ) { 220 | return src_pitch; 221 | } 222 | let pool = []; 223 | // add src to pool 224 | pool.push( src_pitch ); 225 | let last_pitch = src_pitch 226 | switch ( direction ) { 227 | // determine direction 228 | case -1: 229 | // add pitches to pool by adding from src or last pitch by distance 230 | for (let index = 0; index < choices; index++) { 231 | let new_pitch = last_pitch - distance; 232 | if ( new_pitch < 0 ) { 233 | new_pitch = 0; 234 | } 235 | pool.push( new_pitch ); 236 | last_pitch = new_pitch; 237 | } 238 | break; 239 | case 0: 240 | for (let index = 0; index < choices; index++) { 241 | let new_pitch = last_pitch - distance; 242 | if ( new_pitch < 0 ) { 243 | new_pitch = 0; 244 | } 245 | pool.push( new_pitch ); 246 | last_pitch = new_pitch; 247 | } 248 | last_pitch = src_pitch; 249 | for (let index = 0; index < choices; index++) { 250 | let new_pitch = last_pitch + distance; 251 | if ( new_pitch > 127 ) { 252 | new_pitch = 127; 253 | } 254 | pool.push( new_pitch ); 255 | last_pitch = new_pitch; 256 | } 257 | break; 258 | case 1: 259 | for (let index = 0; index < choices; index++) { 260 | let new_pitch = last_pitch + distance; 261 | if ( new_pitch > 127 ) { 262 | new_pitch = 127; 263 | } 264 | pool.push( new_pitch ); 265 | last_pitch = new_pitch; 266 | } 267 | break; 268 | default: 269 | // error 270 | break; 271 | } 272 | // select a random pitch 273 | pool = [...new Set(pool)]; 274 | // Trace(pool); 275 | console.log( JSON.stringify( pool ) ); 276 | let r = rInt( 0, pool.length - 1 ); 277 | return pool[ r ]; 278 | } 279 | 280 | /* 281 | root: Integer 0-127; MIDI 282 | first, third . . . thirteenth: Integer 0-36; half-steps, 0 = don't calculate 283 | first = root so it can be turned off in the resulting chord 284 | transpose: Integer -36...0...36: half-steps 285 | returns array of Integers 286 | */ 287 | function chordify( root, first, third, fifth, seventh, ninth, eleventh, thirteenth ) { 288 | let chord = []; 289 | let intervals = [ first, third, fifth, seventh, ninth, eleventh, thirteenth ]; 290 | for (let index = 0; index < intervals.length; index++) { 291 | const interval = intervals[index]; 292 | if ( index == 0 ) { 293 | // root is always needed in the output 294 | let new_root = root + interval; 295 | if ( new_root > 127 ) { 296 | new_root = 127; 297 | } 298 | if ( new_root < 0 ) { 299 | new_root = 0; 300 | } 301 | chord.push( new_root ); 302 | } else { 303 | if ( interval > 0 ) { 304 | let voice = root + interval; 305 | if ( voice > 127 ) { 306 | voice = 127; 307 | } 308 | if ( voice < 0 ) { 309 | voice = 0; 310 | } 311 | chord.push( voice ); 312 | } else { 313 | let voice = root + Math.abs(interval); 314 | if ( voice > 127 ) { 315 | voice = 127; 316 | } 317 | if ( voice < 0 ) { 318 | voice = 0; 319 | } 320 | chord.push( voice ); 321 | } 322 | } 323 | } 324 | return chord; 325 | }; 326 | 327 | /* 328 | ACTIVE NOTE TRACKING STACK 329 | */ 330 | 331 | // tracks active notes based on original pitch in case of modification 332 | // stores pitch caches at the index of the original pitch 333 | // assumes source pitches do not overlap 334 | function add_events_to_active_notes( src_event, cache ) { 335 | /* 336 | NOTE_TRACKING[src_event] = mod_event.pitch; 337 | */ 338 | NOTE_TRACKING[src_event.pitch] = cache; 339 | Trace("ADD " + JSON.stringify(NOTE_TRACKING)); 340 | } 341 | 342 | // returns the modified event based on the source event 343 | function remove_events_from_active_notes( src_event ) { 344 | // if ( event.pitch in NOTE_TRACKING ) { 345 | // var temp = event.pitch; 346 | // event.pitch = NOTE_TRACKING[event.pitch]; 347 | // delete NOTE_TRACKING[temp]; 348 | // } 349 | let cache = NOTE_TRACKING[ src_event.pitch ]; 350 | if ( cache ) { 351 | NOTE_TRACKING[ src_event.pitch ] = null; 352 | return cache; 353 | } 354 | // no cache was found, so we return the 355 | // pitch itself to be handled as is 356 | return [src_event.pitch]; 357 | } 358 | 359 | function rInt (x, y) { 360 | if (x > y) { 361 | [x, y] = [x, y]; 362 | } 363 | return Math.floor(Math.random() * (y - x + 1)) + x; 364 | } 365 | 366 | // 0 367 | PluginParameters.push({ 368 | name: "Routing", 369 | type: "text" 370 | }); 371 | 372 | // 1 373 | PluginParameters.push({ 374 | name:"Randomize", 375 | type:"checkbox", 376 | defaultValue:RANDOMIZE_VAL 377 | }); 378 | 379 | // 2 380 | PluginParameters.push({ 381 | name:"Chordify", 382 | type:"checkbox", 383 | defaultValue:CHORDIFY_VAL 384 | }); 385 | 386 | // 3 387 | PluginParameters.push({ 388 | name:"Routing", 389 | type:"menu", 390 | valueStrings:ROUTING, 391 | defaultValue:2 392 | }); 393 | 394 | // 4 395 | PluginParameters.push({ 396 | name: "Randomize", 397 | type: "text" 398 | }); 399 | 400 | // 5 401 | PluginParameters.push({ 402 | name:"Chance", 403 | type:"lin", unit:"\%", 404 | minValue:CHANCE_MIN, 405 | maxValue:CHANCE_MAX, 406 | numberOfSteps:CHANCE_MAX, 407 | defaultValue:CHANCE_DEF 408 | }); 409 | 410 | // 6 411 | PluginParameters.push({ 412 | name:"Choices", 413 | type:"lin", unit:"pitches", 414 | minValue:CHOICES_MIN, 415 | maxValue:CHOICES_MAX, 416 | numberOfSteps:CHOICES_MAX - CHOICES_MIN, 417 | defaultValue:CHOICES_DEF 418 | }); 419 | 420 | // 7 421 | PluginParameters.push({ 422 | name:"Distance", 423 | type:"lin", unit:"st", 424 | minValue:DISTANCE_MIN, 425 | maxValue:DISTANCE_MAX, 426 | numberOfSteps:DISTANCE_MAX - DISTANCE_MIN, 427 | defaultValue:DISTANCE_DEF 428 | }); 429 | 430 | // 8 431 | PluginParameters.push({ 432 | name:"Direction", 433 | type:"menu", 434 | valueStrings:[SIGN_UP_TEXT, SIGN_BI_TEXT, SIGN_DOWN_TEXT], 435 | defaultValue:SIGN_DEF 436 | }); 437 | 438 | // 9 439 | PluginParameters.push({ 440 | name: "Chordify", 441 | type: "text" 442 | }); 443 | 444 | // 10 445 | PluginParameters.push({ 446 | name:"Voice 1 (Root)", 447 | type:"lin", unit:"st", 448 | minValue:HALFSTEPS_MIN, 449 | maxValue:HALFSTEPS_MAX, 450 | numberOfSteps:HALFSTEPS_MAX - HALFSTEPS_MIN, 451 | defaultValue:HALFSTEPS_DEF 452 | }); 453 | 454 | // 11 455 | PluginParameters.push({ 456 | name:"Voice 2", 457 | type:"lin", unit:"st", 458 | minValue:HALFSTEPS_MIN, 459 | maxValue:HALFSTEPS_MAX, 460 | numberOfSteps:HALFSTEPS_MAX - HALFSTEPS_MIN, 461 | defaultValue:4 462 | }); 463 | 464 | // 12 465 | PluginParameters.push({ 466 | name:"Voice 3", 467 | type:"lin", unit:"st", 468 | minValue:HALFSTEPS_MIN, 469 | maxValue:HALFSTEPS_MAX, 470 | numberOfSteps:HALFSTEPS_MAX - HALFSTEPS_MIN, 471 | defaultValue:7 472 | }); 473 | 474 | // 13 475 | PluginParameters.push({ 476 | name:"Voice 4", 477 | type:"lin", unit:"st", 478 | minValue:HALFSTEPS_MIN, 479 | maxValue:HALFSTEPS_MAX, 480 | numberOfSteps:HALFSTEPS_MAX - HALFSTEPS_MIN, 481 | defaultValue:10 482 | }); 483 | 484 | // 14 485 | PluginParameters.push({ 486 | name:"Voice 5", 487 | type:"lin", unit:"st", 488 | minValue:HALFSTEPS_MIN, 489 | maxValue:HALFSTEPS_MAX, 490 | numberOfSteps:HALFSTEPS_MAX - HALFSTEPS_MIN, 491 | defaultValue:-8 492 | }); 493 | 494 | // 15 495 | PluginParameters.push({ 496 | name:"Voice 6", 497 | type:"lin", unit:"st", 498 | minValue:HALFSTEPS_MIN, 499 | maxValue:HALFSTEPS_MAX, 500 | numberOfSteps:HALFSTEPS_MAX - HALFSTEPS_MIN, 501 | defaultValue:-5 502 | }); -------------------------------------------------------------------------------- /generative_music/oc_live_chord_generator.js: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | Name: Live Chord Generator 3 | Author(s): Philip Regan 4 | Purpose: 5 | * Creates diatonic chords based on a single root note within a particular scale 6 | and modifies them based on specific musical needs 7 | * Intended to assist in improvisation and sketching new musical ideas 8 | * A Scripter-based implementation of the Chord Trigger MIDI Effects Plug-In 9 | 10 | Roadmap 11 | * Update to handle suspended chords 12 | * Dependency: MUSIC_LIB needs to be updated to create suspended chords 13 | 14 | This script is released under the MIT License. 15 | 16 | Permissions 17 | * Commercial use 18 | * Modification 19 | * Distribution 20 | * Private use 21 | 22 | Limitations 23 | x Liability 24 | x Warranty 25 | 26 | Conditions 27 | ! License and copyright notice 28 | 29 | Copyright Philip Regan and Pilcrow Records 30 | 31 | Permission is hereby granted, free of charge, to any person obtaining 32 | a copy of this software and associated documentation files (the 33 | "Software"), to deal in the Software without restriction, including 34 | without limitation the rights to use, copy, modify, merge, publish, 35 | distribute, sublicense, and/or sell copies of the Software, and to 36 | permit persons to whom the Software is furnished to do so, subject to 37 | the following conditions: 38 | 39 | The above copyright notice and this permission notice shall be 40 | included in all copies or substantial portions of the Software. 41 | 42 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 43 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 44 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 45 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 46 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 47 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 48 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 49 | 50 | ****************************************************************************/ 51 | 52 | /* 53 | SCRIPTER GLOBAL VARIABLES 54 | */ 55 | 56 | var NeedsTimingInfo = true; 57 | var PluginParameters = []; 58 | 59 | const TARGET_OCTAVE_LIB = { 60 | "8" : 10, 61 | "7" : 9, 62 | "6" : 8, 63 | "5" : 7, 64 | "4" : 6, 65 | "3 (Middle C)" : 5, 66 | "2" : 4, 67 | "1" : 3, 68 | "0" : 2, 69 | "-1" : 1, 70 | "-2" : 0 71 | }; 72 | const TARGET_OCTAVE_KEYS = ["8", "7", "6", "5", "4", "3 (Middle C)", "2", "1", "0", "-1", "-2"]; 73 | var TARGET_OCTAVE = TARGET_OCTAVE_LIB[TARGET_OCTAVE_KEYS[5]]; 74 | 75 | const CHROMATIC_HALF_STEPS = 12; 76 | 77 | const OCTAVE_PITCH_INDEX = [-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8]; 78 | const SCALE_DEGREE_NAMES = ["I tonic", "II supertonic", "III mediant", "IV subdominant", "V dominant", "VI submediant", "VII leading tone"]; 79 | const CHROMATIC_SCALE_STRINGS = ["C", "C♯/D♭", "D", "D♯/E♭", "E", "F", "F♯/G♭", "G", "G♯/A♭", "A", "A♯/B♭", "B"]; 80 | var KEYBOARD_STRINGS = []; 81 | for (let index = 0; index < 12; index++) { 82 | CHROMATIC_SCALE_STRINGS.forEach( function ( s ) { 83 | KEYBOARD_STRINGS.push(s); 84 | }); 85 | } 86 | 87 | const SCALE_TEMPLATES = { 88 | "Ionian" : [2, 2, 1, 2, 2, 2, 1], 89 | "Dorian" : [2, 1, 2, 2, 2, 1, 2], 90 | "Phrygian" : [1, 2, 2, 2, 1, 2, 2], 91 | "Lydian" : [2, 2, 2, 1, 2, 2, 1], 92 | "Mixolydian" : [2, 2, 1, 2, 2, 1, 2], 93 | "Aeolian" : [2, 1, 2, 2, 1, 2, 2], 94 | "Locrian" : [1, 2, 2, 1, 2, 2, 2] 95 | } 96 | const SCALE_KEYS = Object.keys(SCALE_TEMPLATES); 97 | 98 | const NONDIATONIC_PITCH_VALUE = -1; 99 | const NOTE_PROB_ROOT = 75; 100 | const NOTE_PROB_DIATONIC = 25; 101 | const NOTE_PROB_DEFAULT = 0; 102 | const NOTE_PROB_NONDIATONIC = 10; 103 | const NOTE_PROB_CHROMATIC = 50; 104 | const PITCH_TYPE_ROOT = 'root'; 105 | const PITCH_TYPE_DIATONIC = 'diatonic'; 106 | const PITCH_TYPE_NONDIATONIC = 'non-diatonic'; 107 | const PITCH_RECORD_KEY_WEIGHT = "w"; 108 | const PITCH_RECORD_KEY_TYPE = "t"; 109 | const PITCH_RECORD_KEY_DEGREE = "d"; 110 | const PITCH_RECORD_KEY_NAME = "n"; 111 | 112 | const CHORD_VOICE_ROOT = 0; 113 | const CHORD_VOICE_3RD = 1; 114 | const CHORD_VOICE_5TH = 2; 115 | const CHORD_VOICE_7TH = 3; 116 | const CHORD_VOICE_9TH = 4; 117 | const CHORD_VOICE_11TH = 5; 118 | const CHORD_VOICE_13TH = 6; 119 | const CHORD_VOICE_OPTIONS = { 120 | "Triad (1, 3, 5)" : [1, 1, 1, 0, 0, 0, 0], 121 | "7th (1, 3, 5, 7)" : [1, 1, 1, 1, 0, 0, 0], 122 | "Exc. 5th (1, 3, 7)" : [1, 1, 0, 1, 0, 0, 0], 123 | "Extensions (9, 11, 13)" : [0, 0, 0, 0, 1, 1, 1], 124 | "Pentatonic (1, 3, 5, 9, 11)" : [1, 1, 1, 0, 1, 1, 0], 125 | "Exclude Minor 9ths" : [1, 1, 1, 1, 1, 1, 1], 126 | "Pop VII/I" : [0, 0, 0, 1, 1, 1, 0], 127 | "Pop II/I" : [0, 0, 0, 0, 1, 1, 1], 128 | "Drop 2 (1342)" : [1, 1, 1, 1, 0, 0, 0], 129 | "Drop 3 (1243)" : [1, 1, 1, 1, 0, 0, 0], 130 | "Drop 2+3 (1423)" : [1, 1, 1, 1, 0, 0, 0], 131 | "Drop 2+4 (1324)" : [1, 1, 1, 1, 0, 0, 0], 132 | "Rootless (3, 5, 7, 9)" : [0, 1, 1, 1, 1, 0, 0], 133 | "Rootless V7 (3, 7, 9, 13)" : [0, 1, 0, 1, 1, 0, 1], 134 | "Shell (1, 3, 7)" : [1, 1, 0, 1, 0, 0, 0] 135 | }; 136 | 137 | const CHORD_VOICE_OPTIONS_KEYS = Object.keys( CHORD_VOICE_OPTIONS ); 138 | var CHORD_VOICE_OPTION_SELECTION = 1; 139 | var CHORD_VOICE_OPTION_SELECTION_KEY = "7th (1, 3, 5, 7)"; 140 | var UPDATING_CONTROLS = false; 141 | 142 | var SCALE_ROOT = 0; 143 | var SCALE_TEMPLATE_INDEX = 0; 144 | var CHORD_ROOT = 0; 145 | var CHORD_ORIGINAL = []; 146 | var CHORD_VOICES = []; 147 | // Parameter Changed -> update chord options -> get voices from chord -> CHORD_OPTIONS 148 | var CHORD_OPTIONS = [1, 1, 1, 1, 1, 1, 1]; 149 | var KEYBOARD_SCALE = []; 150 | 151 | var ACTIVE_NOTES = []; 152 | 153 | var music_lib = new MUSIC_LIB(); 154 | 155 | // test(); 156 | // function test() { 157 | // // ["C", "C♯/D♭", "D", "D♯/E♭", "E", "F", "F♯/G♭", "G", "G♯/A♭", "A", "A♯/B♭", "B"] 158 | // // ["Ionian","Dorian","Phrygian","Lydian","Mixolydian","Aeolian","Locrian"] 159 | // var scale_object = music_lib.calculate_scale_pitches(0, 0); 160 | // // var full_keyboard = music_lib.expand_scale_to_midi_range(scale_object); 161 | // // var diatonic_scale = music_lib.collapse_scale_to_diatonic(full_keyboard); 162 | // // KEYBOARD_SCALE = music_lib.collapse_scale_to_integers(diatonic_scale); 163 | // // CHROMATIC_SCALE_STRINGS 164 | // let chord = music_lib.calculate_chord_pitches(0, scale_object); 165 | // let voices = music_lib.get_voices_from_chord( CHORD_VOICE_OPTIONS[CHORD_VOICE_OPTIONS_KEYS[CHORD_VOICE_OPTION_SELECTION]], chord ); 166 | // console.log(KEYBOARD_SCALE); 167 | // console.log(voices); 168 | // } 169 | 170 | function Trace( s ) { 171 | console.log( s ); 172 | } 173 | 174 | function HandleMIDI( event ) { 175 | 176 | // note on and note off assumes a single event is going to equate to a chord 177 | if ( event instanceof NoteOn ) { 178 | let scale_object = music_lib.calculate_scale_pitches( SCALE_ROOT, SCALE_TEMPLATE_INDEX ); 179 | // Trace( JSON.stringify( KEYBOARD_SCALE ) ); 180 | CHORD_ROOT = music_lib.transpose_pitch_to_lowest_octave( event.pitch ); 181 | // Trace( CHORD_ROOT ); 182 | CHORD_ORIGINAL = music_lib.calculate_chord_pitches( CHORD_ROOT, scale_object ); 183 | CHORD_VOICES = music_lib.get_voices_from_chord( CHORD_OPTIONS, CHORD_ORIGINAL ); 184 | Trace( event ); 185 | Trace( JSON.stringify( CHORD_ORIGINAL ) ); 186 | Trace( JSON.stringify( CHORD_OPTIONS ) ); 187 | Trace( JSON.stringify( CHORD_VOICES ) ); 188 | // play the notes in the chord voices 189 | CHORD_VOICES.forEach( function ( pitch ) { 190 | let tp_pitch = ( TARGET_OCTAVE * 12 ) + pitch; 191 | Trace( [ tp_pitch, TARGET_OCTAVE, 12, pitch] ); 192 | let note_on = new NoteOn(); 193 | note_on.pitch = tp_pitch; 194 | note_on.send(); 195 | ACTIVE_NOTES.push( note_on ); 196 | Trace( note_on ); 197 | }); 198 | } else if ( event instanceof NoteOff ) { 199 | // stop the notes in the chord voices 200 | ACTIVE_NOTES.forEach( function ( note_on ) { 201 | let note_off = new NoteOff( note_on ); 202 | note_off.send(); 203 | }); 204 | ACTIVE_NOTES = []; 205 | } else { 206 | event.send(); 207 | } 208 | } 209 | 210 | function MUSIC_LIB () { 211 | 212 | /* GENERAL MUSIC */ 213 | 214 | // index aligns to lowest MIDI octave 215 | this.CHROMATIC_SCALE_STRINGS = ["C", "C♯/D♭", "D", "D♯/E♭", "E", "F", "F♯/G♭", "G", "G♯/A♭", "A", "A♯/B♭", "B"]; 216 | this.CHROMATIC_HALF_STEPS = 12; 217 | this.OCTAVE_PITCH_INDEX = [-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-2,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8]; 218 | 219 | /* SCALES */ 220 | 221 | this.SCALE_DEGREE_NAMES = ["I tonic", "II supertonic", "III mediant", "IV subdominant", "V dominant", "VI submediant", "VII leading tone"]; 222 | this.KEYBOARD_STRINGS = []; 223 | this.SCALE_TEMPLATES = { 224 | "Ionian" : [2, 2, 1, 2, 2, 2, 1], 225 | "Dorian" : [2, 1, 2, 2, 2, 1, 2], 226 | "Phrygian" : [1, 2, 2, 2, 1, 2, 2], 227 | "Lydian" : [2, 2, 2, 1, 2, 2, 1], 228 | "Mixolydian" : [2, 2, 1, 2, 2, 1, 2], 229 | "Aeolian" : [2, 1, 2, 2, 1, 2, 2], 230 | "Locrian" : [1, 2, 2, 1, 2, 2, 2] 231 | } 232 | this.SCALE_KEYS = Object.keys(this.SCALE_TEMPLATES); 233 | 234 | this._PITCH_TYPE_ROOT = 'rt'; 235 | this._PITCH_TYPE_DIATONIC = 'dt'; 236 | this._PITCH_TYPE_NONDIATONIC = 'nd'; 237 | this._PITCH_RECORD_KEY_TYPE = "t"; 238 | this._PITCH_RECORD_KEY_DEGREE = "d"; 239 | this._PITCH_RECORD_KEY_NAME = "n"; 240 | 241 | /* CHORDS */ 242 | 243 | this.CHORD_VOICE_ROOT = 0; 244 | this.CHORD_VOICE_3RD = 1; 245 | this.CHORD_VOICE_5TH = 2; 246 | this.CHORD_VOICE_7TH = 3; 247 | this.CHORD_VOICE_9TH = 4; 248 | this.CHORD_VOICE_11TH = 5; 249 | this.CHORD_VOICE_13TH = 6; 250 | this.CHORD_VOICE_OPTIONS = { 251 | "Triad (1, 3, 5)" : [1, 1, 1, 0, 0, 0, 0], 252 | "7th (1, 3, 5, 7)" : [1, 1, 1, 1, 0, 0, 0], 253 | "Exc. 5th (1, 3, 7)" : [1, 1, 0, 1, 0, 0, 0], 254 | "Extensions (9, 11, 13)" : [0, 0, 0, 0, 1, 1, 1], 255 | "Pentatonic (1, 3, 5, 9, 11)" : [1, 1, 1, 0, 1, 1, 0], 256 | "Exclude Minor 9ths" : [1, 1, 1, 1, 1, 1, 1], 257 | "Pop VII/I" : [0, 0, 0, 1, 1, 1, 0], 258 | "Pop II/I" : [0, 0, 0, 0, 1, 1, 1] 259 | }; 260 | this.CHORD_VOICE_OPTIONS_KEYS = Object.keys( this.CHORD_VOICE_OPTIONS ); 261 | this.CHORD_OPTIONS = [1, 1, 1, 1, 1, 1, 1]; 262 | 263 | /* 264 | 265 | MUSIC_LIB provides fundamental calculations for scales and chords. It 266 | is intended to provide a single source of truth for Scripter by being 267 | accurate, fast, and lightweight. MUSIC_LIB is designed to input and 268 | output a number of formats needed for Scripter (and some personal uses) 269 | but it does not actually store anything aside what's needed to provided 270 | desired data. 271 | 272 | Example: C Major chord in C Major scale 273 | let music_lib = new MUSIC_LIB(); 274 | music_lib.initialize(); 275 | let root = 0; // C 276 | let type = 0; // Major 277 | // build the basic scale object with full metadata 278 | let scale = music_lib.calculate_scale_pitches( root , type ); 279 | // expand to MIDI range 280 | scale = music_lib.expand_scale_to_midi_range( scale ); 281 | // remove non-diatonic pitches 282 | scale = music_lib.collapse_scale_to_diatonic( scale ); 283 | // remove all metadata except MIDI pitch numbers 284 | scale = music_lib.collapse_scale_to_integers( scale ); 285 | // build the C Major chord 286 | let chord = calculate_chord_pitches( root , scale ); 287 | let parameter_index = 0; // Parameter Control Number 288 | let options_value = 0 // "Triad (1, 3, 5)" 289 | music_lib.update_chord_options( index, value ); 290 | chord = music_lib.get_voices_from_chord( options, chord ); 291 | */ 292 | 293 | // initialize prepares values for base calculations 294 | this.initialize = function () { 295 | // build the keyboard strings 296 | let keyboard_strings_cache = []; 297 | for (let index = 0; index < 12; index++) { 298 | this.CHROMATIC_SCALE_STRINGS.forEach( function ( s ) { 299 | keyboard_strings_cache.push( s ); 300 | }); 301 | } 302 | this.KEYBOARD_STRINGS = keyboard_strings_cache; 303 | // populate object key caches 304 | this.SCALE_KEYS = Object.keys(this.SCALE_TEMPLATES); 305 | } 306 | 307 | /* SCALE CALCULATIONS */ 308 | 309 | // returns the chromatic scale, noting root, diatonic, and non-diatonic pitches 310 | this.calculate_scale_pitches = function ( root, templateIndex ) { 311 | 312 | // root index maps directly to MIDI pitches 0-11 313 | let template = this.SCALE_TEMPLATES[this.SCALE_KEYS[templateIndex]]; 314 | let lastPitch = root; 315 | let diatonic_count = 0; 316 | // init 317 | let pitch_weight_map = {}; 318 | pitch_weight_map[lastPitch] = this._create_pitch_record( this._PITCH_TYPE_ROOT, diatonic_count, lastPitch ); 319 | 320 | // build; length - 2 because we ignore the last value 321 | for ( let index = 0 ; index <= template.length - 2 ; index++ ) { 322 | let steps = template[index]; 323 | let pitch = lastPitch + steps; 324 | // non-diatonic pitches 325 | if ( steps > 1 ) { 326 | let non_diatonic_pitch = pitch; 327 | while ( steps > 0 ) { 328 | non_diatonic_pitch--; 329 | if ( !pitch_weight_map[non_diatonic_pitch] ) { 330 | pitch_weight_map[non_diatonic_pitch] = this._create_pitch_record( this._PITCH_TYPE_NONDIATONIC, -1, non_diatonic_pitch ); 331 | } 332 | steps--; 333 | } 334 | } 335 | diatonic_count++; 336 | pitch_weight_map[pitch] = this._create_pitch_record( this._PITCH_TYPE_DIATONIC, diatonic_count, pitch ); 337 | lastPitch = pitch; 338 | } 339 | 340 | // normalize to octave C-2 (MIDI pitches 0-11) 341 | let cache = {}; 342 | let keys = Object.keys(pitch_weight_map); 343 | keys.forEach( function ( key ) { 344 | let pitch = parseInt(key); 345 | let pitchRecord = pitch_weight_map[key] 346 | if ( pitch >= 12 ) { 347 | pitch = pitch - 12; 348 | } 349 | cache[pitch] = pitchRecord; 350 | }); 351 | 352 | return cache; 353 | 354 | } 355 | 356 | this._create_pitch_record = function ( type, degree, pitch ) { 357 | let cache = {}; 358 | cache[this._PITCH_RECORD_KEY_TYPE] = type; 359 | cache[this._PITCH_RECORD_KEY_DEGREE] = this.SCALE_DEGREE_NAMES[degree]; 360 | cache[this._PITCH_RECORD_KEY_NAME] = this.KEYBOARD_STRINGS[pitch]; 361 | return cache; 362 | } 363 | 364 | /* SCALE MANIPULATION */ 365 | 366 | // takes a single C-2 scale and returns a scale object containing all octaves 367 | // creates an object with length 144; does not limit to 0-127 368 | this.expand_scale_to_midi_range = function ( scale ) { 369 | let cache = {}; 370 | let scale_keys = Object.keys( scale ); 371 | for (let index = 0; index < 12; index++) { 372 | scale_keys.forEach( function ( key ) { 373 | let pitch = parseInt(key); 374 | let pitch_record = scale[key]; 375 | let new_pitch = pitch + ( index * 12 ) 376 | cache[new_pitch] = JSON.parse(JSON.stringify(pitch_record)); 377 | }); 378 | } 379 | return cache; 380 | } 381 | 382 | // takes a scale object of any length and return a scale object with only diatonic notes 383 | this.collapse_scale_to_diatonic = function ( scale ) { 384 | let cache = {}; 385 | let scale_keys = Object.keys(scale); 386 | scale_keys.forEach( function ( key ) { 387 | let pitch_record = scale[key]; 388 | if ( pitch_record[ 't' ] === 'dt' || pitch_record[ 't' ] === 'rt' ) { 389 | cache[key] = JSON.parse(JSON.stringify(pitch_record)); 390 | } else { 391 | // exclude silently 392 | } 393 | }); 394 | return cache; 395 | } 396 | 397 | // takes a scale object of any length and type and returns an integer array 398 | this.collapse_scale_to_integers = function ( scale ) { 399 | let cache = []; 400 | let scale_keys = Object.keys(scale); 401 | scale_keys.forEach( function ( key ) { 402 | cache.push(parseInt(key)); 403 | }); 404 | return cache; 405 | } 406 | 407 | // takes a scale object of any length and type and returns an integer array 408 | this.collapse_scale_to_spelling = function ( scale ) { 409 | let cache = []; 410 | let scale_keys = Object.keys(scale); 411 | scale_keys.forEach( function ( key ) { 412 | cache.push(parseInt(key)); 413 | }); 414 | return cache; 415 | } 416 | 417 | /* CHORD CALCULATIONS */ 418 | 419 | // root = integer 420 | // scale = array 421 | this.calculate_chord_pitches = function ( root, scale ) { 422 | 423 | var full_keyboard = music_lib.expand_scale_to_midi_range(scale); 424 | 425 | // update the scale object to only include diatonic notes 426 | let diatonic_scale = this.collapse_scale_to_diatonic( full_keyboard ); 427 | // update the scale to an array of integers of diatonic pitches 428 | let chord_scale = this.collapse_scale_to_spelling( diatonic_scale ); 429 | 430 | let voices = []; 431 | let root_index = chord_scale.indexOf( root ); 432 | // root 433 | voices.push( chord_scale[ root_index ] ); 434 | // 3rd 435 | voices.push( chord_scale[ root_index + 2 ] ); 436 | // 5th 437 | voices.push( chord_scale[ root_index + 4 ] ); 438 | // 7th 439 | voices.push( chord_scale[ root_index + 6 ] ); 440 | // 9th 441 | voices.push( chord_scale[ root_index + 8 ] ); 442 | // 11th 443 | voices.push( chord_scale[ root_index + 10 ] ); 444 | // 13th 445 | voices.push( chord_scale[ root_index + 12 ] ); 446 | return voices; 447 | } 448 | 449 | this.update_chord_options = function ( index, value ) { 450 | this.CHORD_VOICE_OPTION_SELECTION = value; 451 | this.CHORD_VOICE_OPTION_SELECTION_KEY = this.CHORD_VOICE_OPTIONS_KEYS[this.CHORD_VOICE_OPTION_SELECTION]; 452 | if ( index == 4 ) { 453 | UPDATING_CONTROLS = true; 454 | let options = this.CHORD_VOICE_OPTIONS[this.CHORD_VOICE_OPTION_SELECTION_KEY]; 455 | SetParameter( 5, options[ 0 ] ); 456 | SetParameter( 6, options[ 1 ] ); 457 | SetParameter( 7, options[ 2 ] ); 458 | SetParameter( 8, options[ 3 ] ); 459 | SetParameter( 9, options[ 4 ] ); 460 | SetParameter( 10, options[ 5 ] ); 461 | SetParameter( 11, options[ 6 ] ); 462 | UPDATING_CONTROLS = false; 463 | } 464 | this.CHORD_OPTIONS[ 0 ] = GetParameter( 5 ); 465 | this.CHORD_OPTIONS[ 1 ] = GetParameter( 6 ); 466 | this.CHORD_OPTIONS[ 2 ] = GetParameter( 7 ); 467 | this.CHORD_OPTIONS[ 3 ] = GetParameter( 8 ); 468 | this.CHORD_OPTIONS[ 4 ] = GetParameter( 9 ); 469 | this.CHORD_OPTIONS[ 5 ] = GetParameter( 10 ); 470 | this.CHORD_OPTIONS[ 6 ] = GetParameter( 11 ); 471 | } 472 | 473 | this.get_voices_from_chord = function ( options, chord ) { 474 | let voices = []; 475 | if ( this.CHORD_VOICE_OPTION_SELECTION_KEY == "Exclude Minor 9ths" ) { 476 | voices = remove_minor_9ths( chord ); 477 | } else { 478 | for ( let index = 0; index < options.length; index++ ) { 479 | if ( this.CHORD_OPTIONS[index] == 1 ) { 480 | let voice = chord[ index ]; 481 | // shift the extensions down to behave like a chord so the rest of the data stream can handle accordingly 482 | if ( this.CHORD_VOICE_OPTION_SELECTION_KEY == "Pop VII/I" || this.CHORD_VOICE_OPTION_SELECTION_KEY == "Pop II/I" ) { 483 | v -= this.CHROMATIC_HALF_STEPS; 484 | } 485 | voices.push( voice ); 486 | } 487 | // Drop Chords 7ths { [3]1:B, [2]2:G, [1]3:E, [0]4:C } 488 | if ( index == 2 ) { 489 | if ( this.CHORD_VOICE_OPTION_SELECTION_KEY == "Drop 2 (1342)" || this.CHORD_VOICE_OPTION_SELECTION_KEY == "Drop 2+3 (1423)" || this.CHORD_VOICE_OPTION_SELECTION_KEY == "Drop 2+4 (1324)" ) { 490 | v -= this.CHROMATIC_HALF_STEPS; 491 | voices.push( voice ); 492 | } 493 | } 494 | if ( index == 1 ) { 495 | if ( this.CHORD_VOICE_OPTION_SELECTION_KEY == "Drop 3 (1243)" || this.CHORD_VOICE_OPTION_SELECTION_KEY == "Drop 2+3 (1423)" ) { 496 | v -= this.CHROMATIC_HALF_STEPS; 497 | voices.push( voice ); 498 | } 499 | } 500 | if ( index == 0 ) { 501 | if ( this.CHORD_VOICE_OPTION_SELECTION_KEY == "Drop 2+4 (1324)" ) { 502 | v -= this.CHROMATIC_HALF_STEPS; 503 | voices.push( voice ); 504 | } 505 | } 506 | } 507 | } 508 | return voices; 509 | } 510 | 511 | this.remove_minor_9ths = function ( chord ) { 512 | if ( chord.length != 7 ) { 513 | return chord; 514 | } 515 | const vmin = 0; 516 | const vmax = 3; 517 | const emin = 4; 518 | const emax = 6; 519 | for (let e = emin; e <= emax; e++) { 520 | for (let v = vmin; v <= vmax; v++) { 521 | const extension = chord[e]; 522 | const voice = chord[v]; 523 | const interval = extension - voice; 524 | if ( interval == 13 ) { 525 | chord[e] = null; 526 | } 527 | } 528 | } 529 | 530 | let cache = []; 531 | chord.forEach( function ( voice ) { 532 | if ( voice != null ) { 533 | cache.push(voice); 534 | } 535 | }); 536 | 537 | return cache; 538 | } 539 | 540 | this.transpose_pitch_to_lowest_octave = function ( pitch ) { 541 | let tp_pitch = pitch; 542 | while ( tp_pitch > 11 ) { 543 | tp_pitch -= this.CHROMATIC_HALF_STEPS; 544 | } 545 | return tp_pitch; 546 | } 547 | } 548 | 549 | function ParameterChanged( index, value ) { 550 | if ( UPDATING_CONTROLS == true ) { 551 | return; 552 | } 553 | switch ( index) { 554 | case 0: 555 | // target octave 556 | TARGET_OCTAVE = TARGET_OCTAVE_LIB[ TARGET_OCTAVE_KEYS[ value ] ]; 557 | break; 558 | case 1: 559 | // scale root 560 | SCALE_ROOT = value; 561 | music_lib.calculate_scale_pitches( SCALE_ROOT, SCALE_TEMPLATE_INDEX ); 562 | break; 563 | case 2: 564 | // scale type 565 | SCALE_TEMPLATE_INDEX = value; 566 | music_lib.calculate_scale_pitches( SCALE_ROOT, SCALE_TEMPLATE_INDEX ); 567 | break; 568 | case 3: 569 | // chord root 570 | CHORD_ROOT = value; 571 | music_lib.calculate_chord_pitches(CHORD_ROOT, KEYBOARD_SCALE); 572 | break; 573 | case 4: 574 | case 5: 575 | case 6: 576 | case 7: 577 | case 8: 578 | case 9: 579 | case 10: 580 | case 11: 581 | music_lib.update_chord_options(index, value); 582 | break; 583 | default: 584 | 585 | break; 586 | } 587 | } 588 | 589 | // 0 590 | PluginParameters.push({ 591 | name:"Target Octave", 592 | type:"menu", 593 | valueStrings:TARGET_OCTAVE_KEYS, 594 | defaultValue:5 595 | }); 596 | 597 | // 1 598 | PluginParameters.push({ 599 | name:"Scale Root", 600 | type:"menu", 601 | valueStrings: CHROMATIC_SCALE_STRINGS, 602 | defaultValue:0 603 | }); 604 | // 2 605 | PluginParameters.push({ 606 | name:"Scale Type", 607 | type:"menu", 608 | valueStrings: SCALE_KEYS, 609 | defaultValue:1 610 | }); 611 | // 3 612 | PluginParameters.push({ 613 | name:"Chord Root", 614 | type:"menu", 615 | valueStrings: CHROMATIC_SCALE_STRINGS, 616 | defaultValue:0 617 | }); 618 | // 4 619 | PluginParameters.push({ 620 | name:"Chord Voice Options", 621 | type:"menu", 622 | valueStrings: CHORD_VOICE_OPTIONS_KEYS, 623 | defaultValue:0 624 | }); 625 | // 5 626 | PluginParameters.push({ 627 | name:"Root", 628 | type:"checkbox", 629 | defaultValue:1 630 | }); 631 | // 6 632 | PluginParameters.push({ 633 | name:"3rd", 634 | type:"checkbox", 635 | defaultValue:1 636 | }); 637 | // 7 638 | PluginParameters.push({ 639 | name:"5th", 640 | type:"checkbox", 641 | defaultValue:1 642 | }); 643 | // 8 644 | PluginParameters.push({ 645 | name:"7th", 646 | type:"checkbox", 647 | defaultValue:1 648 | }); 649 | // 9 650 | PluginParameters.push({ 651 | name:"9th", 652 | type:"checkbox", 653 | defaultValue:1 654 | }); 655 | // 10 656 | PluginParameters.push({ 657 | name:"11th", 658 | type:"checkbox", 659 | defaultValue:1 660 | }); 661 | // 11 662 | PluginParameters.push({ 663 | name:"13th", 664 | type:"checkbox", 665 | defaultValue:1 666 | }); -------------------------------------------------------------------------------- /generative_music/oc_random_devices.js: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | Name: Randomize Note and Chodify 3 | Author(s): Philip Regan 4 | Purpose: Recreate devices in Ableton to generate random notes and chords and 5 | melodies. Works best with a transposition plug-in. 6 | 7 | Roadmap: 8 | * Add default chord selections 9 | * Add re-lengthen functionality 10 | 11 | This script is released under the MIT License. 12 | 13 | Permissions 14 | * Commercial use 15 | * Modification 16 | * Distribution 17 | * Private use 18 | 19 | Limitations 20 | x Liability 21 | x Warranty 22 | 23 | Conditions 24 | ! License and copyright notice 25 | 26 | Copyright Philip Regan and Pilcrow Records 27 | 28 | Permission is hereby granted, free of charge, to any person obtaining 29 | a copy of this software and associated documentation files (the 30 | "Software"), to deal in the Software without restriction, including 31 | without limitation the rights to use, copy, modify, merge, publish, 32 | distribute, sublicense, and/or sell copies of the Software, and to 33 | permit persons to whom the Software is furnished to do so, subject to 34 | the following conditions: 35 | 36 | The above copyright notice and this permission notice shall be 37 | included in all copies or substantial portions of the Software. 38 | 39 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 40 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 41 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 42 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 43 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 44 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 45 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 46 | 47 | ******************************************************************************/ 48 | 49 | /* 50 | SCRIPTER GLOBAL VARIABLES 51 | */ 52 | 53 | var NeedsTimingInfo = true; 54 | var PluginParameters = []; 55 | 56 | /* 57 | CUSTOM GLOBAL VARIABLES 58 | */ 59 | 60 | const CHANCE_MIN = 0; 61 | const CHANCE_DEF = 50; 62 | const CHANCE_MAX = 100; 63 | const CHOICES_MIN = 1; 64 | const CHOICES_MAX = 24; 65 | const CHOICES_DEF = 3; 66 | const DISTANCE_MIN = 1; 67 | const DISTANCE_MAX = 12; 68 | const DISTANCE_DEF = 3; 69 | const SIGN_DOWN = -1; 70 | const SIGN_BI = 0; 71 | const SIGN_UP = 1; 72 | const SIGN_DOWN_TEXT = "Down"; 73 | const SIGN_BI_TEXT = "Bi-directional"; 74 | const SIGN_UP_TEXT = "Up"; 75 | const SIGN_DEF = 0; 76 | const HALFSTEPS_MIN = -36; 77 | const HALFSTEPS_MAX = 36; 78 | const HALFSTEPS_ITERATIONS = 72; 79 | const HALFSTEPS_DEF = 0; 80 | const ROUTING = [ "Randomize > Chordify", "Chordify > Randomize" ]; 81 | const NOTE_LENGTH_TRIGGERS = ["NoteOn", "NoteOff"]; 82 | const NOTE_LENGTH_TRIGGER_DEF = 0; 83 | const NOTE_LENGTH_MODES = ["sync", "ms"]; 84 | const NOTE_LENGTH_MODES_DEF = 0; 85 | 86 | // 0 87 | let RANDOMIZE_VAL = 0; // 1 88 | let CHORDIFY_VAL = 1; // 2 89 | let RELENGTH_VAL = 0; // 3 90 | let ROUTING_VAL = 2; // 4 91 | // 5 92 | let CHANCE_VAL = CHANCE_DEF; // 6 93 | let CHOICES_VAL = CHOICES_DEF; // 7 94 | let DISTANCE_VAL = DISTANCE_DEF; // 8 95 | let DIRECTION_VAL = SIGN_DEF; // 9 96 | // 10 97 | let VOICE_1_VAL = 0; // 11 98 | let VOICE_2_VAL = 0; // 12 99 | let VOICE_3_VAL = 0; // 13 100 | let VOICE_4_VAL = 0; // 14 101 | let VOICE_5_VAL = 0; // 15 102 | let VOICE_6_VAL = 0; // 16 103 | 104 | let NOTE_TRACKING = []; 105 | 106 | function HandleMIDI( event ) { 107 | if ( event instanceof NoteOn ) { 108 | if ( RANDOMIZE_VAL || CHORDIFY_VAL || RELENGTH_VAL ) { 109 | let cache = []; 110 | if ( ROUTING_VAL == 0 ) { 111 | // Trace("HandleMIDI: " + ROUTING[ROUTING_VAL]); 112 | if ( RANDOMIZE_VAL ) { 113 | let randomized_pitch = randomize( event.pitch, CHANCE_VAL, CHOICES_VAL, DISTANCE_VAL, DIRECTION_VAL ); 114 | cache.push(randomized_pitch); 115 | } else { 116 | cache.push( event.pitch ); 117 | } 118 | if ( CHORDIFY_VAL ) { 119 | cache = chordify( cache[0], VOICE_1_VAL, VOICE_2_VAL, VOICE_3_VAL, VOICE_4_VAL, VOICE_5_VAL, VOICE_6_VAL ); 120 | } 121 | } else { 122 | if ( CHORDIFY_VAL ) { 123 | cache = chordify( event.pitch, VOICE_1_VAL, VOICE_2_VAL, VOICE_3_VAL, VOICE_4_VAL, VOICE_5_VAL, VOICE_6_VAL ); 124 | } 125 | if ( RANDOMIZE_VAL ) { 126 | for (let index = 0; index < cache.length; index++) { 127 | const pitch = cache[index]; 128 | let randomized_pitch = randomize( pitch, CHANCE_VAL, CHOICES_VAL, DISTANCE_VAL, DIRECTION_VAL ); 129 | cache[index] = randomized_pitch; 130 | } 131 | } 132 | } 133 | add_events_to_active_notes( event, cache ); 134 | cache.forEach( function ( pitch ) { 135 | let note_on = new NoteOn(); 136 | note_on.pitch = pitch; 137 | note_on.velocity = event.velocity; 138 | note_on.send(); 139 | }); 140 | } else { 141 | event.send(); 142 | } 143 | } else if ( event instanceof NoteOff ) { 144 | let cache = remove_events_from_active_notes( event ); 145 | cache.forEach( function( pitch ) { 146 | let note_off = new NoteOff; 147 | note_off.pitch = pitch; 148 | note_off.send(); 149 | }); 150 | } else { 151 | event.send() 152 | } 153 | } 154 | 155 | function ParameterChanged( param, value ) { 156 | 157 | switch( param ) { 158 | // 0 159 | case 0: 160 | case 1: 161 | RANDOMIZE_VAL = value; // 1 162 | Trace(JSON.stringify({RANDOMIZE_VAL:RANDOMIZE_VAL})); 163 | break; 164 | case 2: 165 | CHORDIFY_VAL = value; // 2 166 | Trace(JSON.stringify({RANDOMIZE_VAL:CHORDIFY_VAL})); 167 | break; 168 | case 3: 169 | ROUTING_VAL = value; // 4 170 | Trace(JSON.stringify({ROUTING_VAL:ROUTING[ROUTING_VAL]})); 171 | break; 172 | case 4: 173 | case 5: 174 | CHANCE_VAL = value; // 6 175 | break; 176 | case 6: 177 | CHOICES_VAL = value; // 7 178 | break; 179 | case 7: 180 | DISTANCE_VAL = value; // 8 181 | break; 182 | case 8: 183 | DIRECTION_VAL = value; // 9 184 | break; 185 | case 9: 186 | case 10: 187 | VOICE_1_VAL = value; // 11 188 | break; 189 | case 11: 190 | VOICE_2_VAL = value; // 12 191 | break; 192 | case 12: 193 | VOICE_3_VAL = value; // 13 194 | break; 195 | case 13: 196 | VOICE_4_VAL = value; // 14 197 | break; 198 | case 14: 199 | VOICE_5_VAL = value; // 15 200 | break; 201 | case 15: 202 | VOICE_6_VAL = value; // 16 203 | break; 204 | default: 205 | // ERROR 206 | } 207 | } 208 | 209 | /* 210 | src_pitch: integer 0-127; MIDI pitch 211 | chance: integer 0-100; percentage 212 | choices: integer 1-24; half-steps 213 | scale: integer 1-12; half steps 214 | sign: integer {-1, 0, 1}; direction: down, bi, up 215 | returns integer 0-127; MIDI pitch 216 | */ 217 | function randomize( src_pitch, chance, choices, distance, direction ) { 218 | // console.log([src_pitch, chance, choices, distance, direction]); 219 | // are we randomizing this pitch? 220 | if ( rInt(0, 100) > chance ) { 221 | return src_pitch; 222 | } 223 | let pool = []; 224 | // add src to pool 225 | pool.push( src_pitch ); 226 | let last_pitch = src_pitch 227 | switch ( direction ) { 228 | // determine direction 229 | case -1: 230 | // add pitches to pool by adding from src or last pitch by distance 231 | for (let index = 0; index < choices; index++) { 232 | let new_pitch = last_pitch - distance; 233 | if ( new_pitch < 0 ) { 234 | new_pitch = 0; 235 | } 236 | pool.push( new_pitch ); 237 | last_pitch = new_pitch; 238 | } 239 | break; 240 | case 0: 241 | for (let index = 0; index < choices; index++) { 242 | let new_pitch = last_pitch - distance; 243 | if ( new_pitch < 0 ) { 244 | new_pitch = 0; 245 | } 246 | pool.push( new_pitch ); 247 | last_pitch = new_pitch; 248 | } 249 | last_pitch = src_pitch; 250 | for (let index = 0; index < choices; index++) { 251 | let new_pitch = last_pitch + distance; 252 | if ( new_pitch > 127 ) { 253 | new_pitch = 127; 254 | } 255 | pool.push( new_pitch ); 256 | last_pitch = new_pitch; 257 | } 258 | break; 259 | case 1: 260 | for (let index = 0; index < choices; index++) { 261 | let new_pitch = last_pitch + distance; 262 | if ( new_pitch > 127 ) { 263 | new_pitch = 127; 264 | } 265 | pool.push( new_pitch ); 266 | last_pitch = new_pitch; 267 | } 268 | break; 269 | default: 270 | // error 271 | break; 272 | } 273 | // select a random pitch 274 | pool = [...new Set(pool)]; 275 | // Trace(pool); 276 | console.log( JSON.stringify( pool ) ); 277 | let r = rInt( 0, pool.length - 1 ); 278 | return pool[ r ]; 279 | } 280 | 281 | /* 282 | root: Integer 0-127; MIDI 283 | first, third . . . thirteenth: Integer 0-36; half-steps, 0 = don't calculate 284 | first = root so it can be turned off in the resulting chord 285 | transpose: Integer -36...0...36: half-steps 286 | returns array of Integers 287 | */ 288 | function chordify( root, first, third, fifth, seventh, ninth, eleventh, thirteenth ) { 289 | let chord = []; 290 | let intervals = [ first, third, fifth, seventh, ninth, eleventh, thirteenth ]; 291 | for (let index = 0; index < intervals.length; index++) { 292 | const interval = intervals[index]; 293 | if ( index == 0 ) { 294 | // root is always needed in the output 295 | let new_root = root + interval; 296 | if ( new_root > 127 ) { 297 | new_root = 127; 298 | } 299 | if ( new_root < 0 ) { 300 | new_root = 0; 301 | } 302 | chord.push( new_root ); 303 | } else { 304 | if ( interval > 0 ) { 305 | let voice = root + interval; 306 | if ( voice > 127 ) { 307 | voice = 127; 308 | } 309 | if ( voice < 0 ) { 310 | voice = 0; 311 | } 312 | chord.push( voice ); 313 | } else { 314 | let voice = root + Math.abs(interval); 315 | if ( voice > 127 ) { 316 | voice = 127; 317 | } 318 | if ( voice < 0 ) { 319 | voice = 0; 320 | } 321 | chord.push( voice ); 322 | } 323 | } 324 | } 325 | return chord; 326 | }; 327 | 328 | /* 329 | ACTIVE NOTE TRACKING STACK 330 | */ 331 | 332 | // tracks active notes based on original pitch in case of modification 333 | // stores pitch caches at the index of the original pitch 334 | // assumes source pitches do not overlap 335 | function add_events_to_active_notes( src_event, cache ) { 336 | /* 337 | NOTE_TRACKING[src_event] = mod_event.pitch; 338 | */ 339 | NOTE_TRACKING[src_event.pitch] = cache; 340 | Trace("ADD " + JSON.stringify(NOTE_TRACKING)); 341 | } 342 | 343 | // returns the modified event based on the source event 344 | function remove_events_from_active_notes( src_event ) { 345 | // if ( event.pitch in NOTE_TRACKING ) { 346 | // var temp = event.pitch; 347 | // event.pitch = NOTE_TRACKING[event.pitch]; 348 | // delete NOTE_TRACKING[temp]; 349 | // } 350 | let cache = NOTE_TRACKING[ src_event.pitch ]; 351 | if ( cache ) { 352 | NOTE_TRACKING[ src_event.pitch ] = null; 353 | return cache; 354 | } 355 | // no cache was found, so we return the 356 | // pitch itself to be handled as is 357 | return [src_event.pitch]; 358 | } 359 | 360 | function rInt (x, y) { 361 | if (x > y) { 362 | [x, y] = [x, y]; 363 | } 364 | return Math.floor(Math.random() * (y - x + 1)) + x; 365 | } 366 | 367 | // 0 368 | PluginParameters.push({ 369 | name: "Routing", 370 | type: "text" 371 | }); 372 | 373 | // 1 374 | PluginParameters.push({ 375 | name:"Randomize", 376 | type:"checkbox", 377 | defaultValue:RANDOMIZE_VAL 378 | }); 379 | 380 | // 2 381 | PluginParameters.push({ 382 | name:"Chordify", 383 | type:"checkbox", 384 | defaultValue:CHORDIFY_VAL 385 | }); 386 | 387 | // 3 388 | PluginParameters.push({ 389 | name:"Routing", 390 | type:"menu", 391 | valueStrings:ROUTING, 392 | defaultValue:2 393 | }); 394 | 395 | // 4 396 | PluginParameters.push({ 397 | name: "Randomize", 398 | type: "text" 399 | }); 400 | 401 | // 5 402 | PluginParameters.push({ 403 | name:"Chance", 404 | type:"lin", unit:"\%", 405 | minValue:CHANCE_MIN, 406 | maxValue:CHANCE_MAX, 407 | numberOfSteps:CHANCE_MAX, 408 | defaultValue:CHANCE_DEF 409 | }); 410 | 411 | // 6 412 | PluginParameters.push({ 413 | name:"Choices", 414 | type:"lin", unit:"pitches", 415 | minValue:CHOICES_MIN, 416 | maxValue:CHOICES_MAX, 417 | numberOfSteps:CHOICES_MAX - CHOICES_MIN, 418 | defaultValue:CHOICES_DEF 419 | }); 420 | 421 | // 7 422 | PluginParameters.push({ 423 | name:"Distance", 424 | type:"lin", unit:"st", 425 | minValue:DISTANCE_MIN, 426 | maxValue:DISTANCE_MAX, 427 | numberOfSteps:DISTANCE_MAX - DISTANCE_MIN, 428 | defaultValue:DISTANCE_DEF 429 | }); 430 | 431 | // 8 432 | PluginParameters.push({ 433 | name:"Direction", 434 | type:"menu", 435 | valueStrings:[SIGN_UP_TEXT, SIGN_BI_TEXT, SIGN_DOWN_TEXT], 436 | defaultValue:SIGN_DEF 437 | }); 438 | 439 | // 9 440 | PluginParameters.push({ 441 | name: "Chordify", 442 | type: "text" 443 | }); 444 | 445 | // 10 446 | PluginParameters.push({ 447 | name:"Voice 1 (Root)", 448 | type:"lin", unit:"st", 449 | minValue:HALFSTEPS_MIN, 450 | maxValue:HALFSTEPS_MAX, 451 | numberOfSteps:HALFSTEPS_MAX - HALFSTEPS_MIN, 452 | defaultValue:HALFSTEPS_DEF 453 | }); 454 | 455 | // 11 456 | PluginParameters.push({ 457 | name:"Voice 2", 458 | type:"lin", unit:"st", 459 | minValue:HALFSTEPS_MIN, 460 | maxValue:HALFSTEPS_MAX, 461 | numberOfSteps:HALFSTEPS_MAX - HALFSTEPS_MIN, 462 | defaultValue:4 463 | }); 464 | 465 | // 12 466 | PluginParameters.push({ 467 | name:"Voice 3", 468 | type:"lin", unit:"st", 469 | minValue:HALFSTEPS_MIN, 470 | maxValue:HALFSTEPS_MAX, 471 | numberOfSteps:HALFSTEPS_MAX - HALFSTEPS_MIN, 472 | defaultValue:7 473 | }); 474 | 475 | // 13 476 | PluginParameters.push({ 477 | name:"Voice 4", 478 | type:"lin", unit:"st", 479 | minValue:HALFSTEPS_MIN, 480 | maxValue:HALFSTEPS_MAX, 481 | numberOfSteps:HALFSTEPS_MAX - HALFSTEPS_MIN, 482 | defaultValue:10 483 | }); 484 | 485 | // 14 486 | PluginParameters.push({ 487 | name:"Voice 5", 488 | type:"lin", unit:"st", 489 | minValue:HALFSTEPS_MIN, 490 | maxValue:HALFSTEPS_MAX, 491 | numberOfSteps:HALFSTEPS_MAX - HALFSTEPS_MIN, 492 | defaultValue:-8 493 | }); 494 | 495 | // 15 496 | PluginParameters.push({ 497 | name:"Voice 6", 498 | type:"lin", unit:"st", 499 | minValue:HALFSTEPS_MIN, 500 | maxValue:HALFSTEPS_MAX, 501 | numberOfSteps:HALFSTEPS_MAX - HALFSTEPS_MIN, 502 | defaultValue:-5 503 | }); -------------------------------------------------------------------------------- /simple_scripter_tricks/controls_full_keyboard.js: -------------------------------------------------------------------------------- 1 | /* 2 | Adding and managing controls for all 128 MIDI pitches 3 | */ 4 | 5 | var PluginParameters = []; 6 | 7 | const CHROMATIC_SCALE_STRINGS = ["C", "C♯/D♭", "D", "D♯/E♭", "E", "F", "F♯/G♭", "G", "G♯/A♭", "A", "A♯/B♭", "B"]; 8 | const OCTAVE_STRINGS = ["-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8"]; 9 | 10 | var pitch_cursor = -1; 11 | var octave_cursor = -1; 12 | var current_octave = ""; 13 | for (let pitch = 0; pitch < 128; pitch++) { 14 | 15 | pitch_cursor += 1; 16 | if ( pitch_cursor == CHROMATIC_SCALE_STRINGS.length ) { 17 | pitch_cursor = 0; 18 | } 19 | 20 | if ( pitch_cursor == 0 ) { 21 | octave_cursor += 1; 22 | if ( octave_cursor == OCTAVE_STRINGS.length ) { 23 | octave_cursor = 0; 24 | } 25 | } 26 | 27 | PluginParameters.push({ 28 | name:CHROMATIC_SCALE_STRINGS[pitch_cursor] + " " + OCTAVE_STRINGS[octave_cursor] + " (" + pitch + ")", 29 | type:"lin", 30 | unit:"\%", 31 | minValue:0, 32 | maxValue:100, 33 | numberOfSteps:100, 34 | defaultValue:50 35 | }); 36 | } 37 | 38 | function ParameterChanged( param , value ) { 39 | switch ( param ) { 40 | case 0: 41 | // C -2 (0) 42 | break; 43 | case 1: 44 | // C♯/D♭ -2 (1) 45 | break; 46 | case 2: 47 | // D -2 (2) 48 | break; 49 | case 3: 50 | // D♯/E♭ -2 (3) 51 | break; 52 | case 4: 53 | // E -2 (4) 54 | break; 55 | case 5: 56 | // F -2 (5) 57 | break; 58 | case 6: 59 | // F♯/G♭ -2 (6) 60 | break; 61 | case 7: 62 | // G -2 (7) 63 | break; 64 | case 8: 65 | // G♯/A♭ -2 (8) 66 | break; 67 | case 9: 68 | // A -2 (9) 69 | break; 70 | case 10: 71 | // A♯/B♭ -2 (10) 72 | break; 73 | case 11: 74 | // B -2 (11) 75 | break; 76 | case 12: 77 | // C -1 (12) 78 | break; 79 | case 13: 80 | // C♯/D♭ -1 (13) 81 | break; 82 | case 14: 83 | // D -1 (14) 84 | break; 85 | case 15: 86 | // D♯/E♭ -1 (15) 87 | break; 88 | case 16: 89 | // E -1 (16) 90 | break; 91 | case 17: 92 | // F -1 (17) 93 | break; 94 | case 18: 95 | // F♯/G♭ -1 (18) 96 | break; 97 | case 19: 98 | // G -1 (19) 99 | break; 100 | case 20: 101 | // G♯/A♭ -1 (20) 102 | break; 103 | case 21: 104 | // A -1 (21) 105 | break; 106 | case 22: 107 | // A♯/B♭ -1 (22) 108 | break; 109 | case 23: 110 | // B -1 (23) 111 | break; 112 | case 24: 113 | // C 0 (24) 114 | break; 115 | case 25: 116 | // C♯/D♭ 0 (25) 117 | break; 118 | case 26: 119 | // D 0 (26) 120 | break; 121 | case 27: 122 | // D♯/E♭ 0 (27) 123 | break; 124 | case 28: 125 | // E 0 (28) 126 | break; 127 | case 29: 128 | // F 0 (29) 129 | break; 130 | case 30: 131 | // F♯/G♭ 0 (30) 132 | break; 133 | case 31: 134 | // G 0 (31) 135 | break; 136 | case 32: 137 | // G♯/A♭ 0 (32) 138 | break; 139 | case 33: 140 | // A 0 (33) 141 | break; 142 | case 34: 143 | // A♯/B♭ 0 (34) 144 | break; 145 | case 35: 146 | // B 0 (35) 147 | break; 148 | case 36: 149 | // C 1 (36) 150 | break; 151 | case 37: 152 | // C♯/D♭ 1 (37) 153 | break; 154 | case 38: 155 | // D 1 (38) 156 | break; 157 | case 39: 158 | // D♯/E♭ 1 (39) 159 | break; 160 | case 40: 161 | // E 1 (40) 162 | break; 163 | case 41: 164 | // F 1 (41) 165 | break; 166 | case 42: 167 | // F♯/G♭ 1 (42) 168 | break; 169 | case 43: 170 | // G 1 (43) 171 | break; 172 | case 44: 173 | // G♯/A♭ 1 (44) 174 | break; 175 | case 45: 176 | // A 1 (45) 177 | break; 178 | case 46: 179 | // A♯/B♭ 1 (46) 180 | break; 181 | case 47: 182 | // B 1 (47) 183 | break; 184 | case 48: 185 | // C 2 (48) 186 | break; 187 | case 49: 188 | // C♯/D♭ 2 (49) 189 | break; 190 | case 50: 191 | // D 2 (50) 192 | break; 193 | case 51: 194 | // D♯/E♭ 2 (51) 195 | break; 196 | case 52: 197 | // E 2 (52) 198 | break; 199 | case 53: 200 | // F 2 (53) 201 | break; 202 | case 54: 203 | // F♯/G♭ 2 (54) 204 | break; 205 | case 55: 206 | // G 2 (55) 207 | break; 208 | case 56: 209 | // G♯/A♭ 2 (56) 210 | break; 211 | case 57: 212 | // A 2 (57) 213 | break; 214 | case 58: 215 | // A♯/B♭ 2 (58) 216 | break; 217 | case 59: 218 | // B 2 (59) 219 | break; 220 | case 60: 221 | // C 3 (60) 222 | break; 223 | case 61: 224 | // C♯/D♭ 3 (61) 225 | break; 226 | case 62: 227 | // D 3 (62) 228 | break; 229 | case 63: 230 | // D♯/E♭ 3 (63) 231 | break; 232 | case 64: 233 | // E 3 (64) 234 | break; 235 | case 65: 236 | // F 3 (65) 237 | break; 238 | case 66: 239 | // F♯/G♭ 3 (66) 240 | break; 241 | case 67: 242 | // G 3 (67) 243 | break; 244 | case 68: 245 | // G♯/A♭ 3 (68) 246 | break; 247 | case 69: 248 | // A 3 (69) 249 | break; 250 | case 70: 251 | // A♯/B♭ 3 (70) 252 | break; 253 | case 71: 254 | // B 3 (71) 255 | break; 256 | case 72: 257 | // C 4 (72) 258 | break; 259 | case 73: 260 | // C♯/D♭ 4 (73) 261 | break; 262 | case 74: 263 | // D 4 (74) 264 | break; 265 | case 75: 266 | // D♯/E♭ 4 (75) 267 | break; 268 | case 76: 269 | // E 4 (76) 270 | break; 271 | case 77: 272 | // F 4 (77) 273 | break; 274 | case 78: 275 | // F♯/G♭ 4 (78) 276 | break; 277 | case 79: 278 | // G 4 (79) 279 | break; 280 | case 80: 281 | // G♯/A♭ 4 (80) 282 | break; 283 | case 81: 284 | // A 4 (81) 285 | break; 286 | case 82: 287 | // A♯/B♭ 4 (82) 288 | break; 289 | case 83: 290 | // B 4 (83) 291 | break; 292 | case 84: 293 | // C 5 (84) 294 | break; 295 | case 85: 296 | // C♯/D♭ 5 (85) 297 | break; 298 | case 86: 299 | // D 5 (86) 300 | break; 301 | case 87: 302 | // D♯/E♭ 5 (87) 303 | break; 304 | case 88: 305 | // E 5 (88) 306 | break; 307 | case 89: 308 | // F 5 (89) 309 | break; 310 | case 90: 311 | // F♯/G♭ 5 (90) 312 | break; 313 | case 91: 314 | // G 5 (91) 315 | break; 316 | case 92: 317 | // G♯/A♭ 5 (92) 318 | break; 319 | case 93: 320 | // A 5 (93) 321 | break; 322 | case 94: 323 | // A♯/B♭ 5 (94) 324 | break; 325 | case 95: 326 | // B 5 (95) 327 | break; 328 | case 96: 329 | // C 6 (96) 330 | break; 331 | case 97: 332 | // C♯/D♭ 6 (97) 333 | break; 334 | case 98: 335 | // D 6 (98) 336 | break; 337 | case 99: 338 | // D♯/E♭ 6 (99) 339 | break; 340 | case 100: 341 | // E 6 (100) 342 | break; 343 | case 101: 344 | // F 6 (101) 345 | break; 346 | case 102: 347 | // F♯/G♭ 6 (102) 348 | break; 349 | case 103: 350 | // G 6 (103) 351 | break; 352 | case 104: 353 | // G♯/A♭ 6 (104) 354 | break; 355 | case 105: 356 | // A 6 (105) 357 | break; 358 | case 106: 359 | // A♯/B♭ 6 (106) 360 | break; 361 | case 107: 362 | // B 6 (107) 363 | break; 364 | case 108: 365 | // C 7 (108) 366 | break; 367 | case 109: 368 | // C♯/D♭ 7 (109) 369 | break; 370 | case 110: 371 | // D 7 (110) 372 | break; 373 | case 111: 374 | // D♯/E♭ 7 (111) 375 | break; 376 | case 112: 377 | // E 7 (112) 378 | break; 379 | case 113: 380 | // F 7 (113) 381 | break; 382 | case 114: 383 | // F♯/G♭ 7 (114) 384 | break; 385 | case 115: 386 | // G 7 (115) 387 | break; 388 | case 116: 389 | // G♯/A♭ 7 (116) 390 | break; 391 | case 117: 392 | // A 7 (117) 393 | break; 394 | case 118: 395 | // A♯/B♭ 7 (118) 396 | break; 397 | case 119: 398 | // B 7 (119) 399 | break; 400 | case 120: 401 | // C 8 (120) 402 | break; 403 | case 121: 404 | // C♯/D♭ 8 (121) 405 | break; 406 | case 122: 407 | // D 8 (122) 408 | break; 409 | case 123: 410 | // D♯/E♭ 8 (123) 411 | break; 412 | case 124: 413 | // E 8 (124) 414 | break; 415 | case 125: 416 | // F 8 (125) 417 | break; 418 | case 126: 419 | // F♯/G♭ 8 (126) 420 | break; 421 | case 127: 422 | // G 8 (127) 423 | break; 424 | default: 425 | Trace("ERROR: ParameterChanged(" + param + ", " + value + ")"); 426 | break; 427 | } 428 | } -------------------------------------------------------------------------------- /simple_scripter_tricks/global_reverse_pitches.js: -------------------------------------------------------------------------------- 1 | /* 2 | Reverse Pitches 3 | 4 | A simple tool/trick to globally reverse all pitches during play. No practical value, but 5 | it does show there can be multiple ways to solve a problem. 6 | 7 | Option 1: Fast lookup. An array with all of the intended values, which are then looked up 8 | by using the pitch as an index. This is akin to looking up values in an Object. 9 | 10 | Option 2: Simply subtract the pitch from 127. 11 | */ 12 | 13 | var NeedsTimingInfo = true; 14 | var PluginParameters = []; 15 | 16 | var REVERSE_PITCHES = false; 17 | const REVERSE_MAP = [127,126,125,124,123,122,121,120,119,118,117,116,115,114,113,112,111,110,109,108,107,106,105,104,103,102,101,100,99,98,97,96,95,94,93,92,91,90,89,88,87,86,85,84,83,82,81,80,79,78,77,76,75,74,73,72,71,70,69,68,67,66,65,64,63,62,61,60,59,58,57,56,55,54,53,52,51,50,49,48,47,46,45,44,43,42,41,40,39,38,37,36,35,34,33,32,31,30,29,28,27,26,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0]; 18 | 19 | function HandleMIDI(event) { 20 | // Option 1 21 | if ( REVERSE_PITCHES && ( event instanceof NoteOn || event instanceof NoteOff ) ) { 22 | event.pitch = REVERSE_MAP[ event.pitch ]; 23 | } 24 | // Option 2 25 | // event.pitch = 127 - event.pitch; 26 | event.send(); 27 | } 28 | 29 | function ProcessMIDI() { 30 | var timingInfo = GetTimingInfo(); 31 | } 32 | 33 | function ParameterChanged(param, value) { 34 | switch ( param ) { 35 | case 0: 36 | REVERSE_PITCHES = value; 37 | default: 38 | // do nothing 39 | } 40 | } 41 | 42 | PluginParameters.push({ 43 | name:"Reverse Pitches", 44 | type:"checkbox", 45 | defaultValue:0 46 | }); 47 | -------------------------------------------------------------------------------- /simple_scripter_tricks/simple_transposition.js: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | Name: simple transposition 3 | Author(s): Philip Regan 4 | Purpose: 5 | * Intended for use with chord progressions, inverts all chords based on a 6 | fulcrum pitch. Only works on a simple move of a note by one octave. 7 | 8 | This script is released under the MIT License. 9 | 10 | Permissions 11 | * Commercial use 12 | * Modification 13 | * Distribution 14 | * Private use 15 | 16 | Limitations 17 | x Liability 18 | x Warranty 19 | 20 | Conditions 21 | ! License and copyright notice 22 | 23 | Copyright Philip Regan and Pilcrow Records 24 | 25 | Permission is hereby granted, free of charge, to any person obtaining 26 | a copy of this software and associated documentation files (the 27 | "Software"), to deal in the Software without restriction, including 28 | without limitation the rights to use, copy, modify, merge, publish, 29 | distribute, sublicense, and/or sell copies of the Software, and to 30 | permit persons to whom the Software is furnished to do so, subject to 31 | the following conditions: 32 | 33 | The above copyright notice and this permission notice shall be 34 | included in all copies or substantial portions of the Software. 35 | 36 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 37 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 38 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 39 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 40 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 41 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 42 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 43 | 44 | ****************************************************************************/ 45 | 46 | var PluginParameters = []; 47 | 48 | const CHROMATIC_SCALE_STRINGS = ["C", "C♯/D♭", "D", "D♯/E♭", "E", "F", "F♯/G♭", "G", "G♯/A♭", "A", "A♯/B♭", "B"]; 49 | const OCTAVE_STRINGS = ["-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8"]; 50 | var PITCH_STRINGS = [] 51 | // build the pitch strings 52 | var pitch_cursor = -1; 53 | var octave_cursor = -1; 54 | var current_octave = ""; 55 | for ( let pitch = 0; pitch < 128; pitch++ ) { 56 | 57 | pitch_cursor += 1; 58 | if ( pitch_cursor == CHROMATIC_SCALE_STRINGS.length ) { 59 | pitch_cursor = 0; 60 | } 61 | 62 | if ( pitch_cursor == 0 ) { 63 | octave_cursor += 1; 64 | if ( octave_cursor == OCTAVE_STRINGS.length ) { 65 | octave_cursor = 0; 66 | } 67 | } 68 | 69 | PITCH_STRINGS.push( CHROMATIC_SCALE_STRINGS[pitch_cursor] + " " + OCTAVE_STRINGS[octave_cursor] + " (" + pitch + ")" ); 70 | } 71 | 72 | var FULCRUM_PITCH = 60; 73 | const CALC_DIRS = ["Up only", "Down only"]; 74 | var CALC_DIR = CALC_DIRS[0]; 75 | 76 | function HandleMIDI( event ) { 77 | if ( event instanceof NoteOn || event instanceof NoteOff ) { 78 | let new_pitch = calculate_reversal( event.pitch ); 79 | new_pitch = MIDI.normalizeData( new_pitch ); 80 | // Trace(event.pitch + "\t" + FULCRUM_PITCH + "\t" + new_pitch); 81 | event.pitch = new_pitch; 82 | } 83 | event.send(); 84 | } 85 | 86 | function calculate_reversal( orig_pitch ) { 87 | 88 | // up only 89 | if ( CALC_DIR == CALC_DIRS[0] ) { 90 | if ( orig_pitch < FULCRUM_PITCH ) { 91 | return orig_pitch + 12; 92 | } 93 | } 94 | 95 | // down only 96 | if ( CALC_DIR == CALC_DIRS[1] ) { 97 | if ( orig_pitch > FULCRUM_PITCH ) { 98 | return orig_pitch - 12; 99 | } 100 | } 101 | return orig_pitch; 102 | } 103 | 104 | function ParameterChanged( param , value ) { 105 | switch ( param ) { 106 | case 0: 107 | // Fulcrum Pitch 108 | FULCRUM_PITCH = value; 109 | break; 110 | case 1: 111 | // Direction 112 | CALC_DIR = CALC_DIRS[value]; 113 | break; 114 | default: 115 | Trace("ERROR: ParameterChanged(" + param + ", " + value + ")"); 116 | } 117 | } 118 | 119 | PluginParameters.push({ 120 | name:"Fulcrum Pitch", 121 | type:"menu", 122 | valueStrings:PITCH_STRINGS, 123 | defaultValue:60 124 | }); 125 | 126 | PluginParameters.push({ 127 | name:"Direction", 128 | type:"menu", 129 | valueStrings:CALC_DIRS, 130 | defaultValue:0 131 | }); -------------------------------------------------------------------------------- /simple_scripter_tricks/transpose_by_pitch.js: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | Name: transpose by pitch 3 | Author(s): Philip Regan 4 | Purpose: 5 | * The Reverse Pitch MIDI transformation will transpose a pitch based on the 6 | number of half-steps from a fulcrum pitch. This can lead to non-diatonic pitches 7 | in the outcome: For example if working in C Ionian: 8 | Target pitch = 62 = D = diatonic 9 | Fulcrum Pitch = 60 = C = diatonic 10 | Reversed pitch = 58 = A♯/B♭ = non-diatonic 11 | * This script will do both the half-step calculation, and will also transpose 12 | the target pitch by an octave, much like inverting a chord. For example, if 13 | working in C Ionian: 14 | Target pitch = 62 = D-3 = diatonic 15 | Fulcrum Pitch = 60 = C-3 = diatonic 16 | Reversed pitch = 50 = D-2 = diatonic 17 | 18 | * 19 | 20 | Roadmap: 21 | * v1: pitch reversal by half-step or octave 22 | X Select fulcrum pitch 23 | X Select half-step or octave (inversion) 24 | X Select direction 25 | X bi-direction (default) 26 | X Up (if pitch below fulcrum, then push up) 27 | X Down (if pitch above fulcrum, then push down) 28 | X calc reversal 29 | X half-step 30 | X octave 31 | * v2: transpose into pitch range 32 | * select high pitch 33 | * select low pitch 34 | * calc above high pitch to below 35 | * calc below low pitch to above 36 | * v3: reversal within diatonic set 37 | * add scale management 38 | * calc scale degrees to fulcrum 39 | * calc reversal by scale degrees 40 | 41 | This script is released under the MIT License. 42 | 43 | Permissions 44 | * Commercial use 45 | * Modification 46 | * Distribution 47 | * Private use 48 | 49 | Limitations 50 | x Liability 51 | x Warranty 52 | 53 | Conditions 54 | ! License and copyright notice 55 | 56 | Copyright Philip Regan and Pilcrow Records 57 | 58 | Permission is hereby granted, free of charge, to any person obtaining 59 | a copy of this software and associated documentation files (the 60 | "Software"), to deal in the Software without restriction, including 61 | without limitation the rights to use, copy, modify, merge, publish, 62 | distribute, sublicense, and/or sell copies of the Software, and to 63 | permit persons to whom the Software is furnished to do so, subject to 64 | the following conditions: 65 | 66 | The above copyright notice and this permission notice shall be 67 | included in all copies or substantial portions of the Software. 68 | 69 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 70 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 71 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 72 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 73 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 74 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 75 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 76 | 77 | ****************************************************************************/ 78 | 79 | var PluginParameters = []; 80 | 81 | const CHROMATIC_SCALE_STRINGS = ["C", "C♯/D♭", "D", "D♯/E♭", "E", "F", "F♯/G♭", "G", "G♯/A♭", "A", "A♯/B♭", "B"]; 82 | const OCTAVE_STRINGS = ["-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8"]; 83 | var PITCH_STRINGS = [] 84 | // build the pitch strings 85 | var pitch_cursor = -1; 86 | var octave_cursor = -1; 87 | var current_octave = ""; 88 | for ( let pitch = 0; pitch < 128; pitch++ ) { 89 | 90 | pitch_cursor += 1; 91 | if ( pitch_cursor == CHROMATIC_SCALE_STRINGS.length ) { 92 | pitch_cursor = 0; 93 | } 94 | 95 | if ( pitch_cursor == 0 ) { 96 | octave_cursor += 1; 97 | if ( octave_cursor == OCTAVE_STRINGS.length ) { 98 | octave_cursor = 0; 99 | } 100 | } 101 | 102 | PITCH_STRINGS.push( CHROMATIC_SCALE_STRINGS[pitch_cursor] + " " + OCTAVE_STRINGS[octave_cursor] + " (" + pitch + ")" ); 103 | } 104 | 105 | var FULCRUM_PITCH = 60; 106 | const CALC_METHODS = ["Half-step", "Octave"]; 107 | var CALC_METHOD = CALC_METHODS[0]; 108 | const CALC_DIRS = ["Bi-directional", "Up only", "Down only"]; 109 | var CALC_DIR = CALC_DIRS[0]; 110 | 111 | function HandleMIDI( event ) { 112 | if ( event instanceof NoteOn || event instanceof NoteOff ) { 113 | let new_pitch = calculate_reversal( event.pitch ); 114 | new_pitch = MIDI.normalizeData( new_pitch ); 115 | event.pitch = new_pitch; 116 | } 117 | event.send(); 118 | } 119 | 120 | function calculate_reversal( orig_pitch ) { 121 | // handle the cases where nothing needs to change 122 | if ( CALC_DIR == 0 && orig_pitch == FULCRUM_PITCH ) { 123 | return orig_pitch; 124 | } 125 | if ( CALC_DIR == 1 && orig_pitch >= FULCRUM_PITCH ) { 126 | return orig_pitch; 127 | } 128 | if ( CALC_DIR == 2 && orig_pitch <= FULCRUM_PITCH ) { 129 | return orig_pitch; 130 | } 131 | // handle each combination individually; no need to get clever here 132 | // repeated calcs are placed into functions for clarity and maintenance 133 | // bi-directional 134 | if ( CALC_DIR == 0 ) { 135 | // half-step 136 | if ( CALC_METHOD == 0 ) { 137 | if ( orig_pitch > FULCRUM_PITCH ) { 138 | return calc_halfstep_down( orig_pitch ); 139 | } else { 140 | return calc_halfstep_up( orig_pitch ); 141 | } 142 | } 143 | // octave 144 | if ( CALC_METHOD == 1 ) { 145 | if ( orig_pitch > FULCRUM_PITCH ) { 146 | return calc_octave_down( orig_pitch ); 147 | } else { 148 | return calc_octave_up( orig_pitch ); 149 | } 150 | } 151 | } 152 | // up only 153 | if ( CALC_DIR == 1 ) { 154 | // half-step 155 | if ( CALC_METHOD == 0 ) { 156 | if ( orig_pitch < FULCRUM_PITCH ) { 157 | return calc_halfstep_up( orig_pitch ); 158 | } 159 | } 160 | // octave 161 | if ( CALC_METHOD == 1 ) { 162 | if ( orig_pitch < FULCRUM_PITCH ) { 163 | return calc_octave_up( orig_pitch ); 164 | } 165 | } 166 | } 167 | 168 | // down only 169 | if ( CALC_DIR == 2 ) { 170 | // half-step 171 | if ( CALC_METHOD == 0 ) { 172 | if ( orig_pitch > FULCRUM_PITCH ) { 173 | return calc_halfstep_down( orig_pitch ); 174 | } 175 | } 176 | // octave 177 | if ( CALC_METHOD == 1 ) { 178 | if ( orig_pitch > FULCRUM_PITCH ) { 179 | return calc_octave_down( orig_pitch ); 180 | } 181 | } 182 | } 183 | // we've not accounted for all of the possible combinations 184 | // so we don't change anything and post an error 185 | Trace("ERROR: calculate_reversal(): missing settings combination: " + JSON.stringify({CALC_DIR:CALC_DIR, CALC_METHOD:CALC_METHOD})); 186 | return orig_pitch; 187 | } 188 | 189 | function calc_halfstep_up( orig_pitch ) { 190 | let diff = FULCRUM_PITCH - orig_pitch; 191 | let new_pitch = orig_pitch + ( diff * 2 ); 192 | return new_pitch; 193 | } 194 | 195 | function calc_halfstep_down( orig_pitch ) { 196 | let diff = orig_pitch - FULCRUM_PITCH; 197 | let new_pitch = orig_pitch - ( diff * 2 ); 198 | return new_pitch; 199 | } 200 | 201 | function calc_octave_up( orig_pitch ) { 202 | // we need to calculate the diff by octave 203 | // get fulcrum octave 204 | let fulcrum_octave = get_octave_for_pitch( FULCRUM_PITCH ); 205 | // get orig octave 206 | let orig_octave = get_octave_for_pitch( orig_pitch ); 207 | // diff the octaves 208 | let octave_diff = ( fulcrum_octave - orig_octave ); 209 | // update the original pitch by octave diff * 12 210 | let new_pitch = orig_pitch + ( octave_diff * 12 ); 211 | return new_pitch; 212 | } 213 | 214 | function calc_octave_down( orig_pitch ) { 215 | // we need to calculate the diff by octave 216 | // get fulcrum octave 217 | let fulcrum_octave = get_octave_for_pitch( FULCRUM_PITCH ); 218 | // get orig octave 219 | let orig_octave = get_octave_for_pitch( orig_pitch ); 220 | // diff the octaves 221 | let octave_diff = ( orig_octave - fulcrum_octave ); 222 | // update the original pitch by octave diff * 12 223 | let new_pitch = orig_pitch - ( octave_diff * 12 ); 224 | return new_pitch; 225 | } 226 | 227 | function get_octave_for_pitch( pitch ) { 228 | // array for fast lookup 229 | const octaves = [1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,7,7,7,8,8,8,8,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,10,10,10,11,11,11,11,11,11,11,11]; 230 | return octaves[ pitch ]; 231 | /* 232 | if ( pitch >= 0 && pitch <= 11 ) { 233 | return 1; 234 | } else if ( pitch >= 12 && pitch <= 23 ) { 235 | return 2; 236 | } else if ( pitch >= 24 && pitch <= 35 ) { 237 | return 3; 238 | } else if ( pitch >= 36 && pitch <= 47 ) { 239 | return 4; 240 | } else if ( pitch >= 48 && pitch <= 59 ) { 241 | return 5; 242 | } else if ( pitch >= 60 && pitch <= 71 ) { 243 | return 6; 244 | } else if ( pitch >= 72 && pitch <= 83 ) { 245 | return 7; 246 | } else if ( pitch >= 84 && pitch <= 95 ) { 247 | return 8; 248 | } else if ( pitch >= 96 && pitch <= 107 ) { 249 | return 9; 250 | } else if ( pitch >= 108 && pitch <= 119 ) { 251 | return 10; 252 | } else if ( pitch >= 120 && pitch <= 127 ) { 253 | return 11; 254 | } 255 | */ 256 | 257 | } 258 | 259 | function ParameterChanged( param , value ) { 260 | switch ( param ) { 261 | case 0: 262 | FULCRUM_PITCH = value; 263 | break; 264 | case 1: 265 | CALC_METHOD = CALC_METHODS[value]; 266 | break; 267 | case 2: 268 | CALC_DIR = CALC_DIRS[value]; 269 | break; 270 | default: 271 | Trace("ERROR: ParameterChanged(" + param + ", " + value + ")"); 272 | } 273 | } 274 | 275 | PluginParameters.push({ 276 | name:"Fulcrum Pitch", 277 | type:"menu", 278 | valueStrings:PITCH_STRINGS, 279 | defaultValue:60 280 | }); 281 | 282 | PluginParameters.push({ 283 | name:"Reversal Method", 284 | type:"menu", 285 | valueStrings:CALC_METHODS, 286 | defaultValue:0 287 | }); 288 | 289 | PluginParameters.push({ 290 | name:"Direction", 291 | type:"menu", 292 | valueStrings:CALC_DIRS, 293 | defaultValue:0 294 | }); -------------------------------------------------------------------------------- /simple_scripter_tricks/transpose_to_range.js: -------------------------------------------------------------------------------- 1 | /****************************************************************************** 2 | Name: Transposition to Range 3 | Author(s): Philip Regan 4 | Purpose: 5 | * A simple convenience plug-in which transposes all notes based on a 6 | pair of fulcrum pitches. This can be accomplished with the Simple Transposition 7 | but that takes two plug-in slots while this only takes in one. 8 | * There is no bounds checking on whether the selected fulcrum pitches are 9 | inverted from each other. 10 | 11 | This script is released under the MIT License. 12 | 13 | Permissions 14 | * Commercial use 15 | * Modification 16 | * Distribution 17 | * Private use 18 | 19 | Limitations 20 | x Liability 21 | x Warranty 22 | 23 | Conditions 24 | ! License and copyright notice 25 | 26 | Copyright Philip Regan and Pilcrow Records 27 | 28 | Permission is hereby granted, free of charge, to any person obtaining 29 | a copy of this software and associated documentation files (the 30 | "Software"), to deal in the Software without restriction, including 31 | without limitation the rights to use, copy, modify, merge, publish, 32 | distribute, sublicense, and/or sell copies of the Software, and to 33 | permit persons to whom the Software is furnished to do so, subject to 34 | the following conditions: 35 | 36 | The above copyright notice and this permission notice shall be 37 | included in all copies or substantial portions of the Software. 38 | 39 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 40 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 41 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 42 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 43 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 44 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 45 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 46 | 47 | ****************************************************************************/ 48 | 49 | var PluginParameters = []; 50 | 51 | const CHROMATIC_SCALE_STRINGS = ["C", "C♯/D♭", "D", "D♯/E♭", "E", "F", "F♯/G♭", "G", "G♯/A♭", "A", "A♯/B♭", "B"]; 52 | const OCTAVE_STRINGS = ["-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8"]; 53 | var PITCH_STRINGS = [] 54 | // build the pitch strings 55 | var pitch_cursor = -1; 56 | var octave_cursor = -1; 57 | var current_octave = ""; 58 | for ( let pitch = 0; pitch < 128; pitch++ ) { 59 | 60 | pitch_cursor += 1; 61 | if ( pitch_cursor == CHROMATIC_SCALE_STRINGS.length ) { 62 | pitch_cursor = 0; 63 | } 64 | 65 | if ( pitch_cursor == 0 ) { 66 | octave_cursor += 1; 67 | if ( octave_cursor == OCTAVE_STRINGS.length ) { 68 | octave_cursor = 0; 69 | } 70 | } 71 | 72 | PITCH_STRINGS.push( CHROMATIC_SCALE_STRINGS[pitch_cursor] + " " + OCTAVE_STRINGS[octave_cursor] + " (" + pitch + ")" ); 73 | } 74 | 75 | var HIGH_FULCRUM_PITCH = 72; 76 | var LOW_FULCRUM_PITCH = 60; 77 | 78 | function HandleMIDI( event ) { 79 | if ( event instanceof NoteOn || event instanceof NoteOff ) { 80 | let new_pitch = calculate_transposition( event.pitch ); 81 | new_pitch = MIDI.normalizeData( new_pitch ); 82 | Trace(event.pitch + "\t" + new_pitch); 83 | event.pitch = new_pitch; 84 | } 85 | event.send(); 86 | } 87 | 88 | function calculate_transposition( orig_pitch ) { 89 | let cache_pitch = orig_pitch; 90 | 91 | if ( orig_pitch <= HIGH_FULCRUM_PITCH && orig_pitch >= LOW_FULCRUM_PITCH ) { 92 | return orig_pitch; 93 | } 94 | 95 | if ( orig_pitch > HIGH_FULCRUM_PITCH ) { 96 | while ( cache_pitch > HIGH_FULCRUM_PITCH && cache_pitch >= LOW_FULCRUM_PITCH ) { 97 | cache_pitch -= 12; 98 | } 99 | } 100 | 101 | if ( orig_pitch < LOW_FULCRUM_PITCH ) { 102 | while ( cache_pitch <= HIGH_FULCRUM_PITCH && cache_pitch < LOW_FULCRUM_PITCH ) { 103 | cache_pitch += 12; 104 | } 105 | } 106 | 107 | return cache_pitch; 108 | } 109 | 110 | function ParameterChanged( param , value ) { 111 | switch ( param ) { 112 | case 0: 113 | HIGH_FULCRUM_PITCH = value; 114 | break; 115 | case 1: 116 | // Direction 117 | LOW_FULCRUM_PITCH = value; 118 | break; 119 | default: 120 | Trace("ERROR: ParameterChanged(" + param + ", " + value + ")"); 121 | } 122 | } 123 | 124 | PluginParameters.push({ 125 | name:"High Fulcrum Pitch", 126 | type:"menu", 127 | valueStrings:PITCH_STRINGS, 128 | defaultValue:71 129 | }); 130 | 131 | PluginParameters.push({ 132 | name:"Low Fulcrum Pitch", 133 | type:"menu", 134 | valueStrings:PITCH_STRINGS, 135 | defaultValue:60 136 | }); -------------------------------------------------------------------------------- /simple_scripter_tricks/voice_limiter.js: -------------------------------------------------------------------------------- 1 | var PluginParameters = []; 2 | 3 | var VOICE_COUNT = 0; 4 | var VOICE_MAX = 0; 5 | 6 | function HandleMIDI( event ) { 7 | 8 | if ( event instanceof NoteOn ) { 9 | VOICE_COUNT += 1; 10 | if ( VOICE_COUNT <= VOICE_MAX && VOICE_MAX != 0 ) { 11 | event.send(); 12 | } 13 | } else if ( event instanceof NoteOff ) { 14 | VOICE_COUNT -= 1; 15 | event.send(); 16 | } else { 17 | event.send(); 18 | } 19 | 20 | } 21 | 22 | function ParameterChanged( param , value ) { 23 | switch ( param ) { 24 | case 0: 25 | VOICE_MAX = value; 26 | break; 27 | default: 28 | break; 29 | } 30 | } 31 | 32 | PluginParameters.push({ 33 | name:"Max Voices", 34 | type:"lin", 35 | // unit:"\%", 36 | minValue:0, 37 | maxValue:128, 38 | numberOfSteps:128, 39 | defaultValue:16} 40 | ); --------------------------------------------------------------------------------