├── LICENSE.md ├── README.md ├── images ├── MirrorIntervals.png └── Tuning.png └── src ├── MirrorIntervals ├── Makefile ├── README.md ├── mirror-intervals-2.qml ├── mirror-intervals-3.qml └── mirror-intervals.qml.sh ├── PivotChords ├── PivotChords.qml └── README.md ├── Tuning ├── 2.x │ └── tuning.qml ├── 3.x │ └── tuning.qml └── README.md └── VoiceVelocity ├── README.md └── voice-velocity.qml /LICENSE.md: -------------------------------------------------------------------------------- 1 | ### GNU GENERAL PUBLIC LICENSE 2 | 3 | Version 3, 29 June 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | ### Preamble 12 | 13 | The GNU General Public License is a free, copyleft license for 14 | software and other kinds of works. 15 | 16 | The licenses for most software and other practical works are designed 17 | to take away your freedom to share and change the works. By contrast, 18 | the GNU General Public License is intended to guarantee your freedom 19 | to share and change all versions of a program--to make sure it remains 20 | free software for all its users. We, the Free Software Foundation, use 21 | the GNU General Public License for most of our software; it applies 22 | also to any other work released this way by its authors. You can apply 23 | it to your programs, too. 24 | 25 | When we speak of free software, we are referring to freedom, not 26 | price. Our General Public Licenses are designed to make sure that you 27 | have the freedom to distribute copies of free software (and charge for 28 | them if you wish), that you receive source code or can get it if you 29 | want it, that you can change the software or use pieces of it in new 30 | free programs, and that you know you can do these things. 31 | 32 | To protect your rights, we need to prevent others from denying you 33 | these rights or asking you to surrender the rights. Therefore, you 34 | have certain responsibilities if you distribute copies of the 35 | software, or if you modify it: responsibilities to respect the freedom 36 | of others. 37 | 38 | For example, if you distribute copies of such a program, whether 39 | gratis or for a fee, you must pass on to the recipients the same 40 | freedoms that you received. You must make sure that they, too, receive 41 | or can get the source code. And you must show them these terms so they 42 | know their rights. 43 | 44 | Developers that use the GNU GPL protect your rights with two steps: 45 | (1) assert copyright on the software, and (2) offer you this License 46 | giving you legal permission to copy, distribute and/or modify it. 47 | 48 | For the developers' and authors' protection, the GPL clearly explains 49 | that there is no warranty for this free software. For both users' and 50 | authors' sake, the GPL requires that modified versions be marked as 51 | changed, so that their problems will not be attributed erroneously to 52 | authors of previous versions. 53 | 54 | Some devices are designed to deny users access to install or run 55 | modified versions of the software inside them, although the 56 | manufacturer can do so. This is fundamentally incompatible with the 57 | aim of protecting users' freedom to change the software. The 58 | systematic pattern of such abuse occurs in the area of products for 59 | individuals to use, which is precisely where it is most unacceptable. 60 | Therefore, we have designed this version of the GPL to prohibit the 61 | practice for those products. If such problems arise substantially in 62 | other domains, we stand ready to extend this provision to those 63 | domains in future versions of the GPL, as needed to protect the 64 | freedom of users. 65 | 66 | Finally, every program is threatened constantly by software patents. 67 | States should not allow patents to restrict development and use of 68 | software on general-purpose computers, but in those that do, we wish 69 | to avoid the special danger that patents applied to a free program 70 | could make it effectively proprietary. To prevent this, the GPL 71 | assures that patents cannot be used to render the program non-free. 72 | 73 | The precise terms and conditions for copying, distribution and 74 | modification follow. 75 | 76 | ### TERMS AND CONDITIONS 77 | 78 | #### 0. Definitions. 79 | 80 | "This License" refers to version 3 of the GNU General Public License. 81 | 82 | "Copyright" also means copyright-like laws that apply to other kinds 83 | of works, such as semiconductor masks. 84 | 85 | "The Program" refers to any copyrightable work licensed under this 86 | License. Each licensee is addressed as "you". "Licensees" and 87 | "recipients" may be individuals or organizations. 88 | 89 | To "modify" a work means to copy from or adapt all or part of the work 90 | in a fashion requiring copyright permission, other than the making of 91 | an exact copy. The resulting work is called a "modified version" of 92 | the earlier work or a work "based on" the earlier work. 93 | 94 | A "covered work" means either the unmodified Program or a work based 95 | on the Program. 96 | 97 | To "propagate" a work means to do anything with it that, without 98 | permission, would make you directly or secondarily liable for 99 | infringement under applicable copyright law, except executing it on a 100 | computer or modifying a private copy. Propagation includes copying, 101 | distribution (with or without modification), making available to the 102 | public, and in some countries other activities as well. 103 | 104 | To "convey" a work means any kind of propagation that enables other 105 | parties to make or receive copies. Mere interaction with a user 106 | through a computer network, with no transfer of a copy, is not 107 | conveying. 108 | 109 | An interactive user interface displays "Appropriate Legal Notices" to 110 | the extent that it includes a convenient and prominently visible 111 | feature that (1) displays an appropriate copyright notice, and (2) 112 | tells the user that there is no warranty for the work (except to the 113 | extent that warranties are provided), that licensees may convey the 114 | work under this License, and how to view a copy of this License. If 115 | the interface presents a list of user commands or options, such as a 116 | menu, a prominent item in the list meets this criterion. 117 | 118 | #### 1. Source Code. 119 | 120 | The "source code" for a work means the preferred form of the work for 121 | making modifications to it. "Object code" means any non-source form of 122 | a work. 123 | 124 | A "Standard Interface" means an interface that either is an official 125 | standard defined by a recognized standards body, or, in the case of 126 | interfaces specified for a particular programming language, one that 127 | is widely used among developers working in that language. 128 | 129 | The "System Libraries" of an executable work include anything, other 130 | than the work as a whole, that (a) is included in the normal form of 131 | packaging a Major Component, but which is not part of that Major 132 | Component, and (b) serves only to enable use of the work with that 133 | Major Component, or to implement a Standard Interface for which an 134 | implementation is available to the public in source code form. A 135 | "Major Component", in this context, means a major essential component 136 | (kernel, window system, and so on) of the specific operating system 137 | (if any) on which the executable work runs, or a compiler used to 138 | produce the work, or an object code interpreter used to run it. 139 | 140 | The "Corresponding Source" for a work in object code form means all 141 | the source code needed to generate, install, and (for an executable 142 | work) run the object code and to modify the work, including scripts to 143 | control those activities. However, it does not include the work's 144 | System Libraries, or general-purpose tools or generally available free 145 | programs which are used unmodified in performing those activities but 146 | which are not part of the work. For example, Corresponding Source 147 | includes interface definition files associated with source files for 148 | the work, and the source code for shared libraries and dynamically 149 | linked subprograms that the work is specifically designed to require, 150 | such as by intimate data communication or control flow between those 151 | subprograms and other parts of the work. 152 | 153 | The Corresponding Source need not include anything that users can 154 | regenerate automatically from other parts of the Corresponding Source. 155 | 156 | The Corresponding Source for a work in source code form is that same 157 | work. 158 | 159 | #### 2. Basic Permissions. 160 | 161 | All rights granted under this License are granted for the term of 162 | copyright on the Program, and are irrevocable provided the stated 163 | conditions are met. This License explicitly affirms your unlimited 164 | permission to run the unmodified Program. The output from running a 165 | covered work is covered by this License only if the output, given its 166 | content, constitutes a covered work. This License acknowledges your 167 | rights of fair use or other equivalent, as provided by copyright law. 168 | 169 | You may make, run and propagate covered works that you do not convey, 170 | without conditions so long as your license otherwise remains in force. 171 | You may convey covered works to others for the sole purpose of having 172 | them make modifications exclusively for you, or provide you with 173 | facilities for running those works, provided that you comply with the 174 | terms of this License in conveying all material for which you do not 175 | control copyright. Those thus making or running the covered works for 176 | you must do so exclusively on your behalf, under your direction and 177 | control, on terms that prohibit them from making any copies of your 178 | copyrighted material outside their relationship with you. 179 | 180 | Conveying under any other circumstances is permitted solely under the 181 | conditions stated below. Sublicensing is not allowed; section 10 makes 182 | it unnecessary. 183 | 184 | #### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 185 | 186 | No covered work shall be deemed part of an effective technological 187 | measure under any applicable law fulfilling obligations under article 188 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 189 | similar laws prohibiting or restricting circumvention of such 190 | measures. 191 | 192 | When you convey a covered work, you waive any legal power to forbid 193 | circumvention of technological measures to the extent such 194 | circumvention is effected by exercising rights under this License with 195 | respect to the covered work, and you disclaim any intention to limit 196 | operation or modification of the work as a means of enforcing, against 197 | the work's users, your or third parties' legal rights to forbid 198 | circumvention of technological measures. 199 | 200 | #### 4. Conveying Verbatim Copies. 201 | 202 | You may convey verbatim copies of the Program's source code as you 203 | receive it, in any medium, provided that you conspicuously and 204 | appropriately publish on each copy an appropriate copyright notice; 205 | keep intact all notices stating that this License and any 206 | non-permissive terms added in accord with section 7 apply to the code; 207 | keep intact all notices of the absence of any warranty; and give all 208 | recipients a copy of this License along with the Program. 209 | 210 | You may charge any price or no price for each copy that you convey, 211 | and you may offer support or warranty protection for a fee. 212 | 213 | #### 5. Conveying Modified Source Versions. 214 | 215 | You may convey a work based on the Program, or the modifications to 216 | produce it from the Program, in the form of source code under the 217 | terms of section 4, provided that you also meet all of these 218 | conditions: 219 | 220 | - a) The work must carry prominent notices stating that you modified 221 | it, and giving a relevant date. 222 | - b) The work must carry prominent notices stating that it is 223 | released under this License and any conditions added under 224 | section 7. This requirement modifies the requirement in section 4 225 | to "keep intact all notices". 226 | - c) You must license the entire work, as a whole, under this 227 | License to anyone who comes into possession of a copy. This 228 | License will therefore apply, along with any applicable section 7 229 | additional terms, to the whole of the work, and all its parts, 230 | regardless of how they are packaged. This License gives no 231 | permission to license the work in any other way, but it does not 232 | invalidate such permission if you have separately received it. 233 | - d) If the work has interactive user interfaces, each must display 234 | Appropriate Legal Notices; however, if the Program has interactive 235 | interfaces that do not display Appropriate Legal Notices, your 236 | work need not make them do so. 237 | 238 | A compilation of a covered work with other separate and independent 239 | works, which are not by their nature extensions of the covered work, 240 | and which are not combined with it such as to form a larger program, 241 | in or on a volume of a storage or distribution medium, is called an 242 | "aggregate" if the compilation and its resulting copyright are not 243 | used to limit the access or legal rights of the compilation's users 244 | beyond what the individual works permit. Inclusion of a covered work 245 | in an aggregate does not cause this License to apply to the other 246 | parts of the aggregate. 247 | 248 | #### 6. Conveying Non-Source Forms. 249 | 250 | You may convey a covered work in object code form under the terms of 251 | sections 4 and 5, provided that you also convey the machine-readable 252 | Corresponding Source under the terms of this License, in one of these 253 | ways: 254 | 255 | - a) Convey the object code in, or embodied in, a physical product 256 | (including a physical distribution medium), accompanied by the 257 | Corresponding Source fixed on a durable physical medium 258 | customarily used for software interchange. 259 | - b) Convey the object code in, or embodied in, a physical product 260 | (including a physical distribution medium), accompanied by a 261 | written offer, valid for at least three years and valid for as 262 | long as you offer spare parts or customer support for that product 263 | model, to give anyone who possesses the object code either (1) a 264 | copy of the Corresponding Source for all the software in the 265 | product that is covered by this License, on a durable physical 266 | medium customarily used for software interchange, for a price no 267 | more than your reasonable cost of physically performing this 268 | conveying of source, or (2) access to copy the Corresponding 269 | Source from a network server at no charge. 270 | - c) Convey individual copies of the object code with a copy of the 271 | written offer to provide the Corresponding Source. This 272 | alternative is allowed only occasionally and noncommercially, and 273 | only if you received the object code with such an offer, in accord 274 | with subsection 6b. 275 | - d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | - e) Convey the object code using peer-to-peer transmission, 288 | provided you inform other peers where the object code and 289 | Corresponding Source of the work are being offered to the general 290 | public at no charge under subsection 6d. 291 | 292 | A separable portion of the object code, whose source code is excluded 293 | from the Corresponding Source as a System Library, need not be 294 | included in conveying the object code work. 295 | 296 | A "User Product" is either (1) a "consumer product", which means any 297 | tangible personal property which is normally used for personal, 298 | family, or household purposes, or (2) anything designed or sold for 299 | incorporation into a dwelling. In determining whether a product is a 300 | consumer product, doubtful cases shall be resolved in favor of 301 | coverage. For a particular product received by a particular user, 302 | "normally used" refers to a typical or common use of that class of 303 | product, regardless of the status of the particular user or of the way 304 | in which the particular user actually uses, or expects or is expected 305 | to use, the product. A product is a consumer product regardless of 306 | whether the product has substantial commercial, industrial or 307 | non-consumer uses, unless such uses represent the only significant 308 | mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to 312 | install and execute modified versions of a covered work in that User 313 | Product from a modified version of its Corresponding Source. The 314 | information must suffice to ensure that the continued functioning of 315 | the modified object code is in no case prevented or interfered with 316 | solely because modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or 331 | updates for a work that has been modified or installed by the 332 | recipient, or for the User Product in which it has been modified or 333 | installed. Access to a network may be denied when the modification 334 | itself materially and adversely affects the operation of the network 335 | or violates the rules and protocols for communication across the 336 | network. 337 | 338 | Corresponding Source conveyed, and Installation Information provided, 339 | in accord with this section must be in a format that is publicly 340 | documented (and with an implementation available to the public in 341 | source code form), and must require no special password or key for 342 | unpacking, reading or copying. 343 | 344 | #### 7. Additional Terms. 345 | 346 | "Additional permissions" are terms that supplement the terms of this 347 | License by making exceptions from one or more of its conditions. 348 | Additional permissions that are applicable to the entire Program shall 349 | be treated as though they were included in this License, to the extent 350 | that they are valid under applicable law. If additional permissions 351 | apply only to part of the Program, that part may be used separately 352 | under those permissions, but the entire Program remains governed by 353 | this License without regard to the additional permissions. 354 | 355 | When you convey a copy of a covered work, you may at your option 356 | remove any additional permissions from that copy, or from any part of 357 | it. (Additional permissions may be written to require their own 358 | removal in certain cases when you modify the work.) You may place 359 | additional permissions on material, added by you to a covered work, 360 | for which you have or can give appropriate copyright permission. 361 | 362 | Notwithstanding any other provision of this License, for material you 363 | add to a covered work, you may (if authorized by the copyright holders 364 | of that material) supplement the terms of this License with terms: 365 | 366 | - a) Disclaiming warranty or limiting liability differently from the 367 | terms of sections 15 and 16 of this License; or 368 | - b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | - c) Prohibiting misrepresentation of the origin of that material, 372 | or requiring that modified versions of such material be marked in 373 | reasonable ways as different from the original version; or 374 | - d) Limiting the use for publicity purposes of names of licensors 375 | or authors of the material; or 376 | - e) Declining to grant rights under trademark law for use of some 377 | trade names, trademarks, or service marks; or 378 | - f) Requiring indemnification of licensors and authors of that 379 | material by anyone who conveys the material (or modified versions 380 | of it) with contractual assumptions of liability to the recipient, 381 | for any liability that these contractual assumptions directly 382 | impose on those licensors and authors. 383 | 384 | All other non-permissive additional terms are considered "further 385 | restrictions" within the meaning of section 10. If the Program as you 386 | received it, or any part of it, contains a notice stating that it is 387 | governed by this License along with a term that is a further 388 | restriction, you may remove that term. If a license document contains 389 | a further restriction but permits relicensing or conveying under this 390 | License, you may add to a covered work material governed by the terms 391 | of that license document, provided that the further restriction does 392 | not survive such relicensing or conveying. 393 | 394 | If you add terms to a covered work in accord with this section, you 395 | must place, in the relevant source files, a statement of the 396 | additional terms that apply to those files, or a notice indicating 397 | where to find the applicable terms. 398 | 399 | Additional terms, permissive or non-permissive, may be stated in the 400 | form of a separately written license, or stated as exceptions; the 401 | above requirements apply either way. 402 | 403 | #### 8. Termination. 404 | 405 | You may not propagate or modify a covered work except as expressly 406 | provided under this License. Any attempt otherwise to propagate or 407 | modify it is void, and will automatically terminate your rights under 408 | this License (including any patent licenses granted under the third 409 | paragraph of section 11). 410 | 411 | However, if you cease all violation of this License, then your license 412 | from a particular copyright holder is reinstated (a) provisionally, 413 | unless and until the copyright holder explicitly and finally 414 | terminates your license, and (b) permanently, if the copyright holder 415 | fails to notify you of the violation by some reasonable means prior to 416 | 60 days after the cessation. 417 | 418 | Moreover, your license from a particular copyright holder is 419 | reinstated permanently if the copyright holder notifies you of the 420 | violation by some reasonable means, this is the first time you have 421 | received notice of violation of this License (for any work) from that 422 | copyright holder, and you cure the violation prior to 30 days after 423 | your receipt of the notice. 424 | 425 | Termination of your rights under this section does not terminate the 426 | licenses of parties who have received copies or rights from you under 427 | this License. If your rights have been terminated and not permanently 428 | reinstated, you do not qualify to receive new licenses for the same 429 | material under section 10. 430 | 431 | #### 9. Acceptance Not Required for Having Copies. 432 | 433 | You are not required to accept this License in order to receive or run 434 | a copy of the Program. Ancillary propagation of a covered work 435 | occurring solely as a consequence of using peer-to-peer transmission 436 | to receive a copy likewise does not require acceptance. However, 437 | nothing other than this License grants you permission to propagate or 438 | modify any covered work. These actions infringe copyright if you do 439 | not accept this License. Therefore, by modifying or propagating a 440 | covered work, you indicate your acceptance of this License to do so. 441 | 442 | #### 10. Automatic Licensing of Downstream Recipients. 443 | 444 | Each time you convey a covered work, the recipient automatically 445 | receives a license from the original licensors, to run, modify and 446 | propagate that work, subject to this License. You are not responsible 447 | for enforcing compliance by third parties with this License. 448 | 449 | An "entity transaction" is a transaction transferring control of an 450 | organization, or substantially all assets of one, or subdividing an 451 | organization, or merging organizations. If propagation of a covered 452 | work results from an entity transaction, each party to that 453 | transaction who receives a copy of the work also receives whatever 454 | licenses to the work the party's predecessor in interest had or could 455 | give under the previous paragraph, plus a right to possession of the 456 | Corresponding Source of the work from the predecessor in interest, if 457 | the predecessor has it or can get it with reasonable efforts. 458 | 459 | You may not impose any further restrictions on the exercise of the 460 | rights granted or affirmed under this License. For example, you may 461 | not impose a license fee, royalty, or other charge for exercise of 462 | rights granted under this License, and you may not initiate litigation 463 | (including a cross-claim or counterclaim in a lawsuit) alleging that 464 | any patent claim is infringed by making, using, selling, offering for 465 | sale, or importing the Program or any portion of it. 466 | 467 | #### 11. Patents. 468 | 469 | A "contributor" is a copyright holder who authorizes use under this 470 | License of the Program or a work on which the Program is based. The 471 | work thus licensed is called the contributor's "contributor version". 472 | 473 | A contributor's "essential patent claims" are all patent claims owned 474 | or controlled by the contributor, whether already acquired or 475 | hereafter acquired, that would be infringed by some manner, permitted 476 | by this License, of making, using, or selling its contributor version, 477 | but do not include claims that would be infringed only as a 478 | consequence of further modification of the contributor version. For 479 | purposes of this definition, "control" includes the right to grant 480 | patent sublicenses in a manner consistent with the requirements of 481 | this License. 482 | 483 | Each contributor grants you a non-exclusive, worldwide, royalty-free 484 | patent license under the contributor's essential patent claims, to 485 | make, use, sell, offer for sale, import and otherwise run, modify and 486 | propagate the contents of its contributor version. 487 | 488 | In the following three paragraphs, a "patent license" is any express 489 | agreement or commitment, however denominated, not to enforce a patent 490 | (such as an express permission to practice a patent or covenant not to 491 | sue for patent infringement). To "grant" such a patent license to a 492 | party means to make such an agreement or commitment not to enforce a 493 | patent against the party. 494 | 495 | If you convey a covered work, knowingly relying on a patent license, 496 | and the Corresponding Source of the work is not available for anyone 497 | to copy, free of charge and under the terms of this License, through a 498 | publicly available network server or other readily accessible means, 499 | then you must either (1) cause the Corresponding Source to be so 500 | available, or (2) arrange to deprive yourself of the benefit of the 501 | patent license for this particular work, or (3) arrange, in a manner 502 | consistent with the requirements of this License, to extend the patent 503 | license to downstream recipients. "Knowingly relying" means you have 504 | actual knowledge that, but for the patent license, your conveying the 505 | covered work in a country, or your recipient's use of the covered work 506 | in a country, would infringe one or more identifiable patents in that 507 | country that you have reason to believe are valid. 508 | 509 | If, pursuant to or in connection with a single transaction or 510 | arrangement, you convey, or propagate by procuring conveyance of, a 511 | covered work, and grant a patent license to some of the parties 512 | receiving the covered work authorizing them to use, propagate, modify 513 | or convey a specific copy of the covered work, then the patent license 514 | you grant is automatically extended to all recipients of the covered 515 | work and works based on it. 516 | 517 | A patent license is "discriminatory" if it does not include within the 518 | scope of its coverage, prohibits the exercise of, or is conditioned on 519 | the non-exercise of one or more of the rights that are specifically 520 | granted under this License. You may not convey a covered work if you 521 | are a party to an arrangement with a third party that is in the 522 | business of distributing software, under which you make payment to the 523 | third party based on the extent of your activity of conveying the 524 | work, and under which the third party grants, to any of the parties 525 | who would receive the covered work from you, a discriminatory patent 526 | license (a) in connection with copies of the covered work conveyed by 527 | you (or copies made from those copies), or (b) primarily for and in 528 | connection with specific products or compilations that contain the 529 | covered work, unless you entered into that arrangement, or that patent 530 | license was granted, prior to 28 March 2007. 531 | 532 | Nothing in this License shall be construed as excluding or limiting 533 | any implied license or other defenses to infringement that may 534 | otherwise be available to you under applicable patent law. 535 | 536 | #### 12. No Surrender of Others' Freedom. 537 | 538 | If conditions are imposed on you (whether by court order, agreement or 539 | otherwise) that contradict the conditions of this License, they do not 540 | excuse you from the conditions of this License. If you cannot convey a 541 | covered work so as to satisfy simultaneously your obligations under 542 | this License and any other pertinent obligations, then as a 543 | consequence you may not convey it at all. For example, if you agree to 544 | terms that obligate you to collect a royalty for further conveying 545 | from those to whom you convey the Program, the only way you could 546 | satisfy both those terms and this License would be to refrain entirely 547 | from conveying the Program. 548 | 549 | #### 13. Use with the GNU Affero General Public License. 550 | 551 | Notwithstanding any other provision of this License, you have 552 | permission to link or combine any covered work with a work licensed 553 | under version 3 of the GNU Affero General Public License into a single 554 | combined work, and to convey the resulting work. The terms of this 555 | License will continue to apply to the part which is the covered work, 556 | but the special requirements of the GNU Affero General Public License, 557 | section 13, concerning interaction through a network will apply to the 558 | combination as such. 559 | 560 | #### 14. Revised Versions of this License. 561 | 562 | The Free Software Foundation may publish revised and/or new versions 563 | of the GNU General Public License from time to time. Such new versions 564 | will be similar in spirit to the present version, but may differ in 565 | detail to address new problems or concerns. 566 | 567 | Each version is given a distinguishing version number. If the Program 568 | specifies that a certain numbered version of the GNU General Public 569 | License "or any later version" applies to it, you have the option of 570 | following the terms and conditions either of that numbered version or 571 | of any later version published by the Free Software Foundation. If the 572 | Program does not specify a version number of the GNU General Public 573 | License, you may choose any version ever published by the Free 574 | Software Foundation. 575 | 576 | If the Program specifies that a proxy can decide which future versions 577 | of the GNU General Public License can be used, that proxy's public 578 | statement of acceptance of a version permanently authorizes you to 579 | choose that version for the Program. 580 | 581 | Later license versions may give you additional or different 582 | permissions. However, no additional obligations are imposed on any 583 | author or copyright holder as a result of your choosing to follow a 584 | later version. 585 | 586 | #### 15. Disclaimer of Warranty. 587 | 588 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 589 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 590 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT 591 | WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT 592 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 593 | A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND 594 | PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE 595 | DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR 596 | CORRECTION. 597 | 598 | #### 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR 602 | CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 603 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES 604 | ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT 605 | NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR 606 | LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM 607 | TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER 608 | PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 609 | 610 | #### 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | ### How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these 626 | terms. 627 | 628 | To do so, attach the following notices to the program. It is safest to 629 | attach them to the start of each source file to most effectively state 630 | the exclusion of warranty; and each file should have at least the 631 | "copyright" line and a pointer to where the full notice is found. 632 | 633 | 634 | Copyright (C) 635 | 636 | This program is free software: you can redistribute it and/or modify 637 | it under the terms of the GNU General Public License as published by 638 | the Free Software Foundation, either version 3 of the License, or 639 | (at your option) any later version. 640 | 641 | This program is distributed in the hope that it will be useful, 642 | but WITHOUT ANY WARRANTY; without even the implied warranty of 643 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 644 | GNU General Public License for more details. 645 | 646 | You should have received a copy of the GNU General Public License 647 | along with this program. If not, see . 648 | 649 | Also add information on how to contact you by electronic and paper 650 | mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands \`show w' and \`show c' should show the 661 | appropriate parts of the General Public License. Of course, your 662 | program's commands might be different; for a GUI interface, you would 663 | use an "about box". 664 | 665 | You should also get your employer (if you work as a programmer) or 666 | school, if any, to sign a "copyright disclaimer" for the program, if 667 | necessary. For more information on this, and how to apply and follow 668 | the GNU GPL, see . 669 | 670 | The GNU General Public License does not permit incorporating your 671 | program into proprietary programs. If your program is a subroutine 672 | library, you may consider it more useful to permit linking proprietary 673 | applications with the library. If this is what you want to do, use the 674 | GNU Lesser General Public License instead of this License. But first, 675 | please read . 676 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MuseScore-plugins 2 | Various MuseScore2 plugins that people might find useful. 3 | 4 | ## Mirror Intervals 5 | Chromatically mirror intervals about a pivot note while attempting to keep S, A, T and B in their original registers. 6 | 7 | ## Pivot Chords 8 | Given two keys, displays the chords those keys have in common. 9 | 10 | ## Tuning 11 | Alters the tuning of the selection to one of a number of alternatives. Also allows you to create and share your own tunings. 12 | 13 | ## Voice Velocity 14 | Allows you to change the velocity (dynamics) of a single voice part. 15 | -------------------------------------------------------------------------------- /images/MirrorIntervals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/billhails/MuseScore-plugins/2dcf10a15cd09de6628c2c5ed4a5ba8a0aaef077/images/MirrorIntervals.png -------------------------------------------------------------------------------- /images/Tuning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/billhails/MuseScore-plugins/2dcf10a15cd09de6628c2c5ed4a5ba8a0aaef077/images/Tuning.png -------------------------------------------------------------------------------- /src/MirrorIntervals/Makefile: -------------------------------------------------------------------------------- 1 | # Mirror Intervals about a given pivot note. 2 | # Copyright (C) 2018-2019 Bill Hails 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation, either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | TEMPLATE=./mirror-intervals.qml.sh 18 | V2=mirror-intervals-2.qml 19 | V3=mirror-intervals-3.qml 20 | 21 | .PHONY: all 22 | 23 | all: $(V2) $(V3) 24 | 25 | # docs say `segment` is now `segment()` but I get an error using `segment()` in 3.0.2 26 | # leaving the macro in place in case it changes in a later release. 27 | 28 | $(V2): $(TEMPLATE) 29 | MuseScoreImport=1.0 MuseScoreVersion=2.3.2 Segment='segment' Namespace=Element bash $< > $@ 30 | 31 | $(V3): $(TEMPLATE) 32 | MuseScoreImport=3.0 MuseScoreVersion=3.0.2 Segment='segment' Namespace=Ms bash $< > $@ 33 | 34 | $(V2) $(V3): Makefile 35 | 36 | # vim:noet sw=8 37 | -------------------------------------------------------------------------------- /src/MirrorIntervals/README.md: -------------------------------------------------------------------------------- 1 | # Mirror Intervals 2 | 3 | This plugin mirrors intervals chromatically about a chosen pivot note. 4 | 5 | ## Compatibility 6 | 7 | MuseScore2, MuseScore3. 8 | 9 | ## Screen Shot 10 | 11 | ![Tuning Pop Up](https://raw.githubusercontent.com/billhails/MuseScore-plugins/master/images/MirrorIntervals.png) 12 | 13 | ## Behaviour 14 | 15 | The plugin requests a pivot note, then for each voice part in the selection: 16 | 17 | * Identify the first note in the part within the selection. 18 | * Find the pivot tone nearest that first note. 19 | * Mirror the whole part chromatically about that pivot tone. 20 | 21 | The outcome of this is that the parts should stay reasonably close in pitch when mirrored, the bass will 22 | (more or less) stay in the bass and the treble in the treble, provided the voices don't have an extreme 23 | ambitus or start on an uncharacteristic pitch. If they do end up in the wrong range, you can select a single 24 | voice with the standard selection filter `View > Selection Filter` then transpose it up or down an octave. 25 | 26 | ## Some Observations 27 | 28 | This is pure chromatic mirroring. The results can be surprising but here are some guidelines and observations. 29 | 30 | Mirroring a section about either D or G# will map white notes to white notes, specifically: 31 | 32 | | From | To | 33 | | ---- | -- | 34 | | G# | Ab | 35 | | A | G | 36 | | Bb | F# | 37 | | B | F | 38 | | C | E | 39 | | C# | Eb | 40 | | D | D | 41 | | E | C | 42 | | F | B | 43 | | F# | Bb | 44 | | G | A | 45 | 46 | Looking at the above table, you should be able to work out that the tonic in C major maps to the tonic in A minor, but in an odd way: 47 | 48 | | From | To | 49 | | ---- | -- | 50 | | G | A | 51 | | E | C | 52 | | C | E | 53 | 54 | If the tonic C is in root position, the resulting tonic A is in second inversion. 55 | 56 | That means to mirror a major to its relative minor you should pivot about the supertonic or the flattened submediant, and 57 | to mirror a minor to a major you should pivot about the leading tone or the subdominant. 58 | 59 | I've had considerable (perhaps too much) success using this to generate variations on melodies; though the results aren't 60 | always pleasing, they are often surprisingly convincing musically, if the source material is. 61 | 62 | ## Installation 63 | 64 | * Copy the appropriate plugin to your `MuseScore2/Plugins` or `MuseScore3/Plugins` directory 65 | * start MuseScore 66 | * enable the plugin via `Plugins > Plugin Manager...`. 67 | 68 | ## Usage 69 | 70 | Select a passage of music, invoke the plugin via `Plugins > Composing Tools > Mirror Intervals`, choose a pivot tone and apply. 71 | -------------------------------------------------------------------------------- /src/MirrorIntervals/mirror-intervals-2.qml: -------------------------------------------------------------------------------- 1 | // Mirror intervals chromatically about a given pivot note. 2 | // Copyright (C) 2018 Bill Hails 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import MuseScore 1.0 18 | import QtQuick 2.2 19 | import QtQuick.Controls 1.1 20 | import QtQuick.Controls.Styles 1.3 21 | import QtQuick.Layouts 1.1 22 | import QtQuick.Dialogs 1.1 23 | 24 | MuseScore { 25 | version: "2.3.2" 26 | menuPath: "Plugins.Composing Tools.Mirror Intervals" 27 | description: "Mirrors (inverts) intervals about a given pivot note" 28 | pluginType: "dialog" 29 | width: 250 30 | height: 150 31 | 32 | onRun: { 33 | if (!curScore) { 34 | error("No score open.\nThis plugin requires an open score to run.\n") 35 | Qt.quit() 36 | } 37 | } 38 | 39 | function applyMirrorIntervals() 40 | { 41 | var selection = getSelection() 42 | if (selection === null) { 43 | error("No selection.\nThis plugin requires a current selection to run.\n") 44 | Qt.quit() 45 | } 46 | curScore.startCmd() 47 | mapOverSelection(selection, filterNotes, mirrorIntervals(getMirrorType(), getPivotNote())) 48 | curScore.endCmd() 49 | } 50 | 51 | function mapOverSelection(selection, filter, process) { 52 | selection.cursor.rewind(1) 53 | for ( 54 | var segment = selection.cursor.segment; 55 | segment && segment.tick < selection.endTick; 56 | segment = segment.next 57 | ) { 58 | for (var track = selection.startTrack; track < selection.endTrack; track++) { 59 | var element = segment.elementAt(track) 60 | if (element) { 61 | if (filter(element)) { 62 | process(element, track) 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | function filterNotes(element) 70 | { 71 | return element.type == Element.CHORD 72 | } 73 | 74 | function mirrorIntervals(mirrorType, pivotNote) 75 | { 76 | if (mirrorType == 0) { 77 | return diatonicMirror(pivotNote) 78 | } else { 79 | return chromaticMirror(pivotNote) 80 | } 81 | } 82 | 83 | function chromaticMirror(pivotNote) 84 | { 85 | var pivots = []; 86 | return function(chord, track) { 87 | for (var i = 0; i < chord.notes.length; i++) { 88 | var note = chord.notes[i] 89 | note.tpc1 = lookupTpc(pivotNote, note.tpc1) 90 | note.tpc2 = lookupTpc(pivotNote, note.tpc2) 91 | if (!(track in pivots)) { 92 | pivots[track] = nearestPivot(pivotNote, note.pitch); 93 | } 94 | note.pitch = performPivot(pivots[track], note.pitch); 95 | } 96 | } 97 | } 98 | 99 | function nearestPivot(pivotNote, pitch) 100 | { 101 | var root = pitch - (pitch % 12) 102 | var pivot = root + pivotNote 103 | if ((pitch - pivot) > 6) { 104 | pivot += 12 105 | } else if ((pitch - pivot) < -6) { 106 | pivot -= 12 107 | } 108 | return pivot 109 | } 110 | 111 | function performPivot(pivot, pitch) 112 | { 113 | var diff = pivot - pitch; 114 | return pivot + diff 115 | } 116 | 117 | function diatonicMirror(pivotNote) 118 | { 119 | return function(chord) { 120 | error("diatonic\nnot implemented yet"); 121 | } 122 | } 123 | 124 | function getSelection() { 125 | var cursor = curScore.newCursor() 126 | cursor.rewind(1) 127 | if (!cursor.segment) { 128 | return null 129 | } 130 | var selection = { 131 | cursor: cursor, 132 | startTick: cursor.tick, 133 | endTick: null, 134 | startStaff: cursor.staffIdx, 135 | endStaff: null, 136 | startTrack: null, 137 | endTrack: null 138 | } 139 | cursor.rewind(2) 140 | selection.endStaff = cursor.staffIdx + 1 141 | if (cursor.tick == 0) { 142 | selection.endTick = curScore.lastSegment.tick + 1 143 | } else { 144 | selection.endTick = cursor.tick 145 | } 146 | selection.startTrack = selection.startStaff * 4 147 | selection.endTrack = selection.endStaff * 4 148 | return selection 149 | } 150 | 151 | property int mirrorType: 1 152 | 153 | function getMirrorType() 154 | { 155 | return mirrorType 156 | } 157 | 158 | function getPivotNote() 159 | { 160 | return pivotNote.model.get(pivotNote.currentIndex).note 161 | } 162 | 163 | function error(errorMessage) { 164 | errorDialog.text = qsTr(errorMessage) 165 | errorDialog.open() 166 | } 167 | 168 | property var tpcMap: [ 169 | [30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00, 23, 22, 21, 20], 170 | [32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00, 23, 22], 171 | [34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00], 172 | [12, 11, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02], 173 | [14, 13, 12, 11, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04], 174 | [28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00, 23, 22, 21, 20, 19, 18], 175 | [30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00, 23, 22, 21, 20], 176 | [32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00, 23, 22], 177 | [34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00], 178 | [12, 11, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02], 179 | [14, 13, 12, 11, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04], 180 | [28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00, 23, 22, 21, 20, 19, 18] 181 | ]; 182 | 183 | function lookupTpc(pivot, tpc) 184 | { 185 | // tpc starts at -1 186 | return tpcMap[pivot][tpc + 1] - 1; 187 | } 188 | 189 | Rectangle { 190 | color: "lightgrey" 191 | anchors.fill: parent 192 | 193 | GridLayout { 194 | columns: 2 195 | anchors.fill: parent 196 | anchors.margins: 10 197 | Label { 198 | text: "Pivot" 199 | } 200 | ComboBox { 201 | id: pivotNote 202 | model: ListModel { 203 | id: pivotNoteList 204 | ListElement { text: "G"; note: 7; } 205 | ListElement { text: "G♯"; note: 8; } 206 | ListElement { text: "A"; note: 9; } 207 | ListElement { text: "B♭"; note: 10; } 208 | ListElement { text: "B"; note: 11; } 209 | ListElement { text: "C"; note: 0; } 210 | ListElement { text: "C♯"; note: 1; } 211 | ListElement { text: "D"; note: 2; } 212 | ListElement { text: "E♭"; note: 3; } 213 | ListElement { text: "E"; note: 4; } 214 | ListElement { text: "F"; note: 5; } 215 | ListElement { text: "F♯"; note: 6; } 216 | } 217 | currentIndex: 5 218 | style: ComboBoxStyle { 219 | font.family: 'MScore Text' 220 | font.pointSize: 14 221 | } 222 | } 223 | Button { 224 | id: applyButton 225 | text: qsTranslate("PrefsDialogBase", "Apply") 226 | onClicked: { 227 | applyMirrorIntervals() 228 | Qt.quit() 229 | } 230 | } 231 | Button { 232 | id: cancelButton 233 | text: qsTranslate("PrefsDialogBase", "Cancel") 234 | onClicked: { 235 | Qt.quit() 236 | } 237 | } 238 | } 239 | } 240 | 241 | MessageDialog { 242 | id: errorDialog 243 | title: "Error" 244 | text: "" 245 | onAccepted: { 246 | Qt.quit() 247 | } 248 | visible: false 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/MirrorIntervals/mirror-intervals-3.qml: -------------------------------------------------------------------------------- 1 | // Mirror intervals chromatically about a given pivot note. 2 | // Copyright (C) 2018 Bill Hails 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import MuseScore 3.0 18 | import QtQuick 2.2 19 | import QtQuick.Controls 1.1 20 | import QtQuick.Controls.Styles 1.3 21 | import QtQuick.Layouts 1.1 22 | import QtQuick.Dialogs 1.1 23 | 24 | MuseScore { 25 | version: "3.0.2" 26 | menuPath: "Plugins.Composing Tools.Mirror Intervals" 27 | description: "Mirrors (inverts) intervals about a given pivot note" 28 | pluginType: "dialog" 29 | width: 250 30 | height: 150 31 | 32 | onRun: { 33 | if (!curScore) { 34 | error("No score open.\nThis plugin requires an open score to run.\n") 35 | Qt.quit() 36 | } 37 | } 38 | 39 | function applyMirrorIntervals() 40 | { 41 | var selection = getSelection() 42 | if (selection === null) { 43 | error("No selection.\nThis plugin requires a current selection to run.\n") 44 | Qt.quit() 45 | } 46 | curScore.startCmd() 47 | mapOverSelection(selection, filterNotes, mirrorIntervals(getMirrorType(), getPivotNote())) 48 | curScore.endCmd() 49 | } 50 | 51 | function mapOverSelection(selection, filter, process) { 52 | selection.cursor.rewind(1) 53 | for ( 54 | var segment = selection.cursor.segment; 55 | segment && segment.tick < selection.endTick; 56 | segment = segment.next 57 | ) { 58 | for (var track = selection.startTrack; track < selection.endTrack; track++) { 59 | var element = segment.elementAt(track) 60 | if (element) { 61 | if (filter(element)) { 62 | process(element, track) 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | function filterNotes(element) 70 | { 71 | return element.type == Ms.CHORD 72 | } 73 | 74 | function mirrorIntervals(mirrorType, pivotNote) 75 | { 76 | if (mirrorType == 0) { 77 | return diatonicMirror(pivotNote) 78 | } else { 79 | return chromaticMirror(pivotNote) 80 | } 81 | } 82 | 83 | function chromaticMirror(pivotNote) 84 | { 85 | var pivots = []; 86 | return function(chord, track) { 87 | for (var i = 0; i < chord.notes.length; i++) { 88 | var note = chord.notes[i] 89 | note.tpc1 = lookupTpc(pivotNote, note.tpc1) 90 | note.tpc2 = lookupTpc(pivotNote, note.tpc2) 91 | if (!(track in pivots)) { 92 | pivots[track] = nearestPivot(pivotNote, note.pitch); 93 | } 94 | note.pitch = performPivot(pivots[track], note.pitch); 95 | } 96 | } 97 | } 98 | 99 | function nearestPivot(pivotNote, pitch) 100 | { 101 | var root = pitch - (pitch % 12) 102 | var pivot = root + pivotNote 103 | if ((pitch - pivot) > 6) { 104 | pivot += 12 105 | } else if ((pitch - pivot) < -6) { 106 | pivot -= 12 107 | } 108 | return pivot 109 | } 110 | 111 | function performPivot(pivot, pitch) 112 | { 113 | var diff = pivot - pitch; 114 | return pivot + diff 115 | } 116 | 117 | function diatonicMirror(pivotNote) 118 | { 119 | return function(chord) { 120 | error("diatonic\nnot implemented yet"); 121 | } 122 | } 123 | 124 | function getSelection() { 125 | var cursor = curScore.newCursor() 126 | cursor.rewind(1) 127 | if (!cursor.segment) { 128 | return null 129 | } 130 | var selection = { 131 | cursor: cursor, 132 | startTick: cursor.tick, 133 | endTick: null, 134 | startStaff: cursor.staffIdx, 135 | endStaff: null, 136 | startTrack: null, 137 | endTrack: null 138 | } 139 | cursor.rewind(2) 140 | selection.endStaff = cursor.staffIdx + 1 141 | if (cursor.tick == 0) { 142 | selection.endTick = curScore.lastSegment.tick + 1 143 | } else { 144 | selection.endTick = cursor.tick 145 | } 146 | selection.startTrack = selection.startStaff * 4 147 | selection.endTrack = selection.endStaff * 4 148 | return selection 149 | } 150 | 151 | property int mirrorType: 1 152 | 153 | function getMirrorType() 154 | { 155 | return mirrorType 156 | } 157 | 158 | function getPivotNote() 159 | { 160 | return pivotNote.model.get(pivotNote.currentIndex).note 161 | } 162 | 163 | function error(errorMessage) { 164 | errorDialog.text = qsTr(errorMessage) 165 | errorDialog.open() 166 | } 167 | 168 | property var tpcMap: [ 169 | [30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00, 23, 22, 21, 20], 170 | [32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00, 23, 22], 171 | [34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00], 172 | [12, 11, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02], 173 | [14, 13, 12, 11, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04], 174 | [28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00, 23, 22, 21, 20, 19, 18], 175 | [30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00, 23, 22, 21, 20], 176 | [32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00, 23, 22], 177 | [34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00], 178 | [12, 11, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02], 179 | [14, 13, 12, 11, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04], 180 | [28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00, 23, 22, 21, 20, 19, 18] 181 | ]; 182 | 183 | function lookupTpc(pivot, tpc) 184 | { 185 | // tpc starts at -1 186 | return tpcMap[pivot][tpc + 1] - 1; 187 | } 188 | 189 | Rectangle { 190 | color: "lightgrey" 191 | anchors.fill: parent 192 | 193 | GridLayout { 194 | columns: 2 195 | anchors.fill: parent 196 | anchors.margins: 10 197 | Label { 198 | text: "Pivot" 199 | } 200 | ComboBox { 201 | id: pivotNote 202 | model: ListModel { 203 | id: pivotNoteList 204 | ListElement { text: "G"; note: 7; } 205 | ListElement { text: "G♯"; note: 8; } 206 | ListElement { text: "A"; note: 9; } 207 | ListElement { text: "B♭"; note: 10; } 208 | ListElement { text: "B"; note: 11; } 209 | ListElement { text: "C"; note: 0; } 210 | ListElement { text: "C♯"; note: 1; } 211 | ListElement { text: "D"; note: 2; } 212 | ListElement { text: "E♭"; note: 3; } 213 | ListElement { text: "E"; note: 4; } 214 | ListElement { text: "F"; note: 5; } 215 | ListElement { text: "F♯"; note: 6; } 216 | } 217 | currentIndex: 5 218 | style: ComboBoxStyle { 219 | font.family: 'MScore Text' 220 | font.pointSize: 14 221 | } 222 | } 223 | Button { 224 | id: applyButton 225 | text: qsTranslate("PrefsDialogBase", "Apply") 226 | onClicked: { 227 | applyMirrorIntervals() 228 | Qt.quit() 229 | } 230 | } 231 | Button { 232 | id: cancelButton 233 | text: qsTranslate("PrefsDialogBase", "Cancel") 234 | onClicked: { 235 | Qt.quit() 236 | } 237 | } 238 | } 239 | } 240 | 241 | MessageDialog { 242 | id: errorDialog 243 | title: "Error" 244 | text: "" 245 | onAccepted: { 246 | Qt.quit() 247 | } 248 | visible: false 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/MirrorIntervals/mirror-intervals.qml.sh: -------------------------------------------------------------------------------- 1 | cat <. 17 | 18 | import MuseScore $MuseScoreImport 19 | import QtQuick 2.2 20 | import QtQuick.Controls 1.1 21 | import QtQuick.Controls.Styles 1.3 22 | import QtQuick.Layouts 1.1 23 | import QtQuick.Dialogs 1.1 24 | 25 | MuseScore { 26 | version: "$MuseScoreVersion" 27 | menuPath: "Plugins.Composing Tools.Mirror Intervals" 28 | description: "Mirrors (inverts) intervals about a given pivot note" 29 | pluginType: "dialog" 30 | width: 250 31 | height: 150 32 | 33 | onRun: { 34 | if (!curScore) { 35 | error("No score open.\\nThis plugin requires an open score to run.\\n") 36 | Qt.quit() 37 | } 38 | } 39 | 40 | function applyMirrorIntervals() 41 | { 42 | var selection = getSelection() 43 | if (selection === null) { 44 | error("No selection.\\nThis plugin requires a current selection to run.\\n") 45 | Qt.quit() 46 | } 47 | curScore.startCmd() 48 | mapOverSelection(selection, filterNotes, mirrorIntervals(getMirrorType(), getPivotNote())) 49 | curScore.endCmd() 50 | } 51 | 52 | function mapOverSelection(selection, filter, process) { 53 | selection.cursor.rewind(1) 54 | for ( 55 | var segment = selection.cursor.segment; 56 | segment && segment.tick < selection.endTick; 57 | segment = segment.next 58 | ) { 59 | for (var track = selection.startTrack; track < selection.endTrack; track++) { 60 | var element = segment.elementAt(track) 61 | if (element) { 62 | if (filter(element)) { 63 | process(element, track) 64 | } 65 | } 66 | } 67 | } 68 | } 69 | 70 | function filterNotes(element) 71 | { 72 | return element.type == $Namespace.CHORD 73 | } 74 | 75 | function mirrorIntervals(mirrorType, pivotNote) 76 | { 77 | if (mirrorType == 0) { 78 | return diatonicMirror(pivotNote) 79 | } else { 80 | return chromaticMirror(pivotNote) 81 | } 82 | } 83 | 84 | function chromaticMirror(pivotNote) 85 | { 86 | var pivots = []; 87 | return function(chord, track) { 88 | for (var i = 0; i < chord.notes.length; i++) { 89 | var note = chord.notes[i] 90 | note.tpc1 = lookupTpc(pivotNote, note.tpc1) 91 | note.tpc2 = lookupTpc(pivotNote, note.tpc2) 92 | if (!(track in pivots)) { 93 | pivots[track] = nearestPivot(pivotNote, note.pitch); 94 | } 95 | note.pitch = performPivot(pivots[track], note.pitch); 96 | } 97 | } 98 | } 99 | 100 | function nearestPivot(pivotNote, pitch) 101 | { 102 | var root = pitch - (pitch % 12) 103 | var pivot = root + pivotNote 104 | if ((pitch - pivot) > 6) { 105 | pivot += 12 106 | } else if ((pitch - pivot) < -6) { 107 | pivot -= 12 108 | } 109 | return pivot 110 | } 111 | 112 | function performPivot(pivot, pitch) 113 | { 114 | var diff = pivot - pitch; 115 | return pivot + diff 116 | } 117 | 118 | function diatonicMirror(pivotNote) 119 | { 120 | return function(chord) { 121 | error("diatonic\\nnot implemented yet"); 122 | } 123 | } 124 | 125 | function getSelection() { 126 | var cursor = curScore.newCursor() 127 | cursor.rewind(1) 128 | if (!cursor.segment) { 129 | return null 130 | } 131 | var selection = { 132 | cursor: cursor, 133 | startTick: cursor.tick, 134 | endTick: null, 135 | startStaff: cursor.staffIdx, 136 | endStaff: null, 137 | startTrack: null, 138 | endTrack: null 139 | } 140 | cursor.rewind(2) 141 | selection.endStaff = cursor.staffIdx + 1 142 | if (cursor.tick == 0) { 143 | selection.endTick = curScore.lastSegment.tick + 1 144 | } else { 145 | selection.endTick = cursor.tick 146 | } 147 | selection.startTrack = selection.startStaff * 4 148 | selection.endTrack = selection.endStaff * 4 149 | return selection 150 | } 151 | 152 | property int mirrorType: 1 153 | 154 | function getMirrorType() 155 | { 156 | return mirrorType 157 | } 158 | 159 | function getPivotNote() 160 | { 161 | return pivotNote.model.get(pivotNote.currentIndex).note 162 | } 163 | 164 | function error(errorMessage) { 165 | errorDialog.text = qsTr(errorMessage) 166 | errorDialog.open() 167 | } 168 | 169 | property var tpcMap: [ 170 | [30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00, 23, 22, 21, 20], 171 | [32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00, 23, 22], 172 | [34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00], 173 | [12, 11, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02], 174 | [14, 13, 12, 11, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04], 175 | [28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00, 23, 22, 21, 20, 19, 18], 176 | [30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00, 23, 22, 21, 20], 177 | [32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00, 23, 22], 178 | [34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00], 179 | [12, 11, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02], 180 | [14, 13, 12, 11, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04], 181 | [28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01, 00, 23, 22, 21, 20, 19, 18] 182 | ]; 183 | 184 | function lookupTpc(pivot, tpc) 185 | { 186 | // tpc starts at -1 187 | return tpcMap[pivot][tpc + 1] - 1; 188 | } 189 | 190 | Rectangle { 191 | color: "lightgrey" 192 | anchors.fill: parent 193 | 194 | GridLayout { 195 | columns: 2 196 | anchors.fill: parent 197 | anchors.margins: 10 198 | Label { 199 | text: "Pivot" 200 | } 201 | ComboBox { 202 | id: pivotNote 203 | model: ListModel { 204 | id: pivotNoteList 205 | ListElement { text: "G"; note: 7; } 206 | ListElement { text: "G♯"; note: 8; } 207 | ListElement { text: "A"; note: 9; } 208 | ListElement { text: "B♭"; note: 10; } 209 | ListElement { text: "B"; note: 11; } 210 | ListElement { text: "C"; note: 0; } 211 | ListElement { text: "C♯"; note: 1; } 212 | ListElement { text: "D"; note: 2; } 213 | ListElement { text: "E♭"; note: 3; } 214 | ListElement { text: "E"; note: 4; } 215 | ListElement { text: "F"; note: 5; } 216 | ListElement { text: "F♯"; note: 6; } 217 | } 218 | currentIndex: 5 219 | style: ComboBoxStyle { 220 | font.family: 'MScore Text' 221 | font.pointSize: 14 222 | } 223 | } 224 | Button { 225 | id: applyButton 226 | text: qsTranslate("PrefsDialogBase", "Apply") 227 | onClicked: { 228 | applyMirrorIntervals() 229 | Qt.quit() 230 | } 231 | } 232 | Button { 233 | id: cancelButton 234 | text: qsTranslate("PrefsDialogBase", "Cancel") 235 | onClicked: { 236 | Qt.quit() 237 | } 238 | } 239 | } 240 | } 241 | 242 | MessageDialog { 243 | id: errorDialog 244 | title: "Error" 245 | text: "" 246 | onAccepted: { 247 | Qt.quit() 248 | } 249 | visible: false 250 | } 251 | } 252 | EOQML 253 | -------------------------------------------------------------------------------- /src/PivotChords/PivotChords.qml: -------------------------------------------------------------------------------- 1 | // Pivot Chords 2 | // 3 | // Copyright (C) 2018 Bill Hails 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | 18 | import MuseScore 1.0 19 | import QtQuick 2.2 20 | import QtQuick.Controls 1.1 21 | import QtQuick.Controls.Styles 1.3 22 | import QtQuick.Layouts 1.1 23 | import QtQuick.Dialogs 1.1 24 | 25 | MuseScore { 26 | version: "1.0.0" 27 | menuPath: "Plugins.Composing Tools.Pivot Chords" 28 | description: "Compositional Assistant" 29 | pluginType: "dialog" 30 | width: 400 31 | height: 250 32 | 33 | function pivotChords() { 34 | 35 | var chordsOfMajor = { 36 | I: [0, 4, 7], 37 | Ne: [1, 5, 8], 38 | ii: [2, 5, 9], 39 | iii: [4, 7, 11], 40 | IV: [5, 9, 0], 41 | iv: [5, 8, 0], 42 | V: [7, 11, 2], 43 | V7: [7, 11, 2, 5], 44 | Ger: [8, 0, 3, 6], 45 | It: [8, 0, 6], 46 | Fr: [8, 0, 2, 6], 47 | vi: [9, 0, 4], 48 | vii: [11, 2, 5, 8] 49 | }; 50 | 51 | var chordsOfMinor = { 52 | i: [0, 3, 7], 53 | Ne: [1, 5, 8], 54 | iid: [2, 5, 8], 55 | ii: [2, 5, 9], 56 | III: [3, 7, 10], 57 | IIIa: [3, 7, 11], 58 | IV: [5, 9, 0], 59 | iv: [5, 8, 0], 60 | v: [7, 10, 2], 61 | V: [7, 11, 2], 62 | V7: [7, 11, 2, 5], 63 | Ger: [8, 0, 3, 6], 64 | It: [8, 0, 6], 65 | Fr: [8, 0, 2, 6], 66 | VI: [8, 0, 3], 67 | vid: [9, 0, 3, 6], 68 | vii: [11, 2, 5, 8] 69 | }; 70 | 71 | var keys = [ 72 | ["C"], 73 | ["C#", "Db"], 74 | ["D"], 75 | ["D#", "Eb"], 76 | ["E"], 77 | ["F"], 78 | ["F#", "Gb"], 79 | ["G"], 80 | ["G#", "Ab"], 81 | ["A"], 82 | ["A#", "Bb"], 83 | ["B"] 84 | ]; 85 | 86 | this.Key = function(name, mode) { 87 | var found = false; 88 | var position = 0; 89 | keys.forEach( 90 | function(validNames, index, arr) { 91 | validNames.forEach( 92 | function(validName, i, arr) { 93 | if (name == validName) { 94 | found = true; 95 | position = index; 96 | } 97 | } 98 | ) 99 | } 100 | ); 101 | if (!found) { 102 | throw("invalid key: " + name); 103 | } 104 | if (mode != "Major" && mode != "Minor") { 105 | throw("invalid mode: " + mode); 106 | } 107 | this.position = position; 108 | this.name = name; 109 | this.mode = mode; 110 | } 111 | 112 | this.findPivots = function (key1, key2) { 113 | var chords1 = chordsOfKey(key1); 114 | var chords2 = chordsOfKey(key2); 115 | return commonChords(chords1, chords2); 116 | } 117 | 118 | function chordsOfKey(key) { 119 | if (key.mode == "Major") { 120 | var chords = chordsOfMajor; 121 | } else { 122 | var chords = chordsOfMinor; 123 | } 124 | 125 | var result = {}; 126 | for (var chord in chords) { 127 | result[chord] = addOffset(key.position, chords[chord]); 128 | } 129 | 130 | return result; 131 | } 132 | 133 | function addOffset(offset, chord) { 134 | var result = []; 135 | for (var note in chord) { 136 | result.push((chord[note] + offset) % 12); 137 | } 138 | return result.sort(function(a, b){return a - b}) 139 | } 140 | 141 | function commonChords(chords1, chords2) { 142 | var common = []; 143 | for (var chord1 in chords1) { 144 | for (var chord2 in chords2) { 145 | if (chordsAreEqual(chords1[chord1], chords2[chord2])) { 146 | common.push([chord1, chord2, positionsToNotes(chords1[chord1])]); 147 | } 148 | } 149 | } 150 | return common; 151 | } 152 | 153 | function chordsAreEqual(chord1, chord2) { 154 | if (chord1.length != chord2.length) { 155 | return false; 156 | } 157 | var isEq = true; 158 | function cmp(val, index, arr) { 159 | if (val != chord2[index]) { 160 | isEq = false; 161 | } 162 | } 163 | chord1.forEach(cmp); 164 | return isEq; 165 | } 166 | 167 | function positionsToNotes(chord) { 168 | return chord.map( 169 | function(value, index, arr) { 170 | return keys[value][0]; 171 | } 172 | ); 173 | } 174 | } 175 | 176 | property string fromMode: "Major" 177 | property string toMode: "Major" 178 | 179 | function tellPivotChords() { 180 | var fromKey = fromKeyBox.model.get(fromKeyBox.currentIndex).key 181 | var toKey = toKeyBox.model.get(toKeyBox.currentIndex).key 182 | var finder = new pivotChords(); 183 | var result = finder.findPivots(new finder.Key(fromKey, fromMode), new finder.Key(toKey, toMode)); 184 | outputText.text = outputText.text + "

To modulate from " + fromKey + " " + fromMode + 185 | " to " + toKey + " " + toMode + "

"; 186 | for (var i in result) { 187 | outputText.text = outputText.text + "" + result[i][0] + " is " + result[i][1] + " (" + 188 | result[i][2].toString() + 189 | ")
"; 190 | } 191 | } 192 | 193 | Rectangle { 194 | color: "lightgrey" 195 | anchors.fill: parent 196 | 197 | GridLayout { 198 | columns: 3 199 | anchors.fill: parent 200 | anchors.margins: 10 201 | Label { 202 | text: qsTr("From") 203 | } 204 | ComboBox { 205 | id: fromKeyBox 206 | model: ListModel { 207 | id: fromKeyList 208 | ListElement { text: "C"; key: "C" } 209 | ListElement { text: "C#/Db"; key: "C#" } 210 | ListElement { text: "D"; key: "D" } 211 | ListElement { text: "D#/Eb"; key: "Eb" } 212 | ListElement { text: "E"; key: "E" } 213 | ListElement { text: "F"; key: "F" } 214 | ListElement { text: "F#/Gb"; key: "F#" } 215 | ListElement { text: "G"; key: "G" } 216 | ListElement { text: "G#/Ab"; key: "Ab" } 217 | ListElement { text: "A"; key: "A" } 218 | ListElement { text: "A#/Bb"; key: "Bb" } 219 | ListElement { text: "B"; key: "B" } 220 | } 221 | currentIndex: 0 222 | } 223 | RowLayout { 224 | ExclusiveGroup { id: fromModeGroup } 225 | RadioButton { 226 | text: "Maj" 227 | checked: true 228 | exclusiveGroup: fromModeGroup 229 | onClicked: { 230 | fromMode = "Major" 231 | } 232 | } 233 | RadioButton { 234 | text: "Min" 235 | checked: false 236 | exclusiveGroup: fromModeGroup 237 | onClicked: { 238 | fromMode = "Minor" 239 | } 240 | } 241 | } 242 | Label { 243 | text: qsTr("To") 244 | } 245 | ComboBox { 246 | id: toKeyBox 247 | model: ListModel { 248 | id: toKeyList 249 | ListElement { text: "C"; key: "C" } 250 | ListElement { text: "C#/Db"; key: "C#" } 251 | ListElement { text: "D"; key: "D" } 252 | ListElement { text: "D#/Eb"; key: "Eb" } 253 | ListElement { text: "E"; key: "E" } 254 | ListElement { text: "F"; key: "F" } 255 | ListElement { text: "F#/Gb"; key: "F#" } 256 | ListElement { text: "G"; key: "G" } 257 | ListElement { text: "G#/Ab"; key: "Ab" } 258 | ListElement { text: "A"; key: "A" } 259 | ListElement { text: "A#/Bb"; key: "Bb" } 260 | ListElement { text: "B"; key: "B" } 261 | } 262 | currentIndex: 0 263 | } 264 | RowLayout { 265 | ExclusiveGroup { id: toModeGroup } 266 | RadioButton { 267 | text: "Maj" 268 | checked: true 269 | exclusiveGroup: toModeGroup 270 | onClicked: { 271 | toMode = "Major" 272 | } 273 | } 274 | RadioButton { 275 | text: "Min" 276 | checked: false 277 | exclusiveGroup: toModeGroup 278 | onClicked: { 279 | toMode = "Minor" 280 | } 281 | } 282 | } 283 | RowLayout { 284 | Layout.columnSpan: 3 285 | GridLayout { 286 | columns: 1 287 | anchors.fill: parent 288 | anchors.margins: 10 289 | Button { 290 | id: tellButton 291 | text: qsTr("Tell") 292 | onClicked: { 293 | tellPivotChords() 294 | } 295 | } 296 | Button { 297 | id: copyButton 298 | text: "Copy" 299 | onClicked: { 300 | outputText.selectAll() 301 | outputText.copy() 302 | outputText.deselect() 303 | } 304 | } 305 | Button { 306 | id: quitBuuton 307 | text: "Quit" 308 | onClicked: { 309 | Qt.quit(); 310 | } 311 | } 312 | } 313 | GridLayout { // padding 314 | columns: 1 315 | anchors.fill: parent 316 | anchors.margins: 10 317 | } 318 | TextArea { 319 | id: outputText 320 | readOnly: true 321 | text: "" 322 | textFormat: TextEdit.RichText 323 | } 324 | } 325 | } 326 | } 327 | } 328 | 329 | // vim: ft=javascript 330 | -------------------------------------------------------------------------------- /src/PivotChords/README.md: -------------------------------------------------------------------------------- 1 | # Pivot Chords 2 | 3 | Given two keys, displays the chords that those keys have in common. 4 | 5 | Such chords are known as "pivot chords" and can be used to modulate between those two keys. 6 | 7 | ## Installation 8 | 9 | * Copy the file `PivotChords.qml` into your MuseScore2 Plugins directory. 10 | * Start MuseScore2. 11 | * Go to Plugins > Plugin Manager. 12 | * Tick the box next to PivotChords. 13 | 14 | ## Usage 15 | 16 | * Navigate to Plugins > Composing Tools > Pivot Chords. 17 | * The pop up window allows you to select a "from" key and mode (Major or Minor) and a "to" key and mode. 18 | * Click the "Tell" button and a description of the possible pivot chords is shown in the textarea. 19 | * You can choose any number of key combinations, click "Tell" after each one, and the instructions in the text area will accumulate. 20 | * The text in the text area can then be copied to your clipboard with the "Copy" button, from whence you can paste it into a notepad or similar application. 21 | * Finally the "Quit" button quits the plugin. 22 | 23 | ## Notes on the Output 24 | 25 | Each pivot chord is described as its function in the first key and its function in the second key, in the form "func1 is func2". For 26 | those users that don't understand roman numeral analysis, the notes of the chord are supplied in brackets. 27 | 28 | ## Limitations 29 | 30 | * The plugin does not supply any canonical name for the chords, but it wouldn't be hard to do. 31 | * Because of the way the plugin works, the notes of the chord are not guaranteed to be in root position. 32 | * The plugin is really intended for people who already know about modulation, as well as the various chords and their uses. 33 | -------------------------------------------------------------------------------- /src/Tuning/2.x/tuning.qml: -------------------------------------------------------------------------------- 1 | // Apply a choice of tempraments and tunings to a selection 2 | // Copyright (C) 2018-2019 Bill Hails 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import MuseScore 1.0 18 | import QtQuick 2.2 19 | import QtQuick.Controls 1.1 20 | import QtQuick.Controls.Styles 1.3 21 | import QtQuick.Layouts 1.1 22 | import QtQuick.Dialogs 1.1 23 | import FileIO 1.0 24 | 25 | MuseScore { 26 | version: "2.3.2" 27 | menuPath: "Plugins.Playback.Tuning" 28 | description: "Converts between tuning systems" 29 | pluginType: "dialog" 30 | width: 510 31 | height: 500 32 | 33 | property var offsetTextWidth: 40; 34 | property var offsetLabelAlignment: 0x02 | 0x80; 35 | 36 | property var history: 0; 37 | 38 | // set true if customisations are made to the tuning 39 | property var modified: false; 40 | 41 | /** 42 | * See http://leware.net/temper/temper.htm and specifically http://leware.net/temper/cents.htm 43 | * 44 | * I've taken the liberty of adding the Bach/Lehman temperament http://www.larips.com which was 45 | * my original motivation for doing this. 46 | * 47 | * These values are in cents. One cent is defined as 100th of an equal tempered semitone. 48 | * Each row is ordered in the cycle of fifths, so C, G, D, A, E, B, F#, C#, G#/Ab, Eb, Bb, F; 49 | * and the values are offsets from the equal tempered value. 50 | * 51 | * However for tunings who's default root note is not C, the values are pre-rotated so that applying the 52 | * root note rotation will put the first value of the sequence at the root note. 53 | */ 54 | property var equal: { 55 | 'offsets': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 56 | 'root': 0, 57 | 'pure': 0, 58 | 'name': "equal" 59 | } 60 | property var pythagorean: { 61 | 'offsets': [-6.0, -4.0, -2.0, 0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0], 62 | 'root': 9, 63 | 'pure': 3, 64 | 'name': "pythagorean" 65 | } 66 | property var aaron: { 67 | 'offsets': [10.5, 7.0, 3.5, 0.0, -3.5, -7.0, -10.5, -14.0, -17.5, -21.0, -24.5, -28.0], 68 | 'root': 9, 69 | 'pure': 3, 70 | 'name': "aaron" 71 | } 72 | property var silberman: { 73 | 'offsets': [5.0, 3.3, 1.7, 0.0, -1.7, -3.3, -5.0, -6.7, -8.3, -10.0, -11.7, -13.3], 74 | 'root': 9, 75 | 'pure': 3, 76 | 'name': "silberman" 77 | } 78 | property var salinas: { 79 | 'offsets': [16.0, 10.7, 5.3, 0.0, -5.3, -10.7, -16.0, -21.3, -26.7, -32.0, -37.3, -42.7], 80 | 'root': 9, 81 | 'pure': 3, 82 | 'name': "salinas" 83 | } 84 | property var kirnberger: { 85 | 'offsets': [0.0, -3.5, -7.0, -10.5, -14.0, -12.0, -10.0, -10.0, -8.0, -6.0, -4.0, -2.0], 86 | 'root': 0, 87 | 'pure': 0, 88 | 'name': "kirnberger" 89 | } 90 | property var vallotti: { 91 | 'offsets': [0.0, -2.0, -4.0, -6.0, -8.0, -10.0, -8.0, -6.0, -4.0, -2.0, 0.0, 2.0], 92 | 'root': 0, 93 | 'pure': 0, 94 | 'name': "vallotti" 95 | } 96 | property var werkmeister: { 97 | 'offsets': [0.0, -4.0, -8.0, -12.0, -10.0, -8.0, -12.0, -10.0, -8.0, -6.0, -4.0, -2.0], 98 | 'root': 0, 99 | 'pure': 0, 100 | 'name': "werkmeister" 101 | } 102 | property var marpurg: { 103 | 'offsets': [0.0, 2.0, 4.0, 6.0, 0.0, 2.0, 4.0, 6.0, 0.0, 2.0, 4.0, 6.0], 104 | 'root': 0, 105 | 'pure': 0, 106 | 'name': "marpurg" 107 | } 108 | property var just: { 109 | 'offsets': [0.0, 2.0, 4.0, -16.0, -14.0, -12.0, -10.0, -30.0, -28.0, 16.0, 18.0, -2.0], 110 | 'root': 0, 111 | 'pure': 0, 112 | 'name': "just" 113 | } 114 | property var meanSemitone: { 115 | 'offsets': [0.0, -3.5, -7.0, -10.5, -14.0, 3.5, 0.0, -3.5, -7.0, -10.5, -14.0, -17.5], 116 | 'root': 6, 117 | 'pure': 6, 118 | 'name': "meanSemitone" 119 | } 120 | property var grammateus: { 121 | 'offsets': [-2.0, 0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 0.0, 2.0, 4.0, 6.0, 8.0], 122 | 'root': 11, 123 | 'pure': 1, 124 | 'name': "grammateus" 125 | } 126 | property var french: { 127 | 'offsets': [0.0, -2.5, -5.0, -7.5, -10.0, -12.5, -13.0, -13.0, -11.0, -6.0, -1.5, 2.5], 128 | 'root': 0, 129 | 'pure': 0, 130 | 'name': "french" 131 | } 132 | property var french2: { 133 | 'offsets': [0.0, -3.5, -7.0, -10.5, -14.0, -17.5, -18.2, -19.0, -17.0, -10.5, -3.5, 3.5], 134 | 'root': 0, 135 | 'pure': 0, 136 | 'name': "french2" 137 | } 138 | property var rameau: { 139 | 'offsets': [0.0, -3.5, -7.0, -10.5, -14.0, -17.5, -15.5, -13.5, -11.5, -2.0, 7.0, 3.5], 140 | 'root': 0, 141 | 'pure': 0, 142 | 'name': "rameau" 143 | } 144 | property var irrFr17e: { 145 | 'offsets': [-8.0, -2.0, 3.0, 0.0, -3.0, -6.0, -9.0, -12.0, -15.0, -18.0, -21.0, -24.0], 146 | 'root': 9, 147 | 'pure': 3, 148 | 'name': "irrFr17e" 149 | } 150 | property var bachLehman: { 151 | 'offsets': [0.0, -2.0, -3.9, -5.9, -7.8, -5.9, -3.9, -2.0, -2.0, -2.0, -2.0, 2.0], 152 | 'root': 0, 153 | 'pure': 3, 154 | 'name': "bachLehman" 155 | } 156 | 157 | property var currentTemperament: equal; 158 | property var currentRoot: 0; 159 | property var currentPureTone: 0; 160 | 161 | onRun: { 162 | if (!curScore) { 163 | error("No score open.\nThis plugin requires an open score to run.\n") 164 | Qt.quit() 165 | } 166 | } 167 | 168 | function getHistory() { 169 | if (history == 0) { 170 | history = new commandHistory() 171 | } 172 | return history 173 | } 174 | 175 | function applyTemperament() 176 | { 177 | var selection = getSelection() 178 | if (selection === null) { 179 | error("No selection.\nThis plugin requires a current selection to run.\n") 180 | return false 181 | } else { 182 | curScore.startCmd() 183 | mapOverSelection(selection, filterNotes, reTune(getFinalTuning())) 184 | curScore.endCmd() 185 | return true 186 | } 187 | } 188 | 189 | function mapOverSelection(selection, filter, process) { 190 | selection.cursor.rewind(1) 191 | for ( 192 | var segment = selection.cursor.segment; 193 | segment && segment.tick < selection.endTick; 194 | segment = segment.next 195 | ) { 196 | for (var track = selection.startTrack; track < selection.endTrack; track++) { 197 | var element = segment.elementAt(track) 198 | if (element) { 199 | if (filter(element)) { 200 | process(element) 201 | } 202 | } 203 | } 204 | } 205 | } 206 | 207 | function filterNotes(element) 208 | { 209 | return element.type == Element.CHORD 210 | } 211 | 212 | function reTune(tuning) 213 | { 214 | return function(chord) { 215 | for (var i = 0; i < chord.notes.length; i++) { 216 | var note = chord.notes[i] 217 | note.tuning = tuning(note.pitch) 218 | } 219 | } 220 | } 221 | 222 | function getSelection() { 223 | var cursor = curScore.newCursor() 224 | cursor.rewind(1) 225 | if (!cursor.segment) { 226 | return null 227 | } 228 | var selection = { 229 | cursor: cursor, 230 | startTick: cursor.tick, 231 | endTick: null, 232 | startStaff: cursor.staffIdx, 233 | endStaff: null, 234 | startTrack: null, 235 | endTrack: null 236 | } 237 | cursor.rewind(2) 238 | selection.endStaff = cursor.staffIdx + 1 239 | if (cursor.tick == 0) { 240 | selection.endTick = curScore.lastSegment.tick + 1 241 | } else { 242 | selection.endTick = cursor.tick 243 | } 244 | selection.startTrack = selection.startStaff * 4 245 | selection.endTrack = selection.endStaff * 4 246 | return selection 247 | } 248 | 249 | function error(errorMessage) { 250 | errorDialog.text = qsTr(errorMessage) 251 | errorDialog.open() 252 | } 253 | 254 | /** 255 | * map a note (pitch modulo 12) to a value in one of the above tables 256 | * then adjust for the choice of pure note. 257 | */ 258 | function lookUp(note, table) { 259 | var i = ((note * 7) - currentRoot + 12) % 12; 260 | var offset = table.offsets[i]; 261 | var j = (currentPureTone - currentRoot + 12) % 12; 262 | var pureNoteAdjustment = table.offsets[j]; 263 | var finalOffset = offset - pureNoteAdjustment; 264 | return finalOffset 265 | } 266 | 267 | /** 268 | * subtract the equal tempered value from the current tuning value to get the tuning offset. 269 | */ 270 | function getTuning() { 271 | return function(pitch) { 272 | return lookUp(pitch, currentTemperament); 273 | } 274 | } 275 | 276 | function getFinalTuning() { 277 | return function(pitch) { 278 | pitch = pitch % 12 279 | switch (pitch) { 280 | case 0: 281 | return getFinalOffset(final_c) 282 | case 1: 283 | return getFinalOffset(final_c_sharp) 284 | case 2: 285 | return getFinalOffset(final_d) 286 | case 3: 287 | return getFinalOffset(final_e_flat) 288 | case 4: 289 | return getFinalOffset(final_e) 290 | case 5: 291 | return getFinalOffset(final_f) 292 | case 6: 293 | return getFinalOffset(final_f_sharp) 294 | case 7: 295 | return getFinalOffset(final_g) 296 | case 8: 297 | return getFinalOffset(final_g_sharp) 298 | case 9: 299 | return getFinalOffset(final_a) 300 | case 10: 301 | return getFinalOffset(final_b_flat) 302 | case 11: 303 | return getFinalOffset(final_b) 304 | default: 305 | error("unrecognised pitch: " + pitch) 306 | } 307 | } 308 | } 309 | 310 | function getFinalOffset(textField) { 311 | return parseFloat(textField.text) 312 | } 313 | 314 | function recalculate(tuning) { 315 | console.log("recalculate") 316 | var old_final_c = final_c.text 317 | var old_final_c_sharp = final_c_sharp.text 318 | var old_final_d = final_d.text 319 | var old_final_e_flat = final_e_flat.text 320 | var old_final_e = final_e.text 321 | var old_final_f = final_f.text 322 | var old_final_f_sharp = final_f_sharp.text 323 | var old_final_g = final_g.text 324 | var old_final_g_sharp = final_g_sharp.text 325 | var old_final_a = final_a.text 326 | var old_final_b_flat = final_b_flat.text 327 | var old_final_b = final_b.text 328 | getHistory().add( 329 | function () { 330 | final_c.text = old_final_c 331 | final_c.previousText = old_final_c 332 | final_c_sharp.text = old_final_c_sharp 333 | final_c_sharp.previousText = old_final_c_sharp 334 | final_d.text = old_final_d 335 | final_d.previousText = old_final_d 336 | final_e_flat.text = old_final_e_flat 337 | final_e_flat.previousText = old_final_e_flat 338 | final_e.text = old_final_e 339 | final_e.previousText = old_final_e 340 | final_f.text = old_final_f 341 | final_f.previousText = old_final_f 342 | final_f_sharp.text = old_final_f_sharp 343 | final_f_sharp.previousText = old_final_f_sharp 344 | final_g.text = old_final_g 345 | final_g.previousText = old_final_g 346 | final_g_sharp.text = old_final_g_sharp 347 | final_g_sharp.previousText = old_final_g_sharp 348 | final_a.text = old_final_a 349 | final_a.previousText = old_final_a 350 | final_b_flat.text = old_final_b_flat 351 | final_b_flat.previousText = old_final_b_flat 352 | final_b.text = old_final_b 353 | final_b.previousText = old_final_b 354 | }, 355 | function() { 356 | final_c.text = tuning(0).toFixed(1) 357 | final_c.previousText = final_c.text 358 | final_c_sharp.text = tuning(1).toFixed(1) 359 | final_c_sharp.previousText = final_c_sharp.text 360 | final_d.text = tuning(2).toFixed(1) 361 | final_d.previousText = final_d.text 362 | final_e_flat.text = tuning(3).toFixed(1) 363 | final_e_flat.previousText = final_e_flat.text 364 | final_e.text = tuning(4).toFixed(1) 365 | final_e.previousText = final_e.text 366 | final_f.text = tuning(5).toFixed(1) 367 | final_f.previousText = final_f.text 368 | final_f_sharp.text = tuning(6).toFixed(1) 369 | final_f_sharp.previousText = final_f_sharp.text 370 | final_g.text = tuning(7).toFixed(1) 371 | final_g.previousText = final_g.text 372 | final_g_sharp.text = tuning(8).toFixed(1) 373 | final_g_sharp.previousText = final_g_sharp.text 374 | final_a.text = tuning(9).toFixed(1) 375 | final_a.previousText = final_a.text 376 | final_b_flat.text = tuning(10).toFixed(1) 377 | final_b_flat.previousText = final_b_flat.text 378 | final_b.text = tuning(11).toFixed(1) 379 | final_b.previousText = final_b.text 380 | }, 381 | "final offsets" 382 | ) 383 | } 384 | 385 | function setCurrentTemperament(temperament) { 386 | console.log("setCurrentTemperament " + temperament.name) 387 | var oldTemperament = currentTemperament 388 | getHistory().add( 389 | function() { 390 | currentTemperament = oldTemperament 391 | checkCurrentTemperament() 392 | }, 393 | function() { 394 | currentTemperament = temperament 395 | checkCurrentTemperament() 396 | }, 397 | "current temperament" 398 | ) 399 | } 400 | 401 | function checkCurrentTemperament() { 402 | switch (currentTemperament.name) { 403 | case "equal": 404 | equal_button.checked = true 405 | return 406 | case "pythagorean": 407 | pythagorean_button.checked = true 408 | return 409 | case "aaron": 410 | aaron_button.checked = true 411 | return 412 | case "silberman": 413 | silberman_button.checked = true 414 | return 415 | case "salinas": 416 | salinas_button.checked = true 417 | return 418 | case "kirnberger": 419 | kirnberger_button.checked = true 420 | return 421 | case "vallotti": 422 | vallotti_button.checked = true 423 | return 424 | case "werkmeister": 425 | werkmeister_button.checked = true 426 | return 427 | case "marpurg": 428 | marpurg_button.checked = true 429 | return 430 | case "just": 431 | just_button.checked = true 432 | return 433 | case "meanSemitone": 434 | meanSemitone_button.checked = true 435 | return 436 | case "grammateus": 437 | grammateus_button.checked = true 438 | return 439 | case "french": 440 | french_button.checked = true 441 | return 442 | case "french2": 443 | french2_button.checked = true 444 | return 445 | case "rameau": 446 | rameau_button.checked = true 447 | return 448 | case "irrFr17e": 449 | irrFr17e_button.checked = true 450 | return 451 | case "bachLehman": 452 | bachLehman_button.checked = true 453 | return 454 | } 455 | } 456 | 457 | function lookupTemperament(temperamentName) { 458 | switch (temperamentName) { 459 | case "equal": 460 | return equal 461 | case "pythagorean": 462 | return pythagorean 463 | case "aaron": 464 | return aaron 465 | case "silberman": 466 | return silberman 467 | case "salinas": 468 | return salinas 469 | case "kirnberger": 470 | return kirnberger 471 | case "vallotti": 472 | return vallotti 473 | case "werkmeister": 474 | return werkmeister 475 | case "marpurg": 476 | return marpurg 477 | case "just": 478 | return just 479 | case "meanSemitone": 480 | return meanSemitone 481 | case "grammateus": 482 | return grammateus 483 | case "french": 484 | return french 485 | case "french2": 486 | return french2 487 | case "rameau": 488 | return rameau 489 | case "irrFr17e": 490 | return irrFr17e 491 | case "bachLehman": 492 | return bachLehman 493 | } 494 | } 495 | 496 | function setCurrentRoot(root) { 497 | console.log("setCurrentRoot " + root) 498 | var oldRoot = currentRoot 499 | getHistory().add( 500 | function () { 501 | currentRoot = oldRoot 502 | checkCurrentRoot() 503 | }, 504 | function() { 505 | currentRoot = root 506 | checkCurrentRoot() 507 | }, 508 | "current root" 509 | ) 510 | } 511 | 512 | function checkCurrentRoot() { 513 | switch (currentRoot) { 514 | case 0: 515 | root_c.checked = true 516 | break 517 | case 1: 518 | root_g.checked = true 519 | break 520 | case 2: 521 | root_d.checked = true 522 | break 523 | case 3: 524 | root_a.checked = true 525 | break 526 | case 4: 527 | root_e.checked = true 528 | break 529 | case 5: 530 | root_b.checked = true 531 | break 532 | case 6: 533 | root_f_sharp.checked = true 534 | break 535 | case 7: 536 | root_c_sharp.checked = true 537 | break 538 | case 8: 539 | root_g_sharp.checked = true 540 | break 541 | case 9: 542 | root_e_flat.checked = true 543 | break 544 | case 10: 545 | root_b_flat.checked = true 546 | break 547 | case 11: 548 | root_f.checked = true 549 | break 550 | } 551 | } 552 | 553 | function setCurrentPureTone(pureTone) { 554 | console.log("setCurrentPureTone " + pureTone) 555 | var oldPureTone = currentPureTone 556 | getHistory().add( 557 | function () { 558 | currentPureTone = oldPureTone 559 | checkCurrentPureTone() 560 | }, 561 | function() { 562 | currentPureTone = pureTone 563 | checkCurrentPureTone() 564 | }, 565 | "current pure tone" 566 | ) 567 | } 568 | 569 | function checkCurrentPureTone() { 570 | switch (currentPureTone) { 571 | case 0: 572 | pure_c.checked = true 573 | break 574 | case 1: 575 | pure_g.checked = true 576 | break 577 | case 2: 578 | pure_d.checked = true 579 | break 580 | case 3: 581 | pure_a.checked = true 582 | break 583 | case 4: 584 | pure_e.checked = true 585 | break 586 | case 5: 587 | pure_b.checked = true 588 | break 589 | case 6: 590 | pure_f_sharp.checked = true 591 | break 592 | case 7: 593 | pure_c_sharp.checked = true 594 | break 595 | case 8: 596 | pure_g_sharp.checked = true 597 | break 598 | case 9: 599 | pure_e_flat.checked = true 600 | break 601 | case 10: 602 | pure_b_flat.checked = true 603 | break 604 | case 11: 605 | pure_f.checked = true 606 | break 607 | } 608 | } 609 | 610 | function setModified(state) { 611 | console.log("setModified " + state) 612 | var oldModified = modified 613 | getHistory().add( 614 | function () { 615 | modified = oldModified 616 | }, 617 | function () { 618 | modified = state 619 | }, 620 | "modified" 621 | ) 622 | } 623 | 624 | function temperamentClicked(temperament) { 625 | getHistory().begin() 626 | setCurrentTemperament(temperament) 627 | setCurrentRoot(currentTemperament.root) 628 | setCurrentPureTone(currentTemperament.pure) 629 | recalculate(getTuning()) 630 | getHistory().end() 631 | } 632 | 633 | function rootNoteClicked(note) { 634 | getHistory().begin() 635 | setModified(true) 636 | setCurrentRoot(note) 637 | setCurrentPureTone(note) 638 | recalculate(getTuning()) 639 | getHistory().end() 640 | } 641 | 642 | function pureToneClicked(note) { 643 | getHistory().begin() 644 | setModified(true) 645 | setCurrentPureTone(note) 646 | recalculate(getTuning()) 647 | getHistory().end() 648 | } 649 | 650 | function editingFinishedFor(textField) { 651 | var oldText = textField.previousText 652 | var newText = textField.text 653 | getHistory().begin() 654 | setModified(true) 655 | getHistory().add( 656 | function () { 657 | textField.text = oldText 658 | }, 659 | function () { 660 | textField.text = newText 661 | }, 662 | "edit text field" 663 | ) 664 | getHistory().end() 665 | textField.previousText = newText 666 | } 667 | 668 | Rectangle { 669 | color: "lightgrey" 670 | anchors.fill: parent 671 | 672 | GridLayout { 673 | columns: 2 674 | anchors.fill: parent 675 | anchors.margins: 10 676 | GroupBox { 677 | title: "Temperament" 678 | ColumnLayout { 679 | ExclusiveGroup { id: tempamentTypeGroup } 680 | RadioButton { 681 | id: equal_button 682 | text: "Equal" 683 | checked: true 684 | exclusiveGroup: tempamentTypeGroup 685 | onClicked: { temperamentClicked(equal) } 686 | } 687 | RadioButton { 688 | id: pythagorean_button 689 | text: "Pythagorean" 690 | exclusiveGroup: tempamentTypeGroup 691 | onClicked: { temperamentClicked(pythagorean) } 692 | } 693 | RadioButton { 694 | id: aaron_button 695 | text: "Aaron" 696 | exclusiveGroup: tempamentTypeGroup 697 | onClicked: { temperamentClicked(aaron) } 698 | } 699 | RadioButton { 700 | id: silberman_button 701 | text: "Silberman" 702 | exclusiveGroup: tempamentTypeGroup 703 | onClicked: { temperamentClicked(silberman) } 704 | } 705 | RadioButton { 706 | id: salinas_button 707 | text: "Salinas" 708 | exclusiveGroup: tempamentTypeGroup 709 | onClicked: { temperamentClicked(salinas) } 710 | } 711 | RadioButton { 712 | id: kirnberger_button 713 | text: "Kirnberger" 714 | exclusiveGroup: tempamentTypeGroup 715 | onClicked: { temperamentClicked(kirnberger) } 716 | } 717 | RadioButton { 718 | id: vallotti_button 719 | text: "Vallotti" 720 | exclusiveGroup: tempamentTypeGroup 721 | onClicked: { temperamentClicked(vallotti) } 722 | } 723 | RadioButton { 724 | id: werkmeister_button 725 | text: "Werkmeister" 726 | exclusiveGroup: tempamentTypeGroup 727 | onClicked: { temperamentClicked(werkmeister) } 728 | } 729 | RadioButton { 730 | id: marpurg_button 731 | text: "Marpurg" 732 | exclusiveGroup: tempamentTypeGroup 733 | onClicked: { temperamentClicked(marpurg) } 734 | } 735 | RadioButton { 736 | id: just_button 737 | text: "Just" 738 | exclusiveGroup: tempamentTypeGroup 739 | onClicked: { temperamentClicked(just) } 740 | } 741 | RadioButton { 742 | id: meanSemitone_button 743 | text: "Mean Semitone" 744 | exclusiveGroup: tempamentTypeGroup 745 | onClicked: { temperamentClicked(meanSemitone) } 746 | } 747 | RadioButton { 748 | id: grammateus_button 749 | text: "Grammateus" 750 | exclusiveGroup: tempamentTypeGroup 751 | onClicked: { temperamentClicked(grammateus) } 752 | } 753 | RadioButton { 754 | id: french_button 755 | text: "French" 756 | exclusiveGroup: tempamentTypeGroup 757 | onClicked: { temperamentClicked(french) } 758 | } 759 | RadioButton { 760 | id: french2_button 761 | text: "Tempérament Ordinaire" 762 | exclusiveGroup: tempamentTypeGroup 763 | onClicked: { temperamentClicked(french2) } 764 | } 765 | RadioButton { 766 | id: rameau_button 767 | text: "Rameau" 768 | exclusiveGroup: tempamentTypeGroup 769 | onClicked: { temperamentClicked(rameau) } 770 | } 771 | RadioButton { 772 | id: irrFr17e_button 773 | text: "Irr Fr 17e" 774 | exclusiveGroup: tempamentTypeGroup 775 | onClicked: { temperamentClicked(irrFr17e) } 776 | } 777 | RadioButton { 778 | id: bachLehman_button 779 | text: "Bach/Lehman" 780 | exclusiveGroup: tempamentTypeGroup 781 | onClicked: { temperamentClicked(bachLehman) } 782 | } 783 | } 784 | } 785 | 786 | ColumnLayout { 787 | GroupBox { 788 | title: "Advanced" 789 | ColumnLayout { 790 | GroupBox { 791 | title: "Root Note" 792 | GridLayout { 793 | columns: 4 794 | anchors.margins: 10 795 | ExclusiveGroup { id: rootNoteGroup } 796 | RadioButton { 797 | text: "C" 798 | checked: true 799 | exclusiveGroup: rootNoteGroup 800 | id: root_c 801 | onClicked: { rootNoteClicked(0) } 802 | } 803 | RadioButton { 804 | text: "G" 805 | exclusiveGroup: rootNoteGroup 806 | id: root_g 807 | onClicked: { rootNoteClicked(1) } 808 | } 809 | RadioButton { 810 | text: "D" 811 | exclusiveGroup: rootNoteGroup 812 | id: root_d 813 | onClicked: { rootNoteClicked(2) } 814 | } 815 | RadioButton { 816 | text: "A" 817 | exclusiveGroup: rootNoteGroup 818 | id: root_a 819 | onClicked: { rootNoteClicked(3) } 820 | } 821 | RadioButton { 822 | text: "E" 823 | exclusiveGroup: rootNoteGroup 824 | id: root_e 825 | onClicked: { rootNoteClicked(4) } 826 | } 827 | RadioButton { 828 | text: "B" 829 | exclusiveGroup: rootNoteGroup 830 | id: root_b 831 | onClicked: { rootNoteClicked(5) } 832 | } 833 | RadioButton { 834 | text: "F#" 835 | exclusiveGroup: rootNoteGroup 836 | id: root_f_sharp 837 | onClicked: { rootNoteClicked(6) } 838 | } 839 | RadioButton { 840 | text: "C#" 841 | exclusiveGroup: rootNoteGroup 842 | id: root_c_sharp 843 | onClicked: { rootNoteClicked(7) } 844 | } 845 | RadioButton { 846 | text: "G#" 847 | exclusiveGroup: rootNoteGroup 848 | id: root_g_sharp 849 | onClicked: { rootNoteClicked(8) } 850 | } 851 | RadioButton { 852 | text: "Eb" 853 | exclusiveGroup: rootNoteGroup 854 | id: root_e_flat 855 | onClicked: { rootNoteClicked(9) } 856 | } 857 | RadioButton { 858 | text: "Bb" 859 | exclusiveGroup: rootNoteGroup 860 | id: root_b_flat 861 | onClicked: { rootNoteClicked(10) } 862 | } 863 | RadioButton { 864 | text: "F" 865 | exclusiveGroup: rootNoteGroup 866 | id: root_f 867 | onClicked: { rootNoteClicked(11) } 868 | } 869 | } 870 | } 871 | 872 | GroupBox { 873 | title: "Pure Tone" 874 | GridLayout { 875 | columns: 4 876 | anchors.margins: 10 877 | ExclusiveGroup { id: pureToneGroup } 878 | RadioButton { 879 | text: "C" 880 | checked: true 881 | id: pure_c 882 | exclusiveGroup: pureToneGroup 883 | onClicked: { pureToneClicked(0) } 884 | } 885 | RadioButton { 886 | text: "G" 887 | id: pure_g 888 | exclusiveGroup: pureToneGroup 889 | onClicked: { pureToneClicked(1) } 890 | } 891 | RadioButton { 892 | text: "D" 893 | id: pure_d 894 | exclusiveGroup: pureToneGroup 895 | onClicked: { pureToneClicked(2) } 896 | } 897 | RadioButton { 898 | text: "A" 899 | id: pure_a 900 | exclusiveGroup: pureToneGroup 901 | onClicked: { pureToneClicked(3) } 902 | } 903 | RadioButton { 904 | text: "E" 905 | id: pure_e 906 | exclusiveGroup: pureToneGroup 907 | onClicked: { pureToneClicked(4) } 908 | } 909 | RadioButton { 910 | text: "B" 911 | id: pure_b 912 | exclusiveGroup: pureToneGroup 913 | onClicked: { pureToneClicked(5) } 914 | } 915 | RadioButton { 916 | text: "F#" 917 | id: pure_f_sharp 918 | exclusiveGroup: pureToneGroup 919 | onClicked: { pureToneClicked(6) } 920 | } 921 | RadioButton { 922 | text: "C#" 923 | id: pure_c_sharp 924 | exclusiveGroup: pureToneGroup 925 | onClicked: { pureToneClicked(7) } 926 | } 927 | RadioButton { 928 | text: "G#" 929 | id: pure_g_sharp 930 | exclusiveGroup: pureToneGroup 931 | onClicked: { pureToneClicked(8) } 932 | } 933 | RadioButton { 934 | text: "Eb" 935 | id: pure_e_flat 936 | exclusiveGroup: pureToneGroup 937 | onClicked: { pureToneClicked(9) } 938 | } 939 | RadioButton { 940 | text: "Bb" 941 | id: pure_b_flat 942 | exclusiveGroup: pureToneGroup 943 | onClicked: { pureToneClicked(10) } 944 | } 945 | RadioButton { 946 | text: "F" 947 | id: pure_f 948 | exclusiveGroup: pureToneGroup 949 | onClicked: { pureToneClicked(11) } 950 | } 951 | } 952 | } 953 | 954 | GroupBox { 955 | title: "Final Offsets" 956 | GridLayout { 957 | columns: 8 958 | anchors.margins: 0 959 | 960 | Label { 961 | text: "C" 962 | Layout.alignment: offsetLabelAlignment 963 | } 964 | TextField { 965 | Layout.maximumWidth: offsetTextWidth 966 | id: final_c 967 | text: "0.0" 968 | readOnly: false 969 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 970 | property var previousText: "0.0" 971 | onEditingFinished: { editingFinishedFor(final_c) } 972 | } 973 | 974 | Label { 975 | text: "G" 976 | Layout.alignment: offsetLabelAlignment 977 | } 978 | TextField { 979 | Layout.maximumWidth: offsetTextWidth 980 | id: final_g 981 | text: "0.0" 982 | readOnly: false 983 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 984 | property var previousText: "0.0" 985 | onEditingFinished: { editingFinishedFor(final_g) } 986 | } 987 | 988 | Label { 989 | text: "D" 990 | Layout.alignment: offsetLabelAlignment 991 | } 992 | TextField { 993 | Layout.maximumWidth: offsetTextWidth 994 | id: final_d 995 | text: "0.0" 996 | readOnly: false 997 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 998 | property var previousText: "0.0" 999 | onEditingFinished: { editingFinishedFor(final_d) } 1000 | } 1001 | 1002 | Label { 1003 | text: "A" 1004 | Layout.alignment: offsetLabelAlignment 1005 | } 1006 | TextField { 1007 | Layout.maximumWidth: offsetTextWidth 1008 | id: final_a 1009 | text: "0.0" 1010 | readOnly: false 1011 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1012 | property var previousText: "0.0" 1013 | onEditingFinished: { editingFinishedFor(final_a) } 1014 | } 1015 | 1016 | Label { 1017 | text: "E" 1018 | Layout.alignment: offsetLabelAlignment 1019 | } 1020 | TextField { 1021 | Layout.maximumWidth: offsetTextWidth 1022 | id: final_e 1023 | text: "0.0" 1024 | readOnly: false 1025 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1026 | property var previousText: "0.0" 1027 | onEditingFinished: { editingFinishedFor(final_e) } 1028 | } 1029 | 1030 | Label { 1031 | text: "B" 1032 | Layout.alignment: offsetLabelAlignment 1033 | } 1034 | TextField { 1035 | Layout.maximumWidth: offsetTextWidth 1036 | id: final_b 1037 | text: "0.0" 1038 | readOnly: false 1039 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1040 | property var previousText: "0.0" 1041 | onEditingFinished: { editingFinishedFor(final_b) } 1042 | } 1043 | 1044 | Label { 1045 | text: "F#" 1046 | Layout.alignment: offsetLabelAlignment 1047 | } 1048 | TextField { 1049 | Layout.maximumWidth: offsetTextWidth 1050 | id: final_f_sharp 1051 | text: "0.0" 1052 | readOnly: false 1053 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1054 | property var previousText: "0.0" 1055 | onEditingFinished: { editingFinishedFor(final_f_sharp) } 1056 | } 1057 | 1058 | Label { 1059 | text: "C#" 1060 | Layout.alignment: offsetLabelAlignment 1061 | } 1062 | TextField { 1063 | Layout.maximumWidth: offsetTextWidth 1064 | id: final_c_sharp 1065 | text: "0.0" 1066 | readOnly: false 1067 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1068 | property var previousText: "0.0" 1069 | onEditingFinished: { editingFinishedFor(final_c_sharp) } 1070 | } 1071 | 1072 | Label { 1073 | text: "G#" 1074 | Layout.alignment: offsetLabelAlignment 1075 | } 1076 | TextField { 1077 | Layout.maximumWidth: offsetTextWidth 1078 | id: final_g_sharp 1079 | text: "0.0" 1080 | readOnly: false 1081 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1082 | property var previousText: "0.0" 1083 | onEditingFinished: { editingFinishedFor(final_g_sharp) } 1084 | } 1085 | 1086 | Label { 1087 | text: "Eb" 1088 | Layout.alignment: offsetLabelAlignment 1089 | } 1090 | TextField { 1091 | Layout.maximumWidth: offsetTextWidth 1092 | id: final_e_flat 1093 | text: "0.0" 1094 | readOnly: false 1095 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1096 | property var previousText: "0.0" 1097 | onEditingFinished: { editingFinishedFor(final_e_flat) } 1098 | } 1099 | 1100 | Label { 1101 | text: "Bb" 1102 | Layout.alignment: offsetLabelAlignment 1103 | } 1104 | TextField { 1105 | Layout.maximumWidth: offsetTextWidth 1106 | id: final_b_flat 1107 | text: "0.0" 1108 | readOnly: false 1109 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1110 | property var previousText: "0.0" 1111 | onEditingFinished: { editingFinishedFor(final_b_flat) } 1112 | } 1113 | 1114 | Label { 1115 | text: "F" 1116 | Layout.alignment: offsetLabelAlignment 1117 | } 1118 | TextField { 1119 | Layout.maximumWidth: offsetTextWidth 1120 | id: final_f 1121 | text: "0.0" 1122 | readOnly: false 1123 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1124 | property var previousText: "0.0" 1125 | onEditingFinished: { editingFinishedFor(final_f) } 1126 | } 1127 | } 1128 | } 1129 | RowLayout { 1130 | Button { 1131 | id: saveButton 1132 | text: qsTranslate("PrefsDialogBase", "Save") 1133 | onClicked: { 1134 | saveDialog.visible = true 1135 | } 1136 | } 1137 | Button { 1138 | id: loadButton 1139 | text: qsTranslate("PrefsDialogBase", "Load") 1140 | onClicked: { 1141 | loadDialog.visible = true 1142 | } 1143 | } 1144 | Button { 1145 | id: undoButton 1146 | text: qsTranslate("PrefsDialogBase", "Undo") 1147 | onClicked: { 1148 | getHistory().undo() 1149 | } 1150 | } 1151 | Button { 1152 | id: redoButton 1153 | text: qsTranslate("PrefsDialogBase", "Redo") 1154 | onClicked: { 1155 | getHistory().redo() 1156 | } 1157 | } 1158 | } 1159 | } 1160 | } 1161 | 1162 | RowLayout { 1163 | Button { 1164 | id: applyButton 1165 | text: qsTranslate("PrefsDialogBase", "Apply") 1166 | onClicked: { 1167 | if (applyTemperament()) { 1168 | if (modified) { 1169 | quitDialog.open() 1170 | } else { 1171 | Qt.quit() 1172 | } 1173 | } 1174 | } 1175 | } 1176 | Button { 1177 | id: cancelButton 1178 | text: qsTranslate("PrefsDialogBase", "Cancel") 1179 | onClicked: { 1180 | if (modified) { 1181 | quitDialog.open() 1182 | } else { 1183 | Qt.quit() 1184 | } 1185 | } 1186 | } 1187 | 1188 | } 1189 | } 1190 | } 1191 | } 1192 | 1193 | MessageDialog { 1194 | id: errorDialog 1195 | title: "Error" 1196 | text: "" 1197 | onAccepted: { 1198 | errorDialog.close() 1199 | } 1200 | } 1201 | 1202 | MessageDialog { 1203 | id: quitDialog 1204 | title: "Quit?" 1205 | text: "Do you want to quit the plugin?" 1206 | detailedText: "It looks like you have made customisations to this tuning, you could save them to a file before quitting if you like." 1207 | standardButtons: StandardButton.Ok | StandardButton.Cancel 1208 | onAccepted: { 1209 | Qt.quit() 1210 | } 1211 | onRejected: { 1212 | quitDialog.close() 1213 | } 1214 | } 1215 | 1216 | FileIO { 1217 | id: saveFile 1218 | source: "" 1219 | } 1220 | 1221 | FileIO { 1222 | id: loadFile 1223 | source: "" 1224 | } 1225 | 1226 | function getFile(dialog) { 1227 | var source = dialog.fileUrl.toString().substring(7) // strip the 'file://' prefix 1228 | console.log("You chose: " + source) 1229 | return source 1230 | } 1231 | 1232 | function formatCurrentValues() { 1233 | var data = { 1234 | offsets: [ 1235 | parseFloat(final_c.text), 1236 | parseFloat(final_c_sharp.text), 1237 | parseFloat(final_d.text), 1238 | parseFloat(final_e_flat.text), 1239 | parseFloat(final_e.text), 1240 | parseFloat(final_f.text), 1241 | parseFloat(final_f_sharp.text), 1242 | parseFloat(final_g.text), 1243 | parseFloat(final_g_sharp.text), 1244 | parseFloat(final_a.text), 1245 | parseFloat(final_b_flat.text), 1246 | parseFloat(final_b.text) 1247 | ], 1248 | temperament: currentTemperament.name, 1249 | root: currentRoot, 1250 | pure: currentPureTone 1251 | }; 1252 | return(JSON.stringify(data)) 1253 | } 1254 | 1255 | function restoreSavedValues(data) { 1256 | console.log("restoreSavedValues") 1257 | getHistory().begin() 1258 | setCurrentTemperament(lookupTemperament(data.temperament)) 1259 | setCurrentRoot(data.root) 1260 | setCurrentPureTone(data.pure) 1261 | recalculate( 1262 | function(pitch) { 1263 | return data.offsets[pitch % 12] 1264 | } 1265 | ) 1266 | getHistory().end() 1267 | } 1268 | 1269 | FileDialog { 1270 | id: loadDialog 1271 | title: "Please choose a file" 1272 | sidebarVisible: true 1273 | onAccepted: { 1274 | loadFile.source = getFile(loadDialog) 1275 | var data = JSON.parse(loadFile.read()) 1276 | restoreSavedValues(data) 1277 | loadDialog.visible = false 1278 | } 1279 | onRejected: { 1280 | loadDialog.visible = false 1281 | } 1282 | visible: false 1283 | } 1284 | 1285 | FileDialog { 1286 | id: saveDialog 1287 | title: "Please choose a file" 1288 | sidebarVisible: true 1289 | selectExisting: false 1290 | onAccepted: { 1291 | saveFile.source = getFile(saveDialog) 1292 | saveFile.write(formatCurrentValues()) 1293 | saveDialog.visible = false 1294 | } 1295 | onRejected: { 1296 | saveDialog.visible = false 1297 | } 1298 | visible: false 1299 | } 1300 | 1301 | // Command pattern for undo/redo 1302 | function commandHistory() { 1303 | function Command(undo_fn, redo_fn, label) { 1304 | this.undo = undo_fn 1305 | this.redo = redo_fn 1306 | this.label = label // for debugging 1307 | } 1308 | 1309 | var history = [] 1310 | var index = -1 1311 | var transaction = 0 1312 | var maxHistory = 30 1313 | 1314 | function newHistory(commands) { 1315 | if (index < maxHistory) { 1316 | index++ 1317 | history = history.slice(0, index) 1318 | } else { 1319 | history = history.slice(1, index) 1320 | } 1321 | history.push(commands) 1322 | } 1323 | 1324 | this.add = function(undo, redo, label) { 1325 | var command = new Command(undo, redo, label) 1326 | console.log("history add command " + label) 1327 | command.redo() 1328 | if (transaction) { 1329 | history[index].push(command) 1330 | } else { 1331 | newHistory([command]) 1332 | } 1333 | } 1334 | 1335 | this.undo = function() { 1336 | if (index != -1) { 1337 | console.log("history begin undo [" + index + "]") 1338 | history[index].slice().reverse().forEach( 1339 | function(command) { 1340 | console.log("history undo " + command.label) 1341 | command.undo() 1342 | } 1343 | ) 1344 | console.log("history end undo [" + index + "]") 1345 | index-- 1346 | } 1347 | } 1348 | 1349 | this.redo = function() { 1350 | if ((index + 1) < history.length) { 1351 | index++ 1352 | console.log("history begin redo [" + index + "]") 1353 | history[index].forEach( 1354 | function(command) { 1355 | console.log("history redo " + command.label) 1356 | command.redo() 1357 | } 1358 | ) 1359 | console.log("history end redo [" + index + "]") 1360 | } 1361 | } 1362 | 1363 | this.begin = function() { 1364 | if (transaction) { 1365 | throw new Error("already in transaction") 1366 | } 1367 | console.log("history begin transaction [" + (index + 1) + "]") 1368 | newHistory([]) 1369 | transaction = 1 1370 | } 1371 | 1372 | this.end = function() { 1373 | if (!transaction) { 1374 | throw new Error("not in transaction") 1375 | } 1376 | console.log("history end transaction [" + index + "]") 1377 | transaction = 0 1378 | } 1379 | } 1380 | 1381 | } 1382 | -------------------------------------------------------------------------------- /src/Tuning/3.x/tuning.qml: -------------------------------------------------------------------------------- 1 | // Apply a choice of tempraments and tunings. 2 | // Copyright (C) 2018-2019 Bill Hails 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | import MuseScore 3.0 18 | import QtQuick 2.2 19 | import QtQuick.Controls 1.1 20 | import QtQuick.Controls.Styles 1.3 21 | import QtQuick.Layouts 1.1 22 | import QtQuick.Dialogs 1.1 23 | import FileIO 3.0 24 | 25 | MuseScore { 26 | version: "3.0.5" 27 | menuPath: "Plugins.Playback.Tuning" 28 | description: "Apply various temperaments and tunings" 29 | pluginType: "dialog" 30 | width: 550 31 | height: 500 32 | 33 | property var offsetTextWidth: 40; 34 | property var offsetLabelAlignment: 0x02 | 0x80; 35 | 36 | property var history: 0; 37 | 38 | // set true if customisations are made to the tuning 39 | property var modified: false; 40 | 41 | /** 42 | * See http://leware.net/temper/temper.htm and specifically http://leware.net/temper/cents.htm 43 | * 44 | * I've taken the liberty of adding the Bach/Lehman temperament http://www.larips.com which was 45 | * my original motivation for doing this. 46 | * 47 | * These values are in cents. One cent is defined as 100th of an equal tempered semitone. 48 | * Each row is ordered in the cycle of fifths, so C, G, D, A, E, B, F#, C#, G#/Ab, Eb, Bb, F; 49 | * and the values are offsets from the equal tempered value. 50 | * 51 | * However for tunings who's default root note is not C, the values are pre-rotated so that applying the 52 | * root note rotation will put the first value of the sequence at the root note. 53 | */ 54 | property var equal: { 55 | 'offsets': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 56 | 'root': 0, 57 | 'pure': 0, 58 | 'name': "equal" 59 | } 60 | property var pythagorean: { 61 | 'offsets': [-6.0, -4.0, -2.0, 0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0], 62 | 'root': 9, 63 | 'pure': 3, 64 | 'name': "pythagorean" 65 | } 66 | property var aaron: { 67 | 'offsets': [10.5, 7.0, 3.5, 0.0, -3.5, -7.0, -10.5, -14.0, -17.5, -21.0, -24.5, -28.0], 68 | 'root': 9, 69 | 'pure': 3, 70 | 'name': "aaron" 71 | } 72 | property var silberman: { 73 | 'offsets': [5.0, 3.3, 1.7, 0.0, -1.7, -3.3, -5.0, -6.7, -8.3, -10.0, -11.7, -13.3], 74 | 'root': 9, 75 | 'pure': 3, 76 | 'name': "silberman" 77 | } 78 | property var salinas: { 79 | 'offsets': [16.0, 10.7, 5.3, 0.0, -5.3, -10.7, -16.0, -21.3, -26.7, -32.0, -37.3, -42.7], 80 | 'root': 9, 81 | 'pure': 3, 82 | 'name': "salinas" 83 | } 84 | property var kirnberger: { 85 | 'offsets': [0.0, -3.5, -7.0, -10.5, -14.0, -12.0, -10.0, -10.0, -8.0, -6.0, -4.0, -2.0], 86 | 'root': 0, 87 | 'pure': 0, 88 | 'name': "kirnberger" 89 | } 90 | property var vallotti: { 91 | 'offsets': [0.0, -2.0, -4.0, -6.0, -8.0, -10.0, -8.0, -6.0, -4.0, -2.0, 0.0, 2.0], 92 | 'root': 0, 93 | 'pure': 0, 94 | 'name': "vallotti" 95 | } 96 | property var werkmeister: { 97 | 'offsets': [0.0, -4.0, -8.0, -12.0, -10.0, -8.0, -12.0, -10.0, -8.0, -6.0, -4.0, -2.0], 98 | 'root': 0, 99 | 'pure': 0, 100 | 'name': "werkmeister" 101 | } 102 | property var marpurg: { 103 | 'offsets': [0.0, 2.0, 4.0, 6.0, 0.0, 2.0, 4.0, 6.0, 0.0, 2.0, 4.0, 6.0], 104 | 'root': 0, 105 | 'pure': 0, 106 | 'name': "marpurg" 107 | } 108 | property var just: { 109 | 'offsets': [0.0, 2.0, 4.0, -16.0, -14.0, -12.0, -10.0, -30.0, -28.0, 16.0, 18.0, -2.0], 110 | 'root': 0, 111 | 'pure': 0, 112 | 'name': "just" 113 | } 114 | property var meanSemitone: { 115 | 'offsets': [0.0, -3.5, -7.0, -10.5, -14.0, 3.5, 0.0, -3.5, -7.0, -10.5, -14.0, -17.5], 116 | 'root': 6, 117 | 'pure': 6, 118 | 'name': "meanSemitone" 119 | } 120 | property var grammateus: { 121 | 'offsets': [-2.0, 0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 0.0, 2.0, 4.0, 6.0, 8.0], 122 | 'root': 11, 123 | 'pure': 1, 124 | 'name': "grammateus" 125 | } 126 | property var french: { 127 | 'offsets': [0.0, -2.5, -5.0, -7.5, -10.0, -12.5, -13.0, -13.0, -11.0, -6.0, -1.5, 2.5], 128 | 'root': 0, 129 | 'pure': 0, 130 | 'name': "french" 131 | } 132 | property var french2: { 133 | 'offsets': [0.0, -3.5, -7.0, -10.5, -14.0, -17.5, -18.2, -19.0, -17.0, -10.5, -3.5, 3.5], 134 | 'root': 0, 135 | 'pure': 0, 136 | 'name': "french2" 137 | } 138 | property var rameau: { 139 | 'offsets': [0.0, -3.5, -7.0, -10.5, -14.0, -17.5, -15.5, -13.5, -11.5, -2.0, 7.0, 3.5], 140 | 'root': 0, 141 | 'pure': 0, 142 | 'name': "rameau" 143 | } 144 | property var irrFr17e: { 145 | 'offsets': [-8.0, -2.0, 3.0, 0.0, -3.0, -6.0, -9.0, -12.0, -15.0, -18.0, -21.0, -24.0], 146 | 'root': 9, 147 | 'pure': 3, 148 | 'name': "irrFr17e" 149 | } 150 | property var bachLehman: { 151 | 'offsets': [0.0, -2.0, -3.9, -5.9, -7.8, -5.9, -3.9, -2.0, -2.0, -2.0, -2.0, 2.0], 152 | 'root': 0, 153 | 'pure': 3, 154 | 'name': "bachLehman" 155 | } 156 | 157 | property var currentTemperament: equal; 158 | property var currentRoot: 0; 159 | property var currentPureTone: 0; 160 | property var currentTweak: 0.0; 161 | 162 | onRun: { 163 | if (!curScore) { 164 | error("No score open.\nThis plugin requires an open score to run.\n") 165 | Qt.quit() 166 | } 167 | } 168 | 169 | function getHistory() { 170 | if (history == 0) { 171 | history = new commandHistory() 172 | } 173 | return history 174 | } 175 | 176 | function applyTemperament() 177 | { 178 | var selection = new scoreSelection() 179 | curScore.startCmd() 180 | selection.map(filterNotes, reTune(getFinalTuning())) 181 | if (annotateValue.checkedState == Qt.Checked) { 182 | selection.map(filterNotes, annotate) 183 | } 184 | curScore.endCmd() 185 | return true 186 | } 187 | 188 | function filterNotes(element) 189 | { 190 | return element.type == Ms.CHORD 191 | } 192 | 193 | function annotate(chord, cursor) 194 | { 195 | function addText(noteIndex, placement) { 196 | var note = chord.notes[noteIndex] 197 | var text = newElement(Element.STAFF_TEXT); 198 | text.text = '' + note.tuning 199 | text.autoplace = true 200 | text.fontSize = 7 // smaller 201 | text.placement = placement 202 | cursor.add(text) 203 | } 204 | 205 | if (cursor.voice == 0 || cursor.voice == 2) { 206 | for (var index = 0; index < chord.notes.length; index++) { 207 | addText(index, Placement.ABOVE) 208 | } 209 | } else { 210 | for (var index = chord.notes.length - 1; index >= 0; index--) { 211 | addText(index, Placement.BELOW) 212 | } 213 | } 214 | } 215 | 216 | function reTune(tuning) { 217 | return function(chord, cursor) { 218 | for (var i = 0; i < chord.notes.length; i++) { 219 | var note = chord.notes[i] 220 | note.tuning = tuning(note.pitch) 221 | } 222 | } 223 | } 224 | 225 | function scoreSelection() { 226 | const SCORE_START = 0 227 | const SELECTION_START = 1 228 | const SELECTION_END = 2 229 | var fullScore 230 | var startStaff 231 | var endStaff 232 | var endTick 233 | var inRange 234 | var rewind 235 | var cursor = curScore.newCursor() 236 | cursor.rewind(SELECTION_START) 237 | if (cursor.segment) { 238 | startStaff = cursor.staffIdx 239 | cursor.rewind(SELECTION_END) 240 | endStaff = cursor.staffIdx; 241 | endTick = 0 // unused 242 | if (cursor.tick === 0) { 243 | endTick = curScore.lastSegment.tick + 1; 244 | } else { 245 | endTick = cursor.tick; 246 | } 247 | inRange = function() { 248 | return cursor.segment && cursor.tick < endTick 249 | } 250 | rewind = function (voice, staff) { 251 | // no idea why, but if there is a selection then 252 | // we need to rewind the cursor *before* setting 253 | // the voice and staff index. 254 | cursor.rewind(SELECTION_START) 255 | cursor.voice = voice 256 | cursor.staffIdx = staff 257 | } 258 | } else { 259 | startStaff = 0 260 | endStaff = curScore.nstaves - 1 261 | inRange = function () { 262 | return cursor.segment 263 | } 264 | rewind = function (voice, staff) { 265 | // no idea why, but if there's no selection then 266 | // we need to rewind the cursor *after* setting 267 | // the voice and staff index. 268 | cursor.voice = voice 269 | cursor.staffIdx = staff 270 | cursor.rewind(SCORE_START) 271 | } 272 | } 273 | 274 | this.map = function(filter, process) { 275 | for (var staff = startStaff; staff <= endStaff; staff++) { 276 | for (var voice = 0; voice < 4; voice++) { 277 | rewind(voice, staff) 278 | while (inRange()) { 279 | if (cursor.element && filter(cursor.element)) { 280 | process(cursor.element, cursor) 281 | } 282 | cursor.next() 283 | } 284 | } 285 | } 286 | } 287 | } 288 | 289 | function error(errorMessage) { 290 | errorDialog.text = qsTr(errorMessage) 291 | errorDialog.open() 292 | } 293 | 294 | /** 295 | * map a note (pitch modulo 12) to a value in one of the above tables 296 | * then adjust for the choice of pure note and tweak. 297 | */ 298 | function lookUp(note, table) { 299 | var i = ((note * 7) - currentRoot + 12) % 12; 300 | var offset = table.offsets[i]; 301 | var j = (currentPureTone - currentRoot + 12) % 12; 302 | var pureNoteAdjustment = table.offsets[j]; 303 | var finalOffset = offset - pureNoteAdjustment; 304 | var tweakFinalOffset = finalOffset + parseFloat(tweakValue.text); 305 | return tweakFinalOffset 306 | } 307 | 308 | /** 309 | * returns a function for use by recalculate() 310 | * 311 | * We use an abstract function here because recalculate can be passed 312 | * a different function, i.e. when restoring from a save file. 313 | */ 314 | function getTuning() { 315 | return function(pitch) { 316 | return lookUp(pitch, currentTemperament); 317 | } 318 | } 319 | 320 | function getFinalTuning() { 321 | return function(pitch) { 322 | pitch = pitch % 12 323 | switch (pitch) { 324 | case 0: 325 | return getFinalOffset(final_c) 326 | case 1: 327 | return getFinalOffset(final_c_sharp) 328 | case 2: 329 | return getFinalOffset(final_d) 330 | case 3: 331 | return getFinalOffset(final_e_flat) 332 | case 4: 333 | return getFinalOffset(final_e) 334 | case 5: 335 | return getFinalOffset(final_f) 336 | case 6: 337 | return getFinalOffset(final_f_sharp) 338 | case 7: 339 | return getFinalOffset(final_g) 340 | case 8: 341 | return getFinalOffset(final_g_sharp) 342 | case 9: 343 | return getFinalOffset(final_a) 344 | case 10: 345 | return getFinalOffset(final_b_flat) 346 | case 11: 347 | return getFinalOffset(final_b) 348 | default: 349 | error("unrecognised pitch: " + pitch) 350 | } 351 | } 352 | } 353 | 354 | function getFinalOffset(textField) { 355 | return parseFloat(textField.text) 356 | } 357 | 358 | function recalculate(tuning) { 359 | var old_final_c = final_c.text 360 | var old_final_c_sharp = final_c_sharp.text 361 | var old_final_d = final_d.text 362 | var old_final_e_flat = final_e_flat.text 363 | var old_final_e = final_e.text 364 | var old_final_f = final_f.text 365 | var old_final_f_sharp = final_f_sharp.text 366 | var old_final_g = final_g.text 367 | var old_final_g_sharp = final_g_sharp.text 368 | var old_final_a = final_a.text 369 | var old_final_b_flat = final_b_flat.text 370 | var old_final_b = final_b.text 371 | getHistory().add( 372 | function () { 373 | final_c.text = old_final_c 374 | final_c.previousText = old_final_c 375 | final_c_sharp.text = old_final_c_sharp 376 | final_c_sharp.previousText = old_final_c_sharp 377 | final_d.text = old_final_d 378 | final_d.previousText = old_final_d 379 | final_e_flat.text = old_final_e_flat 380 | final_e_flat.previousText = old_final_e_flat 381 | final_e.text = old_final_e 382 | final_e.previousText = old_final_e 383 | final_f.text = old_final_f 384 | final_f.previousText = old_final_f 385 | final_f_sharp.text = old_final_f_sharp 386 | final_f_sharp.previousText = old_final_f_sharp 387 | final_g.text = old_final_g 388 | final_g.previousText = old_final_g 389 | final_g_sharp.text = old_final_g_sharp 390 | final_g_sharp.previousText = old_final_g_sharp 391 | final_a.text = old_final_a 392 | final_a.previousText = old_final_a 393 | final_b_flat.text = old_final_b_flat 394 | final_b_flat.previousText = old_final_b_flat 395 | final_b.text = old_final_b 396 | final_b.previousText = old_final_b 397 | }, 398 | function() { 399 | final_c.text = tuning(0).toFixed(1) 400 | final_c.previousText = final_c.text 401 | final_c_sharp.text = tuning(1).toFixed(1) 402 | final_c_sharp.previousText = final_c_sharp.text 403 | final_d.text = tuning(2).toFixed(1) 404 | final_d.previousText = final_d.text 405 | final_e_flat.text = tuning(3).toFixed(1) 406 | final_e_flat.previousText = final_e_flat.text 407 | final_e.text = tuning(4).toFixed(1) 408 | final_e.previousText = final_e.text 409 | final_f.text = tuning(5).toFixed(1) 410 | final_f.previousText = final_f.text 411 | final_f_sharp.text = tuning(6).toFixed(1) 412 | final_f_sharp.previousText = final_f_sharp.text 413 | final_g.text = tuning(7).toFixed(1) 414 | final_g.previousText = final_g.text 415 | final_g_sharp.text = tuning(8).toFixed(1) 416 | final_g_sharp.previousText = final_g_sharp.text 417 | final_a.text = tuning(9).toFixed(1) 418 | final_a.previousText = final_a.text 419 | final_b_flat.text = tuning(10).toFixed(1) 420 | final_b_flat.previousText = final_b_flat.text 421 | final_b.text = tuning(11).toFixed(1) 422 | final_b.previousText = final_b.text 423 | }, 424 | "final offsets" 425 | ) 426 | } 427 | 428 | function setCurrentTemperament(temperament) { 429 | var oldTemperament = currentTemperament 430 | getHistory().add( 431 | function() { 432 | currentTemperament = oldTemperament 433 | checkCurrentTemperament() 434 | }, 435 | function() { 436 | currentTemperament = temperament 437 | checkCurrentTemperament() 438 | }, 439 | "current temperament" 440 | ) 441 | } 442 | 443 | function checkCurrentTemperament() { 444 | switch (currentTemperament.name) { 445 | case "equal": 446 | equal_button.checked = true 447 | return 448 | case "pythagorean": 449 | pythagorean_button.checked = true 450 | return 451 | case "aaron": 452 | aaron_button.checked = true 453 | return 454 | case "silberman": 455 | silberman_button.checked = true 456 | return 457 | case "salinas": 458 | salinas_button.checked = true 459 | return 460 | case "kirnberger": 461 | kirnberger_button.checked = true 462 | return 463 | case "vallotti": 464 | vallotti_button.checked = true 465 | return 466 | case "werkmeister": 467 | werkmeister_button.checked = true 468 | return 469 | case "marpurg": 470 | marpurg_button.checked = true 471 | return 472 | case "just": 473 | just_button.checked = true 474 | return 475 | case "meanSemitone": 476 | meanSemitone_button.checked = true 477 | return 478 | case "grammateus": 479 | grammateus_button.checked = true 480 | return 481 | case "french": 482 | french_button.checked = true 483 | return 484 | case "french2": 485 | french2_button.checked = true 486 | return 487 | case "rameau": 488 | rameau_button.checked = true 489 | return 490 | case "irrFr17e": 491 | irrFr17e_button.checked = true 492 | return 493 | case "bachLehman": 494 | bachLehman_button.checked = true 495 | return 496 | } 497 | } 498 | 499 | function lookupTemperament(temperamentName) { 500 | switch (temperamentName) { 501 | case "equal": 502 | return equal 503 | case "pythagorean": 504 | return pythagorean 505 | case "aaron": 506 | return aaron 507 | case "silberman": 508 | return silberman 509 | case "salinas": 510 | return salinas 511 | case "kirnberger": 512 | return kirnberger 513 | case "vallotti": 514 | return vallotti 515 | case "werkmeister": 516 | return werkmeister 517 | case "marpurg": 518 | return marpurg 519 | case "just": 520 | return just 521 | case "meanSemitone": 522 | return meanSemitone 523 | case "grammateus": 524 | return grammateus 525 | case "french": 526 | return french 527 | case "french2": 528 | return french2 529 | case "rameau": 530 | return rameau 531 | case "irrFr17e": 532 | return irrFr17e 533 | case "bachLehman": 534 | return bachLehman 535 | } 536 | } 537 | 538 | function setCurrentRoot(root) { 539 | var oldRoot = currentRoot 540 | getHistory().add( 541 | function () { 542 | currentRoot = oldRoot 543 | checkCurrentRoot() 544 | }, 545 | function() { 546 | currentRoot = root 547 | checkCurrentRoot() 548 | }, 549 | "current root" 550 | ) 551 | } 552 | 553 | function checkCurrentRoot() { 554 | switch (currentRoot) { 555 | case 0: 556 | root_c.checked = true 557 | break 558 | case 1: 559 | root_g.checked = true 560 | break 561 | case 2: 562 | root_d.checked = true 563 | break 564 | case 3: 565 | root_a.checked = true 566 | break 567 | case 4: 568 | root_e.checked = true 569 | break 570 | case 5: 571 | root_b.checked = true 572 | break 573 | case 6: 574 | root_f_sharp.checked = true 575 | break 576 | case 7: 577 | root_c_sharp.checked = true 578 | break 579 | case 8: 580 | root_g_sharp.checked = true 581 | break 582 | case 9: 583 | root_e_flat.checked = true 584 | break 585 | case 10: 586 | root_b_flat.checked = true 587 | break 588 | case 11: 589 | root_f.checked = true 590 | break 591 | } 592 | } 593 | 594 | function setCurrentPureTone(pureTone) { 595 | var oldPureTone = currentPureTone 596 | getHistory().add( 597 | function () { 598 | currentPureTone = oldPureTone 599 | checkCurrentPureTone() 600 | }, 601 | function() { 602 | currentPureTone = pureTone 603 | checkCurrentPureTone() 604 | }, 605 | "current pure tone" 606 | ) 607 | } 608 | 609 | function setCurrentTweak(tweak) { 610 | var oldTweak = currentTweak 611 | getHistory().add( 612 | function () { 613 | currentTweak = oldTweak 614 | checkCurrentTweak() 615 | }, 616 | function () { 617 | currentTweak = tweak 618 | checkCurrentTweak() 619 | }, 620 | "current tweak" 621 | ) 622 | } 623 | 624 | function checkCurrentTweak() { 625 | tweakValue.text = currentTweak.toFixed(1) 626 | } 627 | 628 | function checkCurrentPureTone() { 629 | switch (currentPureTone) { 630 | case 0: 631 | pure_c.checked = true 632 | break 633 | case 1: 634 | pure_g.checked = true 635 | break 636 | case 2: 637 | pure_d.checked = true 638 | break 639 | case 3: 640 | pure_a.checked = true 641 | break 642 | case 4: 643 | pure_e.checked = true 644 | break 645 | case 5: 646 | pure_b.checked = true 647 | break 648 | case 6: 649 | pure_f_sharp.checked = true 650 | break 651 | case 7: 652 | pure_c_sharp.checked = true 653 | break 654 | case 8: 655 | pure_g_sharp.checked = true 656 | break 657 | case 9: 658 | pure_e_flat.checked = true 659 | break 660 | case 10: 661 | pure_b_flat.checked = true 662 | break 663 | case 11: 664 | pure_f.checked = true 665 | break 666 | } 667 | } 668 | 669 | function setModified(state) { 670 | var oldModified = modified 671 | getHistory().add( 672 | function () { 673 | modified = oldModified 674 | }, 675 | function () { 676 | modified = state 677 | }, 678 | "modified" 679 | ) 680 | } 681 | 682 | function temperamentClicked(temperament) { 683 | getHistory().begin() 684 | setCurrentTemperament(temperament) 685 | setCurrentRoot(currentTemperament.root) 686 | setCurrentPureTone(currentTemperament.pure) 687 | setCurrentTweak(0.0) 688 | recalculate(getTuning()) 689 | getHistory().end() 690 | } 691 | 692 | function rootNoteClicked(note) { 693 | getHistory().begin() 694 | setModified(true) 695 | setCurrentRoot(note) 696 | setCurrentPureTone(note) 697 | setCurrentTweak(0.0) 698 | recalculate(getTuning()) 699 | getHistory().end() 700 | } 701 | 702 | function pureToneClicked(note) { 703 | getHistory().begin() 704 | setModified(true) 705 | setCurrentPureTone(note) 706 | setCurrentTweak(0.0) 707 | recalculate(getTuning()) 708 | getHistory().end() 709 | } 710 | 711 | function tweaked() { 712 | getHistory().begin() 713 | setModified(true) 714 | setCurrentTweak(parseFloat(tweakValue.text)) 715 | recalculate(getTuning()) 716 | getHistory().end() 717 | } 718 | 719 | function editingFinishedFor(textField) { 720 | var oldText = textField.previousText 721 | var newText = textField.text 722 | getHistory().begin() 723 | setModified(true) 724 | getHistory().add( 725 | function () { 726 | textField.text = oldText 727 | }, 728 | function () { 729 | textField.text = newText 730 | }, 731 | "edit ".concat(textField.name) 732 | ) 733 | getHistory().end() 734 | textField.previousText = newText 735 | } 736 | 737 | Rectangle { 738 | color: "lightgrey" 739 | anchors.fill: parent 740 | 741 | GridLayout { 742 | columns: 2 743 | anchors.fill: parent 744 | anchors.margins: 10 745 | GroupBox { 746 | title: "Temperament" 747 | ColumnLayout { 748 | ExclusiveGroup { id: tempamentTypeGroup } 749 | RadioButton { 750 | id: equal_button 751 | text: "Equal" 752 | checked: true 753 | exclusiveGroup: tempamentTypeGroup 754 | onClicked: { temperamentClicked(equal) } 755 | } 756 | RadioButton { 757 | id: pythagorean_button 758 | text: "Pythagorean" 759 | exclusiveGroup: tempamentTypeGroup 760 | onClicked: { temperamentClicked(pythagorean) } 761 | } 762 | RadioButton { 763 | id: aaron_button 764 | text: "Aaron" 765 | exclusiveGroup: tempamentTypeGroup 766 | onClicked: { temperamentClicked(aaron) } 767 | } 768 | RadioButton { 769 | id: silberman_button 770 | text: "Silberman" 771 | exclusiveGroup: tempamentTypeGroup 772 | onClicked: { temperamentClicked(silberman) } 773 | } 774 | RadioButton { 775 | id: salinas_button 776 | text: "Salinas" 777 | exclusiveGroup: tempamentTypeGroup 778 | onClicked: { temperamentClicked(salinas) } 779 | } 780 | RadioButton { 781 | id: kirnberger_button 782 | text: "Kirnberger" 783 | exclusiveGroup: tempamentTypeGroup 784 | onClicked: { temperamentClicked(kirnberger) } 785 | } 786 | RadioButton { 787 | id: vallotti_button 788 | text: "Vallotti" 789 | exclusiveGroup: tempamentTypeGroup 790 | onClicked: { temperamentClicked(vallotti) } 791 | } 792 | RadioButton { 793 | id: werkmeister_button 794 | text: "Werkmeister" 795 | exclusiveGroup: tempamentTypeGroup 796 | onClicked: { temperamentClicked(werkmeister) } 797 | } 798 | RadioButton { 799 | id: marpurg_button 800 | text: "Marpurg" 801 | exclusiveGroup: tempamentTypeGroup 802 | onClicked: { temperamentClicked(marpurg) } 803 | } 804 | RadioButton { 805 | id: just_button 806 | text: "Just" 807 | exclusiveGroup: tempamentTypeGroup 808 | onClicked: { temperamentClicked(just) } 809 | } 810 | RadioButton { 811 | id: meanSemitone_button 812 | text: "Mean Semitone" 813 | exclusiveGroup: tempamentTypeGroup 814 | onClicked: { temperamentClicked(meanSemitone) } 815 | } 816 | RadioButton { 817 | id: grammateus_button 818 | text: "Grammateus" 819 | exclusiveGroup: tempamentTypeGroup 820 | onClicked: { temperamentClicked(grammateus) } 821 | } 822 | RadioButton { 823 | id: french_button 824 | text: "French" 825 | exclusiveGroup: tempamentTypeGroup 826 | onClicked: { temperamentClicked(french) } 827 | } 828 | RadioButton { 829 | id: french2_button 830 | text: "Tempérament Ordinaire" 831 | exclusiveGroup: tempamentTypeGroup 832 | onClicked: { temperamentClicked(french2) } 833 | } 834 | RadioButton { 835 | id: rameau_button 836 | text: "Rameau" 837 | exclusiveGroup: tempamentTypeGroup 838 | onClicked: { temperamentClicked(rameau) } 839 | } 840 | RadioButton { 841 | id: irrFr17e_button 842 | text: "Irr Fr 17e" 843 | exclusiveGroup: tempamentTypeGroup 844 | onClicked: { temperamentClicked(irrFr17e) } 845 | } 846 | RadioButton { 847 | id: bachLehman_button 848 | text: "Bach/Lehman" 849 | exclusiveGroup: tempamentTypeGroup 850 | onClicked: { temperamentClicked(bachLehman) } 851 | } 852 | } 853 | } 854 | 855 | ColumnLayout { 856 | GroupBox { 857 | title: "Advanced" 858 | ColumnLayout { 859 | GroupBox { 860 | title: "Root Note" 861 | GridLayout { 862 | columns: 4 863 | anchors.margins: 10 864 | ExclusiveGroup { id: rootNoteGroup } 865 | RadioButton { 866 | text: "C" 867 | checked: true 868 | exclusiveGroup: rootNoteGroup 869 | id: root_c 870 | onClicked: { rootNoteClicked(0) } 871 | } 872 | RadioButton { 873 | text: "G" 874 | exclusiveGroup: rootNoteGroup 875 | id: root_g 876 | onClicked: { rootNoteClicked(1) } 877 | } 878 | RadioButton { 879 | text: "D" 880 | exclusiveGroup: rootNoteGroup 881 | id: root_d 882 | onClicked: { rootNoteClicked(2) } 883 | } 884 | RadioButton { 885 | text: "A" 886 | exclusiveGroup: rootNoteGroup 887 | id: root_a 888 | onClicked: { rootNoteClicked(3) } 889 | } 890 | RadioButton { 891 | text: "E" 892 | exclusiveGroup: rootNoteGroup 893 | id: root_e 894 | onClicked: { rootNoteClicked(4) } 895 | } 896 | RadioButton { 897 | text: "B" 898 | exclusiveGroup: rootNoteGroup 899 | id: root_b 900 | onClicked: { rootNoteClicked(5) } 901 | } 902 | RadioButton { 903 | text: "F#" 904 | exclusiveGroup: rootNoteGroup 905 | id: root_f_sharp 906 | onClicked: { rootNoteClicked(6) } 907 | } 908 | RadioButton { 909 | text: "C#" 910 | exclusiveGroup: rootNoteGroup 911 | id: root_c_sharp 912 | onClicked: { rootNoteClicked(7) } 913 | } 914 | RadioButton { 915 | text: "G#" 916 | exclusiveGroup: rootNoteGroup 917 | id: root_g_sharp 918 | onClicked: { rootNoteClicked(8) } 919 | } 920 | RadioButton { 921 | text: "Eb" 922 | exclusiveGroup: rootNoteGroup 923 | id: root_e_flat 924 | onClicked: { rootNoteClicked(9) } 925 | } 926 | RadioButton { 927 | text: "Bb" 928 | exclusiveGroup: rootNoteGroup 929 | id: root_b_flat 930 | onClicked: { rootNoteClicked(10) } 931 | } 932 | RadioButton { 933 | text: "F" 934 | exclusiveGroup: rootNoteGroup 935 | id: root_f 936 | onClicked: { rootNoteClicked(11) } 937 | } 938 | } 939 | } 940 | 941 | GroupBox { 942 | title: "Pure Tone" 943 | GridLayout { 944 | columns: 4 945 | anchors.margins: 10 946 | ExclusiveGroup { id: pureToneGroup } 947 | RadioButton { 948 | text: "C" 949 | checked: true 950 | id: pure_c 951 | exclusiveGroup: pureToneGroup 952 | onClicked: { pureToneClicked(0) } 953 | } 954 | RadioButton { 955 | text: "G" 956 | id: pure_g 957 | exclusiveGroup: pureToneGroup 958 | onClicked: { pureToneClicked(1) } 959 | } 960 | RadioButton { 961 | text: "D" 962 | id: pure_d 963 | exclusiveGroup: pureToneGroup 964 | onClicked: { pureToneClicked(2) } 965 | } 966 | RadioButton { 967 | text: "A" 968 | id: pure_a 969 | exclusiveGroup: pureToneGroup 970 | onClicked: { pureToneClicked(3) } 971 | } 972 | RadioButton { 973 | text: "E" 974 | id: pure_e 975 | exclusiveGroup: pureToneGroup 976 | onClicked: { pureToneClicked(4) } 977 | } 978 | RadioButton { 979 | text: "B" 980 | id: pure_b 981 | exclusiveGroup: pureToneGroup 982 | onClicked: { pureToneClicked(5) } 983 | } 984 | RadioButton { 985 | text: "F#" 986 | id: pure_f_sharp 987 | exclusiveGroup: pureToneGroup 988 | onClicked: { pureToneClicked(6) } 989 | } 990 | RadioButton { 991 | text: "C#" 992 | id: pure_c_sharp 993 | exclusiveGroup: pureToneGroup 994 | onClicked: { pureToneClicked(7) } 995 | } 996 | RadioButton { 997 | text: "G#" 998 | id: pure_g_sharp 999 | exclusiveGroup: pureToneGroup 1000 | onClicked: { pureToneClicked(8) } 1001 | } 1002 | RadioButton { 1003 | text: "Eb" 1004 | id: pure_e_flat 1005 | exclusiveGroup: pureToneGroup 1006 | onClicked: { pureToneClicked(9) } 1007 | } 1008 | RadioButton { 1009 | text: "Bb" 1010 | id: pure_b_flat 1011 | exclusiveGroup: pureToneGroup 1012 | onClicked: { pureToneClicked(10) } 1013 | } 1014 | RadioButton { 1015 | text: "F" 1016 | id: pure_f 1017 | exclusiveGroup: pureToneGroup 1018 | onClicked: { pureToneClicked(11) } 1019 | } 1020 | } 1021 | } 1022 | 1023 | GroupBox { 1024 | title: "Tweak" 1025 | RowLayout { 1026 | TextField { 1027 | Layout.maximumWidth: offsetTextWidth 1028 | id: tweakValue 1029 | text: "0.0" 1030 | readOnly: false 1031 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1032 | property var previousText: "0.0" 1033 | property var name: "tweak" 1034 | onEditingFinished: { tweaked() } 1035 | } 1036 | } 1037 | } 1038 | 1039 | GroupBox { 1040 | title: "Final Offsets" 1041 | GridLayout { 1042 | columns: 8 1043 | anchors.margins: 0 1044 | 1045 | Label { 1046 | text: "C" 1047 | Layout.alignment: offsetLabelAlignment 1048 | } 1049 | TextField { 1050 | Layout.maximumWidth: offsetTextWidth 1051 | id: final_c 1052 | text: "0.0" 1053 | readOnly: false 1054 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1055 | property var previousText: "0.0" 1056 | property var name: "final C" 1057 | onEditingFinished: { editingFinishedFor(final_c) } 1058 | } 1059 | 1060 | Label { 1061 | text: "G" 1062 | Layout.alignment: offsetLabelAlignment 1063 | } 1064 | TextField { 1065 | Layout.maximumWidth: offsetTextWidth 1066 | id: final_g 1067 | text: "0.0" 1068 | readOnly: false 1069 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1070 | property var previousText: "0.0" 1071 | property var name: "final G" 1072 | onEditingFinished: { editingFinishedFor(final_g) } 1073 | } 1074 | 1075 | Label { 1076 | text: "D" 1077 | Layout.alignment: offsetLabelAlignment 1078 | } 1079 | TextField { 1080 | Layout.maximumWidth: offsetTextWidth 1081 | id: final_d 1082 | text: "0.0" 1083 | readOnly: false 1084 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1085 | property var previousText: "0.0" 1086 | property var name: "final D" 1087 | onEditingFinished: { editingFinishedFor(final_d) } 1088 | } 1089 | 1090 | Label { 1091 | text: "A" 1092 | Layout.alignment: offsetLabelAlignment 1093 | } 1094 | TextField { 1095 | Layout.maximumWidth: offsetTextWidth 1096 | id: final_a 1097 | text: "0.0" 1098 | readOnly: false 1099 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1100 | property var previousText: "0.0" 1101 | property var name: "final A" 1102 | onEditingFinished: { editingFinishedFor(final_a) } 1103 | } 1104 | 1105 | Label { 1106 | text: "E" 1107 | Layout.alignment: offsetLabelAlignment 1108 | } 1109 | TextField { 1110 | Layout.maximumWidth: offsetTextWidth 1111 | id: final_e 1112 | text: "0.0" 1113 | readOnly: false 1114 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1115 | property var previousText: "0.0" 1116 | property var name: "final E" 1117 | onEditingFinished: { editingFinishedFor(final_e) } 1118 | } 1119 | 1120 | Label { 1121 | text: "B" 1122 | Layout.alignment: offsetLabelAlignment 1123 | } 1124 | TextField { 1125 | Layout.maximumWidth: offsetTextWidth 1126 | id: final_b 1127 | text: "0.0" 1128 | readOnly: false 1129 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1130 | property var previousText: "0.0" 1131 | property var name: "final B" 1132 | onEditingFinished: { editingFinishedFor(final_b) } 1133 | } 1134 | 1135 | Label { 1136 | text: "F#" 1137 | Layout.alignment: offsetLabelAlignment 1138 | } 1139 | TextField { 1140 | Layout.maximumWidth: offsetTextWidth 1141 | id: final_f_sharp 1142 | text: "0.0" 1143 | readOnly: false 1144 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1145 | property var previousText: "0.0" 1146 | property var name: "final F#" 1147 | onEditingFinished: { editingFinishedFor(final_f_sharp) } 1148 | } 1149 | 1150 | Label { 1151 | text: "C#" 1152 | Layout.alignment: offsetLabelAlignment 1153 | } 1154 | TextField { 1155 | Layout.maximumWidth: offsetTextWidth 1156 | id: final_c_sharp 1157 | text: "0.0" 1158 | readOnly: false 1159 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1160 | property var previousText: "0.0" 1161 | property var name: "final C#" 1162 | onEditingFinished: { editingFinishedFor(final_c_sharp) } 1163 | } 1164 | 1165 | Label { 1166 | text: "G#" 1167 | Layout.alignment: offsetLabelAlignment 1168 | } 1169 | TextField { 1170 | Layout.maximumWidth: offsetTextWidth 1171 | id: final_g_sharp 1172 | text: "0.0" 1173 | readOnly: false 1174 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1175 | property var previousText: "0.0" 1176 | property var name: "final G#" 1177 | onEditingFinished: { editingFinishedFor(final_g_sharp) } 1178 | } 1179 | 1180 | Label { 1181 | text: "Eb" 1182 | Layout.alignment: offsetLabelAlignment 1183 | } 1184 | TextField { 1185 | Layout.maximumWidth: offsetTextWidth 1186 | id: final_e_flat 1187 | text: "0.0" 1188 | readOnly: false 1189 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1190 | property var previousText: "0.0" 1191 | property var name: "final Eb" 1192 | onEditingFinished: { editingFinishedFor(final_e_flat) } 1193 | } 1194 | 1195 | Label { 1196 | text: "Bb" 1197 | Layout.alignment: offsetLabelAlignment 1198 | } 1199 | TextField { 1200 | Layout.maximumWidth: offsetTextWidth 1201 | id: final_b_flat 1202 | text: "0.0" 1203 | readOnly: false 1204 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1205 | property var previousText: "0.0" 1206 | property var name: "final Bb" 1207 | onEditingFinished: { editingFinishedFor(final_b_flat) } 1208 | } 1209 | 1210 | Label { 1211 | text: "F" 1212 | Layout.alignment: offsetLabelAlignment 1213 | } 1214 | TextField { 1215 | Layout.maximumWidth: offsetTextWidth 1216 | id: final_f 1217 | text: "0.0" 1218 | readOnly: false 1219 | validator: DoubleValidator { bottom: -99.9; decimals: 1; notation: DoubleValidator.StandardNotation; top: 99.9 } 1220 | property var previousText: "0.0" 1221 | property var name: "final F" 1222 | onEditingFinished: { editingFinishedFor(final_f) } 1223 | } 1224 | } 1225 | } 1226 | RowLayout { 1227 | Button { 1228 | id: saveButton 1229 | text: qsTranslate("PrefsDialogBase", "Save") 1230 | onClicked: { 1231 | // declaring this directly in the saveDialog's properties doesn't seem to work 1232 | saveDialog.folder = Qt.resolvedUrl("file://" + filePath) 1233 | saveDialog.visible = true 1234 | } 1235 | } 1236 | Button { 1237 | id: loadButton 1238 | text: qsTranslate("PrefsDialogBase", "Load") 1239 | onClicked: { 1240 | loadDialog.folder = Qt.resolvedUrl("file://" + filePath) 1241 | loadDialog.visible = true 1242 | } 1243 | } 1244 | Button { 1245 | id: undoButton 1246 | text: qsTranslate("PrefsDialogBase", "Undo") 1247 | onClicked: { 1248 | getHistory().undo() 1249 | } 1250 | } 1251 | Button { 1252 | id: redoButton 1253 | text: qsTranslate("PrefsDialogBase", "Redo") 1254 | onClicked: { 1255 | getHistory().redo() 1256 | } 1257 | } 1258 | } 1259 | } 1260 | } 1261 | 1262 | RowLayout { 1263 | Button { 1264 | id: applyButton 1265 | text: qsTranslate("PrefsDialogBase", "Apply") 1266 | onClicked: { 1267 | if (applyTemperament()) { 1268 | if (modified) { 1269 | quitDialog.open() 1270 | } else { 1271 | Qt.quit() 1272 | } 1273 | } 1274 | } 1275 | } 1276 | Button { 1277 | id: cancelButton 1278 | text: qsTranslate("PrefsDialogBase", "Cancel") 1279 | onClicked: { 1280 | if (modified) { 1281 | quitDialog.open() 1282 | } else { 1283 | Qt.quit() 1284 | } 1285 | } 1286 | } 1287 | CheckBox { 1288 | id: annotateValue 1289 | text: qsTr("Annotate") 1290 | checked: false 1291 | } 1292 | } 1293 | } 1294 | } 1295 | } 1296 | 1297 | MessageDialog { 1298 | id: errorDialog 1299 | title: "Error" 1300 | text: "" 1301 | onAccepted: { 1302 | errorDialog.close() 1303 | } 1304 | } 1305 | 1306 | MessageDialog { 1307 | id: quitDialog 1308 | title: "Quit?" 1309 | text: "Do you want to quit the plugin?" 1310 | detailedText: "It looks like you have made customisations to this tuning, you could save them to a file before quitting if you like." 1311 | standardButtons: StandardButton.Ok | StandardButton.Cancel 1312 | onAccepted: { 1313 | Qt.quit() 1314 | } 1315 | onRejected: { 1316 | quitDialog.close() 1317 | } 1318 | } 1319 | 1320 | FileIO { 1321 | id: saveFile 1322 | source: "" 1323 | } 1324 | 1325 | FileIO { 1326 | id: loadFile 1327 | source: "" 1328 | } 1329 | 1330 | function getFile(dialog) { 1331 | var source = dialog.fileUrl.toString().substring(7) // strip the 'file://' prefix 1332 | return source 1333 | } 1334 | 1335 | function formatCurrentValues() { 1336 | var data = { 1337 | offsets: [ 1338 | parseFloat(final_c.text), 1339 | parseFloat(final_c_sharp.text), 1340 | parseFloat(final_d.text), 1341 | parseFloat(final_e_flat.text), 1342 | parseFloat(final_e.text), 1343 | parseFloat(final_f.text), 1344 | parseFloat(final_f_sharp.text), 1345 | parseFloat(final_g.text), 1346 | parseFloat(final_g_sharp.text), 1347 | parseFloat(final_a.text), 1348 | parseFloat(final_b_flat.text), 1349 | parseFloat(final_b.text) 1350 | ], 1351 | temperament: currentTemperament.name, 1352 | root: currentRoot, 1353 | pure: currentPureTone, 1354 | tweak: currentTweak 1355 | }; 1356 | return(JSON.stringify(data)) 1357 | } 1358 | 1359 | function restoreSavedValues(data) { 1360 | getHistory().begin() 1361 | setCurrentTemperament(lookupTemperament(data.temperament)) 1362 | setCurrentRoot(data.root) 1363 | setCurrentPureTone(data.pure) 1364 | // support older save files 1365 | if (data.hasOwnProperty('tweak')) { 1366 | setCurrentTweak(data.tweak) 1367 | } else { 1368 | setCurrentTweak(0.0) 1369 | } 1370 | recalculate( 1371 | function(pitch) { 1372 | return data.offsets[pitch % 12] 1373 | } 1374 | ) 1375 | getHistory().end() 1376 | } 1377 | 1378 | FileDialog { 1379 | id: loadDialog 1380 | title: "Please choose a file" 1381 | sidebarVisible: true 1382 | onAccepted: { 1383 | loadFile.source = getFile(loadDialog) 1384 | var data = JSON.parse(loadFile.read()) 1385 | restoreSavedValues(data) 1386 | loadDialog.visible = false 1387 | } 1388 | onRejected: { 1389 | loadDialog.visible = false 1390 | } 1391 | visible: false 1392 | } 1393 | 1394 | FileDialog { 1395 | id: saveDialog 1396 | title: "Please name a file" 1397 | sidebarVisible: true 1398 | selectExisting: false 1399 | onAccepted: { 1400 | saveFile.source = getFile(saveDialog) 1401 | saveFile.write(formatCurrentValues()) 1402 | saveDialog.visible = false 1403 | } 1404 | onRejected: { 1405 | saveDialog.visible = false 1406 | } 1407 | visible: false 1408 | } 1409 | 1410 | // Command pattern for undo/redo 1411 | function commandHistory() { 1412 | function Command(undo_fn, redo_fn, label) { 1413 | this.undo = undo_fn 1414 | this.redo = redo_fn 1415 | this.label = label // for debugging 1416 | } 1417 | 1418 | var history = [] 1419 | var index = -1 1420 | var transaction = 0 1421 | var maxHistory = 30 1422 | 1423 | function newHistory(commands) { 1424 | if (index < maxHistory) { 1425 | index++ 1426 | history = history.slice(0, index) 1427 | } else { 1428 | history = history.slice(1, index) 1429 | } 1430 | history.push(commands) 1431 | } 1432 | 1433 | this.add = function(undo, redo, label) { 1434 | var command = new Command(undo, redo, label) 1435 | command.redo() 1436 | if (transaction) { 1437 | history[index].push(command) 1438 | } else { 1439 | newHistory([command]) 1440 | } 1441 | } 1442 | 1443 | this.undo = function() { 1444 | if (index != -1) { 1445 | history[index].slice().reverse().forEach( 1446 | function(command) { 1447 | command.undo() 1448 | } 1449 | ) 1450 | index-- 1451 | } 1452 | } 1453 | 1454 | this.redo = function() { 1455 | if ((index + 1) < history.length) { 1456 | index++ 1457 | history[index].forEach( 1458 | function(command) { 1459 | command.redo() 1460 | } 1461 | ) 1462 | } 1463 | } 1464 | 1465 | this.begin = function() { 1466 | if (transaction) { 1467 | throw new Error("already in transaction") 1468 | } 1469 | newHistory([]) 1470 | transaction = 1 1471 | } 1472 | 1473 | this.end = function() { 1474 | if (!transaction) { 1475 | throw new Error("not in transaction") 1476 | } 1477 | transaction = 0 1478 | } 1479 | } 1480 | } 1481 | // vim: ft=javascript 1482 | -------------------------------------------------------------------------------- /src/Tuning/README.md: -------------------------------------------------------------------------------- 1 | # Tuning 2 | 3 | I've written a plugin (tested with MuseScore 2.1.0 and 3.0.5) that others may find useful. 4 | However with MuseScore 3.x pretty stable now, I will no longer be back-porting new features into the 5 | 2.x version. 6 | 7 | I've been intrigued for a while by the tuning Bach purportedly used 8 | for his WTC according to Bradley Lehman [Website Here](http://www.larips.com) 9 | and initially set out to just do that, 10 | however I found a great resource on other tunings by Pierre Lewis 11 | [Here](http://leware.net/temper/temper.htm) and went ahead and added 12 | all of those too. 13 | 14 | ## Screen Shot 15 | 16 | ![Tuning Pop Up](https://raw.githubusercontent.com/billhails/MuseScore-plugins/develop/images/Tuning.png) 17 | 18 | ## Tunings and Temperaments Supported 19 | 20 | Supported tunings with a brief description, see the Pierre Lewis 21 | page linked above for an explaination of the descriptions `:-)`. 22 | But in brief, a cent is 1/100 of an equal-tempered semitone, a comma 23 | (or diatonic comma) is the difference between the C you started on 24 | and the B# you finish on when tuning in pure fiths (24 cents) and 25 | the syntonic comma is the difference between a pure third and the 26 | first third you reach when tuning in pure fifths (around 21.5 cents.) 27 | 28 | | Tuning | Description | 29 | | ------ | ----------- | 30 | | Equal | Each fifth is tempered 2 cents short of a pure fifth. equally distributing the comma. | 31 | | Pythagorean | Untempered pure fifths, the entire 24-cent comma is between Eb and G#. | 32 | | Aaron | Each fifth is tempered 5.5 cents so that major thirds are pure, but resulting in a 36.5 cent "wolf" between Eb and G#. | 33 | | Silberman | Compromise tempering each fifth by 1/6 of a syntonic comma. Used by high Baroque organs. | 34 | | Salinas | A negative temperament. 1/3 comma makes the major thirds slightly narrow. | 35 | | Kirnberger | an irregular temperamemt (different fifths tempered differently) means each key has a distinctive sound. | 36 | | Vallotti | Another irregular temperament. | 37 | | Werkmeister | Another, less symmetric irregular temperament. | 38 | | Marpurg | Three fifths tempered by 8 cents and evenly distributed. | 39 | | Just | "Just" intonation, An academic temperament. Near thirds and fifths are pure, at the expense of some intervals being unusable. See [here](https://musescore.com/billhails/scores/5704148)| 40 | | Mean Semitone | Like Aaron, but the remaining comma is distributed between B-F# and Bb-F (15.75 cents each.) | 41 | | Grammateus | Hybrid Pythagorean tuning with the chromatic notes tempered. | 42 | | French | Temperament Ordinaire, first fifths tuned wide of a pure fifth, later fifths narrowed to compensate. | 43 | | French (2) | Similar to French. | 44 | | Rameau | Similar to French. | 45 | | Irregular Fr. 17e | Similar to French. | 46 | | Bach/Lehman | Bach's own irregular temperament used for the 48 according to Lehman. See the Bradley Lehman link above. | 47 | 48 | ## Installation 49 | 50 | * Choose either the `2.x/tuning.qml` or `3.x/tuning.qml` file. 51 | * Click the "raw" button. 52 | * Or choose one of these links: [2.x](https://raw.githubusercontent.com/billhails/MuseScore-plugins/master/src/Tuning/2.x/tuning.qml) or [3.x](https://raw.githubusercontent.com/billhails/MuseScore-plugins/master/src/Tuning/3.x/tuning.qml). 53 | * In your browser do "Save as" and make sure there's no `.txt` extension. 54 | * Save the plugin to your `MuseScore2/Plugins` or `MuseScore3/Plugins` directory as appropriate. 55 | * start MuseScore 56 | * enable the plugin via `Plugins > Plugin Manager...`. 57 | 58 | ## Basic Usage 59 | 60 | If you select a passage of music, then only that passage wiil be 61 | affected, otherwise the entire score will be processed. Invoke the 62 | plugin via `Plugins > Playback > Tuning`, select a tuning and apply. 63 | It changes the tuning offset for every selected note appropriately. 64 | 65 | To reset just hit Ctrl-Z (Cmd-Z on a Mac,) or if you're removing a 66 | tuning that was applied in a previous session, apply the "equal" 67 | tuning (equal temperament.) 68 | 69 | ## Advanced Usage 70 | 71 | You can make the following adjustments to the tuning, before applying 72 | it: 73 | 74 | ### Root Note 75 | 76 | Allows you to choose a different root note to center the tuning on. 77 | This has the effect of rotating the tuning around the cycle of 78 | fifths. For example suppose in a particular tuning, with C as the 79 | root note, the interval from C to G is a pure fifth while the 80 | interval G to D is slightly wide. If you select G as the root note, 81 | then the interval from G to D will be a pure fifth, and the interval 82 | from D to A will be slightly wide, and so on for all the other 83 | intervals. 84 | 85 | This basically allows you to make certain tunings more usable in 86 | remote keys. 87 | 88 | Note that certain tunings, such as the Pythagorean, already specify 89 | a root note other than C. 90 | 91 | ### Pure Tone 92 | 93 | Adjusts each note by a constant amount so that the chosen pure tone 94 | is tuned to its "correct" equal tempered pitch (i.e. offset 0.0), 95 | while maintaining the relationships of the tuning. This is occasionally 96 | necessary to correctly reproduce a desired tuning exactly. 97 | 98 | Note that when you change the Root Note above, the Pure Tone also 99 | changes to the same note. This is usually what you want, but you 100 | can subsequently adjust the Pure Tone separately if needed. 101 | 102 | Also note that certain preset tunings already have a pure tone 103 | other than C, to properly represent the correct tuning. Again 104 | you can override this choice if needed. 105 | 106 | ### Tweak 107 | 108 | This just adds the specified value in cents to each of the final 109 | values. Useful if you require a particular non-zero offset for a 110 | particular note. 111 | 112 | ### Final Values 113 | 114 | Allows you to directly edit all the offsets that will be applied. 115 | 116 | ### Advanced Controls 117 | 118 | #### Save 119 | 120 | Will prompt for a filename and save the current settings. I'd 121 | recomment creating a directory called `tunings` under your `Plugins` 122 | directory, but you can put them where you like. The file is text, 123 | in JSON format, so you can share your tunings with others or save 124 | them for later re-use. 125 | 126 | #### Load 127 | 128 | Loads a file previously saved above. 129 | 130 | #### Undo 131 | 132 | Undoes the last change. There is a hstory limit of 30. 133 | 134 | #### Redo 135 | 136 | Redo a previous undo, where possible. 137 | 138 | ### Caveats 139 | 140 | With the exception of the "Annotate" checkbox, the controls are 141 | applied strictly top to bottom and left to right. This means that 142 | making a change in any of the controls will override any changes 143 | below and to the right of that control, so for example if you 144 | manually edit the Final Values then select a different Root Note 145 | your changes to the Final Values will be overridden. You can use 146 | the Undo button to revert that change however. 147 | 148 | ### Apply and Cancel 149 | 150 | If you have made customisations to a tuning, and you hit "Apply" 151 | or "Cancel", you will be asked to confirm that you want the plugin to 152 | quit. You might be quite annoyed if you spent time entering a set 153 | of offsets manually, hit apply to try them out, and the plugin 154 | applied them then vanished with no record of your work other than the 155 | score. 156 | 157 | ### Annotate 158 | 159 | Annotates each note with the offset in cents that was applied. I wanted 160 | this, so you all get it `:-)`. 161 | 162 | -------------------------------------------------------------------------------- /src/VoiceVelocity/README.md: -------------------------------------------------------------------------------- 1 | # Voice Velocity 2 | 3 | Alters the velocity offset of all the notes of a chosen voice within a selection. 4 | 5 | The dynamics (PP, P etc.) allow a choice of Part (single stave), Stave (both staves in the case of a piano) or System (all staves). 6 | That does not allow the control of the dynamics of a single voice within a part, which this plugin adresses. 7 | 8 | ## Installing 9 | * Copy the file `voice-velocity.qml` to your MuseScore2 Plugins directory. 10 | * Re-start MuseScore. 11 | * Go to Plugins > Plugin Manager and tick the box next to voice-velocity. 12 | 13 | ## Running 14 | * Select a passage of music within a single stave with multiple voices. 15 | * Select Plugins > Playback > Voice Velocity to start the plugin. 16 | * Choose an offset. Negative values make the voice quieter, positive values louder. 17 | * Choose the voice that you want to alter. 18 | * Click Apply. 19 | * You can check that it worked by selecting a single note of that voice and looking at the velocity in the inspector. 20 | -------------------------------------------------------------------------------- /src/VoiceVelocity/voice-velocity.qml: -------------------------------------------------------------------------------- 1 | // Voice Velocity 2 | // 3 | // Copyright (C) 2018 Bill Hails 4 | // 5 | // This program is free software: you can redistribute it and/or modify 6 | // it under the terms of the GNU General Public License as published by 7 | // the Free Software Foundation, either version 3 of the License, or 8 | // (at your option) any later version. 9 | // 10 | // This program is distributed in the hope that it will be useful, 11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | // GNU General Public License for more details. 14 | // 15 | // You should have received a copy of the GNU General Public License 16 | // along with this program. If not, see . 17 | 18 | import MuseScore 1.0 19 | import QtQuick 2.2 20 | import QtQuick.Controls 1.1 21 | import QtQuick.Controls.Styles 1.3 22 | import QtQuick.Layouts 1.1 23 | import QtQuick.Dialogs 1.1 24 | 25 | MuseScore { 26 | menuPath: "Plugins.Playback.Voice Velocity" 27 | version: "1.0.0" 28 | description: qsTr("Offsets the velocity (volume) of a chosen voice in a selection by a specified amount") 29 | pluginType: "dialog" 30 | 31 | width: 240 32 | 33 | onRun: { 34 | if (!curScore) { 35 | error("No score open.\nThis plugin requires an open score to run.\n") 36 | Qt.quit() 37 | } 38 | } 39 | 40 | function applyVoiceVelocity() { 41 | var selection = getSelection() 42 | if (selection === null) { 43 | error("No selection.\nThis plugin requires a current selection to run.\n") 44 | Qt.quit() 45 | } 46 | curScore.startCmd() 47 | mapOverSelection(selection, filterVoice(getChosenVoice()), setVelocity(getVelocityOffset())) 48 | curScore.endCmd() 49 | } 50 | 51 | function mapOverSelection(selection, filter, process) { 52 | selection.cursor.rewind(1) 53 | for ( 54 | var segment = selection.cursor.segment; 55 | segment && segment.tick < selection.endTick; 56 | segment = segment.next 57 | ) { 58 | for (var track = selection.startTrack; track < selection.endTrack; track++) { 59 | var element = segment.elementAt(track) 60 | if (element) { 61 | if (filter(element, track)) { 62 | process(element) 63 | } 64 | } 65 | } 66 | } 67 | } 68 | 69 | function filterVoice(chosenVoice) { 70 | return function (element, track) { 71 | return element.type == Element.CHORD && track % 4 == chosenVoice 72 | } 73 | } 74 | 75 | function setVelocity(velocityOffset) { 76 | return function (chord) { 77 | for (var i = 0; i < chord.notes.length; i++) { 78 | var note = chord.notes[i] 79 | note.veloType = Note.OFFSET_VAL 80 | note.veloOffset = velocityOffset 81 | } 82 | } 83 | } 84 | 85 | function getSelection() { 86 | var cursor = curScore.newCursor() 87 | cursor.rewind(1) 88 | if (!cursor.segment) { 89 | return null 90 | } 91 | var selection = { 92 | cursor: cursor, 93 | startTick: cursor.tick, 94 | endTick: null, 95 | startStaff: cursor.staffIdx, 96 | endStaff: null, 97 | startTrack: null, 98 | endTrack: null 99 | } 100 | cursor.rewind(2) 101 | selection.endStaff = cursor.staffIdx + 1 102 | if (cursor.tick == 0) { 103 | selection.endTick = curScore.lastSegment.tick + 1 104 | } else { 105 | selection.endTick = cursor.tick 106 | } 107 | selection.startTrack = selection.startStaff * 4 108 | selection.endTrack = selection.endStaff * 4 109 | return selection 110 | } 111 | 112 | function error(errorMessage) { 113 | errorDialog.text = qsTr(errorMessage) 114 | errorDialog.open() 115 | } 116 | 117 | function getChosenVoice() { 118 | return chosenVoice 119 | } 120 | 121 | function getVelocityOffset() { 122 | return velocityOffset.value 123 | } 124 | 125 | function getIntFrom(container) { 126 | var text = container.text 127 | if (text == "") { 128 | text = container.placeholderText 129 | } 130 | return parseInt(text) 131 | } 132 | 133 | property int chosenVoice: 0 134 | 135 | Rectangle { 136 | color: "lightgrey" 137 | anchors.fill: parent 138 | 139 | GridLayout { 140 | columns: 2 141 | anchors.fill: parent 142 | anchors.margins: 10 143 | Label { 144 | text: qsTr("offset (-127 to 127): ") 145 | } 146 | SpinBox { 147 | id: velocityOffset 148 | maximumValue: 127 149 | minimumValue: -127 150 | value: 0 151 | } 152 | GroupBox { 153 | Layout.columnSpan: 2 154 | title: "Voice" 155 | RowLayout { 156 | ExclusiveGroup { id: chosenVoiceGroup } 157 | RadioButton { 158 | text: "1" 159 | checked: true 160 | exclusiveGroup: chosenVoiceGroup 161 | onClicked: { 162 | chosenVoice = 0 163 | } 164 | } 165 | RadioButton { 166 | text: "2" 167 | exclusiveGroup: chosenVoiceGroup 168 | onClicked: { 169 | chosenVoice = 1 170 | } 171 | } 172 | RadioButton { 173 | text: "3" 174 | exclusiveGroup: chosenVoiceGroup 175 | onClicked: { 176 | chosenVoice = 2 177 | } 178 | } 179 | RadioButton { 180 | text: "4" 181 | exclusiveGroup: chosenVoiceGroup 182 | onClicked: { 183 | chosenVoice = 3 184 | } 185 | } 186 | } 187 | } 188 | Button { 189 | id: applyButton 190 | text: qsTranslate("PrefsDialogBase", "Apply") 191 | onClicked: { 192 | applyVoiceVelocity() 193 | Qt.quit() 194 | } 195 | } 196 | Button { 197 | id: cancelButton 198 | text: qsTranslate("PrefsDialogBase", "Cancel") 199 | onClicked: { 200 | Qt.quit() 201 | } 202 | } 203 | } 204 | } 205 | 206 | MessageDialog { 207 | id: errorDialog 208 | title: "Error" 209 | text: "" 210 | onAccepted: { 211 | Qt.quit() 212 | } 213 | visible: false 214 | } 215 | } 216 | 217 | // vim: ft=javascript 218 | --------------------------------------------------------------------------------