├── LICENSE ├── README.md └── negativeharmonygenerator.qml /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 RP335 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 | # NegativeHarmonyGenerator 2 | This is a Musescore plugin that generates the negative harmony equivalent of the score selected/ full score. 3 | Gotta make being Jacob Collier easier :) 4 | -------------------------------------------------------------------------------- /negativeharmonygenerator.qml: -------------------------------------------------------------------------------- 1 | 2 | import QtQuick 2.2 3 | import QtQuick.Controls 1.1 4 | import QtQuick.Controls.Styles 1.3 5 | import QtQuick.Layouts 1.1 6 | import QtQuick.Dialogs 1.1 7 | import QtQuick.Window 2.2 8 | 9 | import MuseScore 3.0 10 | 11 | MuseScore 12 | 13 | { 14 | 15 | menuPath: "Plugins.NegativeHarmonyGenerator" 16 | description: "Generates the negative harmony of the selected/ whole score" 17 | version: "1.2" 18 | pluginType: "dialog" 19 | width: 400 20 | height: 400 21 | id: 'pluginId' 22 | property var fifths : ["C", "G", "D", "A","E","B", "F#", "C#", "G#", "D#", "A#", "F"]; 23 | property var split1 : []; 24 | property var split2 : []; 25 | property var allnotes : []; 26 | property var allnumbers : []; 27 | property var notesbasic : ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] 28 | property var mainarraynotes : []; 29 | property var mainarraynumbers : []; 30 | property var selectedscale : ""; 31 | property var tpcc : [14,9,16,11,18,13,8,15,10,17,12,19]; 32 | property var tpccsharp : [14,9,16,11,18,13,8,15,10,17,12,19]; 33 | property var tpcd : [14,21,16,23,18,13,20,15,22,17,24,19]; 34 | property var tpcdsharp : [14,9,16,11,18,13,8,15,10,17,12,19]; 35 | property var tpce : [14,21,16,23,18,13,20,15,22,17,24,19]; 36 | property var tpcf : [14,9,16,11,18,13,8,15,10,17,12,19]; 37 | property var tpcfsharp : [7,9,16,11,18,13,8,15,10,17,12,19]; 38 | property var tpcg : [14,21,16,23,18,13,20,15,22,17,24,19]; 39 | property var tpcgsharp : [14,9,16,11,18,13,8,15,10,17,12,19]; 40 | property var tpca : [14,21,16,23,18,13,20,15,22,17,24,19]; 41 | property var tpcasharp : [14,9,16,11,18,13,8,15,10,17,12,19]; 42 | property var tpcb : [14,21,16,23,18,13,20,15,22,17,24,19]; 43 | property var tpcarray1: []; 44 | property var tpcarrayfull : []; 45 | 46 | function elementObject(track, pitches, tpcs, dur, tupdur, ratio,harmony) { 47 | this.track = track; 48 | this.pitches = pitches; 49 | this.tpcs = tpcs; 50 | this.dur = dur; 51 | this.tupdur = tupdur; 52 | this.ratio = ratio; 53 | this.harmony = harmony; 54 | } 55 | 56 | function activeTracks() { 57 | var tracks = []; 58 | for(var i = 0; i < curScore.selection.elements.length; i++) { 59 | var e = curScore.selection.elements[i]; 60 | if(i == 0) { 61 | tracks.push(e.track); 62 | var previousTrack = e.track; 63 | } 64 | if(i > 0) { 65 | if(e.track != previousTrack) { 66 | tracks.push(e.track); 67 | previousTrack = e.track; 68 | } 69 | } 70 | } 71 | return tracks; 72 | } 73 | 74 | function doNegHarm() { 75 | var startTick; 76 | var endTick; 77 | 78 | // initialize loop object variables 79 | var pitches = []; 80 | var tpcs = []; 81 | var dur = []; 82 | var tupdur = []; 83 | var ratio = []; 84 | //var harmony = []; 85 | var harmony; 86 | var thisElement = []; // the container for chord/note/rest data 87 | var theRetrograde = []; // the container for the full selection 88 | 89 | // initialize cursor 90 | var cursor = curScore.newCursor(); 91 | 92 | // check if a selection exists 93 | cursor.rewind(1); // rewind cursor to beginning of selection to avoid last measure bug 94 | if(!cursor.segment) { 95 | console.log("Nothing Selected"); 96 | Qt.quit; // if nothing selected, quit 97 | } 98 | 99 | // get selection start and end ticks 100 | startTick = cursor.tick; // get tick at beginning of selection 101 | cursor.rewind(2); // go to end of selection 102 | endTick = cursor.tick; // get tick at end of selection 103 | if(endTick === 0) { // if last measure selected, 104 | endTick = curScore.lastSegment.tick; // get last tick of score instead 105 | } 106 | 107 | // get active tracks 108 | var tracks = activeTracks(); 109 | 110 | cursor.rewind(1); // go to beginning of selection before starting the loop 111 | 112 | // go through the selection and copy all elements to an object 113 | for(var trackNum in tracks) { 114 | if(cursor.tick == 0 && trackNum < tracks.length) { 115 | cursor.rewind(1); // rewind to get additional voices 116 | } 117 | cursor.track = tracks[trackNum]; // set staff index 118 | console.log ("cursor.track = ", cursor.track) 119 | // begin loop 120 | while(cursor.segment && cursor.tick < endTick) { 121 | var e = cursor.element; // current chord, note, or rest at cursor 122 | 123 | // get pitch and tpc data 124 | if(e.type == Element.CHORD) { 125 | var notes = e.notes; // get all notes in the chord 126 | var newpitch = 128; 127 | for(var noteLoop = 0; noteLoop < notes.length; noteLoop++) { 128 | var note = notes[noteLoop]; 129 | curScore.startCmd(); 130 | newpitch = generatenegativeharmony(note, newpitch) 131 | curScore.endCmd(); 132 | } 133 | } 134 | cursor.next(); // advance the cursor (unless you want to get stuck in a while loop) 135 | } 136 | if(cursor.tick == endTick && trackNum < tracks.length) { 137 | cursor.rewind(1); // rewind to get additional voices 138 | } 139 | } 140 | 141 | } 142 | 143 | 144 | function makethelargearrays() { 145 | for (var i = 0; i < 128; i++) { 146 | mainarraynumbers[i] = i; 147 | } 148 | for (var i = 0; i < 128; i++) { 149 | mainarraynotes[i] = notesbasic[i % notesbasic.length]; 150 | } 151 | } 152 | 153 | function assignscaleclick(scale) { 154 | console.log("Selected scale: " + scale); 155 | makethelargearrays(); // Assuming this needs to be called here as before 156 | selectedscale = scale; 157 | 158 | // Mapping scales to their respective TPC arrays 159 | var scaleToTPC = { 160 | "C": tpcc, 161 | "C#": tpccsharp, 162 | "D": tpcd, 163 | "D#": tpcdsharp, 164 | "E": tpce, 165 | "F": tpcf, 166 | "F#": tpcfsharp, 167 | "G": tpcg, 168 | "G#": tpcgsharp, 169 | "A": tpca, 170 | "A#": tpcasharp, 171 | "B": tpcb 172 | }; 173 | 174 | tpcarray1 = scaleToTPC[scale].slice(); 175 | for (var i = 0; i < 128; i++) { 176 | tpcarrayfull[i] = tpcarray1[i % tpcarray1.length]; 177 | } 178 | // Split the cycle of fifths based on the selected scale 179 | var splitIndex = fifths.indexOf(scale) + 1; 180 | split1 = fifths.slice(splitIndex, splitIndex + 6); 181 | split2 = fifths.slice(0, splitIndex).reverse(); 182 | 183 | // Fill in remaining slots if not enough elements 184 | if (split1.length < 6) { 185 | split1 = split1.concat(fifths.slice(0, 6 - split1.length)); 186 | } 187 | if (split2.length < 6) { 188 | split2 = split2.concat(fifths.slice(-6 + split2.length).reverse()); 189 | } 190 | 191 | console.log("TPC Full: ", tpcarrayfull.join(", ")); 192 | console.log("Split 1: ", split1.join(", ")); 193 | console.log("Split 2: ", split2.join(", ")); 194 | } 195 | 196 | function generatenegativeharmony(note, oldpitch) 197 | { 198 | console.log("Original pitch: " + note.pitch); 199 | 200 | var curnote = mainarraynotes[note.pitch]; 201 | var split1Map = split1.reduce(function(result, note, index) { 202 | result[note] = split2[index]; 203 | return result; 204 | }, {}); 205 | 206 | var split2Map = split2.reduce(function(result, note, index) { 207 | result[note] = split1[index]; 208 | return result; 209 | }, {}); 210 | var newnote = split1Map[curnote] || split2Map[curnote]; 211 | 212 | console.log("Current note: " + curnote + ", New note: " + newnote); 213 | 214 | var newpitch = 0; 215 | for( var i = note.pitch, j = note.pitch;i<128 && j >=0 ; j--, i++) 216 | { 217 | 218 | if( mainarraynotes[i] == newnote) 219 | { 220 | console.log(i); 221 | newpitch = i; 222 | note.pitch = newpitch; 223 | note.tpc1 = tpcarrayfull[i]; 224 | note.tpc2 = tpcarrayfull[i]; 225 | break; 226 | } 227 | if (mainarraynotes[j] == newnote) 228 | { 229 | console.log(j); 230 | newpitch = j; 231 | note.pitch = newpitch; 232 | note.tpc2 = tpcarrayfull[j]; 233 | note.tpc1 = tpcarrayfull[j]; 234 | break; 235 | } 236 | 237 | } 238 | console.log("new = "+newpitch); 239 | console.log("old = "+oldpitch); 240 | while (newpitch > oldpitch) 241 | newpitch -= 12; 242 | 243 | note.pitch = newpitch; 244 | return newpitch; 245 | 246 | } 247 | 248 | // function generatenegativeharmony(note) { 249 | // console.log("Original pitch: " + note.pitch); 250 | // 251 | // 252 | // var curnote = mainarraynotes[note.pitch]; 253 | 254 | // 255 | // var split1Map = split1.reduce(function(result, note, index) { 256 | // result[note] = split2[index]; 257 | // return result; 258 | // }, {}); 259 | 260 | // var split2Map = split2.reduce(function(result, note, index) { 261 | // result[note] = split1[index]; 262 | // return result; 263 | // }, {}); 264 | 265 | 266 | 267 | // 268 | // var newnote = split1Map[curnote] || split2Map[curnote]; 269 | 270 | // console.log("Current note: " + curnote + ", New note: " + newnote); 271 | 272 | // 273 | // var newpitch = getPitchFromNoteName(newnote); // Implement this function based on your note to pitch mapping. 274 | 275 | // // Adjust pitch if necessary. 276 | // while (newpitch > note.pitch) { 277 | // newpitch -= 12; 278 | // } 279 | 280 | // console.log("New pitch: " + newpitch); 281 | // note.pitch = newpitch; 282 | 283 | // return newpitch; 284 | // } 285 | 286 | // 287 | // function getPitchFromNoteName(noteName) { 288 | // 289 | // return mainarraynotes.indexOf(noteName); 290 | // } 291 | 292 | 293 | Rectangle 294 | { 295 | color: "lightgrey" 296 | anchors.fill: parent 297 | GridLayout 298 | { 299 | columns: 2 300 | anchors.fill: parent 301 | anchors.margins: 10 302 | GroupBox 303 | { 304 | title: "Select Scale" 305 | ColumnLayout 306 | { 307 | ExclusiveGroup {id: availablescales} 308 | RadioButton{ 309 | id: c_scale_button 310 | text: "C" 311 | checked: true 312 | exclusiveGroup: availablescales 313 | onClicked: {assignscaleclick(notesbasic[0])} 314 | } 315 | RadioButton{ 316 | id: csharp_scale_button 317 | text: "C#" 318 | exclusiveGroup: availablescales 319 | onClicked: { assignscaleclick(notesbasic[1])} 320 | 321 | } 322 | RadioButton{ 323 | id: d_scale_button 324 | text: "D" 325 | exclusiveGroup: availablescales 326 | onClicked: { assignscaleclick(notesbasic[2])} 327 | 328 | } 329 | RadioButton{ 330 | id: dsharp_scale_button 331 | text: "D#" 332 | exclusiveGroup: availablescales 333 | onClicked: { assignscaleclick(notesbasic[3])} 334 | 335 | } 336 | RadioButton{ 337 | id: e_scale_button 338 | text: "E" 339 | exclusiveGroup: availablescales 340 | onClicked: { assignscaleclick(notesbasic[4])} 341 | 342 | } 343 | RadioButton{ 344 | id: f_scale_button 345 | text: "F" 346 | exclusiveGroup: availablescales 347 | onClicked: { assignscaleclick(notesbasic[5])} 348 | 349 | } 350 | RadioButton{ 351 | id: fsharp_scale_button 352 | text: "F#" 353 | exclusiveGroup: availablescales 354 | onClicked: { assignscaleclick(notesbasic[6])} 355 | 356 | } 357 | RadioButton{ 358 | id: g_scale_button 359 | text: "G" 360 | exclusiveGroup: availablescales 361 | onClicked: { assignscaleclick(notesbasic[7])} 362 | 363 | } 364 | RadioButton{ 365 | id: gsharp_scale_button 366 | text: "G#" 367 | exclusiveGroup: availablescales 368 | onClicked: { assignscaleclick(notesbasic[8])} 369 | 370 | } 371 | RadioButton{ 372 | id: a_scale_button 373 | text: "A" 374 | exclusiveGroup: availablescales 375 | onClicked: { assignscaleclick(notesbasic[9])} 376 | 377 | } 378 | RadioButton{ 379 | id: asharp_scale_button 380 | text: "A#" 381 | exclusiveGroup: availablescales 382 | onClicked: { assignscaleclick(notesbasic[10])} 383 | 384 | } 385 | RadioButton{ 386 | id: b_scale_button 387 | text: "B" 388 | exclusiveGroup: availablescales 389 | onClicked: { assignscaleclick(notesbasic[11])} 390 | 391 | } 392 | 393 | 394 | 395 | } 396 | } 397 | ColumnLayout 398 | { 399 | 400 | 401 | 402 | GroupBox 403 | { 404 | title: "Apply Changes/ Quit" 405 | RowLayout 406 | { 407 | Button { 408 | id: applyButton 409 | text: qsTranslate("PrefsDialogBase", "Apply") 410 | onClicked: { 411 | var fullScore = !curScore.selection.elements.length 412 | if (fullScore) 413 | { 414 | cmd("select-all") 415 | } 416 | if (selectedscale === "") 417 | assignscaleclick("C"); 418 | doNegHarm(); 419 | if (fullScore) 420 | { 421 | cmd("escape"); 422 | } 423 | pluginId.parent.Window.window.close(); 424 | } 425 | 426 | } 427 | Button { 428 | id: quitbutton 429 | text: qsTranslate("PrefsDialogBase", "Quit") 430 | onClicked: { 431 | pluginId.parent.Window.window.close(); 432 | } 433 | 434 | } 435 | 436 | } 437 | 438 | } 439 | } 440 | } 441 | 442 | 443 | 444 | 445 | } 446 | 447 | 448 | 449 | } 450 | --------------------------------------------------------------------------------