├── 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 | 
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 | 
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 |
--------------------------------------------------------------------------------