├── 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 | 
10 | 
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 | 
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 | 
96 | 
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 | }
--------------------------------------------------------------------------------