├── demo.gif ├── ui.jpg ├── ui.png ├── assign.gif ├── seqs2.gif ├── seqs4.gif ├── MidiMorph.amxd ├── LICENSE ├── README.md ├── lap.js ├── clipSelect.maxpat └── midimorph.js /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mganss/MidiMorph/HEAD/demo.gif -------------------------------------------------------------------------------- /ui.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mganss/MidiMorph/HEAD/ui.jpg -------------------------------------------------------------------------------- /ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mganss/MidiMorph/HEAD/ui.png -------------------------------------------------------------------------------- /assign.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mganss/MidiMorph/HEAD/assign.gif -------------------------------------------------------------------------------- /seqs2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mganss/MidiMorph/HEAD/seqs2.gif -------------------------------------------------------------------------------- /seqs4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mganss/MidiMorph/HEAD/seqs4.gif -------------------------------------------------------------------------------- /MidiMorph.amxd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mganss/MidiMorph/HEAD/MidiMorph.amxd -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Michael Ganss 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MidiMorph 2 | 3 | MidiMorph is a Max for Live device that allows smooth interpolation between two MIDI clips. 4 | The output can be played directly from the device, saved to a new clip, or continuously updated to a destination clip. 5 | Source and destination clips are monitored for changes. 6 | 7 | Download under [releases](https://github.com/mganss/MidiMorph/releases) or at [maxforlive.com](http://www.maxforlive.com/library/device.php?id=5550) 8 | 9 | ![UI](https://raw.githubusercontent.com/mganss/MidiMorph/master/ui.png) 10 | ![Demo](https://raw.githubusercontent.com/mganss/MidiMorph/master/demo.gif) 11 | 12 | ## Usage 13 | 14 | 1. Drag the device into a MIDI track 15 | 2. Select the source clip 16 | 3. Click the From button 17 | 4. Select the destination clip 18 | 5. Click the To button 19 | 6. Adjust the Morph dial (0 is identical to source, 1 is destination, 0.5 is half-way) 20 | 21 | Output: 22 | 23 | - Play directly from the device (if the Play toggle is on) 24 | - Click the Clip button to save the current state selected by the Morph dial to a new clip 25 | - Create a new clip, select it, then click the Out button to select permanent output to the newly created clip 26 | (will be overwritten whenever a new Morph value is selected or parameters changed) 27 | 28 | ## Algorithm 29 | 30 | MidiMorph works by assigning pairs of notes from the source and destination clips, 31 | then interpolating between the two notes of each pair to generate the intermediate notes. 32 | The pairs are assigned so that the sum of note distances is minimal, 33 | where distance is defined as the euclidean distance in the pitch/time-plane (like in the piano roll). 34 | Finding the pairs in this way presents the classic [assignment problem](https://en.wikipedia.org/wiki/Assignment_problem) which is 35 | solved here using the Jonker-Volgenant algorithm implemented in https://github.com/Fil/lap-jv. 36 | 37 | Notes that remain unpaired (because the number of notes differ between the source and destination clips) can be 38 | handled in one of two ways: 39 | 40 | 1. They can be paired 41 | in additional assignment rounds, such that one note from the clip that has fewer notes then has multiple notes from 42 | the other clip assigned to it. 43 | 2. They can remain unpaired and get faded or muted. Technically, they get paired with pseudo notes that are silent versions of themselves. 44 | 45 | ## Quantization 46 | 47 | Selecting any of the values from the Quantize menu will quantize notes to the selected value. 48 | This applies to the endpoints as well, i.e. output at 0.0 and 1.0 is quantized, too. 49 | 50 | ## Unpaired Notes 51 | 52 | Using the Assign toggle you can select what happens to notes that remain unpaired after the first assignment 53 | pass as outlined above. 54 | If it's on, the remaining notes are assigned in additional assignment rounds using the same algorithm until all notes have been paired. 55 | 56 | If the Assign toggle is off, the remaining notes will not be assigned to notes from the other clip. Instead, they will 57 | get paired with pseudo-notes that are silent versions of themselves. This means they will stay in place and fade out or get muted 58 | (depending on the Mute/Fade selection described below). 59 | 60 | The image below shows the transition between the same two clips as the demo at the top but with Assign enabled. 61 | The single note at the top right (F3) is now paired and converges to the note at the bottom right (A2). 62 | 63 | ![Assign enabled](https://raw.githubusercontent.com/mganss/MidiMorph/master/assign.gif) 64 | 65 | ## Mute/Fade 66 | 67 | Notes that remained unpaired after the first round of assignment will either fade out or get muted. You can choose either behavior from 68 | the Mute/Fade menu. In the fade case, the velocity will transition to zero and once it reaches zero, the note 69 | will be removed. When mute is selected, the note will stay at its original velocity up to half way, then get removed. 70 | 71 | ## Skip Mute 72 | 73 | If the Skip Mute toggle is on, notes in either clip that are muted will be ignored. If it's off, muted notes will participate in the 74 | assignment and interpolation process. If they are paired with non-muted notes, intermediate notes will be unmuted at half-way. 75 | 76 | ## Overlap 77 | 78 | If intermediate notes overlap and the Overlap toggle is on, the overlapping notes are merged into one note which is identical 79 | to the one that starts earliest. 80 | 81 | ## Drums 82 | 83 | If the Drums toggle is on, all notes that have the same pitch will be handled independently from those that have a different pitch, i.e. notes of one pitch will only be morphed into notes that have the same pitch. 84 | 85 | ## Steps/Sequences 86 | 87 | The Steps dial selects the number of interpolation steps. The Sequences dial selects the number of "sequences" which can be thought of 88 | as sub-steps. Consider the case where you have 10 notes in both the source and destination clips and the number of steps is 100. 89 | If the number of sequences is 100 all notes will be moved 1/100 of the distance at each step. If the number of sequences is 10, 90 | only one note will be moved 1/10 of the distance at each step. The idea is to get more subtle changes at a single step in this way. 91 | 92 | The images below show the same transition, both have steps set to 4, the first one has sequences set to 2, the second one has sequences 93 | set to 4. 94 | 95 | ![2 Sequences](https://raw.githubusercontent.com/mganss/MidiMorph/master/seqs2.gif) 96 | ![4 Sequences](https://raw.githubusercontent.com/mganss/MidiMorph/master/seqs4.gif) 97 | 98 | ## Pitch Scale 99 | 100 | The cost function used to assign note pairs calculates a distance in the pitch/time plane. 101 | The Scale dial selects the number of semitones that are equal in distance to one beat. 102 | 103 | ## Technical Notes 104 | 105 | Whenever a parameter changes, all steps are precalculated. For immediate playback from the device, all values are saved to a `coll` and playback is triggered by a `metro` with resolution of one tick. 106 | -------------------------------------------------------------------------------- /lap.js: -------------------------------------------------------------------------------- 1 | /************************************************************************ 2 | * 3 | * lap.js -- ported to javascript from 4 | 5 | lap.cpp 6 | version 1.0 - 4 September 1996 7 | author: Roy Jonker @ MagicLogic Optimization Inc. 8 | e-mail: roy_jonker@magiclogic.com 9 | 10 | Code for Linear Assignment Problem, according to 11 | 12 | "A Shortest Augmenting Path Algorithm for Dense and Sparse Linear 13 | Assignment Problems," Computing 38, 325-340, 1987 14 | 15 | by 16 | 17 | R. Jonker and A. Volgenant, University of Amsterdam. 18 | 19 | * 20 | PORTED TO JAVASCRIPT 2017-01-02 by Philippe Riviere(fil@rezo.net) 21 | CHANGED 2016-05-13 by Yang Yong(yangyongeducation@163.com) in column reduction part according to 22 | matlab version of LAPJV algorithm(Copyright (c) 2010, Yi Cao All rights reserved)-- 23 | https://www.mathworks.com/matlabcentral/fileexchange/26836-lapjv-jonker-volgenant-algorithm-for-linear-assignment-problem-v3-0: 24 | * 25 | *************************************************************************/ 26 | 27 | /* This function is the jv shortest augmenting path algorithm to solve the assignment problem */ 28 | function lap(dim, cost) { 29 | // input: 30 | // dim - problem size 31 | // cost - cost callback (or matrix) 32 | 33 | // output: 34 | // rowsol - column assigned to row in solution 35 | // colsol - row assigned to column in solution 36 | // u - dual variables, row reduction numbers 37 | // v - dual variables, column reduction numbers 38 | 39 | // convert the cost matrix (old API) to a callback (new API) 40 | if (typeof cost === "object") { 41 | var cost_matrix = cost; 42 | cost = function (i, j) { 43 | return cost_matrix[i][j]; 44 | }; 45 | } 46 | 47 | var sum = 0; 48 | var is, js; 49 | for (is = 0; is < dim; is++) { 50 | for (js = 0; js < dim; js++) 51 | sum += cost(is, js); 52 | } 53 | var BIG = 10000 * (sum / dim); 54 | var epsilon = sum / dim / 10000; 55 | var rowsol = new Int32Array(dim), 56 | colsol = new Int32Array(dim), 57 | u = new Float64Array(dim), 58 | v = new Float64Array(dim); 59 | var unassignedfound; 60 | /* row */ 61 | var i, imin, numfree = 0, prvnumfree, f, i0, k, freerow; // *pred, *free 62 | /* col */ 63 | var j, j1, j2, endofpath, last, low, up; // *collist, *matches 64 | /* cost */ 65 | var min, h, umin, usubmin, v2; // *d 66 | 67 | var free = new Int32Array(dim); // list of unassigned rows. 68 | var collist = new Int32Array(dim); // list of columns to be scanned in various ways. 69 | var matches = new Int32Array(dim); // counts how many times a row could be assigned. 70 | var d = new Float64Array(dim); // 'cost-distance' in augmenting path calculation. 71 | var pred = new Int32Array(dim); // row-predecessor of column in augmenting/alternating path. 72 | 73 | // init how many times a row will be assigned in the column reduction. 74 | for (i = 0; i < dim; i++) 75 | matches[i] = 0; 76 | 77 | // COLUMN REDUCTION 78 | for ( 79 | j = dim; 80 | j--; // reverse order gives better results. 81 | 82 | ) { 83 | // find minimum cost over rows. 84 | min = cost(0, j); 85 | imin = 0; 86 | for (i = 1; i < dim; i++) 87 | if (cost(i, j) < min) { 88 | min = cost(i, j); 89 | imin = i; 90 | } 91 | v[j] = min; 92 | if (++matches[imin] === 1) { 93 | // init assignment if minimum row assigned for first time. 94 | rowsol[imin] = j; 95 | colsol[j] = imin; 96 | } else if (v[j] < v[rowsol[imin]]) { 97 | j1 = rowsol[imin]; 98 | rowsol[imin] = j; 99 | colsol[j] = imin; 100 | colsol[j1] = -1; 101 | } else colsol[j] = -1; // row already assigned, column not assigned. 102 | } 103 | 104 | // REDUCTION TRANSFER 105 | for (i = 0; i < dim; i++) { 106 | if ( 107 | matches[i] === 0 // fill list of unassigned 'free' rows. 108 | ) 109 | free[numfree++] = i; 110 | else if (matches[i] === 1) { 111 | // transfer reduction from rows that are assigned once. 112 | j1 = rowsol[i]; 113 | min = BIG; 114 | for (j = 0; j < dim; j++) 115 | if (j !== j1) 116 | if (cost(i, j) - v[j] < min + epsilon) min = cost(i, j) - v[j]; 117 | v[j1] = v[j1] - min; 118 | } 119 | } 120 | 121 | // AUGMENTING ROW REDUCTION 122 | var loopcnt = 0; // do-loop to be done twice. 123 | do { 124 | loopcnt++; 125 | 126 | // scan all free rows. 127 | // in some cases, a free row may be replaced with another one to be scanned next. 128 | k = 0; 129 | prvnumfree = numfree; 130 | numfree = 0; // start list of rows still free after augmenting row reduction. 131 | while (k < prvnumfree) { 132 | i = free[k]; 133 | k++; 134 | 135 | // find minimum and second minimum reduced cost over columns. 136 | umin = cost(i, 0) - v[0]; 137 | j1 = 0; 138 | usubmin = BIG; 139 | for (j = 1; j < dim; j++) { 140 | h = cost(i, j) - v[j]; 141 | if (h < usubmin) 142 | if (h >= umin) { 143 | usubmin = h; 144 | j2 = j; 145 | } else { 146 | usubmin = umin; 147 | umin = h; 148 | j2 = j1; 149 | j1 = j; 150 | } 151 | } 152 | 153 | i0 = colsol[j1]; 154 | if (umin < usubmin + epsilon) 155 | // change the reduction of the minimum column to increase the minimum 156 | // reduced cost in the row to the subminimum. 157 | v[j1] = v[j1] - (usubmin + epsilon - umin); 158 | else if (i0 > -1) { 159 | // minimum and subminimum equal. 160 | // minimum column j1 is assigned. 161 | // swap columns j1 and j2, as j2 may be unassigned. 162 | j1 = j2; 163 | i0 = colsol[j2]; 164 | } 165 | 166 | // (re-)assign i to j1, possibly de-assigning an i0. 167 | rowsol[i] = j1; 168 | colsol[j1] = i; 169 | 170 | if (i0 > -1) 171 | if (umin < usubmin) 172 | // minimum column j1 assigned earlier. 173 | // put in current k, and go back to that k. 174 | // continue augmenting path i - j1 with i0. 175 | free[--k] = i0; 176 | else 177 | // no further augmenting reduction possible. 178 | // store i0 in list of free rows for next phase. 179 | free[numfree++] = i0; 180 | } 181 | } while (loopcnt < 2); // repeat once. 182 | 183 | // AUGMENT SOLUTION for each free row. 184 | for (f = 0; f < numfree; f++) { 185 | freerow = free[f]; // start row of augmenting path. 186 | 187 | // Dijkstra shortest path algorithm. 188 | // runs until unassigned column added to shortest path tree. 189 | for (j = dim; j--;) { 190 | d[j] = cost(freerow, j) - v[j]; 191 | pred[j] = freerow; 192 | collist[j] = j; // init column list. 193 | } 194 | 195 | low = 0; // columns in 0..low-1 are ready, now none. 196 | up = 0; // columns in low..up-1 are to be scanned for current minimum, now none. 197 | // columns in up..dim-1 are to be considered later to find new minimum, 198 | // at this stage the list simply contains all columns 199 | unassignedfound = false; 200 | do { 201 | if (up === low) { 202 | // no more columns to be scanned for current minimum. 203 | last = low - 1; 204 | 205 | // scan columns for up..dim-1 to find all indices for which new minimum occurs. 206 | // store these indices between low..up-1 (increasing up). 207 | min = d[collist[up++]]; 208 | for (k = up; k < dim; k++) { 209 | j = collist[k]; 210 | h = d[j]; 211 | if (h <= min) { 212 | if (h < min) { 213 | // new minimum. 214 | up = low; // restart list at index low. 215 | min = h; 216 | } 217 | // new index with same minimum, put on undex up, and extend list. 218 | collist[k] = collist[up]; 219 | collist[up++] = j; 220 | } 221 | } 222 | // check if any of the minimum columns happens to be unassigned. 223 | // if so, we have an augmenting path right away. 224 | for (k = low; k < up; k++) 225 | if (colsol[collist[k]] < 0) { 226 | endofpath = collist[k]; 227 | unassignedfound = true; 228 | break; 229 | } 230 | } 231 | 232 | if (!unassignedfound) { 233 | // update 'distances' between freerow and all unscanned columns, via next scanned column. 234 | j1 = collist[low]; 235 | low++; 236 | i = colsol[j1]; 237 | h = cost(i, j1) - v[j1] - min; 238 | 239 | for (k = up; k < dim; k++) { 240 | j = collist[k]; 241 | v2 = cost(i, j) - v[j] - h; 242 | if (v2 < d[j]) { 243 | pred[j] = i; 244 | if (v2 === min) 245 | if (colsol[j] < 0) { 246 | // new column found at same minimum value 247 | // if unassigned, shortest augmenting path is complete. 248 | endofpath = j; 249 | unassignedfound = true; 250 | break; 251 | } else { 252 | // else add to list to be scanned right away. 253 | collist[k] = collist[up]; 254 | collist[up++] = j; 255 | } 256 | d[j] = v2; 257 | } 258 | } 259 | } 260 | } while (!unassignedfound); 261 | 262 | // update column prices. 263 | for (k = last + 1; k--;) { 264 | j1 = collist[k]; 265 | v[j1] = v[j1] + d[j1] - min; 266 | } 267 | 268 | // reset row and column assignments along the alternating path. 269 | do { 270 | i = pred[endofpath]; 271 | colsol[endofpath] = i; 272 | j1 = endofpath; 273 | endofpath = rowsol[i]; 274 | rowsol[i] = j1; 275 | } while (i !== freerow); 276 | } 277 | 278 | // calculate optimal cost. 279 | var lapcost = 0; 280 | for (i = dim; i--;) { 281 | j = rowsol[i]; 282 | u[i] = cost(i, j) - v[j]; 283 | lapcost = lapcost + cost(i, j); 284 | } 285 | 286 | return { 287 | cost: lapcost, 288 | row: rowsol, 289 | col: colsol, 290 | u: u, 291 | v: v 292 | }; 293 | } 294 | -------------------------------------------------------------------------------- /clipSelect.maxpat: -------------------------------------------------------------------------------- 1 | { 2 | "patcher" : { 3 | "fileversion" : 1, 4 | "appversion" : { 5 | "major" : 8, 6 | "minor" : 0, 7 | "revision" : 3, 8 | "architecture" : "x64", 9 | "modernui" : 1 10 | } 11 | , 12 | "classnamespace" : "box", 13 | "rect" : [ 34.0, 85.0, 1375.0, 1041.0 ], 14 | "bglocked" : 0, 15 | "openinpresentation" : 0, 16 | "default_fontsize" : 12.0, 17 | "default_fontface" : 0, 18 | "default_fontname" : "Arial", 19 | "gridonopen" : 1, 20 | "gridsize" : [ 15.0, 15.0 ], 21 | "gridsnaponopen" : 1, 22 | "objectsnaponopen" : 1, 23 | "statusbarvisible" : 2, 24 | "toolbarvisible" : 1, 25 | "lefttoolbarpinned" : 0, 26 | "toptoolbarpinned" : 0, 27 | "righttoolbarpinned" : 0, 28 | "bottomtoolbarpinned" : 0, 29 | "toolbars_unpinned_last_save" : 0, 30 | "tallnewobj" : 0, 31 | "boxanimatetime" : 200, 32 | "enablehscroll" : 1, 33 | "enablevscroll" : 1, 34 | "devicewidth" : 0.0, 35 | "description" : "", 36 | "digest" : "", 37 | "tags" : "", 38 | "style" : "", 39 | "subpatcher_template" : "", 40 | "boxes" : [ { 41 | "box" : { 42 | "id" : "obj-23", 43 | "maxclass" : "message", 44 | "numinlets" : 2, 45 | "numoutlets" : 1, 46 | "outlettype" : [ "" ], 47 | "patching_rect" : [ 219.0, 356.0, 35.0, 22.0 ], 48 | "text" : "id $1" 49 | } 50 | 51 | } 52 | , { 53 | "box" : { 54 | "comment" : "bang when notes change", 55 | "id" : "obj-9", 56 | "index" : 0, 57 | "maxclass" : "outlet", 58 | "numinlets" : 1, 59 | "numoutlets" : 0, 60 | "patching_rect" : [ 23.0, 517.0, 30.0, 30.0 ] 61 | } 62 | 63 | } 64 | , { 65 | "box" : { 66 | "id" : "obj-1", 67 | "maxclass" : "newobj", 68 | "numinlets" : 2, 69 | "numoutlets" : 2, 70 | "outlettype" : [ "", "" ], 71 | "patching_rect" : [ 23.0, 295.0, 109.0, 22.0 ], 72 | "saved_object_attributes" : { 73 | "_persistence" : 1 74 | } 75 | , 76 | "text" : "live.observer notes" 77 | } 78 | 79 | } 80 | , { 81 | "box" : { 82 | "id" : "obj-14", 83 | "maxclass" : "message", 84 | "numinlets" : 2, 85 | "numoutlets" : 1, 86 | "outlettype" : [ "" ], 87 | "patching_rect" : [ 14.0, 110.0, 236.0, 22.0 ], 88 | "text" : "goto live_set view highlighted_clip_slot clip" 89 | } 90 | 91 | } 92 | , { 93 | "box" : { 94 | "comment" : "selected clip name", 95 | "id" : "obj-4", 96 | "index" : 0, 97 | "maxclass" : "outlet", 98 | "numinlets" : 1, 99 | "numoutlets" : 0, 100 | "patching_rect" : [ 380.0, 517.0, 30.0, 30.0 ] 101 | } 102 | 103 | } 104 | , { 105 | "box" : { 106 | "comment" : "id of selected clip", 107 | "id" : "obj-3", 108 | "index" : 0, 109 | "maxclass" : "outlet", 110 | "numinlets" : 1, 111 | "numoutlets" : 0, 112 | "patching_rect" : [ 453.0, 517.0, 30.0, 30.0 ] 113 | } 114 | 115 | } 116 | , { 117 | "box" : { 118 | "comment" : "", 119 | "id" : "obj-2", 120 | "index" : 0, 121 | "maxclass" : "inlet", 122 | "numinlets" : 0, 123 | "numoutlets" : 1, 124 | "outlettype" : [ "" ], 125 | "patching_rect" : [ 225.0, 11.0, 30.0, 30.0 ] 126 | } 127 | 128 | } 129 | , { 130 | "box" : { 131 | "id" : "obj-29", 132 | "maxclass" : "newobj", 133 | "numinlets" : 2, 134 | "numoutlets" : 2, 135 | "outlettype" : [ "bang", "" ], 136 | "patching_rect" : [ 153.0, 324.0, 51.0, 22.0 ], 137 | "text" : "select 0" 138 | } 139 | 140 | } 141 | , { 142 | "box" : { 143 | "id" : "obj-28", 144 | "maxclass" : "message", 145 | "numinlets" : 2, 146 | "numoutlets" : 1, 147 | "outlettype" : [ "" ], 148 | "patching_rect" : [ 153.0, 295.0, 29.5, 22.0 ], 149 | "text" : "$2" 150 | } 151 | 152 | } 153 | , { 154 | "box" : { 155 | "id" : "obj-21", 156 | "maxclass" : "newobj", 157 | "numinlets" : 1, 158 | "numoutlets" : 1, 159 | "outlettype" : [ "" ], 160 | "patching_rect" : [ 219.0, 188.0, 21.0, 22.0 ], 161 | "text" : "t s" 162 | } 163 | 164 | } 165 | , { 166 | "box" : { 167 | "id" : "obj-13", 168 | "maxclass" : "message", 169 | "numinlets" : 2, 170 | "numoutlets" : 1, 171 | "outlettype" : [ "" ], 172 | "patching_rect" : [ 417.0, 148.0, 29.5, 22.0 ], 173 | "text" : "id 0" 174 | } 175 | 176 | } 177 | , { 178 | "box" : { 179 | "id" : "obj-11", 180 | "maxclass" : "newobj", 181 | "numinlets" : 2, 182 | "numoutlets" : 2, 183 | "outlettype" : [ "bang", "" ], 184 | "patching_rect" : [ 268.0, 73.0, 34.0, 22.0 ], 185 | "text" : "sel 1" 186 | } 187 | 188 | } 189 | , { 190 | "box" : { 191 | "id" : "obj-10", 192 | "maxclass" : "newobj", 193 | "numinlets" : 2, 194 | "numoutlets" : 2, 195 | "outlettype" : [ "", "" ], 196 | "patching_rect" : [ 453.0, 352.0, 90.0, 22.0 ], 197 | "saved_object_attributes" : { 198 | "_persistence" : 1 199 | } 200 | , 201 | "text" : "live.observer id" 202 | } 203 | 204 | } 205 | , { 206 | "box" : { 207 | "id" : "obj-7", 208 | "maxclass" : "newobj", 209 | "numinlets" : 2, 210 | "numoutlets" : 2, 211 | "outlettype" : [ "", "" ], 212 | "patching_rect" : [ 260.5, 352.0, 111.0, 22.0 ], 213 | "saved_object_attributes" : { 214 | "_persistence" : 1 215 | } 216 | , 217 | "text" : "live.observer name" 218 | } 219 | 220 | } 221 | , { 222 | "box" : { 223 | "id" : "obj-32", 224 | "maxclass" : "newobj", 225 | "numinlets" : 1, 226 | "numoutlets" : 5, 227 | "outlettype" : [ "", "", "", "", "" ], 228 | "patching_rect" : [ 153.0, 424.0, 134.0, 22.0 ], 229 | "text" : "regexp \"^name (.*)\" %1" 230 | } 231 | 232 | } 233 | , { 234 | "box" : { 235 | "id" : "obj-31", 236 | "maxclass" : "newobj", 237 | "numinlets" : 1, 238 | "numoutlets" : 1, 239 | "outlettype" : [ "" ], 240 | "patching_rect" : [ 282.5, 466.0, 89.0, 22.0 ], 241 | "text" : "prepend texton" 242 | } 243 | 244 | } 245 | , { 246 | "box" : { 247 | "id" : "obj-30", 248 | "maxclass" : "message", 249 | "numinlets" : 2, 250 | "numoutlets" : 1, 251 | "outlettype" : [ "" ], 252 | "patching_rect" : [ 376.5, 424.0, 29.5, 22.0 ], 253 | "text" : "0" 254 | } 255 | 256 | } 257 | , { 258 | "box" : { 259 | "id" : "obj-24", 260 | "maxclass" : "newobj", 261 | "numinlets" : 1, 262 | "numoutlets" : 1, 263 | "outlettype" : [ "" ], 264 | "patching_rect" : [ 376.5, 352.0, 65.0, 22.0 ], 265 | "text" : "match id 0" 266 | } 267 | 268 | } 269 | , { 270 | "box" : { 271 | "id" : "obj-8", 272 | "maxclass" : "message", 273 | "numinlets" : 2, 274 | "numoutlets" : 1, 275 | "outlettype" : [ "" ], 276 | "patching_rect" : [ 153.0, 356.0, 60.0, 22.0 ], 277 | "text" : "get name" 278 | } 279 | 280 | } 281 | , { 282 | "box" : { 283 | "id" : "obj-6", 284 | "maxclass" : "newobj", 285 | "numinlets" : 2, 286 | "numoutlets" : 1, 287 | "outlettype" : [ "" ], 288 | "patching_rect" : [ 153.0, 390.0, 63.0, 22.0 ], 289 | "saved_object_attributes" : { 290 | "_persistence" : 1 291 | } 292 | , 293 | "text" : "live.object" 294 | } 295 | 296 | } 297 | , { 298 | "box" : { 299 | "id" : "obj-5", 300 | "maxclass" : "newobj", 301 | "numinlets" : 1, 302 | "numoutlets" : 3, 303 | "outlettype" : [ "", "", "" ], 304 | "patching_rect" : [ 139.0, 148.0, 53.0, 22.0 ], 305 | "text" : "live.path" 306 | } 307 | 308 | } 309 | ], 310 | "lines" : [ { 311 | "patchline" : { 312 | "destination" : [ "obj-9", 0 ], 313 | "source" : [ "obj-1", 0 ] 314 | } 315 | 316 | } 317 | , { 318 | "patchline" : { 319 | "destination" : [ "obj-24", 0 ], 320 | "order" : 1, 321 | "source" : [ "obj-10", 0 ] 322 | } 323 | 324 | } 325 | , { 326 | "patchline" : { 327 | "destination" : [ "obj-3", 0 ], 328 | "order" : 0, 329 | "source" : [ "obj-10", 0 ] 330 | } 331 | 332 | } 333 | , { 334 | "patchline" : { 335 | "destination" : [ "obj-13", 0 ], 336 | "source" : [ "obj-11", 1 ] 337 | } 338 | 339 | } 340 | , { 341 | "patchline" : { 342 | "destination" : [ "obj-14", 0 ], 343 | "source" : [ "obj-11", 0 ] 344 | } 345 | 346 | } 347 | , { 348 | "patchline" : { 349 | "destination" : [ "obj-21", 0 ], 350 | "source" : [ "obj-13", 0 ] 351 | } 352 | 353 | } 354 | , { 355 | "patchline" : { 356 | "destination" : [ "obj-5", 0 ], 357 | "source" : [ "obj-14", 0 ] 358 | } 359 | 360 | } 361 | , { 362 | "patchline" : { 363 | "destination" : [ "obj-11", 0 ], 364 | "source" : [ "obj-2", 0 ] 365 | } 366 | 367 | } 368 | , { 369 | "patchline" : { 370 | "destination" : [ "obj-1", 1 ], 371 | "order" : 4, 372 | "source" : [ "obj-21", 0 ] 373 | } 374 | 375 | } 376 | , { 377 | "patchline" : { 378 | "destination" : [ "obj-10", 1 ], 379 | "order" : 0, 380 | "source" : [ "obj-21", 0 ] 381 | } 382 | 383 | } 384 | , { 385 | "patchline" : { 386 | "destination" : [ "obj-24", 0 ], 387 | "order" : 1, 388 | "source" : [ "obj-21", 0 ] 389 | } 390 | 391 | } 392 | , { 393 | "patchline" : { 394 | "destination" : [ "obj-28", 0 ], 395 | "order" : 3, 396 | "source" : [ "obj-21", 0 ] 397 | } 398 | 399 | } 400 | , { 401 | "patchline" : { 402 | "destination" : [ "obj-7", 1 ], 403 | "order" : 2, 404 | "source" : [ "obj-21", 0 ] 405 | } 406 | 407 | } 408 | , { 409 | "patchline" : { 410 | "destination" : [ "obj-6", 1 ], 411 | "source" : [ "obj-23", 0 ] 412 | } 413 | 414 | } 415 | , { 416 | "patchline" : { 417 | "destination" : [ "obj-30", 0 ], 418 | "source" : [ "obj-24", 0 ] 419 | } 420 | 421 | } 422 | , { 423 | "patchline" : { 424 | "destination" : [ "obj-29", 0 ], 425 | "source" : [ "obj-28", 0 ] 426 | } 427 | 428 | } 429 | , { 430 | "patchline" : { 431 | "destination" : [ "obj-23", 0 ], 432 | "order" : 0, 433 | "source" : [ "obj-29", 1 ] 434 | } 435 | 436 | } 437 | , { 438 | "patchline" : { 439 | "destination" : [ "obj-8", 0 ], 440 | "order" : 1, 441 | "source" : [ "obj-29", 1 ] 442 | } 443 | 444 | } 445 | , { 446 | "patchline" : { 447 | "destination" : [ "obj-4", 0 ], 448 | "source" : [ "obj-30", 0 ] 449 | } 450 | 451 | } 452 | , { 453 | "patchline" : { 454 | "destination" : [ "obj-4", 0 ], 455 | "source" : [ "obj-31", 0 ] 456 | } 457 | 458 | } 459 | , { 460 | "patchline" : { 461 | "destination" : [ "obj-31", 0 ], 462 | "source" : [ "obj-32", 0 ] 463 | } 464 | 465 | } 466 | , { 467 | "patchline" : { 468 | "destination" : [ "obj-21", 0 ], 469 | "source" : [ "obj-5", 0 ] 470 | } 471 | 472 | } 473 | , { 474 | "patchline" : { 475 | "destination" : [ "obj-32", 0 ], 476 | "source" : [ "obj-6", 0 ] 477 | } 478 | 479 | } 480 | , { 481 | "patchline" : { 482 | "destination" : [ "obj-31", 0 ], 483 | "source" : [ "obj-7", 0 ] 484 | } 485 | 486 | } 487 | , { 488 | "patchline" : { 489 | "destination" : [ "obj-6", 0 ], 490 | "source" : [ "obj-8", 0 ] 491 | } 492 | 493 | } 494 | ], 495 | "dependency_cache" : [ ], 496 | "autosave" : 0 497 | } 498 | 499 | } 500 | -------------------------------------------------------------------------------- /midimorph.js: -------------------------------------------------------------------------------- 1 | autowatch = 1; 2 | outlets = 3; 3 | 4 | setoutletassist(0, "note output"); 5 | setoutletassist(1, "output clip length in ticks"); 6 | setoutletassist(2, "bang after generation caused by midi change"); 7 | 8 | include("lap.js"); 9 | 10 | var clips = { 11 | from: null, 12 | to: null, 13 | out: null 14 | }; 15 | var ids = { 16 | from: 0, 17 | to: 0, 18 | out: 0 19 | }; 20 | var init = false; 21 | 22 | function liveInit() { 23 | init = true; 24 | if (ids.from !== 0) { 25 | setClip("from", ids.from); 26 | } 27 | if (ids.to !== 0) { 28 | setClip("to", ids.to); 29 | } 30 | if (ids.out !== 0) { 31 | setOut(ids.out); 32 | } 33 | } 34 | 35 | var ticksPerBeat = 480; 36 | 37 | function setTicksPerBeat(ticks) { 38 | ticksPerBeat = ticks; 39 | } 40 | 41 | var quantizeTicks = 1; 42 | 43 | function setQuantize(t) { 44 | if (t === "1/4") { 45 | quantizeTicks = 480; 46 | } 47 | else if (t === "1/8") { 48 | quantizeTicks = 240; 49 | } 50 | else if (t === "1/16") { 51 | quantizeTicks = 120; 52 | } 53 | else if (t === "1/32") { 54 | quantizeTicks = 60; 55 | } 56 | else if (t === "1/64") { 57 | quantizeTicks = 30; 58 | } 59 | else { 60 | quantizeTicks = 1; 61 | } 62 | 63 | generateOut(); 64 | } 65 | 66 | var mergeOverlap = true; 67 | 68 | function setOverlap(v) { 69 | mergeOverlap = v === 1; 70 | generateOut(); 71 | } 72 | 73 | var assignUnpaired = true; 74 | 75 | function setUnpaired(v) { 76 | assignUnpaired = v === 1; 77 | generateOut(); 78 | } 79 | 80 | var muteFade = "Mute"; 81 | 82 | function setMuteFade(v) { 83 | muteFade = v; 84 | generateOut(); 85 | } 86 | 87 | var skipMuted = true; 88 | 89 | function setSkipMuted(v) { 90 | skipMuted = v === 1; 91 | generateOut(); 92 | } 93 | 94 | function setClip(name, id) { 95 | if (!init) { 96 | ids[name] = id; 97 | return; 98 | } 99 | if (id === 0) { 100 | clips[name] = null; 101 | return; 102 | } 103 | var clipId = "id " + id; 104 | clips[name] = new LiveAPI(clipId); 105 | } 106 | 107 | function setFrom(id) { 108 | setClip("from", id); 109 | } 110 | 111 | function setTo(id) { 112 | setClip("to", id); 113 | } 114 | 115 | function setOut(id) { 116 | setClip("out", id); 117 | clipOut(); 118 | } 119 | 120 | function midiChange() { 121 | generate(); 122 | outlet(2, "bang"); 123 | } 124 | 125 | function Note(pitch, start, duration, velocity, muted, pseudo) { 126 | this.Pitch = pitch; 127 | this.Start = start; 128 | this.Duration = duration; 129 | this.Velocity = velocity; 130 | this.Muted = muted; 131 | this.Pseudo = pseudo || false; 132 | } 133 | 134 | var sequencesNumber = 10.0; 135 | 136 | function setSequence(v) { 137 | sequencesNumber = v; 138 | generateOut(); 139 | } 140 | 141 | var steps = 10; 142 | 143 | function setSteps(v) { 144 | steps = v; 145 | generateOut(); 146 | } 147 | 148 | function interpolate(from, to, f) { 149 | return from + (to - from) * f; 150 | } 151 | 152 | function quantize(beats) { 153 | if (quantizeTicks === 1) return beats; 154 | var ticks = ticksPerBeat * beats; 155 | var quantizedTicks = Math.round(ticks / quantizeTicks) * quantizeTicks; 156 | var quantizedBeats = quantizedTicks / ticksPerBeat; 157 | return quantizedBeats; 158 | } 159 | 160 | function interpolateNotes(noteFrom, noteTo, f) { 161 | var note = new Note( 162 | Math.round(interpolate(noteFrom.Pitch, noteTo.Pitch, f)), 163 | quantize(interpolate(noteFrom.Start, noteTo.Start, f)), 164 | interpolate(noteFrom.Duration, noteTo.Duration, f), 165 | Math.round(interpolate(noteFrom.Velocity, noteTo.Velocity, f)), 166 | Math.round(interpolate(noteFrom.Muted, noteTo.Muted, f)), 167 | (noteFrom.Pseudo && f < 1.0) || (noteTo.Pseudo && f > 0.0) 168 | ); 169 | return note; 170 | } 171 | 172 | var notes = []; 173 | var clipLength; 174 | 175 | function generateOut() { 176 | generate(); 177 | clipOut(); 178 | } 179 | 180 | function generate() { 181 | var fromClip = clips.from; 182 | var toClip = clips.to; 183 | 184 | notes = []; 185 | 186 | if (fromClip === null || toClip === null) { 187 | return; 188 | } 189 | 190 | var fromNotes = getMidiFromClip(fromClip); 191 | var toNotes = getMidiFromClip(toClip); 192 | var pairs = assignPairs(fromNotes, toNotes); 193 | var sequenceSize = steps / sequencesNumber; 194 | 195 | clipLength = Math.max(fromClip.get("length"), toClip.get("length")); 196 | 197 | outlet(1, clipLength * ticksPerBeat); 198 | outlet(0, "clear"); 199 | 200 | for (var i = 0; i <= steps; i++) { 201 | var step = (i % sequenceSize) / sequenceSize * pairs.length; 202 | var loStep = Math.floor(i / sequenceSize) * sequenceSize; 203 | var hiStep = loStep + sequenceSize; 204 | var stepNotes = []; 205 | var j, k; 206 | var note, note2; 207 | 208 | for (j = 0; j < pairs.length; j++) { 209 | var f = (j >= step ? loStep : hiStep) / steps; 210 | var noteFrom = pairs[j][0]; 211 | var noteTo = pairs[j][1]; 212 | note = interpolateNotes(noteFrom, noteTo, f); 213 | 214 | stepNotes.push(note); 215 | } 216 | 217 | if (mergeOverlap) { 218 | stepNotes.sort(function (a, b) { 219 | if (a.Muted < b.Muted) return -1; 220 | if (a.Muted > b.Muted) return 1; 221 | if (a.Start < b.Start) return -1; 222 | if (a.Start > b.Start) return 1; 223 | if (a.Duration > b.Duration) return -1; 224 | if (a.Duration < b.Duration) return 1; 225 | if (a.Velocity > b.Velocity) return -1; 226 | if (a.Velocity < b.Velocity) return 1; 227 | return 0; 228 | }); 229 | 230 | var mergedStepNotes = []; 231 | for (j = 0; j < stepNotes.length; j++) { 232 | note = stepNotes[j]; 233 | for (k = 0; k < mergedStepNotes.length; k++) { 234 | note2 = mergedStepNotes[k]; 235 | if (overlap(note, note2)) { 236 | break; 237 | } 238 | } 239 | if (k === mergedStepNotes.length) mergedStepNotes.push(note); 240 | } 241 | 242 | stepNotes = mergedStepNotes; 243 | } 244 | 245 | notes.push(stepNotes); 246 | 247 | var outLists = []; 248 | 249 | for (j = 0; j < stepNotes.length; j++) { 250 | var stepNote = stepNotes[j]; 251 | if (stepNote.Pseudo && (stepNote.Muted === 1 || stepNote.Velocity === 0)) continue; 252 | var ticks = Math.round(stepNote.Start * ticksPerBeat); 253 | var ix = (steps + 1) * ticks + i; 254 | var durationTicks = Math.round(stepNote.Duration * ticksPerBeat); 255 | var outNotes = [stepNote.Pitch, stepNote.Muted === 1 ? 0 : stepNote.Velocity, durationTicks]; 256 | var outList = outLists[ix]; 257 | if (outList === undefined) { 258 | outLists[ix] = outNotes; 259 | } else { 260 | outLists[ix] = outList.concat(outNotes); 261 | } 262 | } 263 | 264 | for (var oix in outLists) { 265 | outlet(0, "list", parseInt(oix), outLists[oix]); 266 | } 267 | } 268 | } 269 | 270 | function overlap(note1, note2) { 271 | if (note1.Pitch !== note2.Pitch) return false; 272 | var end1 = note1.Start + note1.Duration; 273 | var end2 = note2.Start + note2.Duration; 274 | return (note1.Start < end2 && note2.Start < end1); 275 | } 276 | 277 | var drumsMode = false; 278 | 279 | function setDrumsMode(v) { 280 | drumsMode = v === 1; 281 | generateOut(); 282 | } 283 | 284 | function groupByPitch(res, note) { 285 | var pitch = note.Pitch; 286 | res[pitch] = res[pitch] || []; 287 | res[pitch].push(note); 288 | return res; 289 | } 290 | 291 | function assignPairs(fromNotes, toNotes) { 292 | if (!drumsMode) { 293 | return findPairs(fromNotes, toNotes); 294 | } else { 295 | var fromNotesByPitch = fromNotes.reduce(groupByPitch, []); 296 | var toNotesByPitch = toNotes.reduce(groupByPitch, []); 297 | var fromPitches = Object.keys(fromNotesByPitch); 298 | var toPitches = Object.keys(toNotesByPitch); 299 | var pitches = fromPitches.concat(toPitches).reduce(function (r, p) { 300 | r[p] = p; 301 | return r; 302 | }, []); 303 | var pitch; 304 | var pairs = []; 305 | for (pitch in pitches) { 306 | var fromPitchNotes = fromNotesByPitch[pitch] || []; 307 | var toPitchNotes = toNotesByPitch[pitch] || []; 308 | var pitchPairs = findPairs(fromPitchNotes, toPitchNotes); 309 | pairs = pairs.concat(pitchPairs); 310 | } 311 | return pairs; 312 | } 313 | } 314 | 315 | function findPairs(fromNotes, toNotes) { 316 | var i, j; 317 | var pairs = []; 318 | var totalDim = Math.max(fromNotes.length, toNotes.length); 319 | var fromIndexes = [], toIndexes = []; 320 | var cost = function (il, jl) { 321 | var ilx = fromIndexes[il]; 322 | var jlx = toIndexes[jl]; 323 | if (ilx < fromNotes.length && jlx < toNotes.length) { 324 | var fromNote = fromNotes[ilx]; 325 | var toNote = toNotes[jlx]; 326 | return notesDistance(fromNote, toNote); 327 | } 328 | return 0; 329 | }; 330 | var round = 0; 331 | var pairNote; 332 | var note; 333 | 334 | if (toNotes.length === 0) { 335 | for (i = 0; i < fromNotes.length; i++) { 336 | note = fromNotes[i]; 337 | pairNote = createPairNote(note); 338 | pairs.push([note, pairNote]); 339 | } 340 | return pairs; 341 | } 342 | if (fromNotes.length === 0) { 343 | for (i = 0; i < toNotes.length; i++) { 344 | note = toNotes[i]; 345 | pairNote = createPairNote(note); 346 | pairs.push([pairNote, note]); 347 | } 348 | return pairs; 349 | } 350 | 351 | do { 352 | if (fromIndexes.length === 0) { 353 | for (i = 0; i < fromNotes.length; i++) 354 | fromIndexes[i] = i; 355 | } 356 | if (toIndexes.length === 0) { 357 | for (j = 0; j < toNotes.length; j++) 358 | toIndexes[j] = j; 359 | } 360 | var dim = Math.max(fromIndexes.length, toIndexes.length); 361 | var r = lap(dim, cost); 362 | var ix, jx; 363 | var nextFromIndexes = []; 364 | var nextToIndexes = []; 365 | for (i = 0; i < dim; i++) { 366 | j = r.row[i]; 367 | if (i < fromIndexes.length) { 368 | ix = fromIndexes[i]; 369 | if (j < toIndexes.length) { 370 | jx = toIndexes[j]; 371 | var fromNote = fromNotes[ix]; 372 | var toNote = toNotes[jx]; 373 | if (round > 0) { 374 | if (fromNotes.length > toNotes.length) { 375 | toNote = createPairNote(toNote); 376 | } else { 377 | fromNote = createPairNote(fromNote); 378 | } 379 | } 380 | pairs.push([fromNote, toNote]); 381 | } else { 382 | nextFromIndexes.push(ix); 383 | } 384 | } else { 385 | jx = toIndexes[j]; 386 | nextToIndexes.push(jx); 387 | } 388 | } 389 | fromIndexes = nextFromIndexes; 390 | toIndexes = nextToIndexes; 391 | round++; 392 | } 393 | while (assignUnpaired && pairs.length < totalDim); 394 | 395 | if (!assignUnpaired) { 396 | for (i = 0; i < fromIndexes.length; i++) { 397 | note = fromNotes[fromIndexes[i]]; 398 | pairNote = createPairNote(note); 399 | pairs.push([note, pairNote]); 400 | } 401 | for (i = 0; i < toIndexes.length; i++) { 402 | note = toNotes[toIndexes[i]]; 403 | pairNote = createPairNote(note); 404 | pairs.push([pairNote, note]); 405 | } 406 | } 407 | 408 | return pairs; 409 | } 410 | 411 | function createPairNote(note) { 412 | if (muteFade === "Mute") { 413 | return new Note(note.Pitch, note.Start, note.Duration, note.Velocity, 1, true); 414 | } else { 415 | return new Note(note.Pitch, note.Start, note.Duration, 0, note.Muted, true); 416 | } 417 | } 418 | 419 | var pitchScale = 12.0; 420 | 421 | function setPitchScale(v) { 422 | pitchScale = v; 423 | generateOut(); 424 | } 425 | 426 | function notesDistance(noteA, noteB) { 427 | var startDist = noteA.Start - noteB.Start; 428 | var pitchDist = noteA.Pitch / pitchScale - noteB.Pitch / pitchScale; 429 | var dist = Math.sqrt(startDist * startDist + pitchDist * pitchDist); 430 | return dist; 431 | } 432 | 433 | function getMidiFromClip(clip) { 434 | var len = clip.get("length"); 435 | var data = clip.call("get_notes", 0, 0, len, 128); 436 | var notes = []; 437 | 438 | for (var i = 2; i < (data.length - 1); i += 6) { 439 | var pitch = parseInt(data[i + 1]); 440 | var start = parseFloat(data[i + 2]); 441 | var duration = parseFloat(data[i + 3]); 442 | var velocity = parseInt(data[i + 4]); 443 | var muted = parseInt(data[i + 5]); 444 | if (muted === 1 && skipMuted) continue; 445 | var note = new Note(pitch, start, duration, velocity, muted); 446 | notes.push(note); 447 | } 448 | 449 | return notes; 450 | } 451 | 452 | var morphValue = 0.5; 453 | 454 | function setMorphValue(v) { 455 | morphValue = v; 456 | clipOut(); 457 | } 458 | 459 | function clip() { 460 | var step = Math.round(morphValue * steps); 461 | var stepNotes = notes[step]; 462 | createClip(stepNotes); 463 | } 464 | 465 | function createClip(notes) { 466 | var track = new LiveAPI("this_device canonical_parent"); 467 | var clipSlots = track.getcount("clip_slots"); 468 | var clipSlot; 469 | 470 | for (var clipSlotNum = 0; clipSlotNum < clipSlots; clipSlotNum++) { 471 | clipSlot = new LiveAPI("this_device canonical_parent clip_slots " + clipSlotNum); 472 | var hasClip = clipSlot.get("has_clip").toString() !== "0"; 473 | if (!hasClip) break; 474 | } 475 | 476 | if (clipSlotNum === clipSlots) { 477 | // have to create new clip slot (scene) 478 | var set = new LiveAPI("live_set"); 479 | set.call("create_scene", -1); 480 | clipSlot = new LiveAPI("this_device canonical_parent clip_slots " + clipSlotNum); 481 | } 482 | 483 | var fromClip = clips.from; 484 | var toClip = clips.to; 485 | var len = Math.max(fromClip.get("length"), toClip.get("length")); 486 | 487 | clipSlot.call("create_clip", len); 488 | var clip = new LiveAPI("this_device canonical_parent clip_slots " + clipSlotNum + " clip"); 489 | 490 | setNotes(clip, notes); 491 | } 492 | 493 | function setNotes(clip, notes) { 494 | var filteredNotes = filterPseudoNotes(notes); 495 | 496 | clip.call("set_notes"); 497 | clip.call("notes", filteredNotes.length); 498 | 499 | for (var i = 0; i < filteredNotes.length; i++) { 500 | var note = filteredNotes[i]; 501 | callNote(clip, note); 502 | } 503 | 504 | clip.call("done"); 505 | } 506 | 507 | function clipOut() { 508 | if (clips.out !== null) { 509 | var outClip = clips.out; 510 | var step = Math.round(morphValue * steps); 511 | var stepNotes = notes[step]; 512 | if (stepNotes === undefined) stepNotes = []; 513 | replaceAllNotes(outClip, stepNotes); 514 | } 515 | } 516 | 517 | function filterPseudoNotes(notes) { 518 | var filteredNotes = []; 519 | for (var i = 0; i < notes.length; i++) { 520 | var note = notes[i]; 521 | if (note.Pseudo && (note.Muted === 1 || note.Velocity === 0)) continue; 522 | filteredNotes.push(note); 523 | } 524 | return filteredNotes; 525 | } 526 | 527 | function callNote(clip, note) { 528 | clip.call("note", note.Pitch, note.Start.toFixed(4), note.Duration.toFixed(4), note.Velocity, note.Muted); 529 | } 530 | 531 | function replaceAllNotes(clip, notes) { 532 | var filteredNotes = filterPseudoNotes(notes); 533 | 534 | clip.call("select_all_notes"); 535 | clip.call("replace_selected_notes"); 536 | clip.call("notes", filteredNotes.length); 537 | 538 | for (var i = 0; i < filteredNotes.length; i++) { 539 | var note = filteredNotes[i]; 540 | callNote(clip, note); 541 | } 542 | 543 | clip.call("done"); 544 | } --------------------------------------------------------------------------------