diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ee16f4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,17 @@ +* text=auto eol=lf + +# Git +.gitattributes text +.gitignore text + +# Code +*.py text diff=python +*.txt text +*.pyc binary + +# Docs +*.html text diff=html +*.pdf binary + +# Samples +*.als binary diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/COPYING.LESSER b/COPYING.LESSER new file mode 100644 index 0000000..4362b49 --- /dev/null +++ b/COPYING.LESSER @@ -0,0 +1,502 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! diff --git a/ClyphX Manual.pdf b/ClyphX Manual.pdf new file mode 100644 index 0000000..eecc7b4 Binary files /dev/null and b/ClyphX Manual.pdf differ diff --git a/ClyphX/ActionList.py b/ClyphX/ActionList.py new file mode 100644 index 0000000..3df9082 --- /dev/null +++ b/ClyphX/ActionList.py @@ -0,0 +1,34 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +class ActionList: + __module__ = __name__ + __doc__ = ' Simple class that allows X-Triggers with no name to trigger Action Lists and can also be used to trigger ClyphX Actions via UserActions. ' + + def __init__(self, name = 'none'): + self.name = name + + +# local variables: +# tab-width: 4 \ No newline at end of file diff --git a/ClyphX/CSLinker.py b/ClyphX/CSLinker.py new file mode 100644 index 0000000..2ea3dd6 --- /dev/null +++ b/ClyphX/CSLinker.py @@ -0,0 +1,303 @@ +""" +# Copyright (C) 2014-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +from functools import partial +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent +from _Framework.ControlSurface import ControlSurface +from _Framework.SessionComponent import SessionComponent + +from consts import * + +class CSLinker(ControlSurfaceComponent): + """ CSLinker links the SessionComponents of two control surface scripts in Live 9. """ + + def __init__(self): + ControlSurfaceComponent.__init__(self) + self._slave_objects = [None, None] + self._script_names = None + self._horizontal_link = False + self._matched_link = False + self._multi_axis_link = False + + + def disconnect(self): + """ Extends standard to disconnect and remove slave objects. """ + for obj in self._slave_objects: + if obj: + obj.disconnect() + self._slave_objects = None + ControlSurfaceComponent.disconnect(self) + + + def parse_settings(self, settings_string): + """ Parses settings data read from UserPrefs for linker settings. """ + line_data = settings_string.split('=') + if 'MATCHED' in line_data[0]: + self._matched_link = line_data[1].strip() == 'TRUE' + elif 'HORIZ' in line_data[0] and not self._matched_link: + self._horizontal_link = line_data[1].strip() == 'TRUE' + elif 'AXIS' in line_data[0] and not self._matched_link: + self._multi_axis_link = line_data[1].strip() == 'TRUE' + else: + if 'NONE' in line_data[1]: + self._script_names = None + else: + if '1' in line_data[0]: + self._script_names = [line_data[1].strip()] + else: + if self._script_names: + self._script_names.append(line_data[1].strip()) + if 'PUSH2' in self._script_names: + self.canonical_parent.schedule_message(20, partial(self.connect_script_instances, + self.canonical_parent._control_surfaces())) + else: + self.connect_script_instances(self.canonical_parent._control_surfaces()) + + + def connect_script_instances(self, instanciated_scripts): + """ Attempts to find the two specified scripts, find their SessionComponents and create slave objects for them. """ + if self._script_names: + scripts = [None, None] + found_scripts = False + scripts_have_same_name = self._script_names[0] == self._script_names[1] + for script in instanciated_scripts: + script_name = script.__class__.__name__.upper() + if (IS_LIVE_9_5 and script_name in ('PUSH', 'PUSH2')) or (isinstance(script, ControlSurface) and script.components): + if script_name == self._script_names[0]: + if scripts_have_same_name: + scripts[scripts[0] != None] = script + else: + scripts[0] = script + elif script_name == self._script_names[1]: + scripts[1] = script + found_scripts = scripts[0] and scripts[1] + if found_scripts: + break + if found_scripts: + self.canonical_parent.log_message('CSLINKER SUCCESS: Specified scripts located!') + ssn_comps = [] + for script in scripts: + if IS_LIVE_9_5 and script.__class__.__name__.upper() in ('PUSH', 'PUSH2'): + ssn_comps.append(script._session_ring) + for c in script.components: + if isinstance (c, SessionComponent): + ssn_comps.append(c) + break + if len(ssn_comps) == 2: + self.canonical_parent.log_message('CSLINKER SUCCESS: SessionComponents for specified scripts located!') + if self._matched_link: + for s in ssn_comps: + s._link() + else: + if IS_LIVE_9_5 and self._script_names[0] in ('PUSH', 'PUSH2'): + h_offset = ssn_comps[0].num_tracks + v_offset = ssn_comps[0].num_scenes + else: + h_offset = ssn_comps[0].width() + v_offset = ssn_comps[0].height() + h_offset_1 = 0 if not self._horizontal_link and self._multi_axis_link else -(h_offset) + v_offset_1 = 0 if self._horizontal_link and self._multi_axis_link else -(v_offset) + h_offset_2 = 0 if not self._horizontal_link and self._multi_axis_link else h_offset + v_offset_2 = 0 if self._horizontal_link and self._multi_axis_link else v_offset + self._slave_objects[0] = SessionSlave(self._horizontal_link, self._multi_axis_link, ssn_comps[0], ssn_comps[1], h_offset_1, v_offset_1) + self._slave_objects[1] = SessionSlaveSecondary(self._horizontal_link, self._multi_axis_link, ssn_comps[1], ssn_comps[0], h_offset_2, v_offset_2) + self.canonical_parent.schedule_message(10, self._refresh_slave_objects) + else: + self.canonical_parent.log_message('CSLINKER ERROR: Unable to locate SessionComponents for specified scripts!') + else: + self.canonical_parent.log_message('CSLINKER ERROR: Unable to locate specified scripts!') + + + def on_track_list_changed(self): + """ Refreshes slave objects if horizontally linked. """ + if not self._matched_link and (self._horizontal_link or self._multi_axis_link): + self._refresh_slave_objects() + + + def on_scene_list_changed(self): + """ Refreshes slave objects if vertically linked. """ + if not self._matched_link and (not self._horizontal_link or self._multi_axis_link): + self._refresh_slave_objects() + + + def _refresh_slave_objects(self): + """ Refreshes offsets of slave objects. """ + for obj in self._slave_objects: + if obj: + obj._on_offsets_changed() + + + def update(self): + pass + + +class SessionSlave(object): + """ SessionSlave is the base class for linking two SessionComponents. """ + + def __init__(self, horz_link, multi_axis, self_comp, observed_comp, h_offset, v_offset): + self._horizontal_link = horz_link + self._multi_axis_link = multi_axis + self._h_offset = h_offset + self._v_offset = v_offset + self._self_ssn_comp = self_comp + self._observed_ssn_comp = observed_comp + self._last_self_track_offset = -1 + self._last_self_scene_offset = -1 + self._last_observed_track_offset = -1 + self._last_observed_scene_offset = -1 + self._num_tracks = -1 + self._num_scenes = -1 + self._observed_ssn_comp.add_offset_listener(self._on_offsets_changed) + + + def disconnect(self): + self._self_ssn_comp = None + self._observed_ssn_comp.remove_offset_listener(self._on_offsets_changed) + self._observed_ssn_comp = None + + + def _on_offsets_changed(self, arg_a=None, arg_b=None): + """ Called on offset changes to the observed SessionComponent to handle moving offsets if possible. """ + if self._horizontal_link or self._multi_axis_link: + new_num_tracks = len(self._self_ssn_comp.tracks_to_use()) + if new_num_tracks != self._num_tracks: # if track list changed, need to completely refresh offsets + self._num_tracks = new_num_tracks + self._last_self_track_offset = -1 + self._last_observed_track_offset = -1 + observed_offset = self._observed_track_offset() + if observed_offset != self._last_observed_track_offset: # if observed offset unchanged, do nothing + self._last_observed_track_offset = observed_offset + if self._track_offset_change_possible(): + self_offset = max(self._min_track_offset(), min(self._num_tracks, (self._last_observed_track_offset + self._h_offset))) + if self_offset != self._last_self_track_offset: # if self offset unchanged, do nothing + self._last_self_track_offset = self_offset + self._self_ssn_comp.set_offsets(self._last_self_track_offset, self._self_scene_offset()) + else: + return + if not self._horizontal_link or self._multi_axis_link: + if hasattr(self._self_ssn_comp.song, '__call__'): + new_num_scenes = len(self._self_ssn_comp.song().scenes) + else: + new_num_scenes = len(self._self_ssn_comp.song.scenes) + if new_num_scenes != self._num_scenes: # if scene list changed, need to completely refresh offsets + self._num_scenes = new_num_scenes + self._last_self_scene_offset = -1 + self._last_observed_scene_offset = -1 + observed_offset = self._observed_scene_offset() + if observed_offset != self._last_observed_scene_offset: # if observed offset unchanged, do nothing + self._last_observed_scene_offset = observed_offset + if self._scene_offset_change_possible(): + self_offset = max(self._min_scene_offset(), min(self._num_scenes, (self._last_observed_scene_offset + self._v_offset))) + if self_offset != self._last_self_scene_offset: # if self offset unchanged, do nothing + self._last_self_scene_offset = self_offset + self._self_ssn_comp.set_offsets(self._self_track_offset(), self._last_self_scene_offset) + else: + return + + def _observed_track_offset(self): + if hasattr(self._observed_ssn_comp.track_offset, '__call__'): + return self._observed_ssn_comp.track_offset() + return self._observed_ssn_comp.track_offset + + + def _self_track_offset(self): + if hasattr(self._self_ssn_comp.track_offset, '__call__'): + return self._self_ssn_comp.track_offset() + return self._self_ssn_comp.track_offset + + + def _observed_scene_offset(self): + if hasattr(self._observed_ssn_comp.scene_offset, '__call__'): + return self._observed_ssn_comp.scene_offset() + return self._observed_ssn_comp.scene_offset + + + def _self_scene_offset(self): + if hasattr(self._self_ssn_comp.scene_offset, '__call__'): + return self._self_ssn_comp.scene_offset() + return self._self_ssn_comp.scene_offset + + + def _track_offset_change_possible(self): + """ Returns whether or not moving the track offset is possible. """ + if hasattr(self._self_ssn_comp, 'width'): + w = self._self_ssn_comp.width() + else: + w = self._self_ssn_comp.num_tracks + return self._num_tracks > w + + + def _min_track_offset(self): + """ Returns the minimum track offset. """ + return 0 + + + def _scene_offset_change_possible(self): + """ Returns whether or not moving the scene offset is possible. """ + if hasattr(self._self_ssn_comp, 'height'): + h = self._self_ssn_comp.height() + else: + h = self._self_ssn_comp.num_scenes + return self._num_scenes > h + + + def _min_scene_offset(self): + """ Returns the minimum scene offset. """ + return 0 + + +class SessionSlaveSecondary(SessionSlave): + """ SessionSlaveSecondary is the second of the two linked slave objects. + This overrides the functions that return whether offsets can be changed as well as the functions that return minimum offsets. """ + + + def _track_offset_change_possible(self): + if hasattr(self._self_ssn_comp, 'width'): + self_width = self._self_ssn_comp.width() + else: + self_width = self._self_ssn_comp.num_tracks + if hasattr(self._observed_ssn_comp, 'width'): + obs_width = self._observed_ssn_comp.width() + else: + obs_width = self._observed_ssn_comp.num_tracks + return self._num_tracks >= self_width + obs_width + + + def _min_track_offset(self): + return self._last_observed_track_offset + + + def _scene_offset_change_possible(self): + if hasattr(self._self_ssn_comp, 'height'): + self_h = self._self_ssn_comp.height() + else: + self_h = self._self_ssn_comp.num_scenes + if hasattr(self._observed_ssn_comp, 'height'): + obs_h = self._observed_ssn_comp.height() + else: + obs_h = self._observed_ssn_comp.num_scenes + return self._num_scenes >= self_h + obs_h + + + def _min_scene_offset(self): + return self._last_observed_scene_offset + + \ No newline at end of file diff --git a/ClyphX/ClyphX.py b/ClyphX/ClyphX.py new file mode 100644 index 0000000..9737177 --- /dev/null +++ b/ClyphX/ClyphX.py @@ -0,0 +1,774 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +from __future__ import with_statement +import Live +import sys +from functools import partial +from _Framework.ControlSurface import ControlSurface +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent +from _Framework import Task +from Macrobat import Macrobat +from ExtraPrefs import ExtraPrefs +from CSLinker import CSLinker +from ClyphXTrackActions import ClyphXTrackActions +from ClyphXSnapActions9 import ClyphXSnapActions +from ClyphXGlobalActions import ClyphXGlobalActions +from ClyphXDeviceActions import ClyphXDeviceActions +from ClyphXDRActions9 import ClyphXDRActions9 +from ClyphXClipActions import ClyphXClipActions +from ClyphXControlSurfaceActions9 import ClyphXControlSurfaceActions9 # specialized version for L9 +from ClyphXTriggers import ClyphXTrackComponent, ClyphXControlComponent, ClyphXCueComponent +from ClyphXUserActions import ClyphXUserActions +from ClyphXM4LBrowserInterface import ClyphXM4LBrowserInterface +from ActionList import ActionList +from Push_APC_Combiner import Push_APC_Combiner +from consts import * +if IS_LIVE_9_5: + from PushEmuHandler import MockHandshakeTask, MockHandshake + +FOLDER = '/ClyphX/' +SCRIPT_NAME = 'nativeKONTROL ClyphX v2.6.2 for Live 9' + + +class ClyphX(ControlSurface): + __module__ = __name__ + __doc__ = " ClyphX Main for Live 9 " + + def __init__(self, c_instance): + ControlSurface.__init__(self, c_instance) + self._user_settings_logged = False + self._is_debugging = False + self._push_emulation = False + self._push_apc_combiner = None + self._process_xclips_if_track_muted = True + with self.component_guard(): + self._macrobat = Macrobat(self) + self._extra_prefs = ExtraPrefs(self) + self._cs_linker = CSLinker() + self._track_actions = ClyphXTrackActions(self) + self._snap_actions = ClyphXSnapActions(self) + self._global_actions = ClyphXGlobalActions(self) + self._device_actions = ClyphXDeviceActions(self) + self._dr_actions = ClyphXDRActions9(self) + self._clip_actions = ClyphXClipActions(self) + self._control_surface_actions = ClyphXControlSurfaceActions9(self) # specialized version for L9 + self._user_actions = ClyphXUserActions(self) + self._control_component = ClyphXControlComponent(self) + ClyphXM4LBrowserInterface(self) + ClyphXCueComponent(self) + self._startup_actions_complete = False + self._user_variables = {} + self._play_seq_clips = {} + self._loop_seq_clips = {} + self._current_tracks = [] + live = Live.Application.get_application() + self._can_have_nested_devices = True + self.setup_tracks() + self.log_message('nativeKONTROL LOG ------- ' + SCRIPT_NAME + ' ------- Live Version: ' + str(live.get_major_version()) + '.' + str(live.get_minor_version()) + '.' + str(live.get_bugfix_version()) + ' ------- END LOG') + self.show_message(SCRIPT_NAME) + + + def disconnect(self): + self._push_apc_combiner = None + self._macrobat = None + self._extra_prefs = None + self._cs_linker = None + self._track_actions = None + self._snap_actions = None + self._global_actions = None + self._device_actions = None + self._dr_actions = None + self._clip_actions = None + self._control_surface_actions = None + self._user_actions = None + self._control_component = None + self._user_variables = {} + self._play_seq_clips = {} + self._loop_seq_clips = {} + self._current_tracks = [] + ControlSurface.disconnect(self) + + + def action_dispatch(self, tracks, xclip, action_name, args, ident): + """ Main dispatch for calling appropriate class of actions, passes all necessary arguments to class method """ + if tracks: + if action_name.startswith('SNAP'): + self._snap_actions.store_track_snapshot(tracks, xclip, ident, action_name, args) + elif action_name.startswith('SURFACE') or action_name.startswith('CS'): + self._control_surface_actions.dispatch_cs_action(tracks[0], xclip, ident, action_name, args) + elif action_name.startswith('ARSENAL'): + self._control_surface_actions.dispatch_arsenal_action(tracks[0], xclip, ident, action_name, args) + elif action_name.startswith('PUSH'): + self._control_surface_actions.dispatch_push_action(tracks[0], xclip, ident, action_name, args) + elif action_name.startswith('PXT'): + self._control_surface_actions.dispatch_pxt_action(tracks[0], xclip, ident, action_name, args) + elif action_name.startswith('MXT'): + self._control_surface_actions.dispatch_mxt_action(tracks[0], xclip, ident, action_name, args) + elif action_name in GLOBAL_ACTIONS: + getattr(self._global_actions, GLOBAL_ACTIONS[action_name])(tracks[0], xclip, ident, args) + elif action_name == 'PSEQ' and args== 'RESET': + for key, value in self._play_seq_clips.items(): + value[1] = -1 + elif action_name == 'DEBUG': + if type(xclip) is Live.Clip.Clip: + xclip.name = str(xclip.name).upper().replace('DEBUG', 'Debugging Activated') + self.start_debugging() + else: + for t in tracks: + if action_name in TRACK_ACTIONS: + getattr(self._track_actions, TRACK_ACTIONS[action_name])(t, xclip, ident, args) + elif action_name == 'LOOPER': + if args and args.split()[0] in LOOPER_ACTIONS: + getattr(self._device_actions, LOOPER_ACTIONS[args.split()[0]])(t, xclip, ident, args) + elif action_name in LOOPER_ACTIONS: + getattr(self._device_actions, LOOPER_ACTIONS[action_name])(t, xclip, ident, args) + elif action_name.startswith('DEV'): + device_action = self.get_device_to_operate_on(t, action_name, args) + device_args = None + if device_action[0]: + if len(device_action) > 1: + device_args = device_action[1] + if device_args and device_args.split()[0] in DEVICE_ACTIONS: + getattr(self._device_actions, DEVICE_ACTIONS[device_args.split()[0]])(device_action[0], t, xclip, ident, device_args) + elif device_args and 'CHAIN' in device_args: + self._device_actions.dispatch_chain_action(device_action[0], t, xclip, ident, device_args) + elif action_name.startswith('DEV'): + self._device_actions.set_device_on_off(device_action[0], t, xclip, ident, device_args) + elif action_name.startswith('CLIP') and t in self.song().tracks: + clip_action = self.get_clip_to_operate_on(t, action_name, args) + clip_args = None + if clip_action[0]: + if len(clip_action) > 1: + clip_args = clip_action[1] + if clip_args and clip_args.split()[0] in CLIP_ACTIONS: + getattr(self._clip_actions, CLIP_ACTIONS[clip_args.split()[0]])(clip_action[0], t, xclip, ident, clip_args.replace(clip_args.split()[0], '')) + elif clip_args and clip_args.split()[0].startswith('NOTES'): + self._clip_actions.do_clip_note_action(clip_action[0], t, xclip, ident, args) + elif action_name.startswith('CLIP'): + self._clip_actions.set_clip_on_off(clip_action[0], t, xclip, ident, args) + elif action_name.startswith('DR'): + dr = self.get_drum_rack_to_operate_on(t) + arg = args.split()[0] + if dr and args: + if arg in DR_ACTIONS: + getattr(self._dr_actions, DR_ACTIONS[arg])(dr, t, xclip, ident, args.strip()) + elif 'PAD' in args: + self._dr_actions.dispatch_pad_action(dr, t, xclip, ident, args.strip()) + elif action_name in self._user_actions._action_dict: + getattr(self._user_actions, self._user_actions._action_dict[action_name])(t, args) + if self._is_debugging: + self.log_message('action_dispatch triggered, ident=' + str(ident) + ' and track(s)=' + str(self.track_list_to_string(tracks)) + ' and action=' + str(action_name) + ' and args=' + str(args)) + + + def handle_external_trigger(self, xtrigger): + """ This replaces the below method for compatibility with scripts that also work with ClyphX Pro. """ + xtrigger.name = '[] %s' % xtrigger.name + self.handle_action_list_trigger(self.song().view.selected_track, xtrigger) + + + def handle_xclip_name(self, track, xclip): + """ This is just here for backwards compatibility (primarily with MapEase ClyphX Strip and ClyphX XT) and shouldn't be used if possible. """ + self.handle_action_list_trigger(track, xclip) + + + def handle_m4l_trigger(self, name): + """ Convenience method for triggering actions from M4L by simply supplying an action name. """ + self.handle_action_list_trigger(self.song().view.selected_track, ActionList('[]' + name)) + + + def handle_action_list_trigger(self, track, xtrigger): + """ Directly dispatches snapshot recall, X-Control overrides and Seq X-Clips. Otherwise, seperates ident from action names, splits up lists of action names and calls action dispatch. """ + if self._is_debugging: + self.log_message('---') + name = None + if xtrigger is not None: + name = self.get_name(xtrigger.name).strip() + if name and name[0] == '[' and ']' in name: + # Snap action, so pass directly to snap component + if ' || (' in name and type(xtrigger) is Live.Clip.Clip and xtrigger.is_playing: + self._snap_actions.recall_track_snapshot(name, xtrigger) + # Control reassignment, so pass directly to control component + elif '[[' in name and ']]' in name: + self._control_component.assign_new_actions(name) + # Standard trigger + else: + ident = name[name.index('['):name.index(']')+1].strip() + raw_action_list = name.replace(ident, '', 1).strip() + if raw_action_list == '': + return + is_play_seq = False + is_loop_seq = False + + # X-Clips can have on and off action lists, the following handles this + if type(xtrigger) is Live.Clip.Clip: + raw_action_list = self.get_xclip_action_list(xtrigger, raw_action_list) + if not raw_action_list: + return + + # Check if the trigger is a PSEQ (accessible to any type of X-Trigger) + if raw_action_list[0] == '(' and '(PSEQ)' in raw_action_list: + is_play_seq = True + raw_action_list = raw_action_list.replace('(PSEQ)', '').strip() + + # Check if the trigger is a LSEQ (accessible only to X-Clips) + elif type(xtrigger) is Live.Clip.Clip and raw_action_list[0] == '(' and '(LSEQ)' in raw_action_list: + is_loop_seq = True + raw_action_list = raw_action_list.replace('(LSEQ)', '').strip() + + # Build formatted action list + formatted_action_list = [] + for action in raw_action_list.split(';'): + action_data = self.format_action_name(track, action.strip()) + if action_data: + formatted_action_list.append(action_data) + + # If seq, pass to appropriate function, else call action dispatch for each action in the formatted action list + if formatted_action_list: + if is_play_seq: + self.handle_play_seq_action_list(formatted_action_list, xtrigger, ident) + elif is_loop_seq: + self._loop_seq_clips[xtrigger.name] = [ident, formatted_action_list] + self.handle_loop_seq_action_list(xtrigger, 0) + else: + for action in formatted_action_list: + self.action_dispatch(action['track'], xtrigger, action['action'], action['args'], ident) + if self._is_debugging: + self.log_message('handle_action_list_trigger triggered, ident=' + str(ident) + ' and track(s)=' + str(self.track_list_to_string(action['track'])) + ' and action=' + str(action['action']) + ' and args=' + str(action['args'])) + + + def get_xclip_action_list(self, xclip, full_action_list): + """ Get the action list to perform. X-Clips can have an on and off action list seperated by a comma. This will return which action list to perform + based on whether the clip is playing. If the clip is not playing and there is no off action, this returns None. """ + result = None + split_list = full_action_list.split(',') + if xclip.is_playing: + result = split_list[0] + else: + if len(split_list) == 2: + if split_list[1].strip() == '*': + result = split_list[0] + else: + result = split_list[1] + if self._is_debugging: + self.log_message('get_xclip_action_list returning ' + str(result)) + return result + + + def replace_user_variables(self, string_with_vars): + """ Replace any user variables in the given string with the value the variable represents. """ + while '%' in string_with_vars: + var_name = string_with_vars[string_with_vars.index('%')+1:] + if '%' in var_name: + var_name = var_name[0:var_name.index('%')] + string_with_vars = string_with_vars.replace('%' + var_name + '%', self.get_user_variable_value(var_name), 1) + else: + string_with_vars = string_with_vars.replace('%', '', 1) + if '$' in string_with_vars: # For compat with old-style variables + for string in string_with_vars.split(): + if '$' in string and not '=' in string: + var_name = string.replace('$', '') + string_with_vars = string_with_vars.replace('$' + var_name, self.get_user_variable_value(var_name), 1) + if self._is_debugging: + self.log_message('replace_user_variables returning ' + str(string_with_vars)) + return string_with_vars + + + def get_user_variable_value(self, var_name): + """ Get the value of the given variable name or 0 if var name not found. """ + result = '0' + if self._user_variables.has_key(var_name): + result = self._user_variables[var_name] + if self._is_debugging: + self.log_message('get_user_variable_value returning ' + str(var_name) + '=' + str(result)) + return result + + + def handle_user_variable_assignment(self, string_with_assign): + """ Handle assigning new value to variable with either assignment or expression enclosed in parens. """ + string_with_assign = string_with_assign.replace('$', '')# For compat with old-style variables + var_data = string_with_assign.split('=') + if len(var_data) >= 2 and not ';' in var_data[1] and not '%' in var_data[1] and not '=' in var_data[1]: + if '(' in var_data[1] and ')' in var_data[1]: + try: self._user_variables[var_data[0].strip()] = str(eval(var_data[1].strip())) + except: pass + else: + self._user_variables[var_data[0].strip()] = var_data[1].strip() + if self._is_debugging: + self.log_message('handle_user_variable_assignment, ' + str(var_data[0].strip()) + '=' + str(var_data[1].strip())) + + + def format_action_name(self, origin_track, origin_name): + """ Replaces vars (if any) then splits up track, action name and arguments (if any) and returns dict """ + result_name = self.replace_user_variables(origin_name) + if '=' in result_name: + self.handle_user_variable_assignment(result_name) + return + result_track = [origin_track] + if len(result_name) >= 4 and (('/' in result_name[:4]) or ('-' in result_name[:4] and '/' in result_name[4:]) or (result_name[0] == '"' and '"' in result_name[1:])): + track_data = self.get_track_to_operate_on(result_name) + result_track = track_data[0] + result_name = track_data[1] + args = '' + name = result_name.split() + if len(name) > 1: + args = result_name.replace(name[0], '', 1) + result_name = result_name.replace(args, '') + if self._is_debugging: + self.log_message('format_action_name returning, track(s)=' + str(self.track_list_to_string(result_track)) + ' and action=' + str(result_name.strip()) + ' and args=' + str(args.strip())) + return {'track' : result_track, 'action' : result_name.strip(), 'args' : args.strip()} + + + def handle_loop_seq_action_list(self, xclip, count): + """ Handles sequenced action lists, triggered by xclip looping """ + if self._loop_seq_clips.has_key(xclip.name): + if count >= len(self._loop_seq_clips[xclip.name][1]): + count = count - ((count / len(self._loop_seq_clips[xclip.name][1]))*len(self._loop_seq_clips[xclip.name][1])) + action = self._loop_seq_clips[xclip.name][1][count] + self.action_dispatch(action['track'], xclip, action['action'], action['args'], self._loop_seq_clips[xclip.name][0]) + if self._is_debugging: + self.log_message('handle_loop_seq_action_list triggered, xclip.name=' + str(xclip.name) + ' and track(s)=' + str(self.track_list_to_string(action['track'])) + ' and action=' + str(action['action']) + ' and args=' + str(action['args'])) + + + def handle_play_seq_action_list(self, action_list, xclip, ident): + """ Handles sequenced action lists, triggered by replaying/retriggering the xtrigger """ + if self._play_seq_clips.has_key(xclip.name): + count = self._play_seq_clips[xclip.name][1] + 1 + if count > len(self._play_seq_clips[xclip.name][2])-1: + count = 0 + self._play_seq_clips[xclip.name] = [ident, count, action_list] + else: + self._play_seq_clips[xclip.name] = [ident, 0, action_list] + action = self._play_seq_clips[xclip.name][2][self._play_seq_clips[xclip.name][1]] + self.action_dispatch(action['track'], xclip, action['action'], action['args'], ident) + if self._is_debugging: + self.log_message('handle_play_seq_action_list triggered, ident=' + str(ident) + ' and track(s)=' + str(self.track_list_to_string(action['track'])) + ' and action=' + str(action['action']) + ' and args=' + str(action['args'])) + + + def do_parameter_adjustment(self, param, value): + """" Adjust (, reset, random, set val) continuous params, also handles quantized param adjustment (should just use +1/-1 for those) """ + if not param.is_enabled: + return() + step = (param.max - param.min) / 127 + new_value = param.value + if value.startswith(('<', '>')): + factor = self.get_adjustment_factor(value) + if not param.is_quantized: + new_value += step * factor + else: + new_value += factor + elif value == 'RESET' and not param.is_quantized: + new_value = param.default_value + elif 'RND' in value and not param.is_quantized: + rnd_min = 0 + rnd_max = 128 + if value != 'RND' and '-' in value: + rnd_range_data = value.replace('RND', '').split('-') + if len(rnd_range_data) == 2: + new_min = 0 + new_max = 128 + try: new_min = int(rnd_range_data[0]) + except: new_min = 0 + try: new_max = int(rnd_range_data[1]) + 1 + except: new_max = 128 + if new_min in range(0, 129) and new_max in range(0, 129) and new_min < new_max: + rnd_min = new_min + rnd_max = new_max + rnd_value = (Live.Application.get_random_int(0, 128) * (rnd_max - rnd_min) / 127) + rnd_min + new_value = (rnd_value * step) + param.min + + else: + try: + if int(value) in range (128): + try: new_value = (int(value) * step) + param.min + except: new_value = param.value + except: pass + if new_value >= param.min and new_value <= param.max: + param.value = new_value + if self._is_debugging: + self.log_message('do_parameter_adjustment called on ' + str(param.name) + ', set value to ' + str(new_value)) + + + def get_adjustment_factor(self, string, as_float = False): + """ Get factor for use with < > actions """ + factor = 1 + if len(string) > 1: + if as_float: + try: factor = float(string[1:]) + except: factor = 1 + else: + try: factor = int(string[1:]) + except: factor = 1 + if string.startswith('<'): + factor = -(factor) + if self._is_debugging: + self.log_message('get_adjustment_factor returning factor=' + str(factor)) + return factor + + + def get_track_to_operate_on(self, origin_name): + """ Gets track or tracks to operate on """ + result_tracks = [] + result_name = origin_name + if '/' in origin_name: + tracks = list(tuple(self.song().tracks) + tuple(self.song().return_tracks) + (self.song().master_track,)) + sel_track_index = tracks.index(self.song().view.selected_track) + if(origin_name.index('/') > 0): + track_spec = origin_name.split('/')[0].strip() + if '"' in track_spec: + track_spec = self.get_track_index_by_name(track_spec, tracks) + if 'SEL' in track_spec: + track_spec = track_spec.replace('SEL', str(sel_track_index + 1), 1) + if 'MST' in track_spec: + track_spec = track_spec.replace('MST', str(len(tracks)), 1) + if track_spec == 'ALL': + result_tracks = tracks + else: + track_range_spec = track_spec.split('-') + if len(track_range_spec) <= 2: + track_range = [] + try: + for spec in track_range_spec: + track_index = -1 + if spec.startswith(('<', '>')): + try: track_index = self.get_adjustment_factor(spec) + sel_track_index + except: pass + else: + try: track_index = int(spec) - 1 + except: track_index = (ord(spec) - 65) + len(self.song().tracks) + if track_index in range(len(tracks)): + track_range.append(track_index) + except: track_range = [] + if track_range: + if len(track_range) == 2: + if (track_range[0] < track_range[1]): + for index in range(track_range[0], track_range[1] + 1): + result_tracks.append(tracks[index]) + else: + result_tracks = [tracks[track_range[0]]] + result_name = origin_name[origin_name.index('/')+1:].strip() + if self._is_debugging: + self.log_message('get_track_to_operate_on returning result_tracks=' + str(self.track_list_to_string(result_tracks)) + ' and result_name=' + str(result_name)) + return (result_tracks, result_name) + + + def get_track_index_by_name(self, name, tracks): + """ Gets the index(es) associated with the track name(s) specified in name. """ + while '"' in name: + track_name = name[name.index('"')+1:] + if '"' in track_name: + track_name = track_name[0:track_name.index('"')] + track_index = '' + def_name = '' + if ' AUDIO' or ' MIDI' in track_name: + def_name = track_name.replace(' ', '-')# In Live GUI, default names are 'n Audio' or 'n MIDI', in API it's 'n-Audio' or 'n-MIDI' + for track in tracks: + current_track_name = self.get_name(track.name) + if current_track_name == track_name or current_track_name == def_name: + track_index = str(tracks.index(track) + 1) + break + name = name.replace('"' + track_name + '"', track_index, 1) + name = name.replace('"' + def_name + '"', track_index, 1) + else: + name = name.replace('"', '', 1) + return name + + + def get_device_to_operate_on(self, track, action_name, args): + """ Get device to operate on and action to perform with args """ + device = None + device_args = args + if 'DEV"' in action_name: + dev_name = action_name[action_name.index('"')+1:] + if '"' in args: + dev_name = action_name[action_name.index('"')+1:] + ' ' + args + device_args = args[args.index('"')+1:].strip() + if '"' in dev_name: + dev_name = dev_name[0:dev_name.index('"')] + for dev in track.devices: + if dev.name.upper() == dev_name: + device = dev + break + else: + if action_name == 'DEV': + device = track.view.selected_device + if device == None: + if track.devices: + device = track.devices[0] + else: + try: + dev_num = action_name.replace('DEV', '') + if '.' in dev_num and self._can_have_nested_devices: + dev_split = dev_num.split('.') + top_level = track.devices[int(dev_split[0]) - 1] + if top_level and top_level.can_have_chains: + device = top_level.chains[int(dev_split[1]) - 1].devices[0] + if len(dev_split) > 2: + device = top_level.chains[int(dev_split[1]) - 1].devices[int(dev_split[2]) - 1] + else: + device = track.devices[int(dev_num) - 1] + except: pass + if self._is_debugging: + debug_string = 'None' + if device: + debug_string = device.name + self.log_message('get_device_to_operate_on returning device=' + str(debug_string) + ' and device args=' + str(device_args)) + return (device, device_args) + + + def get_clip_to_operate_on(self, track, action_name, args): + """ Get clip to operate on and action to perform with args """ + clip = None + clip_args = args + if 'CLIP"' in action_name: + clip_name = action_name[action_name.index('"')+1:] + if '"' in args: + clip_name = action_name[action_name.index('"')+1:] + ' ' + args + clip_args = args[args.index('"')+1:].strip() + if '"' in clip_name: + clip_name = clip_name[0:clip_name.index('"')] + for slot in track.clip_slots: + if slot.has_clip and slot.clip.name.upper() == clip_name: + clip = slot.clip + break + else: + sel_slot_idx = list(self.song().scenes).index(self.song().view.selected_scene) + slot_idx = sel_slot_idx + if action_name == 'CLIP': + if track.playing_slot_index >= 0: + slot_idx = track.playing_slot_index + elif action_name == 'CLIPSEL': + if self.application().view.is_view_visible('Arranger'): + clip = self.song().view.detail_clip + else: + try: slot_idx = int(action_name.replace('CLIP', ''))-1 + except: slot_idx = sel_slot_idx + if clip == None and track.clip_slots[slot_idx].has_clip: + clip = track.clip_slots[slot_idx].clip + if self._is_debugging: + debug_string = 'None' + if clip: + debug_string = clip.name + self.log_message('get_clip_to_operate_on returning clip=' + str(debug_string) + ' and clip args=' + str(clip_args)) + return (clip, clip_args) + + + def get_drum_rack_to_operate_on(self, track): + """ Get drum rack to operate on """ + dr = None + for device in track.devices: + if device.can_have_drum_pads: + dr = device + break + if self._is_debugging: + debug_string = 'None' + if dr: + debug_string = dr.name + self.log_message('get_drum_rack_to_operate_on returning dr=' + str(debug_string)) + return dr + + + def get_user_settings(self, midi_map_handle): + """ Get user settings (variables, prefs and control settings) from text file and perform startup actions if any """ + list_to_build = None + ctrl_data = [] + prefs_data = [] + try: + mrs_path = '' + for path in sys.path: + if 'MIDI Remote Scripts' in path: + mrs_path = path + break + user_file = mrs_path + FOLDER + 'UserSettings.txt' + if not self._user_settings_logged: + self.log_message(' ------- Attempting to read UserSettings file: ' + user_file + '------- ') + for line in open(user_file): + line = self.get_name(line.rstrip('\n')) + if not line.startswith(('#', '"', '*')) and not line.strip() == '': + if not self._user_settings_logged: + self.log_message(str(line)) + if not line.startswith(('#', '"', 'STARTUP_', 'INCLUDE_NESTED_', 'SNAPSHOT_', 'PROCESS_XCLIPS_', 'PUSH_EMU', 'APC_PUSH_EMU', 'CSLINKER')) and not line == '': + if '[USER CONTROLS]' in line: + list_to_build = 'controls' + elif '[USER VARIABLES]' in line: + list_to_build = 'vars' + elif '[EXTRA PREFS]' in line: + list_to_build = 'prefs' + else: + if list_to_build == 'vars' and '=' in line: + line = self.replace_user_variables(line) + self.handle_user_variable_assignment(line) + elif list_to_build == 'controls' and '=' in line: + ctrl_data.append(line) + elif list_to_build == 'prefs' and '=' in line: + prefs_data.append(line) + elif 'PUSH_EMULATION' in line: + self._push_emulation = line.split('=')[1].strip() == 'ON' + if self._push_emulation: + if 'APC' in line: + with self.component_guard(): + self._push_apc_combiner = Push_APC_Combiner(self) + self.enable_push_emulation(self._control_surfaces()) + elif line.startswith('INCLUDE_NESTED_DEVICES_IN_SNAPSHOTS ='): + include_nested = self.get_name(line[37:].strip()) + include_nested_devices = False + if include_nested.startswith('ON'): + include_nested_devices = True + self._snap_actions._include_nested_devices = include_nested_devices + elif line.startswith('SNAPSHOT_PARAMETER_LIMIT ='): + try: limit = int(line[26:].strip()) + except: limit = 500 + self._snap_actions._parameter_limit = limit + elif line.startswith('PROCESS_XCLIPS_IF_TRACK_MUTED ='): + self._process_xclips_if_track_muted = line.split('=')[1].strip() == 'TRUE' + elif line.startswith('STARTUP_ACTIONS =') and not self._startup_actions_complete: + actions = line[17:].strip() + if actions != 'OFF': + action_list = '[]' + actions + self.schedule_message(2, partial(self.perform_startup_actions, action_list)) + self._startup_actions_complete = True + elif line.startswith('CSLINKER'): + self._cs_linker.parse_settings(line) + if ctrl_data: + self._control_component.get_user_control_settings(ctrl_data, midi_map_handle) + if prefs_data: + self._extra_prefs.get_user_settings(prefs_data) + except: pass + + + def enable_push_emulation(self, scripts): + """ Try to disable Push's handshake to allow for emulation. + If emulating for APC, set scripts on combiner component """ + for script in scripts: + script_name = script.__class__.__name__ + if script_name == 'Push': + if IS_LIVE_9_5: + with script._component_guard(): + script._start_handshake_task = MockHandshakeTask() + script._handshake = MockHandshake() + if self._push_apc_combiner: + self._push_apc_combiner.set_up_scripts(self._control_surfaces()) + else: + script._handshake._identification_timeout_task.kill() + script._handshake._identification_timeout_task = Task.Task() + break + + + def start_debugging(self): + """ Turn on debugging and write all user vars/controls/actions to Live's log file to assist in troubleshooting. """ + if not self._is_debugging: + self._is_debugging = True + self.log_message('------- ClyphX Log: Logging User Variables -------') + for key, value in self._user_variables.items(): + self.log_message(str(key) + '=' + str(value)) + self.log_message('------- ClyphX Log: Logging User Controls -------') + for key, value in self._control_component._control_list.items(): + self.log_message(str(key) + ' on_action=' + str(value['on_action']) + ' and off_action=' + str(value['off_action'])) + self.log_message('------- ClyphX Log: Logging User Actions -------') + for key, value in self._user_actions._action_dict.items(): + self.log_message(str(key) + '=' + str(value)) + self.log_message('------- ClyphX Log: Debugging Started -------') + + + def track_list_to_string(self, track_list): + """ Convert list of tracks to a string of track names or None if no tracks. This is used for debugging. """ + result = 'None' + if track_list: + result = '[' + for track in track_list: + result += track.name + ', ' + result = result[:len(result)-2] + return result + ']' + + + def perform_startup_actions(self, action_list): + """ Delay startup action so it can perform actions on values that are changed upon set load (like overdub) """ + self.handle_action_list_trigger(self.song().view.selected_track, ActionList(action_list)) + + + def setup_tracks(self): + """ Setup component tracks on ini and track list changes. Also call Macrobat's get rack """ + for t in self.song().tracks: + self._macrobat.setup_tracks(t) + if (self._current_tracks and t in self._current_tracks): + pass + else: + self._current_tracks.append(t) + ClyphXTrackComponent(self, t) + for r in tuple(self.song().return_tracks) + (self.song().master_track,): + self._macrobat.setup_tracks(r) + self._snap_actions.setup_tracks() + + + def get_name(self, name): + """ Convert name to upper-case string or return blank string if couldn't be converted """ + try: name = str(name).upper() + except: name = '' + return name + + + def _on_track_list_changed(self): + ControlSurface._on_track_list_changed(self) + self.setup_tracks() + + + def connect_script_instances(self, instanciated_scripts): + """ Pass connect scripts call to control component """ + self._control_component.connect_script_instances(instanciated_scripts) + self._control_surface_actions.connect_script_instances(instanciated_scripts) + if self._push_emulation: + self.enable_push_emulation(instanciated_scripts) + + + def build_midi_map(self, midi_map_handle): + """ Build user-defined list of midi messages for controlling ClyphX track """ + ControlSurface.build_midi_map(self, midi_map_handle) + if self._user_settings_logged: + self._control_component.rebuild_control_map(midi_map_handle) + else: + self.get_user_settings(midi_map_handle) + self._user_settings_logged = True + if self._push_emulation: + self.enable_push_emulation(self._control_surfaces()) + + + def receive_midi(self, midi_bytes): + """ Receive user-specified messages and send to control script """ + ControlSurface.receive_midi(self, midi_bytes) + self._control_component.receive_midi(midi_bytes) + + + def handle_sysex(self, midi_bytes): + """ Handle sysex received from controller """ + pass + + + def handle_nonsysex(self, midi_bytes): + """ Override so that ControlSurface doesn't report this to Log.txt """ + pass + + +# local variables: +# tab-width: 4 + diff --git a/ClyphX/ClyphXArsenalActions.py b/ClyphX/ClyphXArsenalActions.py new file mode 100644 index 0000000..e9cb43c --- /dev/null +++ b/ClyphX/ClyphXArsenalActions.py @@ -0,0 +1,196 @@ +""" +# Copyright (C) 2016-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +import Live +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent + +try: + from _NKFW2.Utils import parse_int + from _NKFW2.consts import NOTE_NAMES + from _NKFW2.Scales import SCALE_TYPES + from _NKFW2.ScaleSettingsComponent import SEQ_OFFSET, FOURTHS_OFFSET, OFFSET_NAMES + S_TYPES = [s.name.upper() for s in SCALE_TYPES] + O_NAMES = [o.upper() for o in OFFSET_NAMES] +except: + pass + + +def adjust_property(obj, prop, min_v, max_v, arg, setter=None, v_list=None): + """ Adjusts the given property absolutely or relatively. """ + if arg: + arg = arg[0].strip() + current_v = getattr(obj, prop) + new_v = current_v + # get absolute value + if arg.isdigit(): + new_v = parse_int(arg, current_v + 1, min_v + 1, max_v + 1) - 1 + # get relative value with wrapping + elif arg == '>': + new_v = current_v + 1 + if new_v > max_v: + new_v = min_v + elif arg == '<': + new_v = current_v - 1 + if new_v < min_v: + new_v = max_v + # get index of arg from v_list + elif v_list and arg in v_list: + new_v = v_list.index(arg) + if setter: + getattr(obj, setter)(new_v) + else: + setattr(obj, prop, new_v) + + +def toggle_property(obj, prop, arg): + """ Toggles the given property or turns it off/on. """ + if arg: + setattr(obj, prop, arg[0].strip() == 'ON') + else: + setattr(obj, prop, not getattr(obj, prop)) + + +def get_component(script, comp_name): + """ Returns the component of the given name. """ + for c in script._components: + if c.name == comp_name: + return c + return None + + +class ClyphXArsenalActions(ControlSurfaceComponent): + """ Actions related to Arsenal control surface scripts. """ + + def __init__(self, parent): + super(ControlSurfaceComponent, self).__init__() + self._parent = parent + self._scripts = {} + + def disconnect(self): + super(ControlSurfaceComponent, self).disconnect() + self._parent = None + self._scripts = None + + def set_script(self, script): + """ Adds the given script to the dict of scripts to work with. """ + self._scripts[script.script_name.upper()] =\ + {'top' : script, + 'scl': get_component(script, 'Scale_Settings_Control'), + 'targets': get_component(script, 'Targets_Component')} + + def dispatch_action(self, track, xclip, ident, script_name, action): + """ Dispatches the action to the appropriate handler. """ + script = self._scripts.get(script_name, None) + if script: + action_spec = action.split() + if action_spec: + action_name = action_spec[0].strip() + with script['top'].component_guard(): + if '_MODE' in action_name: + self._handle_mode_action(script, action_spec) + elif 'LOCK' in action_name: + self._handle_lock_action(script, action_spec) + elif 'SCL' in action_name: + self._handle_scale_action(script, action_spec, xclip, ident) + + def _handle_mode_action(self, script, spec): + """ Handles selecting a specific mode or incrementing modes with wrapping. """ + mc = (script['top'].matrix_modes_component if spec[0].startswith('M_MODE') + else script['top'].encoder_modes_component) + if mc: + adjust_property(mc, 'selected_mode_index', 0, mc.num_modes - 1, spec[1:]) + + def _handle_lock_action(self, script, spec): + """ Handles toggling the locking of the current track or mode-specific locks. """ + tc = script['targets'] + if tc: + if 'MODES' in spec: + tc.toggle_mode_specific_lock() + else: + tc.toggle_lock() + + def _handle_scale_action(self, script, spec, xclip, ident): + """ Handles scale actions or dispatches them to the appropriate handler. """ + if script['scl']: + scl = script['scl'] + if len(spec) == 1: + self._capture_scale_settings(script, xclip, ident) + return + elif len(spec) >= 5: + self._recall_scale_settings(scl, spec) + else: + if spec[1] == 'INKEY': + toggle_property(scl, '_in_key', spec[2:]) + elif spec[1] == 'HORZ': + toggle_property(scl, '_orientation_is_horizontal', spec[2:]) + elif spec[1] == 'ROOT': + adjust_property(scl._tonics, '_page_index', 0, + scl._tonics.num_pages - 1, spec[2:], + 'set_page_index', NOTE_NAMES) + elif spec[1] == 'TYPE': + adjust_property(scl._scales, '_page_index', 0, + scl._scales.num_pages - 1, [' '.join(spec[2:])], + 'set_page_index', S_TYPES) + elif spec[1] == 'OFFSET': + adjust_property(scl._offsets, '_page_index', 0, + scl._offsets.num_pages - 1, spec[2:], + 'set_page_index', O_NAMES) + elif spec[1] == 'SEQ': # deprecated + self._toggle_scale_offset(scl, spec[2:]) + scl._notify_scale_settings() + + def _capture_scale_settings(self, script, xclip, ident): + """ Captures the current scale type, tonic, in key state, offset and orientation + and adds them to the given xclip's name. """ + if type(xclip) is Live.Clip.Clip: + comp = script['scl'] + xclip.name = '%s %s SCL %s %s %s %s %s' % (ident, script['top'].script_name, + comp._scales.page_index, + comp.tonic, comp.in_key, + comp._offsets.page_index, + comp.orientation_is_horizontal) + + def _recall_scale_settings(self, comp, spec): + """ Recalls previously stored scale settings. """ + if len(spec) >= 5: + scale = parse_int(spec[1], None, 0, comp._scales.num_pages - 1) + if scale is not None: + comp._scales.set_page_index(scale) + tonic = parse_int(spec[2], None, 0, comp._tonics.num_pages - 1) + if tonic is not None: + comp._tonics.set_page_index(tonic) + comp._in_key = spec[3].strip() == 'TRUE' + if len(spec) == 5: # deprecated + self._toggle_scale_offset(comp, ['ON'] if spec[4].strip() == 'TRUE' + else ['OFF']) + else: + offset = parse_int(spec[4], None, 0, comp._offsets.num_pages - 1) + if offset is not None: + comp._offsets.set_page_index(offset) + comp._orientation_is_horizontal = spec[5].strip() == 'TRUE' + + def _toggle_scale_offset(self, comp, arg): + """ Toggles between sequent and 4ths offsets. This is deprecated, but maintained + for backwards compatibility. """ + offset = FOURTHS_OFFSET + if (arg and arg[0].strip() == 'ON') or (not arg and comp._offsets.page_index): + offset = SEQ_OFFSET + comp._offsets.page_index = offset diff --git a/ClyphX/ClyphXClipActions.py b/ClyphX/ClyphXClipActions.py new file mode 100644 index 0000000..b4677c3 --- /dev/null +++ b/ClyphX/ClyphXClipActions.py @@ -0,0 +1,952 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +import Live +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent +from ClyphXClipEnvCapture import ClyphXClipEnvCapture +from consts import * +if IS_LIVE_9: + import random + +ENV_TYPES = ('IRAMP', 'DRAMP', 'IPYR', 'DPYR', 'SQR', 'SAW') + +class ClyphXClipActions(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' Clip-related actions ' + + def __init__(self, parent): + ControlSurfaceComponent.__init__(self) + self._parent = parent + self._env_capture = ClyphXClipEnvCapture() + + + def disconnect(self): + self._parent = None + if IS_LIVE_9: + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def set_clip_name(self, clip, track, xclip, ident, args): + """ Set clip's name """ + args = args.strip() + if args: + clip.name = args + + + def set_clip_on_off(self, clip, track, xclip, ident, value = None): + """ Toggles or turns clip on/off """ + if value in KEYWORDS: + clip.muted = not(KEYWORDS[value]) + else: + clip.muted = not(clip.muted) + + + def set_warp(self, clip, track, xclip, ident, value = None): + """ Toggles or turns clip warp on/off """ + if clip.is_audio_clip: + value = value.strip() + if value in KEYWORDS: + clip.warping = KEYWORDS[value] + else: + clip.warping = not(clip.warping) + + + def adjust_time_signature(self, clip, track, xclip, ident, args): + """ Adjust clip's time signature """ + if '/' in args: + name_split = args.split('/') + try: + clip.signature_numerator = int(name_split[0].strip()) + clip.signature_denominator = int(name_split[1].strip()) + except: pass + + + def adjust_detune(self, clip, track, xclip, ident, args): + """ Adjust/set audio clip detune """ + if clip.is_audio_clip: + args = args.strip() + if args.startswith(('<', '>')): + factor = self._parent.get_adjustment_factor(args) + clip.pitch_fine = clip.pitch_fine + factor + else: + try: + clip.pitch_fine = int(args) + except: pass + + + def adjust_transpose(self, clip, track, xclip, ident, args): + """ Adjust audio or midi clip transpose, also set audio clip transpose """ + args = args.strip() + if args.startswith(('<', '>')): + factor = self._parent.get_adjustment_factor(args) + if clip.is_audio_clip: + clip.pitch_coarse = max(-48, min(48, (clip.pitch_coarse + factor))) + elif clip.is_midi_clip: + self.do_note_pitch_adjustment(clip, factor) + else: + if clip.is_audio_clip: + try: + clip.pitch_coarse = int(args) + except: pass + + + def adjust_gain(self, clip, track, xclip, ident, args): + """ Adjust/set clip gain for Live 9. For settings, range is 0 - 127. """ + if IS_LIVE_9 and clip.is_audio_clip: + args = args.strip() + if args.startswith(('<', '>')): + factor = self._parent.get_adjustment_factor(args, True) + clip.gain = max(0.0, min(1.0, (clip.gain + factor * float(1.0 / 127.0)))) + else: + try: + clip.gain = int(args) * float(1.0 / 127.0) + except: pass + + + def adjust_start(self, clip, track, xclip, ident, args): + """ Adjust/set clip start exclusively for Live 9. In Live 8, same as adjust_loop_start. """ + args = args.strip() + if args.startswith(('<', '>')): + factor = self._parent.get_adjustment_factor(args, True) + if IS_LIVE_9 and clip.looping: + clip.start_marker = max(0.0, min(clip.end_marker - factor, (clip.start_marker + factor))) + else: + clip.loop_start = max(0.0, min(clip.loop_end - factor, (clip.loop_start + factor))) + else: + try: + if IS_LIVE_9 and clip.looping: + clip.start_marker = float(args) + else: + clip.loop_start = float(args) + except: pass + + + def adjust_loop_start(self, clip, track, xclip, ident, args): + """ Adjust/set clip loop start if loop is on or clip start otherwise. """ + args = args.strip() + if args.startswith(('<', '>')): + factor = self._parent.get_adjustment_factor(args, True) + clip.loop_start = max(0.0, min(clip.loop_end - factor, (clip.loop_start + factor))) + else: + try: + clip.loop_start = float(args) + except: pass + + + def adjust_end(self, clip, track, xclip, ident, args): + """ Adjust/set clip end exclusively for Live 9. In Live 8, same as adjust_loop_end. """ + args = args.strip() + if args.startswith(('<', '>')): + factor = self._parent.get_adjustment_factor(args, True) + if IS_LIVE_9 and clip.looping: + clip.end_marker = max((clip.start_marker - factor), (clip.end_marker + factor)) + else: + clip.loop_end = max((clip.loop_start - factor), (clip.loop_end + factor)) + else: + try: + if IS_LIVE_9 and clip.looping: + clip.end_marker = float(args) + else: + clip.loop_end = float(args) + except: pass + + + def adjust_loop_end(self, clip, track, xclip, ident, args): + """ Adjust/set clip loop end if loop is on or close end otherwise. """ + args = args.strip() + if args.startswith(('<', '>')): + factor = self._parent.get_adjustment_factor(args, True) + clip.loop_end = max((clip.loop_start - factor), (clip.loop_end + factor)) + else: + try: + clip.loop_end = float(args) + except: pass + + + def adjust_cue_point(self, clip, track, xclip, ident, args): + """ Adjust clip's start point and fire (also stores cue point if not specified). Will not fire xclip itself as this causes a loop """ + if clip.is_midi_clip or (clip.is_audio_clip and clip.warping): + if args: + args = args.strip() + if args.startswith(('<', '>')): + factor = self._parent.get_adjustment_factor(args, True) + args = clip.loop_start + factor + try: + clip.loop_start = float(args) + if clip.looping: + clip.looping = False + clip.loop_start = float(args) + clip.looping = True + if clip != xclip: + clip.fire() + except: pass + else: + if type(xclip) is Live.Clip.Clip: + xclip.name = xclip.name.strip() + ' ' + str(clip.loop_start) + + + def adjust_warp_mode(self, clip, track, xclip, ident, args): + """ Adjusts the warp mode of the clip. This cannot be applied if the warp mode is currently rex (5). """ + if IS_LIVE_9 and clip.is_audio_clip and clip.warping and not clip.warp_mode == 5: + args = args.strip() + if args in WARP_MODES: + clip.warp_mode = WARP_MODES[args] + elif args in ('<', '>'): + factor = self._parent.get_adjustment_factor(args) + new_mode = clip.warp_mode + factor + if new_mode == 5 and '>' in args: + new_mode = 6 + elif new_mode == 5 and '<' in args: + new_mode = 4 + if new_mode in range(7) and new_mode != 5: + clip.warp_mode = new_mode + + + def adjust_grid_quantization(self, clip, track, xclip, ident, args): + """ Adjusts clip grid quantization. """ + if IS_LIVE_9: + args = args.strip() + if args in CLIP_GRID_STATES: + clip.view.grid_quantization = CLIP_GRID_STATES[args] + + + def set_triplet_grid(self, clip, track, xclip, ident, args): + """ Toggles or turns triplet grid on or off. """ + if IS_LIVE_9: + if args in KEYWORDS: + clip.view.grid_is_triplet = KEYWORDS[args] + else: + clip.view.grid_is_triplet = not(clip.view.grid_is_triplet) + + + def capture_to_envelope(self, clip, track, xclip, ident, args): + if IS_LIVE_9_1: + self._env_capture.capture(clip, track, args) + + + def insert_envelope(self, clip, track, xclip, ident, args): + """ Inserts an envelope for the given parameter into the clip. + This doesn't apply to quantized parameters. + Requires 9.1 or later. """ + if IS_LIVE_9_1: + args = args.strip() + arg_array = args.split() + if len(arg_array) > 1: + # used to determine whether env_type is last arg...otherwise a range is specified + last_arg_index = len(arg_array) - 1 + env_type_index = last_arg_index + env_type = None + for index in range(len(arg_array)): + if arg_array[index] in ENV_TYPES: + env_type_index = index + env_type = arg_array[index] + break + if env_type: + env_param_spec = '' + for index in range(env_type_index): + env_param_spec += arg_array[index] + ' ' + param = self._get_envelope_parameter(track, env_param_spec) + if param and not param.is_quantized: + env_range = (param.min, param.max) + # calculate range if specified in args + if env_type_index != last_arg_index: + try: + min_factor = int(arg_array[-2]) + max_factor = int(arg_array[-1]) + if min_factor in range(101) and max_factor in range(101) and min_factor < max_factor: + env_range = ((min_factor / 100.0) * param.max, (max_factor / 100.0) * param.max) + except: pass + self.song().view.detail_clip = clip + clip.view.show_envelope() + clip.view.select_envelope_parameter(param) + clip.clear_envelope(param) + self._perform_envelope_insertion(clip, param, env_type, env_range) + + + def _perform_envelope_insertion(self, clip, param, env_type, env_range): + """ Performs the actual insertion of the envelope into the clip. """ + env = clip.automation_envelope(param) + if env: + median = ((clip.loop_end - clip.loop_start) / 2.0) + clip.loop_start + num_beats = int(clip.loop_end - clip.loop_start) + 1 + start_beat = int(clip.loop_start) + if env_type == 'IRAMP': + env.insert_step(clip.loop_start, 0.0, env_range[0]) + env.insert_step(clip.loop_end, 0.0, env_range[1]) + elif env_type == 'DRAMP': + env.insert_step(clip.loop_start, 0.0, env_range[1]) + env.insert_step(clip.loop_end, 0.0, env_range[0]) + elif env_type == 'IPYR': + env.insert_step(clip.loop_start, 0.0, env_range[0]) + env.insert_step(median, 0.0, env_range[1]) + env.insert_step(clip.loop_end, 0.0, env_range[0]) + elif env_type == 'DPYR': + env.insert_step(clip.loop_start, 0.0, env_range[1]) + env.insert_step(median, 0.0, env_range[0]) + env.insert_step(clip.loop_end, 0.0, env_range[1]) + elif env_type == 'SAW': + for b in range(num_beats): + beat = float(b + start_beat) + env.insert_step(beat, 0.0, env_range[1]) + if beat < clip.loop_end: + env.insert_step(beat + 0.5, 0.0, env_range[0]) + elif env_type == 'SQR': + for b in range(num_beats): + beat = float(b + start_beat) + if beat < clip.loop_end: + if b % 2 == 0: + env.insert_step(beat, 1.0, env_range[1]) + else: + env.insert_step(beat, 1.0, env_range[0]) + + + def clear_envelope(self, clip, track, xclip, ident, args): + """ Clears the envelope of the specified param or all envelopes from the given clip. """ + if IS_LIVE_9_1: + if args: + param = self._get_envelope_parameter(track, args.strip()) + if param: + clip.clear_envelope(param) + else: + clip.clear_all_envelopes() + + + def show_envelope(self, clip, track, xclip, ident, args): + """ Shows the clip's envelope view and a particular envelope if specified. + Requires 9.1 or later. """ + if IS_LIVE_9_1: + self.song().view.detail_clip = clip + clip.view.show_envelope() + if args: + param = self._get_envelope_parameter(track, args.strip()) + if param: + clip.view.select_envelope_parameter(param) + + + def _get_envelope_parameter(self, track, args): + """ Gets the selected, mixer or device parameter for envelope-related actions. """ + param = None + if 'SEL' in args: + param = self.song().view.selected_parameter + elif 'VOL' in args: + param = track.mixer_device.volume + elif 'PAN' in args: + param = track.mixer_device.panning + elif 'SEND' in args: + param = self._parent._track_actions.get_send_parameter(track, args.replace('SEND', '').strip()) + elif 'DEV' in args: + arg_array = args.split() + if len(arg_array) > 1: + dev_array = self._parent.get_device_to_operate_on(track, arg_array[0], args.replace(arg_array[0], '').strip()) + if len(dev_array) == 2: + param_array = dev_array[1].strip().split() + param = None + if len(param_array) > 1: + param = self._parent._device_actions.get_banked_parameter(dev_array[0], param_array[0], param_array[1]) + else: + param = self._parent._device_actions.get_bob_parameter(dev_array[0], param_array[0]) + return param + + + def hide_envelopes(self, clip, track, xclip, ident, args): + """ Hides the clip's envelope view. """ + if IS_LIVE_9_1: + clip.view.hide_envelope() + + + def quantize(self, clip, track, xclip, ident, args): + """ + Quantizes notes or warp markers to the given quantization value, at the (optional) given strength and with the (optional) percentage of swing. + Can optionally be applied to specific notes or ranges of notes. + """ + if IS_LIVE_9: + args = args.strip() + arg_array = args.split() + array_offset = 0 + rate_to_apply = None + # standard qntz to all + if arg_array[0] in R_QNTZ_STATES: + rate_to_apply = R_QNTZ_STATES[arg_array[0]] + # qntz to specific note or note range + elif arg_array[1] in R_QNTZ_STATES and clip.is_midi_clip: + array_offset = 1 + rate_to_apply = R_QNTZ_STATES[arg_array[1]] + if rate_to_apply: + strength = 1.0 + swing_to_apply = 0.0 + current_swing = self.song().swing_amount + if len(arg_array) > (1 + array_offset): + try: + strength = float(arg_array[1 + array_offset]) / 100.0 + if strength > 1.0 or strength < 0.0: + strength = 1.0 + except: strength = 1.0 + if len(arg_array) > (2 + array_offset): + try: + swing_to_apply = float(arg_array[2 + array_offset]) / 100.0 + if swing_to_apply > 1.0 or swing_to_apply < 0.0: + swing_to_apply = 0.0 + except: swing_to_apply = 0.0 + self.song().swing_amount = swing_to_apply + # apply standard qntz to all + if array_offset == 0: + clip.quantize(rate_to_apply, strength) + # apply qntz to specific note or note range + else: + note_range = self.get_note_range(arg_array[0]) + for note in range(note_range[0], note_range[1]): + clip.quantize_pitch(note, rate_to_apply, strength) + self.song().swing_amount = current_swing + + + def duplicate_clip_content(self, clip, track, xclip, ident, args): + """ Duplicates all the content in a MIDI clip and doubles loop length. Will also zoom out to show entire loop if loop is on. """ + if IS_LIVE_9 and clip.is_midi_clip: + try: + clip.duplicate_loop() + except: pass + + + def delete_clip(self, clip, track, xclip, ident, args): + """ Deletes the given clip. """ + if IS_LIVE_9: + clip.canonical_parent.delete_clip() + + + def duplicate_clip(self, clip, track, xclip, ident, args): + """ Duplicates the given clip. This will overwrite clips if any exist in the slots used for duplication. """ + if IS_LIVE_9: + try: + track.duplicate_clip_slot(list(track.clip_slots).index(clip.canonical_parent)) + except: pass + + + def chop_clip(self, clip, track, xclip, ident, args): + """ Duplicates the clip the number of times specified and sets evenly distributed start points across all duplicates. This will overwrite clips if any exist in the slots used for duplication. """ + if IS_LIVE_9: + args = args.strip() + num_chops = 8 + if args: + try: num_chops = int(args) + except: pass + slot_index = list(track.clip_slots).index(clip.canonical_parent) + current_start = clip.start_marker + chop_length = (clip.loop_end - current_start) / num_chops + try: + for index in range(num_chops - 1): + track.duplicate_clip_slot(slot_index + index) + dupe_start = (chop_length * (index + 1)) + current_start + dupe = track.clip_slots[slot_index + index + 1].clip + dupe.start_marker = dupe_start + dupe.loop_start = dupe_start + dupe.name = clip.name + '-' + str(index + 1) + except: pass + + + def split_clip(self, clip, track, xclip, ident, args): + """ Duplicates the clip and sets each duplicate to have the length specified in args. This will overwrite clips if any exist in the slots used for duplication. """ + if IS_LIVE_9: + try: + bar_arg = float(args) + bar_length = (4.0 / clip.signature_denominator) * clip.signature_numerator + split_size = bar_length * bar_arg + num_splits = int(clip.length / split_size) + if split_size * num_splits < clip.end_marker: + num_splits += 1 + if num_splits >= 2: + slot_index = list(track.clip_slots).index(clip.canonical_parent) + current_start = clip.start_marker + actual_end = clip.end_marker + for index in xrange(num_splits): + track.duplicate_clip_slot(slot_index + index) + dupe_start = (split_size * index) + current_start + dupe_end = dupe_start + split_size + if dupe_end > actual_end: + dupe_end = actual_end + dupe = track.clip_slots[slot_index + index + 1].clip + dupe.loop_end = dupe_end + dupe.start_marker = dupe_start + dupe.loop_start = dupe_start + dupe.name = clip.name + '-' + str(index + 1) + except: pass + + + def do_clip_loop_action(self, clip, track, xclip, ident, args): + """ Handle clip loop actions """ + args = args.strip() + if args == '' or args in KEYWORDS: + self.set_loop_on_off(clip, args) + else: + if args.startswith('START'): + self.adjust_loop_start(clip, track, xclip, ident, args.replace('START', '', 1).strip()) + elif args.startswith('END'): + self.adjust_loop_end(clip, track, xclip, ident, args.replace('END', '', 1).strip()) + elif args == 'SHOW' and IS_LIVE_9: + clip.view.show_loop() + if clip.looping: + clip_stats = self.get_clip_stats(clip) + new_start = clip.loop_start + new_end = clip.loop_end + if args.startswith(('<', '>')): + self.move_clip_loop_by_factor(clip, args, clip_stats) + return() + elif args == 'RESET': + new_start = 0.0 + new_end = clip_stats['real_end'] + elif args.startswith('*'): + try: + new_end = (clip.loop_end - clip_stats['loop_length']) + (clip_stats['loop_length'] * float(args[1:])) + except: pass + else: + self.do_loop_set(clip, args, clip_stats) + return() + self.set_new_loop_position(clip, new_start, new_end, clip_stats) + + + def set_loop_on_off(self, clip, value = None): + """ Toggles or turns clip loop on/off """ + if value in KEYWORDS: + clip.looping = KEYWORDS[value] + else: + clip.looping = not(clip.looping) + + + def move_clip_loop_by_factor(self, clip, args, clip_stats): + """ Move clip loop by its length or by a specified factor """ + factor = clip_stats['loop_length'] + if args == '<': + factor = -(factor) + if len(args) > 1: + factor = self._parent.get_adjustment_factor(args, True) + new_end = clip.loop_end + factor + new_start = clip.loop_start + factor + if new_start < 0.0: + new_end = new_end - new_start + new_start = 0.0 + self.set_new_loop_position(clip, new_start, new_end, clip_stats) + + + def do_loop_set(self, clip, args, clip_stats): + """ Set loop length and (if clip is playing) position, quantizes to 1/4 by default or bar if specified """ + try: + qntz = False + if 'B' in args: + qntz = True + bars_to_loop = float(args.strip('B')) + bar = (4.0 / clip.signature_denominator) * clip.signature_numerator + start = clip.loop_start + if clip.is_playing: + start = round(clip.playing_position) + if qntz: + distance = start % bar + if distance <= bar / 2: + start = start - distance + else: + start = start + (bar - distance) + end = start + (bar * bars_to_loop) + self.set_new_loop_position(clip, start, end, clip_stats) + except: pass + + + def set_new_loop_position(self, clip, new_start, new_end, clip_stats): + """ For use with other clip loop actions, ensures that loop settings are within range and applies in correct order """ + if new_end <= clip_stats['real_end'] and new_start >= 0: + if new_end > clip.loop_start: + clip.loop_end = new_end + clip.loop_start = new_start + else: + clip.loop_start = new_start + clip.loop_end = new_end + + + def do_clip_note_action(self, clip, track, xclip, ident, args): + """ Handle clip note actions """ + if clip.is_audio_clip: + return() + note_data = self.get_notes_to_operate_on(clip, args.strip()) + if note_data['notes_to_edit']: + if note_data['args'] == '' or note_data['args'] in KEYWORDS: + self.set_notes_on_off(clip, note_data['args'], note_data['notes_to_edit'], note_data['other_notes']) + elif note_data['args'] == 'REV': + self.do_note_reverse(clip, note_data['args'], note_data['notes_to_edit'], note_data['other_notes']) + elif note_data['args'] == 'INV': + self.do_note_invert(clip, note_data['args'], note_data['notes_to_edit'], note_data['other_notes']) + elif note_data['args'] == 'COMP': + self.do_note_compress(clip, note_data['args'], note_data['notes_to_edit'], note_data['other_notes']) + elif note_data['args'] == 'EXP': + self.do_note_expand(clip, note_data['args'], note_data['notes_to_edit'], note_data['other_notes']) + elif note_data['args'] == 'SCRN': + self.do_pitch_scramble(clip, note_data['args'], note_data['notes_to_edit'], note_data['other_notes']) + elif note_data['args'] == 'SCRP': + self.do_position_scramble(clip, note_data['args'], note_data['notes_to_edit'], note_data['other_notes']) + elif note_data['args'] in ('CMB', 'SPLIT'): + self.do_note_split_or_combine(clip, note_data['args'], note_data['notes_to_edit'], note_data['other_notes']) + elif note_data['args'].startswith(('GATE <', 'GATE >')): + self.do_note_gate_adjustment(clip, note_data['args'], note_data['notes_to_edit'], note_data['other_notes']) + elif note_data['args'].startswith(('NUDGE <', 'NUDGE >')): + self.do_note_nudge_adjustment(clip, note_data['args'], note_data['notes_to_edit'], note_data['other_notes']) + elif note_data['args'] == 'DEL': + self.do_note_delete(clip, note_data['args'], note_data['notes_to_edit'], note_data['other_notes']) + elif note_data['args'] in ('VELO <<', 'VELO >>'): + self.do_note_crescendo(clip, note_data['args'], note_data['notes_to_edit'], note_data['other_notes']) + elif note_data['args'].startswith('VELO'): + self.do_note_velo_adjustment(clip, note_data['args'], note_data['notes_to_edit'], note_data['other_notes']) + + + def set_notes_on_off(self, clip, args, notes_to_edit, other_notes): + """ Toggles or turns note mute on/off """ + edited_notes = [] + for n in notes_to_edit: + new_mute = False + if args == '': + new_mute = not(n[4]) + elif args == 'ON': + new_mute = True + edited_notes.append((n[0], n[1], n[2], n[3], new_mute)) + if edited_notes: + self.write_all_notes(clip, edited_notes, other_notes) + + + def do_note_pitch_adjustment(self, clip, factor): + """ Adjust note pitch. This isn't a note action, it's called via Clip Semi """ + edited_notes = [] + note_data = self.get_notes_to_operate_on(clip) + if note_data['notes_to_edit']: + for n in note_data['notes_to_edit']: + new_pitch = n[0] + factor + if not new_pitch in range (128): + edited_notes = [] + return() + else: + edited_notes.append((new_pitch, n[1], n[2], n[3], n[4])) + if edited_notes: + self.write_all_notes(clip, edited_notes, note_data['other_notes']) + + + def do_note_gate_adjustment(self, clip, args, notes_to_edit, other_notes): + """ Adjust note gate """ + edited_notes = [] + factor = self._parent.get_adjustment_factor(args.split()[1], True) + for n in notes_to_edit: + new_gate = n[2] + (factor * 0.03125) + if n[1] + new_gate > clip.loop_end or new_gate < 0.03125: + edited_notes = [] + return() + else: + edited_notes.append((n[0], n[1], new_gate, n[3], n[4])) + if edited_notes: + self.write_all_notes(clip, edited_notes, other_notes) + + + def do_note_nudge_adjustment(self, clip, args, notes_to_edit, other_notes): + """ Adjust note position """ + edited_notes = [] + factor = self._parent.get_adjustment_factor(args.split()[1], True) + for n in notes_to_edit: + new_pos = n[1] + (factor * 0.03125) + if n[2] + new_pos > clip.loop_end or new_pos < 0.0: + edited_notes = [] + return() + else: + edited_notes.append((n[0], new_pos, n[2], n[3], n[4])) + if edited_notes: + self.write_all_notes(clip, edited_notes, other_notes) + + + def do_note_velo_adjustment(self, clip, args, notes_to_edit, other_notes): + """ Adjust/set/randomize note velocity """ + edited_notes = [] + args = args.replace('VELO ', '') + args = args.strip() + for n in notes_to_edit: + if args == 'RND': + edited_notes.append((n[0], n[1], n[2], Live.Application.get_random_int(64, 64), n[4])) + elif args.startswith(('<', '>')): + factor = self._parent.get_adjustment_factor(args) + new_velo = n[3] + factor + if not new_velo in range (128): + edited_notes = [] + return() + else: + edited_notes.append((n[0], n[1], n[2], new_velo, n[4])) + else: + try: + edited_notes.append((n[0], n[1], n[2], float(args), n[4])) + except: pass + if edited_notes: + self.write_all_notes(clip, edited_notes, other_notes) + + + def do_pitch_scramble(self, clip, args, notes_to_edit, other_notes): + """ Scrambles the pitches in the clip, but maintains rhythm. """ + if IS_LIVE_9: + edited_notes = [] + pitches = [n[0] for n in notes_to_edit] + random.shuffle(pitches) + for i in range(len(notes_to_edit)): + edited_notes.append((pitches[i], notes_to_edit[i][1], notes_to_edit[i][2], notes_to_edit[i][3], notes_to_edit[i][4])) + if edited_notes: + self.write_all_notes(clip, edited_notes, other_notes) + + + def do_position_scramble(self, clip, args, notes_to_edit, other_notes): + """ Scrambles the position of notes in the clip, but maintains pitches. """ + if IS_LIVE_9: + edited_notes = [] + positions = [n[1] for n in notes_to_edit] + random.shuffle(positions) + for i in range(len(notes_to_edit)): + edited_notes.append((notes_to_edit[i][0], positions[i], notes_to_edit[i][2], notes_to_edit[i][3], notes_to_edit[i][4])) + if edited_notes: + self.write_all_notes(clip, edited_notes, other_notes) + + + def do_note_reverse(self, clip, args, notes_to_edit, other_notes): + """ Reverse the position of notes """ + edited_notes = [] + for n in notes_to_edit: + edited_notes.append((n[0], abs(clip.loop_end - (n[1] + n[2]) + clip.loop_start), n[2], n[3], n[4])) + if edited_notes: + self.write_all_notes(clip, edited_notes, other_notes) + + + def do_note_invert(self, clip, args, notes_to_edit, other_notes): + """ Inverts the pitch of notes. """ + edited_notes = [] + for n in notes_to_edit: + edited_notes.append((127 - n[0], n[1], n[2], n[3], n[4])) + if edited_notes: + self.write_all_notes(clip, edited_notes, other_notes) + + + def do_note_compress(self, clip, args, notes_to_edit, other_notes): + """ Compresses the position and duration of notes by half. """ + edited_notes = [] + for n in notes_to_edit: + edited_notes.append((n[0], n[1] / 2, n[2] / 2, n[3], n[4])) + if edited_notes: + self.write_all_notes(clip, edited_notes, other_notes) + + + def do_note_expand(self, clip, args, notes_to_edit, other_notes): + """ Expands the position and duration of notes by 2. """ + edited_notes = [] + for n in notes_to_edit: + edited_notes.append((n[0], n[1] * 2, n[2] * 2, n[3], n[4])) + if edited_notes: + self.write_all_notes(clip, edited_notes, other_notes) + + + def do_note_split_or_combine(self, clip, args, notes_to_edit, other_notes): + """ Split notes into 2 equal parts or combine each consecutive set of 2 notes """ + edited_notes = [] ; current_note = [] ; check_next_instance = False + if args == 'SPLIT': + for n in notes_to_edit: + if n[2] / 2 < 0.03125: + return() + else: + edited_notes.append(n) + edited_notes.append((n[0], n[1] + (n[2] / 2), n[2] / 2, n[3], n[4])) + else: + for n in notes_to_edit: + edited_notes.append(n) + if current_note and check_next_instance: + if current_note[0] == n[0] and current_note[1] + current_note[2] == n[1]: + edited_notes[edited_notes.index(current_note)] = [current_note[0], current_note[1], current_note[2] + n[2], current_note[3], current_note[4]] + edited_notes.remove(n) + current_note = [] ; check_next_instance = False + else: + current_note = n + else: + current_note = n + check_next_instance = True + if edited_notes: + self.write_all_notes(clip, edited_notes, other_notes) + + + def do_note_crescendo(self, clip, args, notes_to_edit, other_notes): + """ Applies crescendo/decrescendo to notes """ + edited_notes = []; last_pos = -1; pos_index = 0; new_pos = -1; new_index = 0 + sorted_notes = sorted(notes_to_edit, key=lambda note: note[1], reverse=False) + if args == 'VELO <<': + sorted_notes = sorted(notes_to_edit, key=lambda note: note[1], reverse=True) + for n in sorted_notes: + if n[1] != last_pos: + last_pos = n[1] + pos_index += 1 + for n in sorted_notes: + if n[1] != new_pos: + new_pos = n[1] + new_index += 1 + edited_notes.append((n[0], n[1], n[2], ((128 / pos_index) * new_index) - 1, n[4])) + if edited_notes: + self.write_all_notes(clip, edited_notes, other_notes) + + + def do_note_delete(self, clip, args, notes_to_edit, other_notes): + """ Delete notes """ + self.write_all_notes(clip, [], other_notes) + + + def get_clip_stats(self, clip): + """ Get real length and end of looping clip """ + clip.looping = 0 + length = clip.length + end = clip.loop_end + clip.looping = 1 + loop_length = clip.loop_end - clip.loop_start + return {'clip_length' : length, 'real_end' : end, 'loop_length' : loop_length} + + + def get_notes_to_operate_on(self, clip, args = None): + """ Get notes within loop braces to operate on """ + notes_to_edit = [] + other_notes = [] + new_args = None + note_range = (0, 128) + pos_range = (clip.loop_start, clip.loop_end) + if args: + new_args = [a.strip() for a in args.split()] + note_range = self.get_note_range(new_args[0]) + new_args.remove(new_args[0]) + if new_args and '@' in new_args[0]: + pos_range = self.get_pos_range(clip, new_args[0]) + new_args.remove(new_args[0]) + new_args = " ".join(new_args) + clip.select_all_notes() + all_notes = clip.get_selected_notes() + clip.deselect_all_notes() + for n in all_notes: + if n[0] in range(note_range[0], note_range[1]) and n[1] < pos_range[1] and n[1] >= pos_range[0]: + notes_to_edit.append(n) + else: + other_notes.append(n) + return {'notes_to_edit' : notes_to_edit, 'other_notes' : other_notes, 'args' : new_args} + + + def get_pos_range(self, clip, string): + """ Get note position or range to operate on """ + pos_range = (clip.loop_start, clip.loop_end) + user_range = string.split('-') + try: start = float(user_range[0].replace('@', '')) + except: start = None + if start != None and start >= 0.0: + pos_range = (start, start) + if len(user_range) > 1: + try: end = float(user_range[1]) + except: end = None + if end != None: + pos_range = (start, end) + return pos_range + + + def get_note_range(self, string): + """ Get note lane or range to operate on """ + note_range = (0,128) + string = string.replace('NOTES', '') + if len(string) > 1: + int_range = self.get_note_range_from_string(string) + if int_range: + note_range = int_range + else: + start_note_name = self.get_note_name_from_string(string) + start_note_num = self.string_to_note(start_note_name) + note_range = (start_note_num, start_note_num + 1) + string = string.replace(start_note_name, '').strip() + if len(string) > 1 and string.startswith('-'): + string = string[1:] + end_note_name = self.get_note_name_from_string(string) + end_note_num = self.string_to_note(end_note_name) + if end_note_num > start_note_num: + note_range = (start_note_num, end_note_num + 1) + return note_range + + + def get_note_range_from_string(self, string): + """ Attempt to get note range (specified in ints) from string and return it or None if not specified or invalid. """ + result = None + int_split = string.split('-') + try: + start = int(int_split[0]) + end = start + 1 + if(len(int_split) > 1): + end = int(int_split[1]) + 1 + if start < end and start in range(128) and end in range(129): + result = (start, end) + else: + result = None + except: result = None + return result + + + def get_note_name_from_string(self, string): + """ Get the first note name specified in the given string. """ + result = None + if len(string) >= 2: + result = string[0:2].strip() + if (result.endswith('#') or result.endswith('-')) and len(string) >= 3: + result = string[0:3].strip() + if result.endswith('-') and len(string) >= 4: + result = string[0:4].strip() + return result + + + def string_to_note(self, string): + """ Get note value from string """ + converted_note = None + base_note = None + octave = None + for s in string: + if s in NOTE_NAMES: + base_note = NOTE_NAMES.index(s) + if base_note != None and s == '#': + base_note += 1 + if base_note != None: + for o in OCTAVE_NAMES: + if o in string: + base_note = base_note + (OCTAVE_NAMES.index(o) * 12) + break + if base_note in range (128): + converted_note = base_note + return converted_note + + + def write_all_notes(self, clip, edited_notes, other_notes): + """ Writes new notes to clip """ + edited_notes.extend(other_notes) + clip.select_all_notes() + clip.replace_selected_notes(tuple(edited_notes)) + clip.deselect_all_notes() + + +# local variables: +# tab-width: 4 diff --git a/ClyphX/ClyphXClipEnvCapture.py b/ClyphX/ClyphXClipEnvCapture.py new file mode 100644 index 0000000..ea509bc --- /dev/null +++ b/ClyphX/ClyphXClipEnvCapture.py @@ -0,0 +1,113 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent +from consts import IS_LIVE_9 + +class ClyphXClipEnvCapture(ControlSurfaceComponent): + """ Captures mixer/device parameters as clip envelopes. """ + + def disconnect(self): + self._parent = None + if IS_LIVE_9: + ControlSurfaceComponent.disconnect(self) + + def update(self): + pass + + def capture(self, clip, track, args): + clip.clear_all_envelopes() + if args == '' or 'MIX' in args: + self._capture_mix_settings(clip, track, args) + if (args == '' or 'DEV' in args) and track.devices: + self._capture_device_settings(clip, track, args) + + def _capture_mix_settings(self, clip, track, args): + if not 'MIXS' in args: + self._insert_envelope(clip, track.mixer_device.volume) + self._insert_envelope(clip, track.mixer_device.panning) + if not 'MIX-' in args: + for s in track.mixer_device.sends: + self._insert_envelope(clip, s) + + def _capture_device_settings(self, clip, track, args): + dev_range = self._get_device_range(args, track) + if dev_range: + for dev_index in range (dev_range[0], dev_range[1]): + if dev_index < (len(track.devices)): + current_device = track.devices[dev_index] + for p in current_device.parameters: + self._insert_envelope(clip, p) + if current_device.can_have_chains: + self._capture_nested_devices(clip, current_device) + + def _capture_nested_devices(self, clip, rack): + if rack.chains: + for chain in rack.chains: + for device in chain.devices: + for p in device.parameters: + self._insert_envelope(clip, p) + if not rack.class_name.startswith('Midi'): + self._insert_envelope(clip, chain.mixer_device.volume) + self._insert_envelope(clip, chain.mixer_device.panning) + self._insert_envelope(clip, chain.mixer_device.chain_activator) + sends = chain.mixer_device.sends + if sends: + for s in sends: + self._insert_envelope(clip, s) + if device.can_have_chains and device.chains: + self._capture_nested_devices(clip, device) + + def _insert_envelope(self, clip, param): + env = clip.automation_envelope(param) + if env: + env.insert_step(clip.loop_start, 0.0, param.value) + + def _get_device_range(self, args, track): + """ Returns range of devices to capture """ + dev_args = args.replace('MIX', '') + dev_args = dev_args.replace('DEV', '') + start = 0 + end = start + 1 + if dev_args: + if 'ALL' in dev_args: + start = 0 + end = len(track.devices) + elif '-' in dev_args: + try: + name_split = dev_args.split('-') + start = int(name_split[0].strip()) - 1 + end = int(name_split[1].strip()) + except: pass + else: + try: + start = int(dev_args) - 1 + end = start + 1 + except: pass + if start > len(track.devices) or start < 0 or end > len(track.devices) or end < start: + return() + return (start, end) + +# local variables: +# tab-width: 4 \ No newline at end of file diff --git a/ClyphX/ClyphXControlSurfaceActions.py b/ClyphX/ClyphXControlSurfaceActions.py new file mode 100644 index 0000000..3009aa6 --- /dev/null +++ b/ClyphX/ClyphXControlSurfaceActions.py @@ -0,0 +1,491 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +import Live +from functools import partial +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent +from _Framework.ControlSurface import ControlSurface +from _Framework.SessionComponent import SessionComponent +from _Framework.MixerComponent import MixerComponent +from _Framework.DeviceComponent import DeviceComponent +from consts import * +from ClyphXPushActions import ClyphXPushActions +from ClyphXPXTActions import ClyphXPXTActions +from ClyphXMXTActions import ClyphXMXTActions +from ClyphXArsenalActions import ClyphXArsenalActions +from ableton.v2.control_surface import ControlSurface as CS + + +class ClyphXControlSurfaceActions(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' Actions related to control surfaces ' + + def __init__(self, parent): + ControlSurfaceComponent.__init__(self) + self._parent = parent + self._push_actions = ClyphXPushActions(parent) + self._pxt_actions = ClyphXPXTActions(parent) + self._mxt_actions = ClyphXMXTActions(parent) + self._arsenal_actions = ClyphXArsenalActions(parent) + self._scripts = {} + + + def disconnect(self): + self._scripts = {} + self._parent = None + self._arsenal_actions = None + self._push_actions = None + self._pxt_actions = None + self._mxt_actions = None + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def connect_script_instances(self, instanciated_scripts): + """ Build dict of connected scripts and their components, doesn't work with non-Framework scripts, but does work with User Remote Scripts """ + instanciated_scripts = self._parent._control_surfaces() + self._scripts = {} + for index in range (len(instanciated_scripts)): + script = instanciated_scripts[index] + self._scripts[index] = {'script' : script, 'name' : None, 'repeat' : False, 'mixer' : None, 'device' : None, 'last_ring_pos' : None, + 'session' : None, 'track_link' : False, 'scene_link' : False, 'centered_link' : False, 'color' : False} + script_name = script.__class__.__name__ + if isinstance (script, (ControlSurface, CS)): + if script_name == 'GenericScript': + script_name = script._suggested_input_port + if script_name.startswith('Arsenal'): + self._arsenal_actions.set_script(script) + if script_name == 'Push': + self._push_actions.set_script(script) + if script_name.startswith('PXT_Live'): + self._pxt_actions.set_script(script) + if script_name == 'MXT_Live': + self._mxt_actions.set_script(script) + if not script_name.startswith('ClyphX'): + if script._components == None: + return + else: + self._scripts[index]['name'] = script_name.upper() + for c in script.components: + if isinstance (c, SessionComponent): + self._scripts[index]['session'] = c + if script_name.startswith('APC'): + self._scripts[index]['color'] = {'GREEN' : (1, 2), 'RED' : (3, 4), 'AMBER' : (5, 6)} + self._scripts[index]['metro'] = {'controls' : c._stop_track_clip_buttons, 'component' : None, 'override' : None} + if script_name == 'Launchpad': + self._scripts[index]['color'] = {'GREEN' : (52, 56), 'RED' : (7, 11), 'AMBER' : (55, 59)} + self._scripts[index]['metro'] = {'controls' : script._selector._side_buttons, 'component' : None, 'override' : script._selector} + if isinstance (c, MixerComponent): + self._scripts[index]['mixer'] = c + if isinstance (c, DeviceComponent): + self._scripts[index]['device'] = c + if script_name == 'Push': + self._scripts[index]['session'] = script._session_ring + self._scripts[index]['mixer'] = script._mixer + self._scripts[index]['device'] = script._device_component + elif script_name == 'Push2': + # hackish way to delay for Push2 init, using monkey patching doesn't work for some reason + self.canonical_parent.schedule_message(50, partial(self._handle_push2_init, index)) + elif script_name == 'Nocturn': + self._scripts[index]['device'] = script.device_controller + script.device_controller.canonical_parent = script + + + def _handle_push2_init(self, index): + script = self._scripts[index]['script'] + self._push_actions.set_script(script, is_push2=True) + self._scripts[index]['session'] = script._session_ring + self._scripts[index]['device'] = script._device_component + + + def dispatch_push_action(self, track, xclip, ident, action, args): + """ Dispatch Push-related actions to PushActions. """ + if self._push_actions: + self._push_actions.dispatch_action(track, xclip, ident, action, args) + + + def dispatch_pxt_action(self, track, xclip, ident, action, args): + """ Dispatch PXT-related actions to PXTActions. """ + if self._pxt_actions: + self._pxt_actions.dispatch_action(track, xclip, ident, action, args) + + + def dispatch_mxt_action(self, track, xclip, ident, action, args): + """ Dispatch MXT-related actions to MXTActions. """ + if self._mxt_actions: + self._mxt_actions.dispatch_action(track, xclip, ident, action, args) + + + def dispatch_arsenal_action(self, track, xclip, ident, action, args): + """ Dispatch Arsenal-related actions to ArsenalActions. """ + if self._arsenal_actions: + self._arsenal_actions.dispatch_action(track, xclip, ident, action, args) + + + def dispatch_cs_action(self, track, xclip, ident, action, args): + """ Dispatch appropriate control surface actions """ + script = self._get_script_to_operate_on(action) + if script != None: + if 'METRO ' in args and self._scripts[script].has_key('metro'): + self.handle_visual_metro(self._scripts[script], args) + elif 'RINGLINK ' in args and self._scripts[script]['session']: + self.handle_ring_link(self._scripts[script]['session'], script, args[9:]) + elif 'RING ' in args and self._scripts[script]['session']: + self.handle_session_offset(script, self._scripts[script]['session'], args[5:]) + elif 'COLORS ' in args and self._scripts[script]['session'] and self._scripts[script]['color']: + self.handle_session_colors(self._scripts[script]['session'], self._scripts[script]['color'], args[7:]) + elif 'DEV LOCK' in args and self._scripts[script]['device']: + self._scripts[script]['device'].canonical_parent.toggle_lock() + elif 'BANK ' in args and self._scripts[script]['mixer']: + self.handle_track_bank(script, xclip, ident, self._scripts[script]['mixer'], self._scripts[script]['session'], args[5:]) + elif 'RPT' in args: + self.handle_note_repeat(self._scripts[script]['script'], script, args) + else: + if self._scripts[script]['mixer'] and '/' in args[:4]: + self.handle_track_action(script, self._scripts[script]['mixer'], xclip, ident, args) + + + def _get_script_to_operate_on(self, script_info): + """ Returns the script index to operate on, which can be specified in terms of its index + or its name. Also, can use SURFACE (legacy) or CS (new) to indicate a surface action. """ + script = None + try: + script_spec = None + if 'SURFACE' in script_info: + script_spec = script_info.strip('SURFACE') + elif 'CS' in script_info: + script_spec = script_info.strip('CS') + if len(script_spec) == 1: + script = int(script_spec) - 1 + if not self._scripts.has_key(script): + script = None + else: + script_spec = script_spec.strip('"').strip() + for k, v in self._scripts.items(): + if v['name'] == script_spec: + script = k + except: script = None + return script + + + def handle_note_repeat(self, script, script_index, args): + """ Set note repeat for the given surface """ + args = args.replace('RPT', '').strip() + if args in REPEAT_STATES: + if args == 'OFF': + script._c_instance.note_repeat.enabled = False + self._scripts[script_index]['repeat'] = False + else: + script._c_instance.note_repeat.repeat_rate = REPEAT_STATES[args] + script._c_instance.note_repeat.enabled = True + self._scripts[script_index]['repeat'] = True + else: + self._scripts[script_index]['repeat'] = not self._scripts[script_index]['repeat'] + script._c_instance.note_repeat.enabled = self._scripts[script_index]['repeat'] + + + def handle_track_action(self, script_key, mixer, xclip, ident, args): + """ Get control surface track(s) to operate on and call main action dispatch """ + track_start = None + track_end = None + track_range = args.split('/')[0] + actions = str(args[args.index('/')+1:].strip()).split() + new_action = actions[0] + new_args = '' + if len(actions) > 1: + new_args = ' '.join(actions[1:]) + if 'ALL' in track_range: + track_start = 0 + track_end = len(mixer._channel_strips) + elif '-' in track_range: + track_range = track_range.split('-') + try: + track_start = int(track_range[0]) - 1 + track_end = int(track_range[1]) + except: + track_start = None + track_end = None + else: + try: + track_start = int(track_range) - 1 + track_end = track_start + 1 + except: + track_start = None + track_end = None + if track_start != None and track_end != None: + if track_start in range (len(mixer._channel_strips) + 1) and track_end in range (len(mixer._channel_strips) + 1) and track_start < track_end: + track_list = [] + if self._scripts[script_key]['name'] == 'PUSH': + offset, _ = self._push_actions.get_session_offsets(self._scripts[script_key]['session']) + tracks_to_use = self._scripts[script_key]['session'].tracks_to_use() + else: + offset = mixer._track_offset + tracks_to_use = mixer.tracks_to_use() + for index in range (track_start, track_end): + if index + offset in range (len(tracks_to_use)): + track_list.append(tracks_to_use[index + offset]) + if track_list: + self._parent.action_dispatch(track_list, xclip, new_action, new_args, ident) + + + def handle_track_bank(self, script_key, xclip, ident, mixer, session, args): + """ Move track bank (or session bank) and select first track in bank...this works even with controllers without banks like User Remote Scripts """ + if self._scripts[script_key]['name'] == 'PUSH': + t_offset, s_offset = self._push_actions.get_session_offsets(session) + tracks = session.tracks_to_use() + else: + t_offset, s_offset = mixer._track_offset, session._scene_offset if session else None + tracks = mixer.tracks_to_use() + new_offset = None + if args == 'FIRST': + new_offset = 0 + elif args == 'LAST': + new_offset = len(tracks) - len(mixer._channel_strips) + else: + try: + offset = int(args) + if offset + t_offset in range (len(tracks)): + new_offset = offset + t_offset + except: new_offset = None + if new_offset >= 0: + if session: + session.set_offsets(new_offset, s_offset) + else: + mixer.set_track_offset(new_offset) + self.handle_track_action(script_key, mixer, xclip, ident, '1/SEL') + + + def handle_session_offset(self, script_key, session, args): + """ Handle moving session offset absolutely or relatively as well as storing/recalling its last position. """ + if self._scripts[script_key]['name'] in ('PUSH', 'PUSH2'): + last_pos = self._push_actions.handle_session_offset(session, self._scripts[script_key]['last_ring_pos'], args, self._parse_ring_spec) + self._scripts[script_key]['last_ring_pos'] = last_pos or None + return + try: + new_track = session._track_offset + new_scene = session._scene_offset + if args.strip() == 'LAST': + last_pos = self._scripts[script_key]['last_ring_pos'] + if last_pos: + session.set_offsets(last_pos[0], last_pos[1]) + return + else: + self._scripts[script_key]['last_ring_pos'] = (new_track, new_scene) + new_track, args = self._parse_ring_spec('T', args, new_track, self.song().tracks) + new_scene, args = self._parse_ring_spec('S', args, new_scene, self.song().scenes) + if new_track == -1 or new_scene == -1: + return + session.set_offsets(new_track, new_scene) + except: pass + + + def _parse_ring_spec(self, spec_id, arg_string, default_index, list_to_search): + """ Parses a ring action specification and returns the specified track/scene index + as well as the arg_string without the specification that was parsed. """ + index = default_index + arg_array = arg_string.split() + for a in arg_array: + if a.startswith(spec_id): + if a[1].isdigit(): + index = int(a.strip(spec_id)) - 1 + arg_string = arg_string.replace(a, '', 1).strip() + break + elif a[1] in ('<', '>'): + index += self._parent.get_adjustment_factor(a.strip(spec_id)) + arg_string = arg_string.replace(a, '', 1).strip() + break + elif a[1] == '"': + name_start_pos = arg_string.index(spec_id + '"') + name = arg_string[name_start_pos + 2:] + name_end_pos = name.index('"') + name = name[:name_end_pos] + for i, item in enumerate(list_to_search): + if name == item.name.upper(): + index = i + break + arg_string = arg_string.replace(spec_id + '"' + name + '"', '', 1).strip() + break + return (index, arg_string) + + + def handle_ring_link(self, session, script_index, args): + """ Handles linking/unliking session offsets to the selected track or scene with centering if specified. """ + self._scripts[script_index]['track_link'] = args == 'T' or 'T ' in args or ' T' in args + self._scripts[script_index]['scene_link'] = 'S' in args + self._scripts[script_index]['centered_link'] = 'CENTER' in args + + + def handle_session_colors(self, session, colors, args): + """ Handle changing clip launch LED colors """ + args = args.split() + if len(args) == 3: + for a in args: + if not a in colors: + return + for scene_index in range(session.height()): + scene = session.scene(scene_index) + for track_index in range(session.width()): + clip_slot = scene.clip_slot(track_index) + clip_slot.set_started_value(colors[args[0]][0]) + clip_slot.set_triggered_to_play_value(colors[args[0]][1]) + clip_slot.set_recording_value(colors[args[1]][0]) + clip_slot.set_triggered_to_record_value(colors[args[1]][1]) + clip_slot.set_stopped_value(colors[args[2]][0]) + clip_slot.update() + + + def handle_visual_metro(self, script, args): + """ Handle visual metro for APCs and Launchpad. """ + if 'ON' in args and not script['metro']['component']: + m = VisualMetro(self._parent, script['metro']['controls'], script['metro']['override']) + script['metro']['component'] = m + elif 'OFF' in args and script['metro']['component']: + script['metro']['component'].disconnect() + script['metro']['component'] = None + + + def on_selected_track_changed(self): + """ Moves the track offset of all track linked surfaces to the selected track with centering if specified. """ + trk = self.song().view.selected_track + if trk in self.song().tracks: + trk_id = list(self.song().visible_tracks).index(trk) + for k, v in self._scripts.items(): + if v['track_link']: + new_trk_id = trk_id + try: + session = self._scripts[k]['session'] + if v['name'] in ('PUSH', 'PUSH2'): + width = self._push_actions.get_session_dimensions(session)[0] + t_offset, s_offset = self._push_actions.get_session_offsets(session) + else: + width = session.width() + t_offset, s_offset = session._track_offset, session._scene_offset + if self._scripts[k]['centered_link']: + mid_point = (width / 2) + if new_trk_id < mid_point: + if t_offset <= new_trk_id: + return + else: + new_trk_id = 0 + else: + centered_id = new_trk_id - mid_point + if centered_id in range(len(self.song().visible_tracks)): + new_trk_id = centered_id + session.set_offsets(new_trk_id, s_offset) + except: pass + + + def on_selected_scene_changed(self): + """ Moves the scene offset of all scene linked surfaces to the selected scene with centering if specified. """ + scn_id = list(self.song().scenes).index(self.song().view.selected_scene) + for k, v in self._scripts.items(): + if v['scene_link']: + new_scn_id = scn_id + try: + session = self._scripts[k]['session'] + if v['name'] in ('PUSH', 'PUSH2'): + height = self._push_actions.get_session_dimensions(session)[1] + t_offset, s_offset = self._push_actions.get_session_offsets(session) + else: + height = session.height() + t_offset, s_offset = session._track_offset, session._scene_offset + + if self._scripts[k]['centered_link']: + mid_point = (height / 2) + if new_scn_id < mid_point: + if s_offset <= new_scn_id: + return + else: + new_scn_id = 0 + else: + centered_id = new_scn_id - mid_point + if centered_id in range(len(self.song().scenes)): + new_scn_id = centered_id + session.set_offsets(t_offset, new_scn_id) + except: pass + + +class VisualMetro(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' Visual metro for APCs and Launchpad ' + + def __init__(self, parent, controls, override): + ControlSurfaceComponent.__init__(self) + self._parent = parent + self._controls = controls + self._override = override + self._last_beat = -1 + self.song().add_current_song_time_listener(self.on_time_changed) + self.song().add_is_playing_listener(self.on_time_changed) + + + def disconnect(self): + if self._controls: + self.clear() + self._controls = None + self.song().remove_current_song_time_listener(self.on_time_changed) + self.song().remove_is_playing_listener(self.on_time_changed) + self._override = None + self._parent = None + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def on_time_changed(self): + """ Show visual metronome via control LEDs upon beat changes (will not be shown if in Launchpad User 1) """ + if self.song().is_playing and (not self._override or (self._override and self._override._mode_index != 1)): + time = str(self.song().get_current_beats_song_time()).split('.') + if self._last_beat != int(time[1])-1: + self._last_beat = int(time[1])-1 + self.clear() + if self._last_beat < len(self._controls): + self._controls[self._last_beat].turn_on() + else: + self._controls[len(self._controls)-1].turn_on() + else: + self.clear() + + + def clear(self): + """ Clear all control LEDs """ + for c in self._controls: + c.turn_off() + + +# local variables: +# tab-width: 4 diff --git a/ClyphX/ClyphXControlSurfaceActions9.py b/ClyphX/ClyphXControlSurfaceActions9.py new file mode 100644 index 0000000..7c9bfab --- /dev/null +++ b/ClyphX/ClyphXControlSurfaceActions9.py @@ -0,0 +1,47 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +from __future__ import with_statement +from ClyphXControlSurfaceActions import ClyphXControlSurfaceActions, VisualMetro + +class ClyphXControlSurfaceActions9(ClyphXControlSurfaceActions): + __module__ = __name__ + __doc__ = ' Actions related to control surfaces. This is a specialized version for Live 9.' + + def __init__(self, parent): + ClyphXControlSurfaceActions.__init__(self, parent) + + def handle_visual_metro(self, script, args): + """ Handle visual metro for APCs and Launchpad. + This is a specialized version for L9 that uses component guard to avoid dependency issues. """ + if 'ON' in args and not script['metro']['component']: + with self._parent.component_guard(): + m = VisualMetro(self._parent, script['metro']['controls'], script['metro']['override']) + script['metro']['component'] = m + elif 'OFF' in args and script['metro']['component']: + script['metro']['component'].disconnect() + script['metro']['component'] = None + +# local variables: +# tab-width: 4 \ No newline at end of file diff --git a/ClyphX/ClyphXDRActions9.py b/ClyphX/ClyphXDRActions9.py new file mode 100644 index 0000000..21117d2 --- /dev/null +++ b/ClyphX/ClyphXDRActions9.py @@ -0,0 +1,164 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +import Live +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent +from consts import * + +MAX_SCROLL_POS = 28 + +class ClyphXDRActions9(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = 'Drum Rack actions ' + + def __init__(self, parent): + ControlSurfaceComponent.__init__(self) + self._parent = parent + + + def disconnect(self): + self._parent = None + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def scroll_selector(self, dr, track, xclip, ident, args): + """ Scroll Drum Rack selector up/down """ + args = args.replace('SCROLL', '').strip() + if args.startswith(('<', '>')): + factor = self._parent.get_adjustment_factor(args) + pos = dr.view.drum_pads_scroll_position + if factor > 0: + if pos < MAX_SCROLL_POS - factor: + dr.view.drum_pads_scroll_position = pos + factor + else: + dr.view.drum_pads_scroll_position = MAX_SCROLL_POS + else: + if pos + factor > 0: + dr.view.drum_pads_scroll_position = pos + factor + else: + dr.view.drum_pads_scroll_position = 0 + + + def unmute_all(self, dr, track, xclip, ident, args): + """ Unmute all pads in the Drum Rack """ + for pad in dr.drum_pads: + pad.mute = False + + + def unsolo_all(self, dr, track, xclip, ident, args): + """ Unsolo all pads in the Drum Rack """ + for pad in dr.drum_pads: + pad.solo = False + + + def dispatch_pad_action(self, dr, track, xclip, ident, args): + """ Dispatches pad-based actions """ + arg_split = args.split() + if len(arg_split) > 1: + pads = self._get_pads_to_operate_on(dr, arg_split[0].replace('PAD', '').strip()) + if pads: + action = arg_split[1] + action_arg = None + if len(arg_split) > 2: + action_arg = arg_split[2] + if arg_split[1] == 'MUTE': + self._mute_pads(pads, action_arg) + elif arg_split[1] == 'SOLO': + self._solo_pads(pads, action_arg) + elif arg_split[1] == 'SEL': + dr.view.selected_drum_pad = pads[-1] + elif arg_split[1] == 'VOL' and action_arg: + self._adjust_pad_volume(pads, action_arg) + elif arg_split[1] == 'PAN' and action_arg: + self._adjust_pad_pan(pads, action_arg) + elif 'SEND' in arg_split[1] and len(arg_split) > 3: + self._adjust_pad_send(pads, arg_split[3], action_arg) + + + def _mute_pads(self, pads, action_arg): + """ Toggles or turns on/off pad mute """ + for pad in pads: + if action_arg in KEYWORDS: + pad.mute = KEYWORDS[action_arg] + else: + pad.mute = not pad.mute + + + def _solo_pads(self, pads, action_arg): + """ Toggles or turns on/off pad solo """ + for pad in pads: + if action_arg in KEYWORDS: + pad.solo = KEYWORDS[action_arg] + else: + pad.solo = not pad.solo + + + def _adjust_pad_volume(self, pads, action_arg): + """ Adjust/set pad volume """ + for pad in pads: + if pad.chains: + self._parent.do_parameter_adjustment(pad.chains[0].mixer_device.volume, action_arg) + + + def _adjust_pad_pan(self, pads, action_arg): + """ Adjust/set pad pan """ + for pad in pads: + if pad.chains: + self._parent.do_parameter_adjustment(pad.chains[0].mixer_device.panning, action_arg) + + + def _adjust_pad_send(self, pads, action_arg, send): + """ Adjust/set pad send """ + try: + for pad in pads: + if pad.chains: + param = pad.chains[0].mixer_device.sends[ord(send) - 65] + self._parent.do_parameter_adjustment(param, action_arg) + except: pass + + + def _get_pads_to_operate_on(self, dr, pads): + """ Get the Drum Rack pad or pads to operate on """ + pads_to_operate_on = [dr.view.selected_drum_pad] + if pads == 'ALL': + pads_to_operate_on = dr.visible_drum_pads + elif pads: + try: + index = int(pads) - 1 + if index in range(16): + pads_to_operate_on = [dr.visible_drum_pads[index]] + except: pass + return pads_to_operate_on + + +# local variables: +# tab-width: 4 \ No newline at end of file diff --git a/ClyphX/ClyphXDeviceActions.py b/ClyphX/ClyphXDeviceActions.py new file mode 100644 index 0000000..21007e2 --- /dev/null +++ b/ClyphX/ClyphXDeviceActions.py @@ -0,0 +1,277 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +import Live +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent +from _Generic.Devices import * +from consts import * + +class ClyphXDeviceActions(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' Device and Looper actions ' + + def __init__(self, parent): + ControlSurfaceComponent.__init__(self) + self._parent = parent + self._looper_data = {} + + + def disconnect(self): + self._looper_data = {} + self._parent = None + if IS_LIVE_9: + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def set_all_params(self, device, track, xclip, ident, args): + """ Set the value of all macros in a rack in one go. So don't need to use a whole string of DEV Px actions to do this. + Can also capture the values of macros and store them in X-Clip name if no values specified. """ + if device.class_name.endswith('GroupDevice'): + args = args.replace('SET', '', 1).strip() + if args: + param_values = args.split(' ') + if len(param_values) == 8: + for index in range(8): + self._parent.do_parameter_adjustment(device.parameters[index + 1], param_values[index].strip()) + else: + if type(xclip) is Live.Clip.Clip: + assign_string = xclip.name + ' ' + for param in device.parameters: + if 'Macro' in param.original_name: + assign_string += str(int(param.value)) + ' ' + xclip.name = assign_string + + + def adjust_selected_chain(self, device, track, xclip, ident, args): + """ Adjust the selected chain in a rack. """ + if IS_LIVE_9 and device.can_have_chains and device.chains: + args = args.replace('CSEL', '', 1).strip() + if args in ('<', '>'): + factor = self._parent.get_adjustment_factor(args) + new_index = list(device.chains).index(device.view.selected_chain) + factor + else: + try: new_index = int(args) - 1 + except: new_index = list(device.chains).index(device.view.selected_chain) + if new_index in range(len(device.chains)): + device.view.selected_chain = device.chains[new_index] + + + def adjust_best_of_bank_param(self, device, track, xclip, ident, args): + """ Adjust device best-of-bank parameter """ + param = None + name_split = args.split() + if len(name_split) > 1: + param = self.get_bob_parameter(device, name_split[0]) + if param and param.is_enabled: + self._parent.do_parameter_adjustment(param, name_split[-1]) + + + def adjust_banked_param(self, device, track, xclip, ident, args): + """ Adjust device banked parameter """ + param = None + name_split = args.split() + if len(name_split) > 2: + param = self.get_banked_parameter(device, name_split[0], name_split[1]) + if param and param.is_enabled: + self._parent.do_parameter_adjustment(param, name_split[-1]) + + + def adjust_chain_selector(self, device, track, xclip, ident, args): + """ Adjust device chain selector parameter """ + param = self.get_chain_selector(device) + name_split = args.split() + if param and param.is_enabled and len(name_split) > 1: + self._parent.do_parameter_adjustment(param, name_split[-1]) + + + def randomize_params(self, device, track, xclip, ident, args): + """ Randomize device parameters """ + name = self._parent.get_name(device.name) + if not name.startswith(('NK RND', 'NK RST', 'NK CHAIN MIX', 'NK DR', 'NK LEARN', 'NK RECEIVER', 'NK TRACK', 'NK SIDECHAIN')): + for p in device.parameters: + if p and p.is_enabled and not p.is_quantized and p.name != 'Chain Selector': + p.value = (((p.max - p.min) / 127) * Live.Application.get_random_int(0, 128)) + p.min + + + def reset_params(self, device, track, xclip, ident, args): + """ Reset device parameters """ + name = self._parent.get_name(device.name) + if not name.startswith(('NK RND', 'NK RST', 'NK CHAIN MIX', 'NK DR', 'NK LEARN', 'NK RECEIVER', 'NK TRACK', 'NK SIDECHAIN')): + for p in device.parameters: + if p and p.is_enabled and not p.is_quantized and p.name != 'Chain Selector': + p.value = p.default_value + + + def select_device(self, device, track, xclip, ident, args): + """ Select device and bring it and the track it's on into view """ + if self.song().view.selected_track != track: + self.song().view.selected_track = track + self.application().view.show_view('Detail') + self.application().view.show_view('Detail/DeviceChain') + self.song().view.select_device(device) + + + def set_device_on_off(self, device, track, xclip, ident, value = None): + """ Toggles or turns device on/off """ + on_off = self.get_device_on_off(device) + if on_off and on_off.is_enabled: + if value in KEYWORDS: + on_off.value = KEYWORDS[value] + else: + on_off.value = not(on_off.value) + + + def set_looper_on_off(self, track, xclip, ident, value = None): + """ Toggles or turns looper on/off """ + self.get_looper(track) + if self._looper_data and self._looper_data['Looper'] and self._looper_data['Device On'].is_enabled: + if value in KEYWORDS: + self._looper_data['Device On'].value = KEYWORDS[value] + else: + self._looper_data['Device On'].value = not(self._looper_data['Device On'].value) + + + def set_looper_rev(self, track, xclip, ident, value = None): + """ Toggles or turns looper reverse on/off """ + self.get_looper(track) + if self._looper_data and self._looper_data['Looper'] and self._looper_data['Reverse'].is_enabled: + if value in KEYWORDS: + self._looper_data['Reverse'].value = KEYWORDS[value] + else: + self._looper_data['Reverse'].value = not(self._looper_data['Reverse'].value) + + + def set_looper_state(self, track, xclip, ident, value = None): + """ Sets looper state """ + self.get_looper(track) + if self._looper_data and self._looper_data['Looper'] and value in LOOPER_STATES and self._looper_data['State'].is_enabled: + self._looper_data['State'].value = LOOPER_STATES[value] + + + def dispatch_chain_action(self, device, track, xclip, ident, args): + """ Handle actions related to device chains """ + if self._parent._can_have_nested_devices and device.can_have_chains and device.chains: + arg_list = args.split() + try: chain = device.chains[int(arg_list[0].replace('CHAIN', '')) - 1] + except: chain = None + if chain == None and IS_LIVE_9: + chain = device.view.selected_chain + if chain: + if len(arg_list) > 1 and arg_list[1] == 'MUTE': + if len(arg_list) > 2 and arg_list[2] in KEYWORDS: + chain.mute = KEYWORDS[arg_list[2]] + else: + chain.mute = not(chain.mute) + elif len(arg_list) > 1 and arg_list[1] == 'SOLO': + if len(arg_list) > 2 and arg_list[2] in KEYWORDS: + chain.solo = KEYWORDS[arg_list[2]] + else: + chain.solo = not(chain.solo) + elif len(arg_list) > 2 and arg_list[1] == 'VOL' and not device.class_name.startswith('Midi'): + self._parent.do_parameter_adjustment(chain.mixer_device.volume, arg_list[2].strip()) + elif len(arg_list) > 2 and arg_list[1] == 'PAN' and not device.class_name.startswith('Midi'): + self._parent.do_parameter_adjustment(chain.mixer_device.panning, arg_list[2].strip()) + + + def get_device_on_off(self, device): + """ Get device on/off param """ + result = None + for parameter in device.parameters: + if str(parameter.name).startswith('Device On'): + result = parameter + break + return result + + + def get_chain_selector(self, device): + """ Get rack chain selector param """ + result = None + if device.class_name.endswith('GroupDevice'): + for parameter in device.parameters: + if str(parameter.original_name) == 'Chain Selector': + result = parameter + break + return result + + + def get_bob_parameter(self, device, param_string): + """ Get best-of-bank parameter 1-8 for Live's devices + The param string should be composed of 'P' followed by the param index (like P5) """ + result = None + if (device.class_name in DEVICE_BOB_DICT.keys()): + param_bank = DEVICE_BOB_DICT[device.class_name][0] + try: + param_num = int(param_string[1])-1 + if param_num in range (8): + parameter = get_parameter_by_name(device, param_bank[param_num]) + if parameter: + result = parameter + except: pass + return result + + + def get_banked_parameter(self, device, bank_string, param_string): + """ Get bank 1-8/parameter 1-8 for Live's devices + The bank string should be composed of 'B' followed by the bank index (like B2) + The param string should be composed of 'P' followed by the param index (like P5)""" + result = None + if device.class_name in DEVICE_DICT.keys(): + device_bank = DEVICE_DICT[device.class_name] + try: + bank_num = int(bank_string[1])-1 + param_num = int(param_string[1])-1 + if param_num in range (8) and bank_num in range (8) and bank_num <= number_of_parameter_banks(device): + param_bank = device_bank[bank_num] + parameter = get_parameter_by_name(device, param_bank[param_num]) + if parameter: + result = parameter + except: pass + return result + + + def get_looper(self, track): + """ Get first looper device on track and its params """ + self._looper_data = {} + for d in track.devices: + if d.class_name == 'Looper': + self._looper_data['Looper'] = d + for p in d.parameters: + if p.name in ('Device On', 'Reverse', 'State'): + self._looper_data[p.name] = p + break + elif not self._looper_data and self._parent._can_have_nested_devices and d.can_have_chains and d.chains: + for c in d.chains: + self.get_looper(c) + + +# local variables: +# tab-width: 4 \ No newline at end of file diff --git a/ClyphX/ClyphXGlobalActions.py b/ClyphX/ClyphXGlobalActions.py new file mode 100644 index 0000000..31f2d2a --- /dev/null +++ b/ClyphX/ClyphXGlobalActions.py @@ -0,0 +1,1054 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +import Live +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent +from consts import * +if IS_LIVE_9: + from functools import partial + +class ClyphXGlobalActions(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' Global actions ' + + def __init__(self, parent): + ControlSurfaceComponent.__init__(self) + self._parent = parent + self._last_gqntz = 4 + self._last_rqntz = 5 + self._repeat_enabled = False + self._tempo_ramp_active = False + self._tempo_ramp_settings = [] + self._last_beat = -1 + self.song().add_current_song_time_listener(self.on_time_changed) + self.song().add_is_playing_listener(self.on_time_changed) + if self.song().clip_trigger_quantization != 0: + self._last_gqntz = int(self.song().clip_trigger_quantization) + if self.song().midi_recording_quantization != 0: + self._last_rqntz = int(self.song().midi_recording_quantization) + self._last_scene_index = list(self.song().scenes).index(self.song().view.selected_scene) + self._scenes_to_monitor = [] + self.setup_scene_listeners() + + + def disconnect(self): + self.remove_scene_listeners() + self.song().remove_current_song_time_listener(self.on_time_changed) + self.song().remove_is_playing_listener(self.on_time_changed) + self._tempo_ramp_settings = [] + self._scenes_to_monitor = None + self._parent = None + if IS_LIVE_9: + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def on_scene_triggered(self, index): + self._last_scene_index = index + + + def on_scene_list_changed(self): + self.setup_scene_listeners() + + + def make_instant_mapping_docs(self, *a): + from InstantMappingMakeDoc import InstantMappingMakeDoc + InstantMappingMakeDoc() + + def send_midi_message(self, track, xclip, ident, args): + """ Send formatted note/cc/pc message or raw midi message. """ + status_values = {'NOTE': 144, 'CC': 176, 'PC': 192} + message_to_send = [] + if args: + byte_array = args.split() + if len(byte_array) >= 2: + if len(byte_array) >= 3 and byte_array[0] in status_values: + data_bytes = self.convert_strings_to_ints(byte_array[1:]) + if data_bytes and data_bytes[0] in range(1, 17): + message_to_send = [status_values[byte_array[0]] + data_bytes[0] - 1] + for byte in data_bytes[1:]: + if byte in range(128): + message_to_send.append(byte) + if (byte_array[0] != 'PC' and len(message_to_send) != 3) or (byte_array[0] == 'PC' and len(message_to_send) != 2): + return + else: + message_to_send = self.convert_strings_to_ints(byte_array) + if message_to_send: + try: + self._parent._send_midi(tuple(message_to_send)) + if byte_array[0] == 'NOTE': #---send matching note off for note messages + message_to_send[-1] = 0 + if IS_LIVE_9: + self._parent.schedule_message(1, partial(self._parent._send_midi, tuple(message_to_send))) + else: + self._parent.schedule_message(1, self._parent._send_midi, tuple(message_to_send)) + except: pass + + + def convert_strings_to_ints(self, strings): + """ Convert list of strings of ints into list of ints. """ + result = [] + try: + for string in strings: + result.append(int(string)) + except: result = [] + return result + + + def do_variable_assignment(self, track, xclip, ident, args): + """ Creates numbered variables for the name given in args from the offset given in args and in the quantity given in args """ + args = args.strip() + arg_array = args.split() + if len(arg_array) == 3: + try: + start = int(arg_array[1]) + length = int(arg_array[2]) + for index in range(length): + self._parent._user_variables[arg_array[0] + str(index + 1)] = str(index + start) + except: pass + + + def create_audio_track(self, track, xclip, ident, value = None): + """ Creates audio track at end of track list or at the specified index. """ + if IS_LIVE_9: + value = value.strip() + if value: + try: + index = int(value) - 1 + if index in range(len(self.song().tracks)): + self.song().create_audio_track(index) + except: pass + else: + self.song().create_audio_track(-1) + + + def create_midi_track(self, track, xclip, ident, value = None): + """ Creates MIDI track at end of track list or at the specified index. """ + if IS_LIVE_9: + value = value.strip() + if value: + try: + index = int(value) - 1 + if index in range(len(self.song().tracks)): + self.song().create_midi_track(index) + except: pass + else: + self.song().create_midi_track(-1) + + + def create_return_track(self, track, xclip, ident, value = None): + """ Creates return track at end of return list. """ + if IS_LIVE_9: + self.song().create_return_track() + + + def insert_and_configure_audio_track(self, track, xclip, ident, value = None): + """ Inserts an audio track next to the selected track routed from the + selected track and armed. """ + self._insert_and_configure_track() + + + def insert_and_configure_midi_track(self, track, xclip, ident, value = None): + """ Inserts a midi track next to the selected track routed from the + selected track and armed. """ + self._insert_and_configure_track(True) + + + def _insert_and_configure_track(self, is_midi=False): + """ Handles inserting tracks and configuring them. This method will only + work if the selected track has the appropriate output/input for the insertion. """ + if IS_LIVE_9: + sel_track = self.song().view.selected_track + if is_midi and not sel_track.has_midi_input: + return + if not is_midi and not sel_track.has_audio_output: + return + try: + ins_index = list(self.song().tracks).index(sel_track) + 1 + create_method = getattr(self.song(), 'create_midi_track' if is_midi + else 'create_audio_track') + create_method(ins_index) + new_track = self.song().tracks[ins_index] + new_track.name = 'From %s' % sel_track.name + new_track.current_input_routing = sel_track.name + new_track.arm = True + except: pass + + + def create_scene(self, track, xclip, ident, value = None): + """ Creates scene at end of scene list or at the specified index. """ + if IS_LIVE_9: + current_name = None + if type(xclip) is Live.Clip.Clip: + current_name = xclip.name + xclip.name = '' + value = value.strip() + if value: + try: + index = int(value) - 1 + if index in range(len(self.song().scenes)): + self.song().create_scene(index) + except: pass + else: + self.song().create_scene(-1) + if current_name: + self._parent.schedule_message(4, partial(self.refresh_xclip_name, (xclip, current_name))) + + + def duplicate_scene(self, track, xclip, ident, args): + """ Duplicates the given scene. """ + if IS_LIVE_9: + current_name = None + if type(xclip) is Live.Clip.Clip and args: + current_name = xclip.name + xclip.name = '' + self.song().duplicate_scene(self.get_scene_to_operate_on(xclip, args.strip())) + if current_name: + self._parent.schedule_message(4, partial(self.refresh_xclip_name, (xclip, current_name))) + + + def refresh_xclip_name(self, clip_info): + """ This is used for both dupe and create scene to prevent the action from getting triggered over and over again. """ + if clip_info[0]: + clip_info[0].name = clip_info[1] + + + def delete_scene(self, track, xclip, ident, args): + """ Deletes the given scene as long as it's not the last scene in the set. """ + if IS_LIVE_9 and len(self.song().scenes) > 1: + self.song().delete_scene(self.get_scene_to_operate_on(xclip, args.strip())) + + + def swap_device_preset(self, track, xclip, ident, args): + """ Activates swapping for the selected device or swaps out the preset for the given device with the given preset or navigates forwards and back through presets. """ + if IS_LIVE_9: + device = track.view.selected_device + if device: + if not args: + self.application().view.toggle_browse() + else: + if self.application().view.browse_mode: + self.application().view.toggle_browse() + tag_target = None + dev_name = device.class_display_name + args = args.strip() + if IS_LIVE_9_5: + if device.type == Live.Device.DeviceType.audio_effect: + tag_target = self.application().browser.audio_effects + elif device.type == Live.Device.DeviceType.midi_effect: + tag_target = self.application().browser.midi_effects + elif device.type == Live.Device.DeviceType.instrument: + tag_target = self.application().browser.instruments + if tag_target: + for dev in tag_target.children: + if dev.name == dev_name: + self._handle_swapping(device, dev, args) + break + else: + if device.type == Live.Device.DeviceType.audio_effect: + tag_target = 'Audio Effects' + elif device.type == Live.Device.DeviceType.midi_effect: + tag_target = 'MIDI Effects' + elif device.type == Live.Device.DeviceType.instrument: + tag_target = 'Instruments' + if tag_target: + for main_tag in self.application().browser.tags: + if main_tag.name == tag_target: + for dev in main_tag.children: + if dev.name == dev_name: + self._handle_swapping(device, dev, args) + break + break + + + def _handle_swapping(self, device, browser_item, args): + dev_items = self._create_device_items(browser_item, []) + if args in ('<', '>'): + factor = self._parent.get_adjustment_factor(args) + index = self._get_current_preset_index(device, dev_items) + new_index = index + factor + if new_index > len(dev_items) - 1: + new_index = 0 + elif new_index < 0: + new_index = -1 + self._load_preset(dev_items[new_index]) + else: + if device.can_have_chains: + args = args + '.ADG' + else: + args = args + '.ADV' + for item in dev_items: + if item.name.upper() == args: + self._load_preset(item) + break + + + def _get_current_preset_index(self, device, presets): + """ Returns the index of the current preset (based on the device's name) in the presets list. Returns -1 if not found. """ + index = -1 + current_preset_name = device.name + if device.can_have_chains: + current_preset_name = current_preset_name + '.adg' + else: + current_preset_name = current_preset_name + '.adv' + for item_index in range(len(presets)): + if presets[item_index].name == current_preset_name: + index = item_index + break + return index + + + def _load_preset(self, preset): + """ Loads the given preset. """ + self.application().view.toggle_browse() + self.application().browser.load_item(preset) + self.application().view.toggle_browse() + + + def _create_device_items(self, device, item_array): + """ Returns the array of loadable items for the given device and handles digging into sub-folders too. """ + for item in device.children: + if item.is_folder: + self._create_device_items(item, item_array) + elif item.is_loadable: + item_array.append(item) + return item_array + + + def load_device(self, track, xclip, ident, args): + """ Loads one of Live's built-in devices onto the selected Track. """ + # using a similar method for loading plugins doesn't seem to work! + if IS_LIVE_9: + args = args.strip() + tag_target = None + name = None + if IS_LIVE_9_5: + if args in AUDIO_DEVS: + tag_target = self.application().browser.audio_effects + name = AUDIO_DEVS[args] + elif args in MIDI_DEVS: + tag_target = self.application().browser.midi_effects + name = MIDI_DEVS[args] + elif args in INS_DEVS: + tag_target = self.application().browser.instruments + name = INS_DEVS[args] + if tag_target: + for dev in tag_target.children: + if dev.name == name: + self.application().browser.load_item(dev) + break + else: + if args in AUDIO_DEVS: + tag_target = 'Audio Effects' + name = AUDIO_DEVS[args] + elif args in MIDI_DEVS: + tag_target = 'MIDI Effects' + name = MIDI_DEVS[args] + elif args in INS_DEVS: + tag_target = 'Instruments' + name = INS_DEVS[args] + if tag_target: + for main_tag in self.application().browser.tags: + if main_tag.name == tag_target: + for dev in main_tag.children: + if dev.name == name: + self.application().browser.load_item(dev) + break + break + + + def load_m4l(self, track, xclip, ident, args): + """ Loads M4L device onto the selected Track. The .amxd should be omitted by the user. """ + if IS_LIVE_9: + args = args.strip() + '.AMXD' + found_dev = False + if IS_LIVE_9_5: + for m in self.application().browser.max_for_live.children: + for device in m.children: + if not found_dev: + if device.is_folder: + for dev in device.children: + if dev.name.upper() == args: + found_dev = True + self.application().browser.load_item(dev) + break + elif device.name.upper() == args: + found_dev = True + self.application().browser.load_item(device) + break + else: + break + else: + for main_tag in self.application().browser.tags: + if main_tag.name == 'Max for Live': + for folder in main_tag.children: + if not found_dev: + if folder.is_folder: + for dev in folder.children: + if dev.name.upper() == args: + found_dev = True + self.application().browser.load_item(dev) + break + else: + break + break + + + def set_session_record(self, track, xclip, ident, value = None): + """ Toggles or turns on/off session record """ + if IS_LIVE_9: + if value in KEYWORDS: + self.song().session_record = KEYWORDS[value] + else: + self.song().session_record = not(self.song().session_record) + + + def trigger_session_record(self, track, xclip, ident, value = None): + """ Triggers session record in all armed tracks for the specified fixed length. """ + if IS_LIVE_9 and value: + # the below fixes an issue where Live will crash instead of creating a new + # scene when triggered via an X-Clip + if type(xclip) is Live.Clip.Clip: + scene = list(xclip.canonical_parent.canonical_parent.clip_slots).index(xclip.canonical_parent) + for t in self.song().tracks: + if t.can_be_armed and t.arm: + if not self._track_has_empty_slot(t, scene): + self.song().create_scene(-1) + break + bar = (4.0 / self.song().signature_denominator) * self.song().signature_numerator + try: length = float(value.strip()) * bar + except: length = bar + self.song().trigger_session_record(length) + + + def _track_has_empty_slot(self, track, start): + """ Returns whether the given track has an empty slot existing after the starting + slot index. """ + for s in track.clip_slots[start:]: + if not s.has_clip: + return True + return False + + + def set_session_automation_record(self, track, xclip, ident, value = None): + """ Toggles or turns on/off session automation record """ + if IS_LIVE_9: + if value in KEYWORDS: + self.song().session_automation_record = KEYWORDS[value] + else: + self.song().session_automation_record = not(self.song().session_automation_record) + + + def retrigger_recording_clips(self, track, xclip, ident, value = None): + """ Retriggers all clips that are currently recording. """ + for track in self.song().tracks: + if track.playing_slot_index >= 0: + slot = track.clip_slots[track.playing_slot_index] + if slot.has_clip and slot.clip.is_recording: + slot.fire() + + + def set_back_to_arrange(self, track, xclip, ident, value = None): + """ Triggers back to arrange button """ + self.song().back_to_arranger = 0 + + + def set_overdub(self, track, xclip, ident, value = None): + """ Toggles or turns on/off overdub """ + if value in KEYWORDS: + self.song().overdub = KEYWORDS[value] + else: + self.song().overdub = not(self.song().overdub) + + + def set_metronome(self, track, xclip, ident, value = None): + """ Toggles or turns on/off metronome """ + if value in KEYWORDS: + self.song().metronome = KEYWORDS[value] + else: + self.song().metronome = not(self.song().metronome) + + + def set_record(self, track, xclip, ident, value = None): + """ Toggles or turns on/off record """ + if value in KEYWORDS: + self.song().record_mode = KEYWORDS[value] + else: + self.song().record_mode = not(self.song().record_mode) + + + def set_punch_in(self, track, xclip, ident, value = None): + """ Toggles or turns on/off punch in """ + if value in KEYWORDS: + self.song().punch_in = KEYWORDS[value] + else: + self.song().punch_in = not(self.song().punch_in) + + + def set_punch_out(self, track, xclip, ident, value = None): + """ Toggles or turns on/off punch out """ + if value in KEYWORDS: + self.song().punch_out = KEYWORDS[value] + else: + self.song().punch_out = not(self.song().punch_out) + + + def restart_transport(self, track, xclip, ident, value = None): + """ Restarts transport to 0.0 """ + self.song().current_song_time = 0 + + + def set_stop_transport(self, track, xclip, ident, value = None): + """ Toggles transport """ + self.song().is_playing = not(self.song().is_playing) + + + def set_continue_playback(self, track, xclip, ident, value = None): + """ Continue playback from stop point """ + self.song().continue_playing() + + + def set_stop_all(self, track, xclip, ident, value = None): + """ Stop all clips w/no quantization option for Live 9 """ + if IS_LIVE_9: + self.song().stop_all_clips(not value.strip() == 'NQ') + else: + self.song().stop_all_clips() + + + def set_tap_tempo(self, track, xclip, ident, value = None): + """ Tap tempo """ + self.song().tap_tempo() + + + def set_undo(self, track, xclip, ident, value = None): + """ Triggers Live's undo """ + if self.song().can_undo: + self.song().undo() + + + def set_redo(self, track, xclip, ident, value = None): + """ Triggers Live's redo """ + if self.song().can_redo: + self.song().redo() + + + def move_up(self, track, xclip, ident, value = None): + """ Scroll up """ + self.application().view.scroll_view(Live.Application.Application.View.NavDirection(0), '', False) + + + def move_down(self, track, xclip, ident, value = None): + """ Scroll down """ + self.application().view.scroll_view(Live.Application.Application.View.NavDirection(1), '', False) + + + def move_left(self, track, xclip, ident, value = None): + """ Scroll left """ + self.application().view.scroll_view(Live.Application.Application.View.NavDirection(2), '', False) + + + def move_right(self, track, xclip, ident, value = None): + """ Scroll right """ + self.application().view.scroll_view(Live.Application.Application.View.NavDirection(3), '', False) + + + def move_to_first_device(self, track, xclip, ident, value = None): + """ Move to the first device on the track and scroll the view """ + self.focus_devices() + self.song().view.selected_track.view.select_instrument() + + + def move_to_last_device(self, track, xclip, ident, value = None): + """ Move to the last device on the track and scroll the view """ + self.focus_devices() + if self.song().view.selected_track.devices: + self.song().view.select_device(self.song().view.selected_track.devices[len(self.song().view.selected_track.devices) - 1]) + self.application().view.scroll_view(Live.Application.Application.View.NavDirection(3), 'Detail/DeviceChain', False) + self.application().view.scroll_view(Live.Application.Application.View.NavDirection(2), 'Detail/DeviceChain', False) + + + def move_to_prev_device(self, track, xclip, ident, value = None): + """ Move to the previous device on the track """ + self.focus_devices() + self.application().view.scroll_view(Live.Application.Application.View.NavDirection(2), 'Detail/DeviceChain', False) + + + def move_to_next_device(self, track, xclip, ident, value = None): + """ Move to the next device on the track """ + self.focus_devices() + self.application().view.scroll_view(Live.Application.Application.View.NavDirection(3), 'Detail/DeviceChain', False) + + + def focus_devices(self): + """ Make sure devices are in focus and visible """ + self.application().view.show_view('Detail') + self.application().view.show_view('Detail/DeviceChain') + + + def show_clip_view(self, track, xclip, ident, value = None): + """ Show clip view """ + self.application().view.show_view('Detail') + self.application().view.show_view('Detail/Clip') + + + def show_track_view(self, track, xclip, ident, value = None): + """ Show track view """ + self.application().view.show_view('Detail') + self.application().view.show_view('Detail/DeviceChain') + + + def show_detail_view(self, track, xclip, ident, value = None): + """ Toggle between showing/hiding detail view """ + if self.application().view.is_view_visible('Detail'): + self.application().view.hide_view('Detail') + else: + self.application().view.show_view('Detail') + + + def toggle_browser(self, track, xclip, ident, value = None): + """ Hide/show browser and move focus to or from browser """ + if self.application().view.is_view_visible('Browser'): + self.application().view.hide_view('Browser') + self.application().view.focus_view('') + else: + self.application().view.show_view('Browser') + self.application().view.focus_view('Browser') + + + def toggle_detail_view(self, track, xclip, ident, value = None): + """ Toggle between clip and track view """ + self.application().view.show_view('Detail') + if self.application().view.is_view_visible('Detail/Clip'): + self.application().view.show_view('Detail/DeviceChain') + else: + self.application().view.show_view('Detail/Clip') + + + def toggle_main_view(self, track, xclip, ident, value = None): + """ Toggle between session and arrange view """ + if self.application().view.is_view_visible('Session'): + self.application().view.show_view('Arranger') + else: + self.application().view.show_view('Session') + + + def focus_browser(self, track, xclip, ident, value = None): + """ Move the focus to the browser, show browser first if necessary """ + if not self.application().view.is_view_visible('Browser'): + self.application().view.show_view('Browser') + self.application().view.focus_view('Browser') + + + def focus_detail(self, track, xclip, ident, value = None): + """ Move the focus to the detail view, show detail first if necessary """ + if not self.application().view.is_view_visible('Detail'): + self.application().view.show_view('Detail') + self.application().view.focus_view('Detail') + + + def focus_main(self, track, xclip, ident, value = None): + """ Move the focus to the main focus """ + self.application().view.focus_view('') + + + def adjust_horizontal_zoom(self, track, xclip, ident, value): + """ Horizontally zoom in in Arrange the number of times specified in value. This can accept ALL, but doesn't have any bearing. """ + zoom_all = 'ALL' in value + value = value.replace('ALL', '').strip() + try: value = int(value) + except: return + direct = (value > 0) + 2 + for index in range(abs(value) + 1): + self.application().view.zoom_view(Live.Application.Application.View.NavDirection(direct), '', zoom_all) + + + def adjust_vertical_zoom(self, track, xclip, ident, value): + """ Vertically zoom in on the selected track in Arrange the number of times specified in value. This can accept ALL for zooming all tracks. """ + zoom_all = 'ALL' in value + value = value.replace('ALL', '').strip() + try: value = int(value) + except: return + direct = (value > 0) + for index in range(abs(value) + 1): + self.application().view.zoom_view(Live.Application.Application.View.NavDirection(direct), '', zoom_all) + + + def adjust_tempo(self, track, xclip, ident, args): + """ Adjust/set tempo or apply smooth synced ramp """ + self._tempo_ramp_active = False + self._tempo_ramp_settings = [] + args = args.strip() + if args.startswith(('<', '>')): + factor = self._parent.get_adjustment_factor(args, True) + self.song().tempo = max(20, min(999, (self.song().tempo + factor))) + elif args.startswith('*'): + try: self.song().tempo = max(20, min(999, (self.song().tempo * float(args[1:])))) + except: pass + elif args.startswith('RAMP') and IS_LIVE_9: + arg_array = args.split() + if len(arg_array) == 3: + try: + ramp_factor = float("%.2f" % (int(arg_array[1]) * self.song().signature_numerator)) + if arg_array[2].startswith('*'): + target_tempo = max(20, min(999, (self.song().tempo * float(arg_array[2][1:])))) + else: + target_tempo = float("%.2f" % float(arg_array[2])) + if target_tempo >= 20.0 and target_tempo <= 999.0: + self._tempo_ramp_settings = [target_tempo, (target_tempo - self.song().tempo) / ramp_factor] + self._tempo_ramp_active = True + except: pass + else: + try: + self.song().tempo = float(args) + except: pass + + + def on_time_changed(self): + """ Smooth BPM changes synced to tempo """ + if self._tempo_ramp_active and self._tempo_ramp_settings and self.song().is_playing: + time = int(str(self.song().get_current_beats_song_time()).split('.')[2]) + if self._last_beat != time: + self._last_beat = time + self._tasks.add(self.apply_tempo_ramp) + + + def apply_tempo_ramp(self, arg=None): + """ Apply tempo smoothing """ + target_reached = False + if self._tempo_ramp_settings[1] > 0: + target_reached = self._tempo_ramp_settings[0] <= self.song().tempo + else: + target_reached = self._tempo_ramp_settings[0] >= self.song().tempo + if target_reached: + self.song().tempo = self._tempo_ramp_settings[0] + self._tempo_ramp_active = False + self._tasks.kill() + self._tasks.clear() + else: + self.song().tempo += self._tempo_ramp_settings[1] + + + def adjust_groove(self, track, xclip, ident, args): + """ Adjust/set global groove """ + args = args.strip() + if args.startswith(('<', '>')): + factor = self._parent.get_adjustment_factor(args, True) + self.song().groove_amount = max(0.0, min(1.3125, (self.song().groove_amount + factor * float(1.3125 / 131.0)))) + else: + try: + self.song().groove_amount = int(args) * float(1.3125 / 131.0) + except: pass + + + def set_note_repeat(self, track, xclip, ident, args): + """ Set/toggle note repeat """ + if IS_LIVE_9: + args = args.strip() + if args in REPEAT_STATES: + if args == 'OFF': + self._parent._c_instance.note_repeat.enabled = False + self._repeat_enabled = False + else: + self._parent._c_instance.note_repeat.repeat_rate = REPEAT_STATES[args] + self._parent._c_instance.note_repeat.enabled = True + self._repeat_enabled = True + else: + self._repeat_enabled = not self._repeat_enabled + self._parent._c_instance.note_repeat.enabled = self._repeat_enabled + + + def adjust_swing(self, track, xclip, ident, args): + """ Adjust swing amount for use with note repeat """ + if IS_LIVE_9: + args = args.strip() + if args.startswith(('<', '>')): + factor = self._parent.get_adjustment_factor(args, True) + self.song().swing_amount = max(0.0, min(1.0, (self.song().swing_amount + factor * 0.01))) + else: + try: + self.song().swing_amount = int(args) * 0.01 + except: pass + + + def adjust_global_quantize(self, track, xclip, ident, args): + """ Adjust/set/toggle global quantization """ + args = args.strip() + if args in GQ_STATES: + self.song().clip_trigger_quantization = GQ_STATES[args] + elif args in ('<', '>'): + factor = self._parent.get_adjustment_factor(args) + new_gq = self.song().clip_trigger_quantization + factor + if new_gq in range (14): + self.song().clip_trigger_quantization = new_gq + else: + if self.song().clip_trigger_quantization != 0: + self._last_gqntz = int(self.song().clip_trigger_quantization) + self.song().clip_trigger_quantization = 0 + else: + self.song().clip_trigger_quantization = self._last_gqntz + + + def adjust_record_quantize(self, track, xclip, ident, args): + """ Adjust/set/toggle record quantization """ + args = args.strip() + if args in RQ_STATES: + self.song().midi_recording_quantization = RQ_STATES[args] + elif args in ('<', '>'): + factor = self._parent.get_adjustment_factor(args) + new_rq = self.song().midi_recording_quantization + factor + if new_rq in range (9): + self.song().midi_recording_quantization = new_rq + else: + if self.song().midi_recording_quantization != 0: + self._last_rqntz = int(self.song().midi_recording_quantization) + self.song().midi_recording_quantization = 0 + else: + self.song().midi_recording_quantization = self._last_rqntz + + + def adjust_time_signature(self, track, xclip, ident, args): + """ Adjust global time signature """ + if '/' in args: + name_split = args.split('/') + try: + self.song().signature_numerator = int(name_split[0].strip()) + self.song().signature_denominator = int(name_split[1].strip()) + except: pass + + + def set_jump_all(self, track, xclip, ident, args): + """ Jump arrange position forward/backward """ + try: self.song().jump_by(float(args.strip())) + except: pass + + + def set_unarm_all(self, track, xclip, ident, args): + """ Unarm all armable tracks """ + for t in self.song().tracks: + if t.can_be_armed and t.arm: + t.arm = 0 + + + def set_unmute_all(self, track, xclip, ident, args): + """ Unmute all tracks """ + for t in (tuple(self.song().tracks) + tuple(self.song().return_tracks)): + if t.mute: + t.mute = 0 + + + def set_unsolo_all(self, track, xclip, ident, args): + """ Unsolo all tracks """ + for t in (tuple(self.song().tracks) + tuple(self.song().return_tracks)): + if t.solo: + t.solo = 0 + + + def set_fold_all(self, track, xclip, ident, value): + """ Toggle or turn/on fold for all tracks """ + state_to_set = None + for t in self.song().tracks: + if t.is_foldable: + if state_to_set == None: + state_to_set = not(t.fold_state) + if value in KEYWORDS: + t.fold_state = KEYWORDS[value] + else: + t.fold_state = state_to_set + + + def set_scene(self, track, xclip, ident, args): + """ Sets scene to play (doesn't launch xclip) """ + args = args.strip() + scene_to_launch = self.get_scene_to_operate_on(xclip, args) + if args != '': + if 'RND' in args and len(self.song().scenes) > 1:#--Don't allow randomization unless more than 1 scene + num_scenes = len(self.song().scenes) + rnd_range = [0, num_scenes] + if '-' in args: + rnd_range_data = args.replace('RND', '').split('-') + if len(rnd_range_data) == 2: + new_min = 0 + new_max = num_scenes + try: new_min = int(rnd_range_data[0]) - 1 + except: new_min = 0 + try: new_max = int(rnd_range_data[1]) + except: new_max = num_scenes + if new_min in range(0, num_scenes) and new_max in range(0, num_scenes + 1) and new_min < new_max - 1: + rnd_range = [new_min, new_max] + scene_to_launch = Live.Application.get_random_int(0, rnd_range[1] - rnd_range[0]) + rnd_range[0] + if scene_to_launch == self._last_scene_index: + while scene_to_launch == self._last_scene_index: + scene_to_launch = Live.Application.get_random_int(0, rnd_range[1] - rnd_range[0]) + rnd_range[0] + elif args.startswith(('<', '>')) and len(self.song().scenes) > 1:#--Don't allow adjustment unless more than 1 scene + factor = self._parent.get_adjustment_factor(args) + if factor < len(self.song().scenes): + scene_to_launch = self._last_scene_index + factor + if scene_to_launch >= len(self.song().scenes): + scene_to_launch -= len(self.song().scenes) + elif scene_to_launch < 0 and abs(scene_to_launch) >= len(self.song().scenes): + scene_to_launch = -(abs(scene_to_launch) - len(self.song().scenes)) + self._last_scene_index = scene_to_launch + for t in self.song().tracks: + if t.is_foldable or (t.clip_slots[scene_to_launch].has_clip and t.clip_slots[scene_to_launch].clip == xclip): + pass + else: + t.clip_slots[scene_to_launch].fire() + + + def get_scene_to_operate_on(self, xclip, args): + scene = list(self.song().scenes).index(self.song().view.selected_scene) + if type(xclip) is Live.Clip.Clip: + scene = xclip.canonical_parent.canonical_parent.playing_slot_index + if args != '': + if '"' in args: + scene_name = args[args.index('"')+1:] + if '"' in scene_name: + scene_name = scene_name[0:scene_name.index('"')] + for index in range(len(self.song().scenes)): + if scene_name == self.song().scenes[index].name.upper(): + scene = index + break + elif args == 'SEL': + scene = list(self.song().scenes).index(self.song().view.selected_scene) + else: + try: + if int(args) in range(len(self.song().scenes) + 1): + scene = int(args)-1 + except: pass + return scene + + + def set_locator(self, track, xclip, ident, args): + """ Set/delete a locator at the current playback position """ + self.song().set_or_delete_cue() + + + def do_locator_loop_action(self, track, xclip, ident, args): + """ Same as do_locator_action with name argument, but also sets arrangement loop start to pos of locator. """ + self.do_locator_action(track, xclip, ident, args, True) + + + def do_locator_action(self, track, xclip, ident, args, move_loop_too=False): + """ Jump between locators or to a particular locator. Can also move loop start to pos of locator if specified. """ + args = args.strip() + if args == '>' and self.song().can_jump_to_next_cue: + self.song().jump_to_next_cue() + elif args == '<' and self.song().can_jump_to_prev_cue: + self.song().jump_to_prev_cue() + else: + try: + for cp in self.song().cue_points: + if self._parent.get_name(cp.name) == args: + cp.jump() + if move_loop_too: + self.song().loop_start = cp.time + break + except: pass + + + def do_loop_action(self, track, xclip, ident, args): + """ Handle arrange loop actions """ + args = args.strip() + if args == '' or args in KEYWORDS: + self.set_loop_on_off(args) + else: + new_start = self.song().loop_start + new_length = self.song().loop_length + if args.startswith(('<', '>')): + self.move_loop_by_factor(args) + return() + elif args == 'RESET': + new_start = 0 + elif args.startswith('*'): + try: + new_length = self.song().loop_length * float(args[1:]) + except: pass + else: + try: + new_length = float(args) * ((4.0 / self.song().signature_denominator) * self.song().signature_numerator) + except: pass + self.set_new_loop_position(new_start, new_length) + + + def set_loop_on_off(self, value = None): + """ Toggles or turns on/off arrange loop """ + if value in KEYWORDS: + self.song().loop = KEYWORDS[value] + else: + self.song().loop = not(self.song().loop) + + + def move_loop_by_factor(self, args): + """ Move arrangement loop by its length or by a specified factor """ + factor = self.song().loop_length + if args == '<': + factor = -(factor) + if len(args) > 1: + factor = self._parent.get_adjustment_factor(args, True) + new_start = self.song().loop_start + factor + if new_start < 0.0: + new_start = 0.0 + self.set_new_loop_position(new_start, self.song().loop_length) + + + def set_new_loop_position(self, new_start, new_length): + """ For use with other loop actions, ensures that loop settings are within range """ + if new_start >= 0 and new_length >= 0 and new_length <= self.song().song_length: + self.song().loop_start = new_start + self.song().loop_length = new_length + + + def setup_scene_listeners(self): + """ Setup listeners for all scenes in set and check that last index is in current scene range. """ + self.remove_scene_listeners() + scenes = self.song().scenes + if not self._last_scene_index in range(len(scenes)): + self._last_scene_index = list(self.song().scenes).index(self.song().view.selected_scene) + for index in range(len(scenes)): + self._scenes_to_monitor.append(scenes[index]) + listener = lambda index = index:self.on_scene_triggered(index) + if not scenes[index].is_triggered_has_listener(listener): + scenes[index].add_is_triggered_listener(listener) + + + def remove_scene_listeners(self): + if self._scenes_to_monitor: + scenes = self._scenes_to_monitor + for index in range(len(scenes)): + if scenes[index]: + listener = lambda index = index:self.on_scene_triggered(index) + if scenes[index].is_triggered_has_listener(listener): + scenes[index].remove_is_triggered_listener(listener) + self._scenes_to_monitor = [] + +# local variables: +# tab-width: 4 diff --git a/ClyphX/ClyphXM4LBrowserInterface.py b/ClyphX/ClyphXM4LBrowserInterface.py new file mode 100644 index 0000000..f1c02c7 --- /dev/null +++ b/ClyphX/ClyphXM4LBrowserInterface.py @@ -0,0 +1,201 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +import Live +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent + +BROWSER_TAGS = ('Drums', 'Instruments', 'Audio Effects', 'MIDI Effects', 'Max for Live') + +class ClyphXM4LBrowserInterface(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = """ ClyphXM4LBrowserInterface provides access to browser data and methods for use in M4L devices. + NOTE: Lazy initialization is used, get_browser_tags method needs to be called first in order to use other methods. """ + + def __init__(self, parent): + ControlSurfaceComponent.__init__(self) + self._parent = parent + self._selected_tag = None + self._selected_device = None + self._selected_folder = None + self._selected_item = None + self._browser = {} + + + def disconnect(self): + self._parent = None + self._selected_tag = None + self._selected_device = None + self._selected_folder = None + self._selected_item = None + self._browser = None + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def load_device(self): + """ Loads the selected device if there is one. """ + if self._selected_device: + self.application().browser.load_item(self._selected_device['device']) + + + def load_item(self): + """ Loads the selected item if there is one. """ + if self._selected_item: + self.application().browser.load_item(self._selected_item) + + + def activate_hotswap(self): + """ Activates hotswap for the device selected in Live, finds the appropriate tag and device to use and returns the items for the device. """ + device = self.song().view.selected_track.view.selected_device + items = [] + if device: + if self.application().view.browse_mode: + self.application().view.toggle_browse() + if device.class_name == 'PluginDevice' or self._track_contains_browser(): + pass + else: + tag_to_use = None + device_to_use = device.class_display_name + if device.can_have_drum_pads: + tag_to_use = 'Drums' + elif device.class_display_name.startswith('Max'): + tag_to_use = 'Max for Live' + elif device.type == Live.Device.DeviceType.audio_effect: + tag_to_use = 'Audio Effects' + elif device.type == Live.Device.DeviceType.midi_effect: + tag_to_use = 'MIDI Effects' + elif device.type == Live.Device.DeviceType.instrument: + tag_to_use = 'Instruments' + if tag_to_use and device_to_use: + self.application().view.toggle_browse() + self._selected_tag = self._browser[tag_to_use] + self._selected_device = self._selected_tag['devices'][device_to_use] + items = sorted(self._selected_device['folders'].keys()) + sorted(self._selected_device['items']) + return items + + + def deactivate_hotswap(self): + """ Deactivates hotswap and closes the browser. """ + if self.application().view.browse_mode: + self.application().view.toggle_browse() + self.application().view.hide_view('Browser') + + + def select_non_folder_item(self, item_name): + """ Stores an item that is not contained within a folder. """ + self._selected_item = self._selected_device['items'][item_name] + + + def select_folder_item(self, item_name): + """ Stores an item that is contained within a folder. """ + self._selected_item = self._selected_folder[item_name] + + + def get_browser_tags(self): + """ Returns the list of browser tags. + Also, initializes browser if it hasn't already been initialized. """ + if not self._browser: + for tag in self.application().browser.tags: + if tag.name in BROWSER_TAGS: + self._browser[tag.name] = {'tag' : tag, 'devices' : self._create_devices_for_tag(tag)} + return BROWSER_TAGS + + + def get_devices_for_tag(self, tag_name): + """ Returns the list of devices for the given tag and stores the tag. """ + self._selected_tag = self._browser[tag_name] + return sorted(self._selected_tag['devices']) + + + def get_items_for_device(self, device_name): + """ Returns the list of folders and items for the given device and stores the device. """ + self._selected_device = self._selected_tag['devices'][device_name] + return sorted(self._selected_device['folders'].keys()) + sorted(self._selected_device['items']) + + + def get_items_for_folder(self, folder_name): + """ Returns the list of items in the given folder and stores the folder. """ + self._selected_folder = self._selected_device['folders'][folder_name] + return sorted(self._selected_folder) + + + def _track_contains_browser(self): + """ Returns whether or not the selected track contains the Device Browser, in which case hotswapping isn't possble. """ + for device in self.song().view.selected_track.devices: + if device and device.name == 'Device Browser': + return True + return False + + + def _create_devices_for_tag(self, tag): + """ Creates dict of devices for the given tag. Special handling is needed for M4L tag, which only contains folders, and Drums tag, which contains devices and folders. """ + device_dict = {} + if tag.name == 'Max for Live': + for child in tag.children: + if child.is_folder: + for device in child.children: + if device.is_device: + device_dict[child.name] = {'device' : device, 'items' : self._create_items_for_device(child), 'folders' : {}} + break + else: + for child in tag.children: + if child.is_device: + if tag.name == 'Drums': + device_dict[child.name] = {'device' : child, 'items' : self._create_items_for_device(tag), 'folders' : {}} + else: + device_dict[child.name] = {'device' : child, 'items' : self._create_items_for_device(child), 'folders' : self._create_folders_for_device(child)} + if len(device_dict) == 1: + device_dict[' '] = {} + return device_dict + + + def _create_items_for_device(self, device): + """ Creates dict of loadable items for the given device or folder. """ + items_dict = {} + for child in device.children: + if child.is_loadable and not child.name == 'Drum Rack': + items_dict[child.name] = child + if len(items_dict) == 1: + items_dict[' '] = {} + return items_dict + + + def _create_folders_for_device(self, device): + """ Creates dict of folders for the given device. """ + folders_dict = {} + for child in device.children: + if child.is_folder: + folders_dict[child.name + ' >'] = self._create_items_for_device(child) + return folders_dict + + +# local variables: +# tab-width: 4 \ No newline at end of file diff --git a/ClyphX/ClyphXMXTActions.py b/ClyphX/ClyphXMXTActions.py new file mode 100644 index 0000000..c71dfc2 --- /dev/null +++ b/ClyphX/ClyphXMXTActions.py @@ -0,0 +1,129 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +import Live +import _Framework.Task +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent + +MAX_CHARS = 52 +FULL_SEGMENT = 26 +FULL_SEGMENT_OFFSETS = (0, 28) + +class ClyphXMXTActions(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' Actions related to the MXT-Live control surface ' + + def __init__(self, parent): + ControlSurfaceComponent.__init__(self) + self._parent = parent + self._script = None + self._seq_comp = None + self._encoders = None + self._message_display_line = None + + + def disconnect(self): + self._script = None + self._seq_comp = None + self._encoders = None + self._message_display_line = None + self._parent = None + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + def set_script(self, mxt_script): + """ Set the MXT script to connect to and get necessary components. """ + self._script = mxt_script + if self._script and self._script._components: + self._message_display_line = self._script._display_lines[0] + for c in self._script.components: + comp_name = c.__class__.__name__ + if 'NoteSeqComponent' in comp_name: + self._seq_comp = c + elif 'EncModeSelector' in comp_name: + self._encoders = c._encoders + + + def dispatch_action(self, track, xclip, ident, action, args): + """ Dispatch action to proper action group handler. """ + if self._script: + if args.startswith('MSG'): + self._display_message(args, xclip) + elif args.startswith('ENC'): + self._handle_encoder_action(args.replace('ENC', '').strip()) + elif args.startswith('SEQ') and self._seq_comp: + self._handle_seq_action(args.replace('SEQ', '').strip(), xclip, ident) + + + def _handle_seq_action(self, args, xclip, ident): + """ Handle note actions related to the note currently being sequenced. """ + comp = self._seq_comp + clip = comp._midi_clip + if clip: + note = comp._note_lane_component._note + start = comp._position_component._start_position + end = comp._position_component._end_position + self._parent._clip_actions.do_clip_note_action(clip, None, None, '', 'NOTES' + str(note) + ' @' + str(start) + '-' + str(end) + ' ' + args) + + + def _handle_encoder_action(self, args): + """ Reset or randomize the values of the parameters the encoders are controlling. """ + if self._encoders: + randomize = args == 'RND' + for enc in self._encoders: + if enc: + p = enc.mapped_parameter() + if p and p.is_enabled and not p.is_quantized: + if randomize: + p.value = (((p.max - p.min) / 127) * Live.Application.get_random_int(0, 128)) + p.min + else: + p.value = p.default_value + + + def _display_message(self, args, xclip): + """ Temporarily displays a message in Maschine's display. """ + if self._message_display_line: + msg = args.replace('MSG', '', 1).strip() + if len(msg) > MAX_CHARS: + msg = msg[:MAX_CHARS] + num_segments = 2 if len(msg) > FULL_SEGMENT else 1 + for i in range(num_segments): + offset = FULL_SEGMENT_OFFSETS[i] + self._message_display_line.write_momentary(offset, FULL_SEGMENT, msg[offset:offset+FULL_SEGMENT], True) + self._tasks.add(_Framework.Task.sequence(_Framework.Task.delay(15), self._revert_display)) + + + def _revert_display(self, args=None): + """ Reverts the display after showing temp message. """ + self._message_display_line.revert() + +# local variables: +# tab-width: 4 \ No newline at end of file diff --git a/ClyphX/ClyphXPXTActions.py b/ClyphX/ClyphXPXTActions.py new file mode 100644 index 0000000..6effa9e --- /dev/null +++ b/ClyphX/ClyphXPXTActions.py @@ -0,0 +1,229 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +import Live +import _Framework.Task +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent + +HAS_PXT = False +try: + from _NKFW.ClipUtils import SEQ_RESOLUTIONS + from _NKFW.Scales import SCALE_TYPES + from _NKFW.ScalesComponent import EDITABLE_SCALE + HAS_PXT = True +except: pass + +UNWRITABLE_INDEXES = (17, 35, 53) +FULL_SEGMENT = 17 +FULL_SEGMENT_OFFSETS = (0, 17, 34, 51) + +class ClyphXPXTActions(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' Actions related to the PXT-Live/PXT-Live Plus control surface ' + + def __init__(self, parent): + ControlSurfaceComponent.__init__(self) + self._parent = parent + self._script = None + self._mono_seq_mode = None + self._poly_seq_mode = None + self._encoders = None + self._message_display_line = None + + + def disconnect(self): + self._script = None + self._mono_seq_mode = None + self._poly_seq_mode = None + self._encoders = None + self._message_display_line = None + self._parent = None + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def set_script(self, pxt_script): + """ Set the PXT script to connect to and get necessary components. """ + self._script = pxt_script + if self._script and self._script._components: + self._message_display_line = self._script._display_lines[2] + for c in self._script.components: + comp_name = c.__class__.__name__ + if 'MatrixModeSelector' in comp_name: + self._mono_seq_mode = c._mono_seq_mode + self._poly_seq_mode = c._poly_seq_mode + elif 'EncModeSelector' in comp_name: + self._encoders = c._encoders + + + def dispatch_action(self, track, xclip, ident, action, args): + """ Dispatch action to proper action group handler. """ + if self._script: + if args.startswith('MSG'): + self._display_message(args, xclip) + elif args.startswith('ENC'): + self._handle_encoder_action(args.replace('ENC', '').strip()) + elif args.startswith('MSEQ') and self._mono_seq_mode and self._mono_seq_mode.is_enabled(): + self._handle_mono_seq_action(args.replace('MSEQ', '').strip(), xclip, ident) + elif args.startswith('PSEQ') and self._poly_seq_mode and self._poly_seq_mode.is_enabled(): + self._handle_poly_seq_action(args.replace('PSEQ', '').strip(), xclip, ident) + + + def _handle_mono_seq_action(self, args, xclip, ident): + """ Handle note actions related to the note currently being sequenced in mono seq mode or capture mono seq mode settings. """ + comp = self._mono_seq_mode + clip = comp._midi_clip + if clip: + if args == 'CAP': + self._capture_seq_settings(xclip, ident, comp, True) + elif args.startswith('CAP'): + self._recall_seq_settings(args.replace('MSEQ', ''), comp) + else: + note = comp._note_lane_components[comp._selected_lane_index]._note + start = comp._position_component._start_position + end = comp._position_component._end_position + self._parent._clip_actions.do_clip_note_action(clip, None, None, '', 'NOTES' + str(note) + ' @' + str(start) + '-' + str(end) + ' ' + args) + + + def _handle_poly_seq_action(self, args, xclip, ident): + """ Handle note actions related to the notes currently being sequenced in poly seq mode or capture poly seq mode settings. """ + comp = self._poly_seq_mode + clip = comp._midi_clip + if clip: + if args == 'CAP': + self._capture_seq_settings(xclip, ident, comp, False) + elif args.startswith('CAP'): + self._recall_seq_settings(args.replace('PSEQ', ''), comp) + else: + start = comp._position_component._start_position + end = comp._position_component._end_position + notes = None + if 'ALL' in args: + notes = str(comp._note_lane_components[0]._note) + '-' + str(comp._note_lane_components[-1]._note) + args = args.replace('ALL', '') + else: + lane_spec = args.split()[0] + try: + lane_num = int(lane_spec) - 1 + if lane_num in range(comp._num_note_lanes): + notes = str(comp._note_lane_components[lane_num]._note) + args = args.replace(lane_spec, '') + except: pass + if notes: + start = comp._position_component._start_position + end = comp._position_component._end_position + self._parent._clip_actions.do_clip_note_action(clip, None, None, '', 'NOTES' + str(notes) + ' @' + str(start) + '-' + str(end) + ' ' + args) + + + def _capture_seq_settings(self, xclip, ident, comp, is_mono): + """ Capture the settings of the given seq comp and store them in the given xclip. """ + if type(xclip) is Live.Clip.Clip and HAS_PXT: + settings = '' + # res settings + settings += str(SEQ_RESOLUTIONS.index(comp._resolution_component._resolution)) + ' ' + # velo settings + velo_comp = comp._velocity_component + settings += str(velo_comp._fixed_velocity) + ' ' + settings += str(velo_comp._velocity_type) + ' ' + # scale settings + scl_comp = comp._scales_component + settings += str(scl_comp._scale_index) + ' ' + settings += str(scl_comp._root_note) + ' ' + settings += str(scl_comp._octave_offset) + ' ' + settings += str(scl_comp._offset_within_octave) + xclip.name = ident + ' PXT ' + ('MSEQ' if is_mono else 'PSEQ') + ' CAP ' + settings + + + def _recall_seq_settings(self, args, comp): + """ Recall the settings for the given seq comp. """ + arg_array = args.replace('CAP', '').strip().split() + if len(arg_array) >= 7: + # res settings + res_comp = comp._resolution_component + if res_comp._resolution_buttons: + res_btn = res_comp._resolution_buttons[int(arg_array[0])] + res_comp._on_resolution_button_value(127, res_btn) + # velo settings + velo_comp = comp._velocity_component + velo_comp._fixed_velocity = int(arg_array[1]) + velo_comp._velocity_type = int(arg_array[2]) + velo_comp.update() + # scale settings + scl_comp = comp._scales_component + scl_index = int(arg_array[3]) + scl_comp._scale_index = scl_index + scl_comp._root_note = int(arg_array[4]) + scl_comp._octave_offset = int(arg_array[5]) + scl_comp._offset_within_octave = int(arg_array[6]) + scl_comp._scale = EDITABLE_SCALE if scl_index == -1 else SCALE_TYPES[scl_index] + scl_comp._set_current_notes() + + + def _handle_encoder_action(self, args): + """ Reset or randomize the values of the parameters the encoders are controlling. """ + if self._encoders: + randomize = args == 'RND' + for enc in self._encoders: + if enc: + p = enc.mapped_parameter() + if p and p.is_enabled and not p.is_quantized: + if randomize: + p.value = (((p.max - p.min) / 127) * Live.Application.get_random_int(0, 128)) + p.min + else: + p.value = p.default_value + + + def _display_message(self, args, xclip): + """ Temporarily displays a message in Push's display + Uses special handling to ensure that empty display spaces aren't written to. """ + if self._message_display_line: + note_as_caps = args.replace('MSG', '', 1).strip() + note_len = len(note_as_caps) + start_index = xclip.name.upper().find(note_as_caps) + note_at_og_case = xclip.name[start_index:note_len+start_index] + for i in UNWRITABLE_INDEXES: + if len(note_at_og_case) > i and note_at_og_case[i] != ' ': + note_at_og_case = note_at_og_case[0:i] + ' ' + note_at_og_case[i:note_len] + note_len += 1 + new_len = len(note_at_og_case) + num_segments = (new_len / FULL_SEGMENT) + 1 + for i in range(num_segments): + offset = FULL_SEGMENT_OFFSETS[i] + self._message_display_line.write_momentary(offset, FULL_SEGMENT, note_at_og_case[offset:offset+FULL_SEGMENT], True) + self._tasks.add(_Framework.Task.sequence(_Framework.Task.delay(15), self._revert_display)) + + + def _revert_display(self, args=None): + """ Reverts the display after showing temp message. """ + self._message_display_line.revert() + +# local variables: +# tab-width: 4 \ No newline at end of file diff --git a/ClyphX/ClyphXPushActions.py b/ClyphX/ClyphXPushActions.py new file mode 100644 index 0000000..7291346 --- /dev/null +++ b/ClyphX/ClyphXPushActions.py @@ -0,0 +1,288 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +from __future__ import with_statement +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent +from consts import * + +UNWRITABLE_INDEXES = (17, 35, 53) + +MATRIX_MODES = {'SESSION': 'session', + 'NOTE': 'note'} +TRACK_MODES = {'STOP': 'stop', + 'SOLO': 'solo', + 'MUTE': 'mute'} +MAIN_MODES = {'VOLUME': 'volumes', + 'PAN': 'pan_sends', + 'TRACK': 'track', + 'CLIP': 'clip', + 'DEVICE': 'device'} +P2_MAIN_MODES = {'DEVICE': 'device', + 'MIX': 'mix', + 'CLIP': 'clip'} + + +class ClyphXPushActions(ControlSurfaceComponent): + """ Actions related to Push/Push2 ???? control surface script. """ + + def __init__(self, parent): + super(ClyphXPushActions, self).__init__() + self._parent = parent + self._script = None + self._ins_component = None + self._note_editor = None + self._scales_component = None + self._is_push2 = False + + def disconnect(self): + self._script = None + self._ins_component = None + self._note_editor = None + self._scales_component = None + self._parent = None + super(ClyphXPushActions, self).disconnect() + + def set_script(self, push_script, is_push2=False): + """ Set the Push script to connect to and get necessary components. """ + self._script = push_script + self._is_push2 = is_push2 + if self._script and self._script._components: + for c in self._script._components: + comp_name = c.__class__.__name__ + if comp_name == 'InstrumentComponent': + self._ins_component = c + elif comp_name.endswith('NoteEditorComponent'): + self._note_editor = c + elif comp_name == 'ScalesComponent': + self._scales_component = c + if not self._is_push2: + s_mode = self._script._scales_enabler._mode_map['enabled'].mode + if hasattr(s_mode, '_component'): + self._scales_component = s_mode._component + else: + self._scales_component = s_mode._enableable + + def handle_session_offset(self, session, last_pos, args, parser): + """ Special offset handling for use with 9.5. """ + try: + new_track = session.track_offset + new_scene = session.scene_offset + if args.strip() == 'LAST': + if last_pos: + session.set_offsets(last_pos[0], last_pos[1]) + return None + return_val = (new_track, new_scene) + new_track, args = parser('T', args, new_track, self.song().tracks) + new_scene, args = parser('S', args, new_scene, self.song().scenes) + if new_track == -1 or new_scene == -1: + return + session.set_offsets(new_track, new_scene) + return return_val + except: pass + + def get_session_offsets(self, session): + return session.track_offset, session.scene_offset + + def get_session_dimensions(self, session): + return session.num_tracks, session.num_scenes + + def dispatch_action(self, track, xclip, ident, action, args): + """ Dispatch action to proper action group handler. """ + if self._script: + if args.startswith('SCL') and self._ins_component: + self._handle_scale_action(args.replace('SCL', '').strip(), xclip, ident) + elif args.startswith('SEQ') and self._note_editor: + self._handle_sequence_action(args.replace('SEQ', '').strip()) + elif args == 'DRINS' and self.song().view.selected_track.has_midi_input: + with self._script.component_guard(): + with self._script._push_injector: + self._script._note_modes.selected_mode = 'instrument' + elif args.startswith('MSG'): + self._display_message(args, xclip) + elif args.startswith('MODE'): + self._handle_mode_selection(args.replace('MODE', '').strip()) + + def _handle_mode_selection(self, mode_name): + """ Handles switching to one of Push's mode if possible. """ + mode_component = None + mode_dict = None + if mode_name in MATRIX_MODES: + mode_component = self._script._matrix_modes + mode_dict = MATRIX_MODES + elif mode_name in TRACK_MODES: + if self._is_push2: + main = self._script._mute_solo_stop + mode = getattr(main, '_%s_button_handler' % TRACK_MODES[mode_name]) + main._mute_button_handler._unlock_mode() + main._solo_button_handler._unlock_mode() + main._stop_button_handler._unlock_mode() + mode._allow_released_immediately_action = False + mode._track_list_component.push_mode(mode._mode) + mode._lock_mode() + return + mode_component = self._script._track_modes + mode_dict = TRACK_MODES + elif ((not self._is_push2 and mode_name in MAIN_MODES) + or (self._is_push2 and mode_name in P2_MAIN_MODES)): + mode_component = self._script._main_modes + mode_dict = P2_MAIN_MODES if self._is_push2 else MAIN_MODES + if mode_component and mode_dict: + if mode_component.selected_mode != mode_dict[mode_name]: + with self._script.component_guard(): + with self._script._push_injector: + mode_component.selected_mode = mode_dict[mode_name] + + def _display_message(self, args, xclip): + """ Temporarily displays a message in Push's display. Uses special handling to + ensure that empty display spaces aren't written to. """ + note_as_caps = args.replace('MSG', '', 1).strip() + note_len = len(note_as_caps) + start_index = xclip.name.upper().find(note_as_caps) + note_at_og_case = xclip.name[start_index:note_len+start_index] + if not self._is_push2: + for i in UNWRITABLE_INDEXES: + if len(note_at_og_case) > i and note_at_og_case[i] != ' ': + note_at_og_case = note_at_og_case[0:i] + ' ' + note_at_og_case[i:note_len] + note_len += 1 + self._script.show_notification(note_at_og_case) + + def _handle_scale_action(self, args, xclip, ident): + """ Handles actions related to scale settings. """ + if args: + arg_array = args.split() + array_len = len(arg_array) + if arg_array[0] == 'INKEY': + self._handle_in_key(arg_array) + elif arg_array[0] == 'FIXED': + self._handle_fixed(arg_array) + elif arg_array[0] == 'ROOT' and array_len == 2: + self._handle_root_note(arg_array) + elif arg_array[0] == 'TYPE' and array_len >= 2: + self._handle_scale_type(arg_array, args) + elif arg_array[0] == 'OCT' and array_len >= 2 and arg_array[1] in ('<', '>'): + self._handle_octave(arg_array) + elif array_len == 6: + self._recall_scale_settings(arg_array) + self._update_scale_display_and_buttons() + else: + self._capture_scale_settings(xclip, ident) + + def _handle_in_key(self, arg_array): + if len(arg_array) == 2 and arg_array[1] in KEYWORDS: + self._ins_component._note_layout.is_in_key = KEYWORDS[arg_array[1]] + else: + self._ins_component._note_layout.is_in_key =\ + not self._ins_component._note_layout.is_in_key + + def _handle_fixed(self, arg_array): + if len(arg_array) == 2 and arg_array[1] in KEYWORDS: + self._ins_component._note_layout.is_fixed = KEYWORDS[arg_array[1]] + else: + self._ins_component._note_layout.is_fixed =\ + not self._ins_component._note_layout.is_fixed + + def _handle_root_note(self, arg_array): + if arg_array[1] in NOTE_NAMES: + self._ins_component._note_layout.root_note = NOTE_NAMES.index(arg_array[1]) + elif arg_array[1] in ('<', '>'): + new_root = (self._parent.get_adjustment_factor(arg_array[1]) + + self._ins_component._note_layout.root_note) + if new_root in range(12): + self._ins_component._note_layout.root_note = new_root + + def _handle_octave(self, arg_array): + if arg_array[1] == '<': + self._ins_component._slider.scroll_page_down() + else: + self._ins_component._slider.scroll_page_up() + + def _handle_scale_type(self, arg_array, args): + if arg_array[1] in ('<', '>'): + factor = self._parent.get_adjustment_factor(arg_array[1]) + if self._is_push2: + idx = self._scales_component.selected_scale_index + factor + self._scales_component._set_selected_scale_index(idx) + else: + scale_list = self._scales_component._scale_list.scrollable_list + if factor < 0: + for index in range(abs(factor)): + scale_list.scroll_up() + else: + for index in range(factor): + scale_list.scroll_down() + else: + scale_type = args.replace('TYPE', '').strip() + if self._is_push2: + for i, s in enumerate(self._scales_component.scale_names): + if s.upper() == scale_type: + self._scales_component._set_selected_scale_index(i) + break + else: + scale_list = self._scales_component._scale_list.scrollable_list + for index in range(len(scale_list.items)): + modus = scale_list.items[index] + if modus.content.name.upper() == scale_type: + scale_list._set_selected_item_index(index) + break + + def _capture_scale_settings(self, xclip, ident): + """ Captures scale settings and writes them to X-Clip's name. """ + if type(xclip) is Live.Clip.Clip: + root = str(self._ins_component._note_layout.root_note) + if self._is_push2: + scl_type = self._scales_component.selected_scale_index + else: + scl_type = str(self._scales_component._scale_list.scrollable_list + .selected_item_index) + octave = '0' + fixed = str(self._ins_component._note_layout.is_fixed) + inkey = str(self._ins_component._note_layout.is_in_key) + orient = '0' + xclip.name = '%s Push SCL %s %s %s %s %s %s' % (ident, root, scl_type, octave, + fixed, inkey, orient) + + def _recall_scale_settings(self, arg_array): + """ Recalls scale settings from X-Trigger name. """ + try: + self._ins_component._note_layout.root_note = int(arg_array[0]) + if self._is_push2: + self._scales_component._set_selected_scale_index(int(arg_array[1])) + else: + self._scales_component._scale_list.scrollable_list.selected_item_index =\ + int(arg_array[1]) + self._ins_component._note_layout.is_fixed = arg_array[3] == 'TRUE' + self._ins_component._note_layout.is_in_key = arg_array[4] == 'TRUE' + except: pass + + def _update_scale_display_and_buttons(self): + """ Updates Push's scale display and buttons to indicate current settings. """ + if not self._is_push2: + self._scales_component._update_data_sources() + self._scales_component.update() + + def _handle_sequence_action(self, args): + """ Handle note actions related to the note currently being sequenced. """ + c = self.song().view.detail_clip + clip = c if c and c.is_midi_clip else None + note = self._script._drum_component.selected_note + if clip and note != None: + self._parent._clip_actions.do_clip_note_action(clip, None, None, '', + 'NOTES%s %s' % (note, args)) diff --git a/ClyphX/ClyphXSnapActions.py b/ClyphX/ClyphXSnapActions.py new file mode 100644 index 0000000..5bcab20 --- /dev/null +++ b/ClyphX/ClyphXSnapActions.py @@ -0,0 +1,422 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +import Live +import math +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent +from consts import * +if IS_LIVE_9: + from functools import partial + +class ClyphXSnapActions(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' Snapshot-related actions ' + + def __init__(self, parent): + ControlSurfaceComponent.__init__(self) + self._parent = parent + self._current_tracks = {} + self._parameters_to_smooth = {} + self._rack_parameters_to_smooth = {} + self._smoothing_active = False + self._synced_smoothing_active = False + self._rack_smoothing_active = False + self._smoothing_speed = 7 + self._smoothing_count = 0 + self._last_beat = -1 + self._control_rack = None + self._snap_id = None + self._is_control_track = False + self._include_nested_devices = False + self._parameter_limit = 500 + self._register_timer_callback(self.on_timer) + self._has_timer = True + self.song().add_current_song_time_listener(self.on_time_changed) + self.song().add_is_playing_listener(self.on_time_changed) + + + def disconnect(self): + if self._has_timer: + self._unregister_timer_callback(self.on_timer) + self.remove_control_rack() + self.remove_track_listeners() + self.song().remove_current_song_time_listener(self.on_time_changed) + self.song().remove_is_playing_listener(self.on_time_changed) + self._current_tracks = {} + self._parameters_to_smooth = {} + self._rack_parameters_to_smooth = {} + self._control_rack = None + self._snap_id = None + self._parent = None + if IS_LIVE_9: + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def store_track_snapshot(self, track_list, xclip, ident, action, args): + """ Store snapshot of track params """ + param_count = 0 + if not type(xclip) is Live.Clip.Clip: + return() + snap_data = {} + if track_list: + for track in track_list: + track_name = self._parent.get_name(track.name) + if not track_name.startswith('CLYPHX SNAP') and not snap_data.has_key(track.name): + track_data = [[], [], None, {}] + if args == '' or 'MIX' in args: + if not 'MIXS' in args: + mix_vals = [track.mixer_device.volume.value, track.mixer_device.panning.value] + else: + mix_vals = [-1, -1] + if not 'MIX-' in args: + mix_vals.extend([s.value for s in track.mixer_device.sends]) + param_count += len(mix_vals) + track_data[0] = mix_vals + if ('MIX+' in args or 'MIX-' in args) and track != self.song().master_track: + track_data[1] = [int(track.mute), int(track.solo), track.mixer_device.crossfade_assign] + param_count += 3 + if 'PLAY' in args and track in self.song().tracks: + track_data[2] = track.playing_slot_index + param_count += 1 + if (args == '' or 'DEV' in args) and track.devices: + dev_range = self.get_snap_device_range(args, track) + if dev_range: + track_devices = {} + for dev_index in range (dev_range[0], dev_range[1]): + if dev_index < (len(track.devices)): + current_device = track.devices[dev_index] + if not track_devices.has_key(current_device.name): + track_devices[current_device.name] = [[p.value for p in current_device.parameters], []] + param_count += len(current_device.parameters) + if self._include_nested_devices and self._parent._can_have_nested_devices and current_device.can_have_chains: + nested_devices = self.get_nested_devices(current_device, [], 0) + if nested_devices: + track_devices[current_device.name][1] = nested_devices[0] + param_count += nested_devices[1] + if track_devices: + track_data[3] = track_devices + snap_data[track.name] = track_data + if snap_data: + if param_count <= self._parameter_limit: + xclip.name = str(ident) + ' || ' + repr(snap_data) + else: + current_name = xclip.name + xclip.name = 'Too many parameters to store!' + if IS_LIVE_9: + self._parent.schedule_message(8, partial(self.refresh_xclip_name, (xclip, current_name))) + else: + self._parent.schedule_message(8, self.refresh_xclip_name, (xclip, current_name)) + + + def refresh_xclip_name(self, xclip_data): + """ Refreshes xclip's previous name in cases where a snap is asking to store too many params """ + xclip_data[0].name = xclip_data[1] + + + def get_nested_devices(self, rack, nested_list, parameter_count): + """ Get list of nested devices and count of parameters """ + if rack.chains: + for c in rack.chains: + for d in c.devices: + new_device_entry = [[p.value for p in d.parameters], []] + parameter_count += len(d.parameters) + if not rack.class_name.startswith('Midi') and list(c.devices).index(d) == 0: + new_device_entry[1] = [c.mixer_device.volume.value, c.mixer_device.panning.value, c.mixer_device.chain_activator.value] + parameter_count += 3 + sends = c.mixer_device.sends + if sends: + for s in sends: + new_device_entry[1].append(s.value) + parameter_count += len(sends) + nested_list.append(new_device_entry) + if d.can_have_chains and d.chains: + self.get_nested_devices(d, nested_list, parameter_count) + return [nested_list, parameter_count] + + + def recall_track_snapshot(self, name, xclip): + """ Recall snapshot of track params """ + self._smoothing_count = 0 + self._snap_id = xclip.name[xclip.name.index('['):xclip.name.index(']')+1].strip().upper() + track_name = self._parent.get_name(xclip.canonical_parent.canonical_parent.name) + snap_data = eval(str(xclip.name)[len(self._snap_id) + 3:]) + self._smoothing_active = False + self._rack_smoothing_active = False + self._synced_smoothing_active = False + self._parameters_to_smooth = {} + self._rack_parameters_to_smooth = {} + self._is_control_track = track_name.startswith('CLYPHX SNAP') + is_synced = False + if self._is_control_track: + self.setup_control_rack(xclip.canonical_parent.canonical_parent) + self._smoothing_speed = 8 + new_speed = 8 + if 'SP:' in self._snap_id: + speed = self._snap_id[self._snap_id.index(':')+1:self._snap_id.index(']')].strip() + is_synced = 'S' in speed + try: new_speed = int(speed.replace('S', '')) + except: new_speed = 8 + else: + if '[' and ']' in track_name: + speed = track_name[track_name.index('[')+1:track_name.index(']')].strip() + is_synced = 'S' in speed + try: new_speed = int(speed.replace('S', '')) + except: new_speed = 8 + if is_synced: + new_speed *= self.song().signature_numerator + if new_speed in range(501): + self._smoothing_speed = new_speed + for track, param_data in snap_data.items(): + if self._current_tracks.has_key(track): + track = self._current_tracks[track] + if param_data[0]: + if track.mixer_device.volume.is_enabled and param_data[0][0] != -1: + self.get_parameter_data_to_smooth(track.mixer_device.volume, param_data[0][0]) + if track.mixer_device.panning.is_enabled and param_data[0][1] != -1: + self.get_parameter_data_to_smooth(track.mixer_device.panning, param_data[0][1]) + if track is not self.song().master_track: + for index in range (len(param_data[0])-2): + if index <= len(track.mixer_device.sends)-1 and track.mixer_device.sends[index].is_enabled: + self.get_parameter_data_to_smooth(track.mixer_device.sends[index], param_data[0][2+index]) + if param_data[1] and track is not self.song().master_track: + track.mute = param_data[1][0] + track.solo = param_data[1][1] + track.mixer_device.crossfade_assign = param_data[1][2] + if param_data[2] != None and not track.is_foldable and track is not self.song().master_track: + if param_data[2] < 0: + track.stop_all_clips() + else: + if track.clip_slots[param_data[2]].has_clip and track.clip_slots[param_data[2]].clip != xclip: + track.clip_slots[param_data[2]].fire() + if param_data[3]: + for device in track.devices: + if param_data[3].has_key(device.name): + self.recall_device_snap(device, param_data[3][device.name][0]) + if self._include_nested_devices and self._parent._can_have_nested_devices and device.can_have_chains and param_data[3][device.name][1]: + self.recall_nested_device_snap(device, param_data[3][device.name][1]) + del param_data[3][device.name] + if self._is_control_track and self._parameters_to_smooth: + if not self._control_rack or (self._control_rack and not self._control_rack.parameters[0].value == 1.0): + self._smoothing_active = not is_synced + self._synced_smoothing_active = is_synced + else: + self._parent.schedule_message(1, self.refresh_control_rack) + + + def recall_device_snap(self, device, device_data): + """ Recall device snap """ + if device and len(device.parameters) == len(device_data): + for index in range (len(device.parameters)): + if device.parameters[index].is_enabled: + self.get_parameter_data_to_smooth(device.parameters[index], device_data[index]) + + + def recall_nested_device_snap(self, rack, device_data): + """ Recall snaps of nested devices """ + if rack.chains and device_data: + for c in rack.chains: + combined_data = zip(c.devices, device_data) + if combined_data: + for cd in combined_data: + device_data.remove(cd[1]) + self.recall_device_snap(cd[0], cd[1][0]) + if not cd[0].class_name.startswith('Midi') and cd[1][1]: + if c.mixer_device.volume.is_enabled: + self.get_parameter_data_to_smooth(c.mixer_device.volume, cd[1][1][0]) + if c.mixer_device.panning.is_enabled: + self.get_parameter_data_to_smooth(c.mixer_device.panning, cd[1][1][1]) + if c.mixer_device.chain_activator.is_enabled: + self.get_parameter_data_to_smooth(c.mixer_device.chain_activator, cd[1][1][2]) + sends = c.mixer_device.sends + if sends: + for i in range(len(cd[1][1]) - 3): + if i < len(sends) and sends[i].is_enabled: + self.get_parameter_data_to_smooth(sends[i], cd[1][1][3 + i]) + if cd[0].can_have_chains: + self.recall_nested_device_snap(cd[0], device_data) + + + def setup_control_rack(self, track): + """ Setup rack to use for morphing between current vals and snapped vals """ + self.remove_control_rack() + for dev in track.devices: + dev_name = self._parent.get_name(dev.name) + if dev.class_name.endswith('GroupDevice') and dev_name.startswith('CLYPHX SNAP'): + self._control_rack = dev + break + + + def refresh_control_rack(self): + """ Refresh rack name and macro value on snap triggered. If triggered when rack off, clear snap id from rack name """ + if self._control_rack and self._snap_id: + if self._control_rack.parameters[0].value == 1.0: + self._control_rack.name = 'ClyphX Snap ' + str(self._snap_id) + self._control_rack.parameters[1].value = 0.0 + self._rack_smoothing_active = True + if not self._control_rack.parameters[1].value_has_listener(self.control_rack_macro_changed): + self._control_rack.parameters[1].add_value_listener(self.control_rack_macro_changed) + else: + self._control_rack.name = 'ClyphX Snap' + + + def control_rack_macro_changed(self): + """ Get param values to set based on macro value and build dict """ + if self._rack_smoothing_active and self._parameters_to_smooth and self._control_rack.parameters[0].value == 1.0: + self._rack_parameters_to_smooth = {} + macro_value = self._control_rack.parameters[1].value + new_dict = {} + for p, v in self._parameters_to_smooth.items(): + param_value = v[2] + (macro_value * v[0]) + if p.is_quantized: + if macro_value < 63 and p.value != v[2]: + param_value = v[2] + elif macro_value > 63 and p.value != v[1]: + param_value = v[1] + else: + param_value = None + if param_value != None: + new_dict[p] = param_value + self._rack_parameters_to_smooth = new_dict + + + def on_timer(self): + """ Smooth parameter value changes via timer """ + if self._smoothing_active and self._parameters_to_smooth: + self.apply_timed_smoothing() + if self._rack_smoothing_active and self._rack_parameters_to_smooth: + for p, v in self._rack_parameters_to_smooth.items(): + p.value = v + del self._rack_parameters_to_smooth[p] + + + def on_time_changed(self): + """ Smooth parameter value changes synced to playback """ + if self._synced_smoothing_active and self._parameters_to_smooth and self.song().is_playing: + time = int(str(self.song().get_current_beats_song_time()).split('.')[2]) + if self._last_beat != time: + self._last_beat = time + self._tasks.add(self.apply_timed_smoothing) + + + def apply_timed_smoothing(self, arg=None): + """ Apply smoothing for either timer or sync """ + self._smoothing_count += 1 + for p, v in self._parameters_to_smooth.items(): + param_value = v[2] + (self._smoothing_count * v[0]) + if p.is_quantized: + p.value = v[1] + del self._parameters_to_smooth[p] + elif param_value == v[1] or self._smoothing_count >= self._smoothing_speed: + del self._parameters_to_smooth[p] + p.value = v[1] + else: + p.value = param_value + + + def get_parameter_data_to_smooth(self, parameter, new_value): + """ Get parameter data to smooth and return list of smoothing value, target value and current value """ + factor = self._smoothing_speed + if self._is_control_track and self._control_rack and self._control_rack.parameters[0].value == 1.0: + factor = 127 + if factor and self._is_control_track: + difference = new_value - parameter.value + if difference and (factor == 127 or (factor != 127 and abs(difference) > 0.01)): + if parameter.is_quantized: + factor = 1 + param_data = [(new_value - parameter.value) / factor, new_value, parameter.value] + if difference < 0.0: + param_data = [((parameter.value - new_value) / factor) * -1, new_value, parameter.value] + self._parameters_to_smooth[parameter] = param_data + else: + parameter.value = new_value + else: + parameter.value = new_value + + + def get_snap_device_range(self, args, track): + """ Get range of devices to snapshot """ + dev_args = args.replace('MIX', '') + dev_args = dev_args.replace('PLAY', '') + dev_args = dev_args.replace('DEV', '') + dev_args = dev_args.replace('IO', '') + start = 0 + end = start + 1 + if dev_args: + if 'ALL' in dev_args: + start = 0 + end = len(track.devices) + elif '-' in dev_args: + try: + name_split = dev_args.split('-') + start = int(name_split[0].strip()) - 1 + end = int(name_split[1].strip()) + except: pass + else: + try: + start = int(dev_args) - 1 + end = start + 1 + except: pass + if start > len(track.devices) or start < 0 or end > len(track.devices) or end < start: + return() + return (start, end) + + + def setup_tracks(self): + """ Store dictionary of tracks by name """ + self._current_tracks = {} + self.remove_track_listeners() + for track in (tuple(self.song().tracks) + tuple(self.song().return_tracks) + (self.song().master_track,)): + if not track.name_has_listener(self.setup_tracks): + track.add_name_listener(self.setup_tracks) + name = self._parent.get_name(track.name) + if not self._current_tracks.has_key(track.name) and not name.startswith('CLYPHX SNAP'): + self._current_tracks[track.name] = track + + + def remove_control_rack(self): + """ Remove control rack listeners """ + if self._control_rack: + self._control_rack.name = 'ClyphX Snap' + if self._control_rack.parameters[1].value_has_listener(self.control_rack_macro_changed): + self._control_rack.parameters[1].remove_value_listener(self.control_rack_macro_changed) + self._control_rack = None + + + def remove_track_listeners(self): + """ Remove track name listeners """ + for track in (tuple(self.song().tracks) + tuple(self.song().return_tracks) + (self.song().master_track,)): + if track.name_has_listener(self.setup_tracks): + track.remove_name_listener(self.setup_tracks) + + +# local variables: +# tab-width: 4 \ No newline at end of file diff --git a/ClyphX/ClyphXSnapActions9.py b/ClyphX/ClyphXSnapActions9.py new file mode 100644 index 0000000..8da6dbd --- /dev/null +++ b/ClyphX/ClyphXSnapActions9.py @@ -0,0 +1,484 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +import Live +import math +import pickle +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent +from consts import * +if IS_LIVE_9: + from functools import partial + +""" The positions of the main categories in the snap data array. """ +MIX_STD_SETTINGS_POS = 0 +MIX_EXT_SETTINGS_POS = 1 +PLAY_SETTINGS_POS = 2 +DEVICE_SETTINGS_POS = 3 + +""" The positions of standard mix settings within the associated array. """ +MIX_VOL_POS = 0 +MIX_PAN_POS = 1 +MIX_SEND_START_POS = 2 + +""" The positions of extended mix settings within the associated array. """ +MIX_MUTE_POS = 0 +MIX_SOLO_POS = 1 +MIX_CF_POS = 2 + +""" The positions of chain mix settings within the associated array. """ +CHAIN_VOL_POS = 0 +CHAIN_PAN_POS = 1 +CHAIN_MUTE_POS = 2 +CHAIN_SEND_START_POS = 3 + +class ClyphXSnapActions(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' Snapshot-related actions ' + + def __init__(self, parent): + ControlSurfaceComponent.__init__(self) + self._parent = parent + self._current_tracks = {} + self._parameters_to_smooth = {} + self._rack_parameters_to_smooth = {} + self._smoothing_active = False + self._synced_smoothing_active = False + self._rack_smoothing_active = False + self._smoothing_speed = 7 + self._smoothing_count = 0 + self._last_beat = -1 + self._control_rack = None + self._snap_id = None + self._is_control_track = False + self._include_nested_devices = False + self._parameter_limit = 500 + self._register_timer_callback(self._on_timer) + self._has_timer = True + self.song().add_current_song_time_listener(self._on_time_changed) + self.song().add_is_playing_listener(self._on_time_changed) + + + def disconnect(self): + if self._has_timer: + self._unregister_timer_callback(self._on_timer) + self._remove_control_rack() + self._remove_track_listeners() + self.song().remove_current_song_time_listener(self._on_time_changed) + self.song().remove_is_playing_listener(self._on_time_changed) + self._current_tracks = {} + self._parameters_to_smooth = {} + self._rack_parameters_to_smooth = {} + self._control_rack = None + self._snap_id = None + self._parent = None + if IS_LIVE_9: + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def store_track_snapshot(self, track_list, xclip, ident, action, args, force=False): + """ Stores snapshot of track params """ + param_count = 0 + if not type(xclip) is Live.Clip.Clip and not force: + return() + snap_data = {} + if track_list: + for track in track_list: + track_name = self._parent.get_name(track.name) + if not track_name.startswith('CLYPHX SNAP') and not snap_data.has_key(track.name): + self._current_track_data = [[], [], None, {}] + if args == '' or 'MIX' in args: + param_count += self._store_mix_settings(track, args) + if 'PLAY' in args and track in self.song().tracks: + self._current_track_data[PLAY_SETTINGS_POS] = track.playing_slot_index + param_count += 1 + if (args == '' or 'DEV' in args) and track.devices: + param_count += self._store_device_settings(track, args) + snap_data[track.name] = self._current_track_data + if snap_data: + if param_count <= self._parameter_limit: + xclip.name = str(ident) + ' || ' + pickle.dumps(snap_data) + else: + current_name = xclip.name + xclip.name = 'Too many parameters to store!' + if IS_LIVE_9: + self._parent.schedule_message(8, partial(self._refresh_xclip_name, (xclip, current_name))) + else: + self._parent.schedule_message(8, self._refresh_xclip_name, (xclip, current_name)) + + + def _store_mix_settings(self, track, args): + """ Stores mixer related settings and returns the number of parameters that were stored. """ + param_count = 0 + if not 'MIXS' in args: + mix_vals = [track.mixer_device.volume.value, track.mixer_device.panning.value] + else: + mix_vals = [-1, -1] + if not 'MIX-' in args: + mix_vals.extend([s.value for s in track.mixer_device.sends]) + param_count += len(mix_vals) + self._current_track_data[MIX_STD_SETTINGS_POS] = mix_vals + if ('MIX+' in args or 'MIX-' in args) and track != self.song().master_track: + self._current_track_data[MIX_EXT_SETTINGS_POS] = [int(track.mute), int(track.solo), track.mixer_device.crossfade_assign] + param_count += 3 + return param_count + + + def _store_device_settings(self, track, args): + """ Stores device related settings and returns the number of parameters that were stored. """ + param_count = 0 + dev_range = self._get_snap_device_range(args, track) + if dev_range: + track_devices = {} + for dev_index in range (dev_range[0], dev_range[1]): + if dev_index < (len(track.devices)): + current_device = track.devices[dev_index] + if not track_devices.has_key(current_device.name): + track_devices[current_device.name] = {'params' : [p.value for p in current_device.parameters]} + param_count += len(current_device.parameters) + if self._include_nested_devices and self._parent._can_have_nested_devices and current_device.can_have_chains: + param_count += self._get_nested_devices(current_device, track_devices[current_device.name], 0) + if track_devices: + self._current_track_data[DEVICE_SETTINGS_POS] = track_devices + return param_count + + + def _get_nested_devices(self, rack, nested_devs, parameter_count): + """ Creates recursive dict of nested devices and returns count of parameters """ + if rack.chains: + nested_devs['chains'] = {} + for chain_index, c in enumerate(rack.chains): + nested_devs['chains'][chain_index] = {'devices' : {}} + for device_index, d in enumerate(c.devices): + nested_devs['chains'][chain_index]['devices'][device_index] = {'params' : [p.value for p in d.parameters]} + parameter_count += len(d.parameters) + if not rack.class_name.startswith('Midi'): + mix_settings = [c.mixer_device.volume.value, c.mixer_device.panning.value, c.mixer_device.chain_activator.value] + parameter_count += 3 + sends = c.mixer_device.sends + if sends: + for s in sends: + mix_settings.append(s.value) + parameter_count += len(sends) + nested_devs['chains'][chain_index]['mixer'] = mix_settings + if d.can_have_chains and d.chains: + self._get_nested_devices(d, nested_devs['chains'][chain_index]['devices'][device_index], parameter_count) + return parameter_count + + + def recall_track_snapshot(self, name, xclip, disable_smooth=False): + """ Recalls snapshot of track params """ + self._snap_id = xclip.name[xclip.name.index('['):xclip.name.index(']')+1].strip().upper() + snap_data = pickle.loads(str(xclip.name)[len(self._snap_id) + 4:]) + self._parameters_to_smooth = {} + self._rack_parameters_to_smooth = {} + is_synced = False if disable_smooth else self._init_smoothing(xclip) + for track, param_data in snap_data.items(): + if self._current_tracks.has_key(track): + track = self._current_tracks[track] + self._recall_mix_settings(track, param_data) + if param_data[PLAY_SETTINGS_POS] != None and not track.is_foldable and track is not self.song().master_track: + if param_data[PLAY_SETTINGS_POS] < 0: + track.stop_all_clips() + else: + if track.clip_slots[param_data[PLAY_SETTINGS_POS]].has_clip and track.clip_slots[param_data[PLAY_SETTINGS_POS]].clip != xclip: + track.clip_slots[param_data[PLAY_SETTINGS_POS]].fire() + if param_data[DEVICE_SETTINGS_POS]: + self._recall_device_settings(track, param_data) + if self._is_control_track and self._parameters_to_smooth: + if not self._control_rack or (self._control_rack and not self._control_rack.parameters[0].value == 1.0): + self._smoothing_active = not is_synced + self._synced_smoothing_active = is_synced + else: + self._parent.schedule_message(1, self._refresh_control_rack) + + + def _recall_mix_settings(self, track, param_data): + """ Recalls mixer related settings. """ + if param_data[MIX_STD_SETTINGS_POS]: + pan_value = param_data[MIX_STD_SETTINGS_POS][MIX_PAN_POS] + if track.mixer_device.volume.is_enabled and param_data[MIX_STD_SETTINGS_POS][MIX_VOL_POS] != -1: + self._get_parameter_data_to_smooth(track.mixer_device.volume, param_data[MIX_STD_SETTINGS_POS][MIX_VOL_POS]) + if track.mixer_device.panning.is_enabled and not isinstance(pan_value, int): + self._get_parameter_data_to_smooth(track.mixer_device.panning, param_data[MIX_STD_SETTINGS_POS][MIX_PAN_POS]) + if track is not self.song().master_track: + for index in range (len(param_data[MIX_STD_SETTINGS_POS])-MIX_SEND_START_POS): + if index <= len(track.mixer_device.sends)-1 and track.mixer_device.sends[index].is_enabled: + self._get_parameter_data_to_smooth(track.mixer_device.sends[index], param_data[MIX_STD_SETTINGS_POS][MIX_SEND_START_POS+index]) + if param_data[1] and track is not self.song().master_track: + track.mute = param_data[MIX_EXT_SETTINGS_POS][MIX_MUTE_POS] + track.solo = param_data[MIX_EXT_SETTINGS_POS][MIX_SOLO_POS] + track.mixer_device.crossfade_assign = param_data[MIX_EXT_SETTINGS_POS][MIX_CF_POS] + + + def _recall_device_settings(self, track, param_data): + """ Recalls device related settings. """ + for device in track.devices: + if param_data[DEVICE_SETTINGS_POS].has_key(device.name): + self._recall_device_snap(device, param_data[DEVICE_SETTINGS_POS][device.name]['params']) + if self._include_nested_devices and self._parent._can_have_nested_devices and device.can_have_chains and param_data[DEVICE_SETTINGS_POS][device.name].has_key('chains'): + self._recall_nested_device_snap(device, param_data[DEVICE_SETTINGS_POS][device.name]['chains']) + del param_data[DEVICE_SETTINGS_POS][device.name] + + + def _recall_device_snap(self, device, stored_params): + """ Recalls the settings of a single device """ + if device and len(device.parameters) == len(stored_params): + for index, param in enumerate(device.parameters): + if param.is_enabled: + self._get_parameter_data_to_smooth(param, stored_params[index]) + + + def _recall_nested_device_snap(self, rack, stored_params): + """ Recalls the settings and mixer settings of nested devices """ + if rack.chains and stored_params: + num_chains = len(rack.chains) + for chain_key in stored_params.keys(): + if chain_key < num_chains: + chain = rack.chains[chain_key] + chain_devices = chain.devices + num_chain_devices = len(chain_devices) + stored_chain = stored_params[chain_key] + stored_devices = stored_chain['devices'] + for device_key in stored_devices.keys(): + if device_key < num_chain_devices: + self._recall_device_snap(chain_devices[device_key], stored_devices[device_key]['params']) + if chain_devices[device_key].can_have_chains and stored_devices[device_key].has_key('chains'): + self._recall_nested_device_snap(chain_devices[device_key], stored_devices[device_key]['chains']) + if not rack.class_name.startswith('Midi') and stored_chain.has_key('mixer'): + if chain.mixer_device.volume.is_enabled: + self._get_parameter_data_to_smooth(chain.mixer_device.volume, stored_chain['mixer'][CHAIN_VOL_POS]) + if chain.mixer_device.panning.is_enabled: + self._get_parameter_data_to_smooth(chain.mixer_device.panning, stored_chain['mixer'][CHAIN_PAN_POS]) + if chain.mixer_device.chain_activator.is_enabled: + self._get_parameter_data_to_smooth(chain.mixer_device.chain_activator, stored_chain['mixer'][CHAIN_MUTE_POS]) + sends = chain.mixer_device.sends + if sends: + num_sends = len(sends) + for i in range(len(stored_chain['mixer']) - CHAIN_SEND_START_POS): + if i < num_sends and sends[i].is_enabled: + self._get_parameter_data_to_smooth(sends[i], stored_chain['mixer'][CHAIN_SEND_START_POS + i]) + + + def _init_smoothing(self, xclip): + """ Initializes smoothing and returns whether or not smoothing is synced to tempo or not. """ + self._smoothing_count = 0 + self._smoothing_active = False + self._rack_smoothing_active = False + self._synced_smoothing_active = False + is_synced = False + track_name = self._parent.get_name(xclip.canonical_parent.canonical_parent.name) + self._is_control_track = track_name.startswith('CLYPHX SNAP') + if self._is_control_track: + self._setup_control_rack(xclip.canonical_parent.canonical_parent) + self._smoothing_speed = 8 + new_speed = 8 + if 'SP:' in self._snap_id: + speed = self._snap_id[self._snap_id.index(':')+1:self._snap_id.index(']')].strip() + is_synced = 'S' in speed + try: new_speed = int(speed.replace('S', '')) + except: new_speed = 8 + else: + if '[' and ']' in track_name: + speed = track_name[track_name.index('[')+1:track_name.index(']')].strip() + is_synced = 'S' in speed + try: new_speed = int(speed.replace('S', '')) + except: new_speed = 8 + if is_synced: + new_speed *= self.song().signature_numerator + if new_speed in range(501): + self._smoothing_speed = new_speed + return is_synced + + + def _setup_control_rack(self, track): + """ Sets up rack to use for morphing between current vals and snapped vals """ + self._remove_control_rack() + for dev in track.devices: + dev_name = self._parent.get_name(dev.name) + if dev.class_name.endswith('GroupDevice') and dev_name.startswith('CLYPHX SNAP'): + self._control_rack = dev + break + + + def _refresh_control_rack(self): + """ Refreshes rack name and macro value on snap triggered. If triggered when rack off, clear snap id from rack name """ + if self._control_rack and self._snap_id: + if self._control_rack.parameters[0].value == 1.0: + self._control_rack.name = 'ClyphX Snap ' + str(self._snap_id) + self._control_rack.parameters[1].value = 0.0 + self._rack_smoothing_active = True + if not self._control_rack.parameters[1].value_has_listener(self._control_rack_macro_changed): + self._control_rack.parameters[1].add_value_listener(self._control_rack_macro_changed) + else: + self._control_rack.name = 'ClyphX Snap' + + + def _control_rack_macro_changed(self): + """ Returns param values to set based on macro value and build dict """ + if self._rack_smoothing_active and self._parameters_to_smooth and self._control_rack.parameters[0].value == 1.0: + self._rack_parameters_to_smooth = {} + macro_value = self._control_rack.parameters[1].value + new_dict = {} + for p, v in self._parameters_to_smooth.items(): + param_value = v[2] + (macro_value * v[0]) + if p.is_quantized: + if macro_value < 63 and p.value != v[2]: + param_value = v[2] + elif macro_value > 63 and p.value != v[1]: + param_value = v[1] + else: + param_value = None + if param_value != None: + new_dict[p] = param_value + self._rack_parameters_to_smooth = new_dict + + + def _on_timer(self): + """ Smoothes parameter value changes via timer """ + if self._smoothing_active and self._parameters_to_smooth: + self._apply_timed_smoothing() + if self._rack_smoothing_active and self._rack_parameters_to_smooth: + for p, v in self._rack_parameters_to_smooth.items(): + p.value = v + del self._rack_parameters_to_smooth[p] + + + def _on_time_changed(self): + """ Smoothes parameter value changes synced to playback """ + if self._synced_smoothing_active and self._parameters_to_smooth and self.song().is_playing: + time = int(str(self.song().get_current_beats_song_time()).split('.')[2]) + if self._last_beat != time: + self._last_beat = time + if IS_LIVE_9: + self._tasks.add(self._apply_timed_smoothing) + else: + self._parent.schedule_message(1, self._apply_timed_smoothing) + + + def _apply_timed_smoothing(self, arg=None): + """ Applies smoothing for either timer or sync """ + self._smoothing_count += 1 + for p, v in self._parameters_to_smooth.items(): + param_value = v[2] + (self._smoothing_count * v[0]) + if p.is_quantized: + p.value = v[1] + del self._parameters_to_smooth[p] + elif param_value == v[1] or self._smoothing_count >= self._smoothing_speed: + del self._parameters_to_smooth[p] + p.value = v[1] + else: + p.value = param_value + + + def _get_parameter_data_to_smooth(self, parameter, new_value): + """ Returns parameter data to smooth and return list of smoothing value, target value and current value """ + factor = self._smoothing_speed + if self._is_control_track and self._control_rack and self._control_rack.parameters[0].value == 1.0: + factor = 127 + if factor and self._is_control_track: + difference = new_value - parameter.value + if difference and (factor == 127 or (factor != 127 and abs(difference) > 0.01)): + if parameter.is_quantized: + factor = 1 + param_data = [(new_value - parameter.value) / factor, new_value, parameter.value] + if difference < 0.0: + param_data = [((parameter.value - new_value) / factor) * -1, new_value, parameter.value] + self._parameters_to_smooth[parameter] = param_data + else: + parameter.value = new_value + else: + parameter.value = new_value + + + def _get_snap_device_range(self, args, track): + """ Returns range of devices to snapshot """ + dev_args = args.replace('MIX', '') + dev_args = dev_args.replace('PLAY', '') + dev_args = dev_args.replace('DEV', '') + dev_args = dev_args.replace('IO', '') + start = 0 + end = start + 1 + if dev_args: + if 'ALL' in dev_args: + start = 0 + end = len(track.devices) + elif '-' in dev_args: + try: + name_split = dev_args.split('-') + start = int(name_split[0].strip()) - 1 + end = int(name_split[1].strip()) + except: pass + else: + try: + start = int(dev_args) - 1 + end = start + 1 + except: pass + if start > len(track.devices) or start < 0 or end > len(track.devices) or end < start: + return() + return (start, end) + + + def setup_tracks(self): + """ Stores dictionary of tracks by name """ + self._current_tracks = {} + self._remove_track_listeners() + for track in (tuple(self.song().tracks) + tuple(self.song().return_tracks) + (self.song().master_track,)): + if not track.name_has_listener(self.setup_tracks): + track.add_name_listener(self.setup_tracks) + name = self._parent.get_name(track.name) + if not self._current_tracks.has_key(track.name) and not name.startswith('CLYPHX SNAP'): + self._current_tracks[track.name] = track + + + def _refresh_xclip_name(self, xclip_data): + """ Refreshes xclip's previous name in cases where a snap is asking to store too many params """ + xclip_data[0].name = xclip_data[1] + + + def _remove_control_rack(self): + """ Removes control rack listeners """ + if self._control_rack: + self._control_rack.name = 'ClyphX Snap' + if self._control_rack.parameters[1].value_has_listener(self._control_rack_macro_changed): + self._control_rack.parameters[1].remove_value_listener(self._control_rack_macro_changed) + self._control_rack = None + + + def _remove_track_listeners(self): + """ Removes track name listeners """ + for track in (tuple(self.song().tracks) + tuple(self.song().return_tracks) + (self.song().master_track,)): + if track.name_has_listener(self.setup_tracks): + track.remove_name_listener(self.setup_tracks) + + +# local variables: +# tab-width: 4 diff --git a/ClyphX/ClyphXTrackActions.py b/ClyphX/ClyphXTrackActions.py new file mode 100644 index 0000000..4d098a5 --- /dev/null +++ b/ClyphX/ClyphXTrackActions.py @@ -0,0 +1,415 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +import Live +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent +from consts import * + +class ClyphXTrackActions(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' Track-related actions ' + + def __init__(self, parent): + ControlSurfaceComponent.__init__(self) + self._parent = parent + + + def disconnect(self): + self._parent = None + if IS_LIVE_9: + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def duplicate_track(self, track, xclip, ident, args): + """ Duplicates the given track (only regular tracks can be duplicated). """ + if IS_LIVE_9 and track in self.song().tracks: + self.song().duplicate_track(list(self.song().tracks).index(track)) + + + def delete_track(self, track, xclip, ident, args): + """ Deletes the given track as long as it's not the last track in the set (only regular tracks can be deleted). """ + if IS_LIVE_9 and track in self.song().tracks: + self.song().delete_track(list(self.song().tracks).index(track)) + + + def delete_device(self, track, xclip, ident, args): + """ Delete the device on the track associated with the given index. Only top level devices can be deleted. """ + if IS_LIVE_9: + try: + index = int(args.strip()) - 1 + if index < len(track.devices): + track.delete_device(index) + except: pass + + + def create_clip(self, track, xclip, ident, args): + """ Creates a clip in the given slot index (or sel if specified) at the given length (in bars). If no args, creates a 1 bar clip in the selected slot. """ + if IS_LIVE_9 and track.has_midi_input: + slot = list(self.song().scenes).index(self.song().view.selected_scene) + bar = (4.0 / self.song().signature_denominator) * self.song().signature_numerator + length = bar + if args: + arg_array = args.split() + if len(arg_array) > 0: + specified_slot = arg_array[0].strip() + if specified_slot != 'SEL': + try: slot = int(specified_slot) - 1 + except: pass + if len(arg_array) > 1: + try: length = float(arg_array[1].strip()) * bar + except: pass + if slot in xrange(len(self.song().scenes)): + if not track.clip_slots[slot].has_clip: + track.clip_slots[slot].create_clip(length) + + + def set_name(self, track, xclip, ident, args): + """ Set track's name """ + if track in tuple(self.song().tracks) + tuple(self.song().return_tracks): + args = args.strip() + if args: + track.name = args + + + def rename_all_clips(self, track, xclip, ident, args): + """ Renames all clips on the track based on the track's name or the name specified in args. """ + if track in self.song().tracks and not track.is_foldable: + name = track.name + if args: + name = args.strip() + for i in range(len(track.clip_slots)): + slot = track.clip_slots[i] + if slot.has_clip: + slot.clip.name = name + ' ' + str(i + 1) + + + def set_mute(self, track, xclip, ident, value = None): + """ Toggles or turns on/off track mute """ + if track in tuple(self.song().tracks) + tuple(self.song().return_tracks): + if value in KEYWORDS: + track.mute = KEYWORDS[value] + else: + track.mute = not(track.mute) + + + def set_solo(self, track, xclip, ident, value = None): + """ Toggles or turns on/off track solo """ + if track in tuple(self.song().tracks) + tuple(self.song().return_tracks): + if value in KEYWORDS: + track.solo = KEYWORDS[value] + else: + track.solo = not(track.solo) + + + def set_arm(self, track, xclip, ident, value = None): + """ Toggles or turns on/off track arm """ + if track in self.song().tracks and track.can_be_armed: + if value in KEYWORDS: + track.arm = KEYWORDS[value] + else: + track.arm = not(track.arm) + + + def set_fold(self, track, xclip, ident, value = None): + """ Toggles or turns on/off track fold """ + if track.is_foldable: + if value in KEYWORDS: + track.fold_state = KEYWORDS[value] + else: + track.fold_state = not(track.fold_state) + + + def set_monitor(self, track, xclip, ident, args): + """ Toggles or sets monitor state """ + if track in self.song().tracks and not track.is_foldable: + if args in MON_STATES: + track.current_monitoring_state = MON_STATES[args] + else: + if track.current_monitoring_state == 2: + track.current_monitoring_state = 0 + else: + track.current_monitoring_state += 1 + + + def set_xfade(self, track, xclip, ident, args): + """ Toggles or sets crossfader assignment """ + if track != self.song().master_track: + if args in XFADE_STATES: + track.mixer_device.crossfade_assign = XFADE_STATES[args] + else: + if track.mixer_device.crossfade_assign == 2: + track.mixer_device.crossfade_assign = 0 + else: + track.mixer_device.crossfade_assign += 1 + + + def set_selection(self, track, xclip, ident, args): + """ Sets track/slot selection """ + self.song().view.selected_track = track + if track in self.song().tracks: + if args: + try: + self.song().view.selected_scene = list(self.song().scenes)[int(args.strip())-1] + except: pass + else: + if track.playing_slot_index >= 0: + self.song().view.selected_scene = list(self.song().scenes)[track.playing_slot_index] + + + def set_jump(self, track, xclip, ident, args): + """ Jumps playing clip on track forward/backward """ + if track in self.song().tracks: + try: track.jump_in_running_session_clip(float(args.strip())) + except: pass + + + def set_stop(self, track, xclip, ident, value = None): + """ Stops all clips on track w/no quantization option for Live 9 """ + if track in self.song().tracks: + if IS_LIVE_9: + track.stop_all_clips(not value.strip() == 'NQ') + else: + track.stop_all_clips() + + + def set_play(self, track, xclip, ident, args): + """ Plays clips normally. Allow empty slots unless using keywords. """ + allow_empty_slots = args != '<' and args != '>' + slot_to_play = self._get_slot_index_to_play(track, xclip, args.strip(), allow_empty_slots) + if slot_to_play != -1: + track.clip_slots[slot_to_play].fire() + + + def set_play_w_legato(self, track, xclip, ident, args): + """ Plays the clip with legato using the current global quantization. This will not launch empty slots. """ + if IS_LIVE_9: + slot_to_play = self._get_slot_index_to_play(track, xclip, args.strip()) + if slot_to_play != -1: + track.clip_slots[slot_to_play].fire(force_legato=True, launch_quantization=self.song().clip_trigger_quantization) + + + def set_play_w_force_qntz(self, track, xclip, ident, args): + """ Plays the clip with a specific quantization regardless of launch/global quantization. This will not launch empty slots. """ + self._handle_force_qntz_play(track, xclip, args, False) + + + def set_play_w_force_qntz_and_legato(self, track, xclip, ident, args): + """ Combination of play_legato and play_w_force_qntz. """ + self._handle_force_qntz_play(track, xclip, args, True) + + + def _handle_force_qntz_play(self, track, xclip, args, w_legato): + """ Handles playing clips with a specific quantization with or without legato. """ + if IS_LIVE_9: + args = args.strip() + arg_array = args.split() + qntz_spec = arg_array[0] + if 'BAR' in args: + qntz_spec = arg_array[0] + ' ' + arg_array[1] + if qntz_spec in GQ_STATES.keys(): + qntz_to_use = GQ_STATES[qntz_spec] + slot_to_play = self._get_slot_index_to_play(track, xclip, args.replace(qntz_spec, '').strip()) + if slot_to_play != -1: + track.clip_slots[slot_to_play].fire(force_legato=w_legato, launch_quantization=qntz_to_use) + + + def _get_slot_index_to_play(self, track, xclip, args, allow_empty_slots=False): + """ Returns the slot index to play based on keywords in the given args. """ + slot_to_play = -1 + if track in self.song().tracks: + play_slot = track.playing_slot_index + select_slot = list(self.song().scenes).index(self.song().view.selected_scene) + if args == '': + if type(xclip) is Live.Clip.Clip: + slot_to_play = xclip.canonical_parent.canonical_parent.playing_slot_index + else: + if play_slot >= 0: + slot_to_play = play_slot + else: + slot_to_play = select_slot + elif args == 'SEL': + slot_to_play = select_slot + elif 'RND' in args and len(self.song().scenes) > 1:#--Don't allow randomization unless more than 1 scene + num_scenes = len(self.song().scenes) + rnd_range = [0, num_scenes] + if '-' in args: + rnd_range_data = args.replace('RND', '').split('-') + if len(rnd_range_data) == 2: + new_min = 0 + new_max = num_scenes + try: new_min = int(rnd_range_data[0]) - 1 + except: new_min = 0 + try: new_max = int(rnd_range_data[1]) + except: new_max = num_scenes + if new_min in range(0, num_scenes) and new_max in range(0, num_scenes + 1) and new_min < new_max - 1: + rnd_range = [new_min, new_max] + slot_to_play = Live.Application.get_random_int(0, rnd_range[1] - rnd_range[0]) + rnd_range[0] + if slot_to_play == play_slot: + while slot_to_play == play_slot: + slot_to_play = Live.Application.get_random_int(0, rnd_range[1] - rnd_range[0]) + rnd_range[0] + elif args.startswith(('<', '>')) and len(self.song().scenes) > 1:#--Don't allow adjustment unless more than 1 scene + if track.is_foldable: + return -1 + factor = self._parent.get_adjustment_factor(args) + if factor < len(self.song().scenes): + if abs(factor) == 1:#---Only launch slots that contain clips + for index in range (len(self.song().scenes)): + play_slot += factor + if play_slot >= len(self.song().scenes): + play_slot = 0 + if track.clip_slots[play_slot].has_clip and track.clip_slots[play_slot].clip != xclip: + break + else: + play_slot += factor + if play_slot >= len(self.song().scenes): + play_slot -= len(self.song().scenes) + elif play_slot < 0 and abs(play_slot) >= len(self.song().scenes): + play_slot = -(abs(play_slot) - len(self.song().scenes)) + slot_to_play = play_slot + elif args.startswith('"') and args.endswith('"'): + clip_name = args.strip('"') + for index in range(len(track.clip_slots)): + slot = track.clip_slots[index] + if slot.has_clip and slot.clip.name.upper() == clip_name: + slot_to_play = index + break + else: + try: + if int(args) in range(len(self.song().scenes) + 1): + slot_to_play = int(args)-1 + except: pass + else: + return -1 + if (not track.clip_slots[slot_to_play].has_clip and allow_empty_slots) or (track.clip_slots[slot_to_play].has_clip and track.clip_slots[slot_to_play].clip != xclip): + return slot_to_play + else: + return -1 + + + def adjust_preview_volume(self, track, xclip, ident, args): + """ Adjust/set master preview volume """ + if track == self.song().master_track: + self._parent.do_parameter_adjustment(self.song().master_track.mixer_device.cue_volume, args.strip()) + + + def adjust_crossfader(self, track, xclip, ident, args): + """ Adjust/set master crossfader """ + if track == self.song().master_track: + self._parent.do_parameter_adjustment(self.song().master_track.mixer_device.crossfader, args.strip()) + + + def adjust_volume(self, track, xclip, ident, args): + """ Adjust/set track volume """ + self._parent.do_parameter_adjustment(track.mixer_device.volume, args.strip()) + + + def adjust_pan(self, track, xclip, ident, args): + """ Adjust/set track pan """ + self._parent.do_parameter_adjustment(track.mixer_device.panning, args.strip()) + + + def adjust_sends(self, track, xclip, ident, args): + """ Adjust/set track sends """ + args = args.split() + if len(args) > 1: + param = self.get_send_parameter(track, args[0].strip()) + if param: + self._parent.do_parameter_adjustment(param, args[1].strip()) + + + def get_send_parameter(self, track, send_string): + """ Gets the send parameter to operate on. """ + param = None + if track != self.song().master_track: + try: param = track.mixer_device.sends[ord(send_string) - 65] + except: pass + return param + + + def adjust_input_routing(self, track, xclip, ident, args): + """ Adjust track input routing """ + if track in self.song().tracks and not track.is_foldable: + routings = list(track.input_routings) + current_routing = 0 + if track.current_input_routing in routings: + current_routing = routings.index(track.current_input_routing) + track.current_input_routing = self.handle_track_routing(args, routings, current_routing) + + + def adjust_input_sub_routing(self, track, xclip, ident, args): + """ Adjust track input sub-routing """ + if track in self.song().tracks and not track.is_foldable: + routings = list(track.input_sub_routings) + current_routing = 0 + if track.current_input_sub_routing in routings: + current_routing = routings.index(track.current_input_sub_routing) + track.current_input_sub_routing = self.handle_track_routing(args, routings, current_routing) + + + def adjust_output_routing(self, track, xclip, ident, args): + """ Adjust track output routing """ + if track != self.song().master_track: + routings = list(track.output_routings) + current_routing = 0 + if track.current_output_routing in routings: + current_routing = routings.index(track.current_output_routing) + track.current_output_routing = self.handle_track_routing(args, routings, current_routing) + + + def adjust_output_sub_routing(self, track, xclip, ident, args): + """ Adjust track output sub-routing """ + if track != self.song().master_track: + routings = list(track.output_sub_routings) + current_routing = 0 + if track.current_output_sub_routing in routings: + current_routing = routings.index(track.current_output_sub_routing) + track.current_output_sub_routing = self.handle_track_routing(args, routings, current_routing) + + + def handle_track_routing(self, args, routings, current_routing): + """ Handle track routing adjustment """ + new_routing = routings[current_routing] + args = args.strip() + if args in ('<', '>'): + factor = self._parent.get_adjustment_factor(args) + if current_routing + factor in range (len(routings)): + new_routing = routings[current_routing + factor] + else: + for i in routings: + if self._parent.get_name(i) == args: + new_routing = i + break + return new_routing + + +# local variables: +# tab-width: 4 diff --git a/ClyphX/ClyphXTriggers.py b/ClyphX/ClyphXTriggers.py new file mode 100644 index 0000000..b2dd920 --- /dev/null +++ b/ClyphX/ClyphXTriggers.py @@ -0,0 +1,358 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +import Live +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent +from ActionList import ActionList +from consts import * +if IS_LIVE_9: + from functools import partial + +class ClyphXControlComponent(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' Control component for ClyphX ' + + def __init__(self, parent): + ControlSurfaceComponent.__init__(self) + self._parent = parent + self._control_list = {} + self._xt_scripts = [] + + + def disconnect(self): + self._control_list = {} + self._xt_scripts = [] + self._parent = None + if IS_LIVE_9: + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def connect_script_instances(self, instanciated_scripts): + """ Try to connect to ClyphX_XT instances """ + ClyphX_XT = None + for i in range (5): + try: + if i == 0: + from ClyphX_XTA.ClyphX_XT import ClyphX_XT + elif i == 1: + from ClyphX_XTB.ClyphX_XT import ClyphX_XT + elif i == 2: + from ClyphX_XTC.ClyphX_XT import ClyphX_XT + elif i == 3: + from ClyphX_XTD.ClyphX_XT import ClyphX_XT + elif i == 4: + from ClyphX_XTE.ClyphX_XT import ClyphX_XT + except: pass + if ClyphX_XT: + for i in instanciated_scripts: + if isinstance(i, ClyphX_XT) and not i in self._xt_scripts: + self._xt_scripts.append(i) + break + + + def assign_new_actions(self, string): + """ Assign new actions to controls via xclips """ + if self._xt_scripts: + for x in self._xt_scripts: + x.assign_new_actions(string) + ident = string[string.index('[')+2:string.index(']')].strip() + actions = string[string.index(']')+2:].strip() + for c, v in self._control_list.items(): + if ident == v['ident']: + new_actions = actions.split(',') + on_action = '[' + ident + '] ' + new_actions[0] + off_action = None + if on_action and len(new_actions) > 1: + if new_actions[1].strip() == '*': + off_action = on_action + else: + off_action = '[' + ident + '] ' + new_actions[1] + if on_action: + v['on_action'] = on_action + v['off_action'] = off_action + break + + + def receive_midi(self, bytes): + """ Receive user-defined midi messages """ + if self._control_list: + ctrl_data = None + if bytes[2] == 0 or bytes[0] < 144: + if (bytes[0], bytes[1]) in self._control_list.keys() and self._control_list[(bytes[0], bytes[1])]['off_action']: + ctrl_data = self._control_list[(bytes[0], bytes[1])] + elif (bytes[0] + 16, bytes[1]) in self._control_list.keys() and self._control_list[(bytes[0] + 16, bytes[1])]['off_action']: + ctrl_data = self._control_list[(bytes[0] + 16, bytes[1])] + if ctrl_data: + ctrl_data['name'].name = ctrl_data['off_action'] + elif bytes[2] != 0 and (bytes[0], bytes[1]) in self._control_list.keys(): + ctrl_data = self._control_list[(bytes[0], bytes[1])] + ctrl_data['name'].name = ctrl_data['on_action'] + if ctrl_data: + self._parent.handle_action_list_trigger(self.song().view.selected_track, ctrl_data['name']) + + + def get_user_control_settings(self, data, midi_map_handle): + """ Receives control data from user settings file and builds control dictionary """ + self._control_list = {} + for d in data: + status_byte = None + channel = None + ctrl_num = None + on_action = None + off_action = None + d = d.split('=') + ctrl_name = d[0].strip() + new_ctrl_data = d[1].split(',') + try: + if new_ctrl_data[0].strip() == 'NOTE': + status_byte = 144 + elif new_ctrl_data[0].strip() == 'CC': + status_byte = 176 + if int(new_ctrl_data[1].strip()) in range(1,17): + channel = int(new_ctrl_data[1].strip()) - 1 + if int(new_ctrl_data[2].strip()) in range(128): + ctrl_num = int(new_ctrl_data[2].strip()) + on_action = '[' + ctrl_name + '] ' + new_ctrl_data[3] + if on_action and len(new_ctrl_data) > 4: + if new_ctrl_data[4].strip() == '*': + off_action = on_action + else: + off_action = '[' + ctrl_name + '] ' + new_ctrl_data[4] + except: pass + if status_byte and channel != None and ctrl_num != None and on_action: + self._control_list[(status_byte + channel, ctrl_num)] = {'ident' : ctrl_name, 'on_action' : on_action, 'off_action' : off_action, 'name' : ActionList(on_action)} + if status_byte == 144: + Live.MidiMap.forward_midi_note(self._parent._c_instance.handle(), midi_map_handle, channel, ctrl_num) + else: + Live.MidiMap.forward_midi_cc(self._parent._c_instance.handle(), midi_map_handle, channel, ctrl_num) + + + def rebuild_control_map(self, midi_map_handle): + """ Called from main when build_midi_map is called. """ + for key in self._control_list.keys(): + if key[0] >= 176: + Live.MidiMap.forward_midi_cc(self._parent._c_instance.handle(), midi_map_handle, key[0] - 176, key[1]) + else: + Live.MidiMap.forward_midi_note(self._parent._c_instance.handle(), midi_map_handle, key[0] - 144, key[1]) + + +class ClyphXTrackComponent(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' Track component that monitors play slot index and calls main script on changes ' + + def __init__(self, parent, track): + ControlSurfaceComponent.__init__(self) + self._parent = parent + self._track = track + self._clip = None + self._loop_count = 0 + self._track.add_playing_slot_index_listener(self.play_slot_index_changed) + self._register_timer_callback(self.on_timer) + self._last_slot_index = -1 + self._triggered_clips = [] + self._triggered_lseq_clip = None + + + def disconnect(self): + self.remove_loop_jump_listener() + self._unregister_timer_callback(self.on_timer) + if self._track and self._track.playing_slot_index_has_listener(self.play_slot_index_changed): + self._track.remove_playing_slot_index_listener(self.play_slot_index_changed) + self._track = None + self._clip = None + self._triggered_clips = [] + self._triggered_lseq_clip = None + self._parent = None + if IS_LIVE_9: + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def play_slot_index_changed(self): + """ Called on track play slot index changes to set up clips to trigger (on play and stop) and set up loop listener for LSEQ. """ + self.remove_loop_jump_listener() + new_clip = self.get_xclip(self._track.playing_slot_index) + prev_clip = self.get_xclip(self._last_slot_index) + self._last_slot_index = self._track.playing_slot_index + if new_clip and prev_clip and new_clip == prev_clip: + self._triggered_clips.append(new_clip) + elif new_clip: + if prev_clip: + self._triggered_clips.append(prev_clip) + self._triggered_clips.append(new_clip) + elif prev_clip: + self._triggered_clips.append(prev_clip) + self._clip = new_clip + if self._clip and '(LSEQ)' in self._clip.name.upper() and not self._clip.loop_jump_has_listener(self.on_loop_jump): + self._clip.add_loop_jump_listener(self.on_loop_jump) + + + def get_xclip(self, slot_index): + """ Get the xclip associated with slot_index or None. """ + clip = None + if self._track and slot_index in xrange(len(self._track.clip_slots)): + slot = self._track.clip_slots[slot_index] + if slot.has_clip and not slot.clip.is_recording and not slot.clip.is_triggered: + clip_name = slot.clip.name + if len(clip_name) > 2 and clip_name[0] == '[' and ']' in clip_name: + clip = slot.clip + return clip + + + def on_loop_jump(self): + """ Called on loop changes to increment loop count and set clip to trigger. """ + self._loop_count += 1 + if self._clip: + self._triggered_lseq_clip = self._clip + + + def on_timer(self): + """ Continuous timer, calls main script if there are any triggered clips. """ + if self._track and (not self._track.mute or self._parent._process_xclips_if_track_muted): + if self._triggered_clips: + for clip in self._triggered_clips: + self._parent.handle_action_list_trigger(self._track, clip) + self._triggered_clips = [] + if self._triggered_lseq_clip: + self._parent.handle_loop_seq_action_list(self._triggered_lseq_clip, self._loop_count) + self._triggered_lseq_clip = None + + + def remove_loop_jump_listener(self): + self._loop_count = 0 + if self._clip and self._clip.loop_jump_has_listener(self.on_loop_jump): + self._clip.remove_loop_jump_listener(self.on_loop_jump) + + +class ClyphXCueComponent(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' Cue component that monitors cue points and calls main script on changes ' + + def __init__(self, parent): + ControlSurfaceComponent.__init__(self) + self._parent = parent + self.song().add_current_song_time_listener(self.arrange_time_changed) + self.song().add_is_playing_listener(self.arrange_time_changed) + self.song().add_cue_points_listener(self.cue_points_changed) + self._x_points = {} + self._x_point_time_to_watch_for = -1 + self._last_arrange_position = -1 + self._sorted_times = [] + self.cue_points_changed() + + + def disconnect(self): + self.remove_cue_point_listeners() + self.song().remove_current_song_time_listener(self.arrange_time_changed) + self.song().remove_is_playing_listener(self.arrange_time_changed) + self.song().remove_cue_points_listener(self.cue_points_changed) + self._x_points = {} + self._parent = None + if IS_LIVE_9: + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def cue_points_changed(self): + """ Called on cue point changes to set up points to watch, cue points can't be named via the API so cue points can't perform any actions requiring naming """ + self.remove_cue_point_listeners() + self._sorted_times = [] + for cp in self.song().cue_points: + if not cp.time_has_listener(self.cue_points_changed): + cp.add_time_listener(self.cue_points_changed) + if not cp.name_has_listener(self.cue_points_changed): + cp.add_name_listener(self.cue_points_changed) + name = self._parent.get_name(cp.name) + if len(name) > 2 and name[0] == '[' and name.count('[') == 1 and name.count(']') == 1: + cue_name = name.replace(name[name.index('['):name.index(']')+1].strip(), '') + self._x_points[cp.time] = cp + self._sorted_times = sorted(self._x_points.keys()) + self.set_x_point_time_to_watch() + + + def arrange_time_changed(self): + """ Called on arrange time changed and schedules actions where necessary """ + if self.song().is_playing: + if self._x_point_time_to_watch_for != -1 and self._last_arrange_position < self.song().current_song_time: + if self.song().current_song_time >= self._x_point_time_to_watch_for and self._x_point_time_to_watch_for < self._last_arrange_position: + if IS_LIVE_9: + self._parent.schedule_message(1, partial(self.schedule_x_point_action_list, self._x_point_time_to_watch_for)) + else: + self._parent.schedule_message(1, self.schedule_x_point_action_list, self._x_point_time_to_watch_for) + self._x_point_time_to_watch_for = -1 + else: + self.set_x_point_time_to_watch() + self._last_arrange_position = self.song().current_song_time + + + def set_x_point_time_to_watch(self): + """ Determine which cue point time to watch for next """ + if self._x_points: + if self.song().is_playing: + for t in self._sorted_times: + if t >= self.song().current_song_time: + self._x_point_time_to_watch_for = t + break + else: + self._x_point_time_to_watch_for = -1 + + + def schedule_x_point_action_list(self, point): + self._parent.handle_action_list_trigger(self.song().view.selected_track, self._x_points[point]) + + + def remove_cue_point_listeners(self): + for cp in self.song().cue_points: + if cp.time_has_listener(self.cue_points_changed): + cp.remove_time_listener(self.cue_points_changed) + if cp.name_has_listener(self.cue_points_changed): + cp.remove_name_listener(self.cue_points_changed) + self._x_points = {} + self._x_point_time_to_watch_for = -1 + + +# local variables: +# tab-width: 4 diff --git a/ClyphX/ClyphXUserActions.py b/ClyphX/ClyphXUserActions.py new file mode 100644 index 0000000..8291b61 --- /dev/null +++ b/ClyphX/ClyphXUserActions.py @@ -0,0 +1,163 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +import Live +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent +from ActionList import ActionList +from consts import IS_LIVE_9 + +""" This script allows you to create your own ClyphX actions that can be used like any other ClyphX action. + +PLEASE NOTE: nativeKONTROL cannot provide support on writing Python code or on accessing Live's API through Python. +These are extremely vast subjects that would be difficult to support, particularly in the context of ClyphX, which +is sometimes difficult to support on its own. However, this script is full of comments to help you. Also, the rest +of the scripts in the ClyphX folder include plenty of examples of how to do things. Lastly, we would make the +following recommendations: + +- You can edit this file with any text editor, but an IDE makes this easier: http://wingware.com/ + +- To learn about Python: http://www.diveintopython.net/toc/index.html and http://www.google.com + +- To learn the basics of MIDI Remote Scripts/Live API access: http://remotescripts.blogspot.com/ + +- Reference of Live functions and such that you can access: http://cycling74.com/docs/max5/refpages/m4l-ref/m4l_live_object_model.html + +- After making changes to this file, you will need to recompile. The quickest way to do that is by loading a set. Note, however, that if you + make a change that results in errors, this can sometimes break all control surface scripts (cause them to throw strange errors for no reason) + until you reload Live. + + +TO CREATE ACTIONS: + - You'll first add the action to the action_dict (dictionary below). + + - Then you'll add and implement a function that will be called when the action is triggered. + + - The function has to receive the following parameters: (self, track, args) + + - See the example functions below (example_action_one and example_action_two) for some examples of how to set up functions. + +PARAMETER EXPLANATION: +- track = the track to apply the action to. If the action isn't applied to any particular track, this will either be the selected track + (in the case of X-Controls and X-Cues) or the track the X-Clip resides on. All of the actions CAN be (but don't HAVE to be) applied + to ranges of tracks (like 1-8/MY_ACTION, which will apply to tracks 1-8). If applied to a range of tracks, the function associated with + the action will be called once for each track in the range. + +- args = these are arguments that follow the name of the action. For example, with the action VOL 10. The 10 is an argument. These arguments will always be in all caps. + +RESTRICTIONS: +- Action names (which you define in the dictionary below) should NOT be the same as any current ClyphX action and should be composed of + just letters and numbers. + +- Arguments should NOT use any of the special characters used in ClyphX: semi-colon(;), comma(,), percent sign(%), equals sign(=) +""" + +class ClyphXUserActions(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' User actions ' + + def __init__(self, parent): + ControlSurfaceComponent.__init__(self) + + """ Below is the dictionary of actions that this script provides. + + For each entry: + - The key = the one-word (not case-sensitive) name of the action. This is the name that is used when accessing the action from an X-Trigger. + + - The value = the name of the function in this script to call to perform the action. + + Except for the last entry, every entry should be followed by a comma. You can remove the 2 example entries from the dictionary if you wish. """ + self._action_dict = {#<--DO NOTE REMOVE THIS + 'EX_ACTION_1' : 'example_action_one', + 'EX_ACTION_2' : 'example_action_two' + }#<--DO NOTE REMOVE THIS + + """ The parent ClyphX script. Through this you can access things such as the log_message function (writes to Live's Log.txt file), + which you'll likely use quite a bit. The Troubleshooting section of the ClyphX manual covers how to access Log.txt. """ + self._parent = parent + + + def disconnect(self): + """ Called by the control surface on disconnect (app closed, script closed). DO NOT REMOVE THIS. """ + self._parent = None + if IS_LIVE_9: + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + """ Called when this script is enabled/disabled (by calling set_enabled on it). DO NOT REMOVE THIS. """ + pass + + + def update(self): + """ Called by the control surface on instantiation and in other cases such as when exiting MIDI map mode. DO NOT REMOVE THIS. """ + pass + + + def example_action_one(self, track, args): + """ Example action that writes to Live's log file and then triggers standard ClyphX METRO action. + This can receive the same args as the METRO action (like EX_ACTION_1 ON), so it just passes args it receives to the METRO function. + + NOTE: The arguments passed to handle_action_list_trigger are: + - The track associated with the trigger. Since our function here is not associated with any particular track, we pass the selected track. + + - The ActionList object, which is just a simple object that contains a name field. You just instantiate one of these with the action list + as a string(proceeded by an identifier). """ + self._parent.log_message('example_action_one triggered with args=' + str(args)) + self._parent.handle_action_list_trigger(self.song().view.selected_track, ActionList('[] METRO ' + str(args))) + + + def example_action_two(self, track, args): + """ Example action that sets mixer settings of the given track to be the same as the master track. + If no args or args contains VOL, sets volume. + If no args or args contains PAN, sets panning. + Obviously, does nothing if the given track is the master track. """ + if track != self.song().master_track: + if not args or 'VOL' in args: + track.mixer_device.volume.value = self.song().master_track.mixer_device.volume.value + if not args or 'PAN' in args: + track.mixer_device.panning.value = self.song().master_track.mixer_device.panning.value + + + def on_track_list_changed(self): + """ Called by the control surface if tracks are added/removed, to be overridden """ + pass + + + def on_scene_list_changed(self): + """ Called by the control surface if scenes are added/removed, to be overridden """ + pass + + + def on_selected_track_changed(self): + """ Called by the control surface when a track is selected, to be overridden """ + pass + + + def on_selected_scene_changed(self): + """ Called by the control surface when a scene is selected, to be overridden """ + pass + + +# local variables: +# tab-width: 4 \ No newline at end of file diff --git a/ClyphX/ExtraPrefs.py b/ClyphX/ExtraPrefs.py new file mode 100644 index 0000000..5e0b1c2 --- /dev/null +++ b/ClyphX/ExtraPrefs.py @@ -0,0 +1,197 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +import Live +from consts import * +if IS_LIVE_9: + from functools import partial +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent + +class ExtraPrefs(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = " Extra prefs component for ClyphX " + + def __init__(self, parent): + ControlSurfaceComponent.__init__(self) + self._parent = parent + self._show_highlight = True + self._exclusive_arm = False + self._exclusive_fold = False + self._clip_record = False + self._clip_record_slot = None + self._midi_clip_length = False + self._midi_clip_length_slot = None + self._last_track = self.song().view.selected_track + self.on_selected_track_changed() + + + def disconnect(self): + self.remove_listeners() + self._last_track = None + self._clip_record_slot = None + self._midi_clip_length_slot = None + self._parent = None + if IS_LIVE_9: + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def get_user_settings(self, data): + """ Get user settings from config file and make sure they are in proper range """ + for d in data: + d = d.split('=') + if 'NAVIGATION_HIGHLIGHT' in d[0]: + self._show_highlight = 'ON' in d[1] + elif 'EXCLUSIVE_ARM_ON_SELECT' in d[0] and 'ON' in d[1]: + self._exclusive_arm = True + elif 'EXCLUSIVE_SHOW_GROUP_ON_SELECT' in d[0] and 'ON' in d[1]: + self._exclusive_fold = True + elif 'CLIP_RECORD_LENGTH_SET_BY_GLOBAL_QUANTIZATION' in d[0] and 'ON' in d[1]: + self._clip_record = True + elif 'DEFAULT_INSERTED_MIDI_CLIP_LENGTH' in d[0]: + try: + if int(d[1].strip()) in range (2,17): + self._midi_clip_length = int(d[1].strip()) + except: pass + self.on_selected_track_changed() + + + def on_selected_track_changed(self): + """ Handles navigation highlight, triggering exclusive arm/fold functions and removes/sets up listeners for clip-related functions """ + ControlSurfaceComponent.on_selected_track_changed(self) + track = self.song().view.selected_track + clip_slot = self.song().view.highlighted_clip_slot + self.remove_listeners() + if self._show_highlight: + tracks = list(tuple(self.song().visible_tracks) + tuple(self.song().return_tracks)) + tracks.append(self.song().master_track) + if self.song().view.selected_track in tracks: + self._parent._set_session_highlight(tracks.index(self.song().view.selected_track), list(self.song().scenes).index(self.song().view.selected_scene), 1, 1, True) + else: + self._parent._set_session_highlight(-1, -1, -1, -1, False) + if self._exclusive_arm and track != self._last_track: + if IS_LIVE_9: + self._parent.schedule_message(1, partial(self.do_exclusive_arm, track)) + else: + self._parent.schedule_message(1, self.do_exclusive_arm, track) + if self._exclusive_fold and track != self._last_track: + if IS_LIVE_9: + self._parent.schedule_message(1, partial(self.do_exclusive_fold, track)) + else: + self._parent.schedule_message(1, self.do_exclusive_fold, track) + if self._clip_record: + if track.can_be_armed and not clip_slot.has_clip: + self._clip_record_slot = clip_slot + if not self._clip_record_slot.has_clip_has_listener(self.clip_record_slot_changed): + self._clip_record_slot.add_has_clip_listener(self.clip_record_slot_changed) + if self._midi_clip_length: + if track.has_midi_input and not clip_slot.has_clip and not track.is_foldable: + self._midi_clip_length_slot = clip_slot + if not self._midi_clip_length_slot.has_clip_has_listener(self.midi_clip_length_slot_changed): + self._midi_clip_length_slot.add_has_clip_listener(self.midi_clip_length_slot_changed) + self._last_track = track + + + def do_exclusive_fold(self, track): + """ Called on track change. Collapses all group tracks except for the current group track """ + if (track.is_foldable): + for t in self.song().tracks: + if (t.is_foldable): + if t == track: + t.fold_state = 0 + else: + t.fold_state = 1 + + + def do_exclusive_arm(self, track): + """ Called on track change. Disarams all tracks except for the current track """ + for t in self.song().tracks: + if (t.can_be_armed): + if t == track: + t.arm = True + else: + t.arm = False + + + def clip_record_slot_changed(self): + """ Called on slot has clip changed. Checks if clip is recording and retriggers it if so """ + track = self.song().view.selected_track + if self.song().clip_trigger_quantization != 0 and track.arm: + clip = self._clip_record_slot.clip + if clip and clip.is_recording: + clip.fire() + + + def midi_clip_length_slot_changed(self): + """Called on slot has clip changed to trigger set length function. Checks if clip is not playing/triggered, is 1-bar in length, has no name and no notes.""" + clip = self._midi_clip_length_slot.clip + if clip and not clip.is_playing and not clip.is_triggered: + one_bar = (4.0 / self.song().signature_denominator) * self.song().signature_numerator + if clip.length == one_bar and clip.name == '': + clip.select_all_notes() + all_notes = clip.get_selected_notes() + clip.deselect_all_notes() + if not all_notes: + if IS_LIVE_9: + self._parent.schedule_message(1, partial(self.do_midi_clip_set_length, (clip, one_bar))) + else: + self._parent.schedule_message(1, self.do_midi_clip_set_length, (clip, one_bar)) + + + def do_midi_clip_set_length(self, clip_params): + """ Sets clip length and loop end to user-defined length """ + clip = clip_params[0] + new_length = clip_params[1] * self._midi_clip_length + clip.loop_end = new_length + clip.looping = False + clip.loop_end = new_length + clip.looping = True + + + def remove_listeners(self): + """ Remove parameter listeners. """ + if self._clip_record_slot: + if self._clip_record_slot.has_clip_has_listener(self.clip_record_slot_changed): + self._clip_record_slot.remove_has_clip_listener(self.clip_record_slot_changed) + self._clip_record_slot = None + if self._midi_clip_length_slot: + if self._midi_clip_length_slot.has_clip_has_listener(self.midi_clip_length_slot_changed): + self._midi_clip_length_slot.remove_has_clip_listener(self.midi_clip_length_slot_changed) + self._midi_clip_length_slot = None + + + def on_selected_scene_changed(self): + ControlSurfaceComponent.on_selected_scene_changed(self) + self.on_selected_track_changed() + + +# local variables: +# tab-width: 4 diff --git a/ClyphX/InstantMappingMakeDoc.py b/ClyphX/InstantMappingMakeDoc.py new file mode 100644 index 0000000..bed0a37 --- /dev/null +++ b/ClyphX/InstantMappingMakeDoc.py @@ -0,0 +1,138 @@ +""" +# Copyright (C) 2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +import os +import logging +logger = logging.getLogger(__name__) + +import Live +live_app = Live.Application.get_application() + +from _Generic.Devices import DEVICE_DICT, DEVICE_BOB_DICT, BANK_NAME_DICT + +""" Translation table between API names and friendly names. """ +DEV_NAME_TRANSLATION_TABLE = {'UltraAnalog': 'Analog', + 'MidiArpeggiator': 'Arpeggiator', + 'AudioEffectGroupDevice': 'Audio Effect Rack', + 'MidiChord': 'Chord', + 'Compressor2': 'Compressor', + 'DrumGroupDevice': 'Drum Rack', + 'Tube': 'Dynamic Tube', + 'Eq8': 'EQ Eight', + 'FilterEQ3': 'EQ Three', + 'LoungeLizard': 'Electric', + 'InstrumentImpulse': 'Impulse', + 'InstrumentGroupDevice': 'Instrument Rack', + 'MidiEffectGroupDevice': 'MIDI Effect Rack', + 'MidiNoteLength': 'Note Length', + 'MidiPitcher': 'Pitch', + 'MidiRandom': 'Random', + 'MultiSampler': 'Sampler', + 'MidiScale': 'Scale', + 'CrossDelay': 'Simple Delay', + 'OriginalSimpler': 'Simpler', + 'SpectrumAnalyzer': 'Spectrum', + 'StringStudio': 'Tension', + 'StereoGain': 'Utility', + 'MidiVelocity': 'Velocity', + 'Vinyl': 'Vinyl Distortion'} + +""" The version of Live we're running in. """ +LIVE_VER = 'Live v%s.%s.%s' % (live_app.get_major_version(), live_app.get_minor_version(), + live_app.get_bugfix_version()) + +""" The header of the html file. """ +HEADER = ('

Live Instant Mapping Info for {0}

Brought to ' + + 'you by nativeKONTROL.

' + + 'The following document covers the parameter banks accessible via Live\'s ' + + 'Instant Mapping feature for each built in device. This info also applies ' + + 'to controlling device parameters via ClyphX\'s Device Actions.

' + + 'NOTE: The order of parameter banks is sometimes changed by ' + + 'Ableton. If you find the information in this document to be incorrect, you ' + + 'can recreate it with ClyphX by triggering an action named MAKE_DEV_DOC. ' + + 'That will create a new version of this file in your user/home directory.' + + '
').format(LIVE_VER) + + +class InstantMappingMakeDoc(object): + """ InstantMappingMakeDoc creates a html file in the user's home directory containing + information on the parameter banks defined for all Live devices in Devices.pyc. """ + + def __init__(self): + self.log('InstantMappingMakeDoc initialized.') + self._create_html_file(self._collect_device_infos()) + self.log('InstantMappingMakeDoc finished.') + + def log(self, msg): + """ Writes a message to Live's log file. """ + logger.info(str(msg)) + + def _collect_device_infos(self): + """ Returns a dict of dicts for each device containing its friendly name, bob + parameters and bank names/bank parameters if applicable. """ + dev_dict = {} + for k, v in DEVICE_DICT.iteritems(): + has_banks = len(v) > 1 + info = {'name': DEV_NAME_TRANSLATION_TABLE.get(k, k), + 'bob': DEVICE_BOB_DICT[k][0], + 'bank_names': BANK_NAME_DICT.get(k, ()) if has_banks else (), + 'banks': (v if has_banks else ())} + dev_dict[k] = info + return dev_dict + + def _create_html_file(self, dev_dict): + """ Creates an html in the user's home directory. """ + html_file = os.path.join(os.path.expanduser('~'), + 'Live Instant Mapping Info.html') + try: + with open(html_file, 'w') as f: + f.write(HEADER) + f.write('

Device Index

') + for dev in self._get_device_index(dev_dict): + f.write(dev) + f.write('
') + for key, value in sorted(dev_dict.iteritems(), + key=lambda (k, v): (v['name'], k)): + self._write_device_info(f, value) + f.write('') + except IOError: + self.log('IOError: Unable to write file.') + + def _get_device_index(self, dev_dict): + """ Returns a sorted device index for quickly navigating the file. """ + return sorted(['{0}
'.format(v['name']) + for v in dev_dict.values()]) + + def _write_device_info(self, file, info): + """ Writes info to the file for a device. """ + file.write('

{0}

'.format(info['name'])) + self._write_bank_parameters(file, 'Best Of Banks', info['bob']) + for i, bn in enumerate(info['bank_names']): + self._write_bank_parameters(file, 'B%s: %s' % (i + 1, bn), info['banks'][i]) + file.write('
Back to Device Index
') + + def _write_bank_parameters(self, file, bank_name, bank): + """ Writes the bank name and its parameters to the file. """ + file.write('%s
' % bank_name) + for i, p in enumerate(bank): + if p: + file.write('P%s: %s
' % (i + 1, p)) + file.write('
') diff --git a/ClyphX/Macrobat.py b/ClyphX/Macrobat.py new file mode 100644 index 0000000..0a40e02 --- /dev/null +++ b/ClyphX/Macrobat.py @@ -0,0 +1,196 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +import Live +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent +from MacrobatMidiRack import MacrobatMidiRack +from MacrobatRnRRack import MacrobatRnRRack +from MacrobatSidechainRack import MacrobatSidechainRack +from MacrobatParameterRacks import MacrobatLearnRack, MacrobatChainMixRack, MacrobatDRMultiRack, MacrobatDRRack, MacrobatReceiverRack, MacrobatTrackRack +from consts import IS_LIVE_9, IS_LIVE_9_5 +if IS_LIVE_9_5: + from MacrobatPushRack import MacrobatPushRack +if IS_LIVE_9: + from MacrobatParameterRacks9 import MacrobatChainSelectorRack, MacrobatDRPadMixRack + +class Macrobat(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = " Macrobat script component for ClyphX " + + def __init__(self, parent): + ControlSurfaceComponent.__init__(self) + self._parent = parent + self._current_tracks = [] + + + def disconnect(self): + self._current_tracks = [] + self._parent = None + if IS_LIVE_9: + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def setup_tracks(self, track): + """ Setup component tracks on ini and track list changes """ + if not track in self._current_tracks: + self._current_tracks.append(track) + MacrobatTrackComponent(track, self._parent) + + +class MacrobatTrackComponent(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' Track component that monitors track devices ' + + def __init__(self, track, parent): + ControlSurfaceComponent.__init__(self) + self._parent = parent + self._track = track + self._track.add_devices_listener(self.setup_devices) + self._current_devices = [] + self._update_in_progress = False + self._has_learn_rack = False + self.setup_devices() + + + def disconnect(self): + self.remove_listeners() + if self._track: + if self._track.devices_has_listener(self.setup_devices): + self._track.remove_devices_listener(self.setup_devices) + self.remove_devices(self._track.devices) + self._track = None + self._current_devices = [] + self._parent = None + if IS_LIVE_9: + ControlSurfaceComponent.disconnect(self) + + + def update(self): + if self._track and self.song().view.selected_track == self._track: + self.setup_devices() + + + def on_enabled_changed(self): + pass + + + def reallow_updates(self): + """ Reallow device updates, used to prevent updates happening in quick succession """ + self._update_in_progress = False + + + def setup_devices(self): + """ Get devices on device/chain list and device name changes """ + if self._track and not self._update_in_progress: + self._update_in_progress = True + self._has_learn_rack = False + self.remove_listeners() + self.get_devices(self._track.devices) + self._parent.schedule_message(5, self.reallow_updates) + + + def remove_listeners(self): + """ Disconnect Macrobat rack components """ + for d in self._current_devices: + d[0].disconnect() + self._current_devices = [] + + + def get_devices(self, dev_list): + """ Go through device and chain lists and setup Macrobat racks """ + for d in dev_list: + self.setup_macrobat_rack(d) + if not d.name_has_listener(self.setup_devices): + d.add_name_listener(self.setup_devices) + if self._parent._can_have_nested_devices and d.can_have_chains: + if not d.chains_has_listener(self.setup_devices): + d.add_chains_listener(self.setup_devices) + for c in d.chains: + if not c.devices_has_listener(self.setup_devices): + c.add_devices_listener(self.setup_devices) + self.get_devices(c.devices) + + + def setup_macrobat_rack(self, rack): + """ Setup Macrobat rack if meets criteria """ + if rack.class_name.endswith('GroupDevice'): + name = self._parent.get_name(rack.name) + m = None + if name.startswith('NK RECEIVER'): + m = MacrobatReceiverRack(self._parent, rack, self._track) + elif name.startswith('NK TRACK') and not self._track.has_midi_output: + m = MacrobatTrackRack(self._parent, rack, self._track) + elif name.startswith('NK DR MULTI') and self._parent._can_have_nested_devices: + m = MacrobatDRMultiRack(self._parent, rack, self._track) + elif name.startswith('NK DR PAD MIX') and IS_LIVE_9_5: + m = MacrobatDRPadMixRack(self._parent, rack, self._track) + elif name.startswith('NK CHAIN MIX') and self._parent._can_have_nested_devices: + m = MacrobatChainMixRack(self._parent, rack, self._track) + elif name.startswith('NK DR') and self._parent._can_have_nested_devices: + m = MacrobatDRRack(self._parent, rack, self._track) + elif name.startswith('NK LEARN') and self._parent._can_have_nested_devices and self._track == self.song().master_track and not self._has_learn_rack: + m = MacrobatLearnRack(self._parent, rack, self._track) + self._has_learn_rack = True + elif name.startswith('NK MIDI'): + m = MacrobatMidiRack(self._parent, rack, name) + elif name.startswith(('NK RST', 'NK RND')): + m = MacrobatRnRRack(self._parent, rack, name, self._track) + elif name.startswith('NK SIDECHAIN'): + m = MacrobatSidechainRack(self._parent, rack, self._track) + elif name.startswith('NK SCL') and IS_LIVE_9_5: + m = MacrobatPushRack(self._parent, rack) + elif name.startswith('NK CS') and IS_LIVE_9: + m = MacrobatChainSelectorRack(self._parent, rack, self._track) + if m: + self._current_devices.append((m, rack)) + + + def remove_devices(self, dev_list): + """ Remove all device listeners """ + for d in dev_list: + if d.name_has_listener(self.setup_devices): + d.remove_name_listener(self.setup_devices) + if self._parent._can_have_nested_devices and d.can_have_chains: + if d.chains_has_listener(self.setup_devices): + d.remove_chains_listener(self.setup_devices) + for c in d.chains: + if c.devices_has_listener(self.setup_devices): + c.remove_devices_listener(self.setup_devices) + self.remove_devices(c.devices) + + + def on_selected_track_changed(self): + self.update() + + +# local variables: +# tab-width: 4 \ No newline at end of file diff --git a/ClyphX/MacrobatMidiRack.py b/ClyphX/MacrobatMidiRack.py new file mode 100644 index 0000000..31d4f1f --- /dev/null +++ b/ClyphX/MacrobatMidiRack.py @@ -0,0 +1,204 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +import Live +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent +from MacrobatUserConfig import * +from consts import IS_LIVE_9 + +class MacrobatMidiRack(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' Macros To Midi CCs + PCs + SysEx ' + + def __init__(self, parent, rack, name): + ControlSurfaceComponent.__init__(self) + self._parent = parent + self._macro_to_cc = [] + self._macro_to_pc = [] + self._macro_to_sysex = [] + self._sysex_list = [] + self.build_sysex_list() + self.setup_device(rack, name) + + + def disconnect(self): + self.remove_macro_listeners() + self._macro_to_cc = [] + self._macro_to_pc = [] + self._macro_to_sysex = [] + self._sysex_list = [] + self._parent = None + if IS_LIVE_9: + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def setup_device(self, rack, name): + """ - Rack name needs to start with 'nK MIDI' + - Default channel is 0. Can change with '[CHn]' in rack name + - Macro names needs to start with = functions: + * [CCn] = Where n is the CC# to send + * [PC] = Program Change + * SysEx_identifier = Identifier specified in SysEx List in user config """ + self.remove_macro_listeners() + channel = self.check_for_channel(name) + for p in rack.parameters: + if p.is_enabled: + name = self._parent.get_name(p.name) + if name.startswith('[CC') and not p.value_has_listener(self.do_cc): + cc_num = self.check_for_cc_num(name) + if cc_num != None: + self._macro_to_cc.append((p, cc_num, -1, rack, channel)) + p.add_value_listener(self.do_cc) + elif name.startswith('[PC]') and not p.value_has_listener(self.do_pc): + self._macro_to_pc.append((p, -1, rack, channel)) + p.add_value_listener(self.do_pc) + else: + sysex_entry = self.check_sysex_list(name) + if sysex_entry and not p.value_has_listener(self.do_sysex): + self._macro_to_sysex.append((p, sysex_entry, -1, rack)) + p.add_value_listener(self.do_sysex) + + + def do_cc(self): + """ Send out CC on macro value change """ + if self._macro_to_cc: + for p in self._macro_to_cc: + if int(p[0].value) != p[2]: + self._parent._send_midi((int(176 + p[4]), p[1], int(p[0].value))) + self._macro_to_cc[self._macro_to_cc.index(p)] = ((p[0], p[1], int(p[0].value), p[3], p[4])) + + + def do_pc(self): + """ Send out PC on macro value change """ + if self._macro_to_pc: + for p in self._macro_to_pc: + if int(p[0].value) != p[1]: + self._parent._send_midi((int(192 + p[3]), int(p[0].value))) + self._macro_to_pc[self._macro_to_pc.index(p)] = ((p[0], int(p[0].value), p[2], p[3])) + + + def do_sysex(self): + """ Send out SysEx on macro value change """ + if self._macro_to_sysex: + for p in self._macro_to_sysex: + if int(p[0].value) != p[2]: + new_string = [] + send_new_val = True + for byte in p[1][0]: + if byte == -1: + new_val = int((((p[1][2] - p[1][1]) / 127.0) * int(p[0].value)) + p[1][1]) + if int((((p[1][2] - p[1][1]) / 127.0) * p[2]) + p[1][1]) != new_val: + new_string.append(new_val) + else: + send_new_val = False + else: + new_string.append(byte) + if send_new_val: + self._parent._send_midi(tuple(new_string)) + self._macro_to_sysex[self._macro_to_sysex.index(p)] = ((p[0], p[1], int(p[0].value), p[3])) + + + def build_sysex_list(self): + """ Build SysEx list (in decimal) based on user-defined list """ + self._sysex_list = [] + if SYSEX_LIST: + for s in SYSEX_LIST: + if len(s) == 4: + bytes = s[1].split() + current_entry = [] + if bytes[0] == 'F0' and bytes[-1] == 'F7' and s[2] in range (128) and s[3] in range (128): + for byte in bytes: + if byte == 'nn': + current_entry.append(-1) + else: + if int(byte, 16) in range (248): + current_entry.append(int(byte, 16)) + self._sysex_list.append((s[0], current_entry, s[2], s[3])) + + + def check_sysex_list(self, name_string): + """ Check that SysEx list exists and identifier exists in list """ + result = None + if self._sysex_list: + for entry in self._sysex_list: + if self._parent.get_name(entry[0]) == name_string: + result = [entry[1], entry[2], entry[3]] + return result + + + def check_for_channel(self, name): + """ Check for user-specified channel in rack name """ + result = 0 + if '[CH' in name and ']' in name and not name.count('[') > 1 and not name.count(']') > 1: + try: + get_ch = int(name[name.index('[')+3:name.index(']')]) + if get_ch in range (1, 17): + result = get_ch - 1 + except: + pass + return result + + + def check_for_cc_num(self, name): + """ Check for user-specified CC# in macro name """ + result = None + if '[CC' in name and ']' in name and not name.count('[') > 1 and not name.count(']') > 1: + try: + get_cc = int(name[name.index('[')+3:name.index(']')]) + if get_cc in range (128): + result = get_cc + except: + pass + return result + + + def remove_macro_listeners(self): + """ Remove listeners """ + if self._macro_to_cc: + for p in self._macro_to_cc: + if p[3] and p[0].value_has_listener(self.do_cc): + p[0].remove_value_listener(self.do_cc) + self._macro_to_cc = [] + if self._macro_to_pc: + for p in self._macro_to_pc: + if p[2] and p[0].value_has_listener(self.do_pc): + p[0].remove_value_listener(self.do_pc) + self._macro_to_pc = [] + if self._macro_to_sysex: + for p in self._macro_to_sysex: + if p[3] and p[0].value_has_listener(self.do_sysex): + p[0].remove_value_listener(self.do_sysex) + self._macro_to_sysex = [] + + +# local variables: +# tab-width: 4 \ No newline at end of file diff --git a/ClyphX/MacrobatParameterRackTemplate8.py b/ClyphX/MacrobatParameterRackTemplate8.py new file mode 100644 index 0000000..dc92bbc --- /dev/null +++ b/ClyphX/MacrobatParameterRackTemplate8.py @@ -0,0 +1,205 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +import Live +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent + +class MacrobatParameterRackTemplate(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' Template for Macrobat racks that control parameters in Live 8 ' + + def __init__(self, parent, rack, track): + self._parent = parent + ControlSurfaceComponent.__init__(self) + self._register_timer_callback(self.on_timer) + self._has_timer = True + self._on_off_param = [] + self._param_macros = {} + self._update_macro = 0 + self._update_param = 0 + self._param_update_in_progress = True + self._macro_update_in_progress = True + self._parent.schedule_message(8, self.allow_macro_updates) + self._parent.schedule_message(8, self.allow_param_updates) + self._get_initial_value = False + self._track = track + self.setup_device(rack) + + + def disconnect(self): + self.remove_macro_listeners() + if self._has_timer: + self._unregister_timer_callback(self.on_timer) + self._has_timer = False + self._on_off_param = [] + self._param_macros = {} + self._track = None + self._parent = None + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def setup_device(self, rack): + """ Remove any current listeners and set up listener for on/off (used for resetting assigned params) """ + self.remove_macro_listeners() + if not rack.parameters[0].value_has_listener(self.on_off_changed): + self._on_off_param = [rack.parameters[0], rack.parameters[0].value] + rack.parameters[0].add_value_listener(self.on_off_changed) + + + def macro_changed(self, index): + """ Called on macro changes to update param values """ + if not self._update_macro and not self._macro_update_in_progress: + if self._param_macros.has_key(index) and self._param_macros[index][0] and self._param_macros[index][1]: + scaled_value = self.scale_param_value_to_macro(self._param_macros[index][1]) + if scaled_value != self._param_macros[index][0].value: + self._update_param = index + self._param_update_in_progress = True + self._parent.schedule_message(8, self.allow_macro_updates) + + + def param_changed(self, index): + """ Called on param changes to update macros """ + if not self._update_param and not self._param_update_in_progress: + if self._param_macros.has_key(index) and self._param_macros[index][0] and self._param_macros[index][1]: + scaled_value = self.scale_macro_value_to_param(self._param_macros[index][0], self._param_macros[index][1]) + if scaled_value != self._param_macros[index][1].value: + self._update_macro = index + self._macro_update_in_progress = True + self._parent.schedule_message(8, self.allow_param_updates) + + + def on_off_changed(self): + """ On/off changed, schedule param reset """ + if self._on_off_param and self._on_off_param[0]: + if self._on_off_param[0].value != self._on_off_param[1] and self._on_off_param[0].value == 1.0: + self._parent.schedule_message(1, self.do_reset) + self._on_off_param[1] = self._on_off_param[0].value + + + def do_reset(self): + """ Reset assigned params to default """ + self._update_param = False + self._update_macro = False + self._param_update_in_progress = False + self._macro_update_in_progress = False + for k, v in self._param_macros.items(): + if v[1] and not v[1].is_quantized and v[1].name != 'Chain Selector': + v[1].value = v[1].default_value + v[0].value = self.scale_param_value_to_macro(v[1]) + + + def on_timer(self): + """ Handle updating values and getting initial values """ + if not self._get_initial_value: + if self._update_macro and not self._update_param: + if self._param_macros.has_key(self._update_macro): + if self._param_macros[self._update_macro][0] and self._param_macros[self._update_macro][1]: + self._param_macros[self._update_macro][0].value = self.scale_param_value_to_macro(self._param_macros[self._update_macro][1]) + self._update_macro = 0 + self._parent.schedule_message(8, self.allow_param_updates) + elif self._update_param and not self._update_macro: + if self._param_macros.has_key(self._update_param): + if self._param_macros[self._update_param][0] and self._param_macros[self._update_param][1]: + self._param_macros[self._update_param][1].value = self.scale_macro_value_to_param(self._param_macros[self._update_param][0], self._param_macros[self._update_param][1]) + self._update_param = 0 + self._parent.schedule_message(8, self.allow_macro_updates) + else: + for index in range(1,9): + if self._param_macros.has_key(index): + if self._param_macros[index][0] and self._param_macros[index][1]: + if self._param_macros[index][0].value != self.scale_param_value_to_macro(self._param_macros[index][1]): + self._param_macros[index][0].value = self.scale_param_value_to_macro(self._param_macros[index][1]) + self._get_initial_value = False + + + def scale_macro_value_to_param(self, macro, param): + return (((param.max - param.min) / 127.0) * macro.value) + param.min + + + def scale_param_value_to_macro(self, param): + return int(((param.value - param.min) / (param.max - param.min)) * 127.0) + + + def allow_macro_updates(self): + """ Used to prevent param change getting triggered while param update is in progress (new issue in 8.2.2) """ + if not self._update_param: + self._param_update_in_progress = False + + + def allow_param_updates(self): + """ Used to prevent macro change getting triggered while macro update is in progress (new issue in 8.2.2) """ + if not self._update_macro: + self._macro_update_in_progress = False + + + def get_drum_rack(self): + """ For use with DR racks, get drum rack to operate on as well as the params of any simplers/samplers in the rack """ + drum_rack = {} + drum_rack['devs_by_index'] = {} + drum_rack['devs_by_name'] = {} + if self._track and self._track.devices: + for d in self._track.devices: + if d.class_name == 'DrumGroupDevice': + drum_rack['rack'] = d + rack_devices_by_index = {} + rack_devices_by_name = {} + for chain_index in range (len(d.chains)): + for device in d.chains[chain_index].devices: + if device.class_name in ('OriginalSimpler', 'MultiSampler'): + current_params = {} + for p in device.parameters: + current_params[str(p.name).upper()] = p + rack_devices_by_index[str(chain_index + 1)] = current_params + rack_devices_by_name[str(device.name)] = current_params + break + drum_rack['devs_by_index'] = rack_devices_by_index + drum_rack['devs_by_name'] = rack_devices_by_name + break + return drum_rack + + + def remove_macro_listeners(self): + for index in range(1,9): + if self._param_macros.has_key(index): + m_listener = lambda index = index:self.macro_changed(index) + p_listener = lambda index = index:self.param_changed(index) + if self._param_macros[index][0] and self._param_macros[index][0].value_has_listener(m_listener): + self._param_macros[index][0].remove_value_listener(m_listener) + if self._param_macros[index][1] and self._param_macros[index][1].value_has_listener(p_listener): + self._param_macros[index][1].remove_value_listener(p_listener) + self._param_macros = {} + if self._on_off_param and self._on_off_param[0] and self._on_off_param[0].value_has_listener(self.on_off_changed): + self._on_off_param[0].remove_value_listener(self.on_off_changed) + self._on_off_param = [] + + +# local variables: +# tab-width: 4 \ No newline at end of file diff --git a/ClyphX/MacrobatParameterRackTemplate9.py b/ClyphX/MacrobatParameterRackTemplate9.py new file mode 100644 index 0000000..2ca6f55 --- /dev/null +++ b/ClyphX/MacrobatParameterRackTemplate9.py @@ -0,0 +1,187 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +import Live +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent + +class MacrobatParameterRackTemplate(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' Template for Macrobat racks that control parameters in Live 9 ' + + def __init__(self, parent, rack, track): + self._parent = parent + ControlSurfaceComponent.__init__(self) + self._on_off_param = [] + self._param_macros = {} + self._update_macro = 0 + self._update_param = 0 + self._track = track + self.setup_device(rack) + + + def disconnect(self): + self.remove_macro_listeners() + self._on_off_param = [] + self._param_macros = {} + self._track = None + self._parent = None + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def setup_device(self, rack): + """ Remove any current listeners and set up listener for on/off (used for resetting assigned params) """ + self.remove_macro_listeners() + if not rack.parameters[0].value_has_listener(self.on_off_changed): + self._on_off_param = [rack.parameters[0], rack.parameters[0].value] + rack.parameters[0].add_value_listener(self.on_off_changed) + + + def macro_changed(self, index): + """ Called on macro changes to update param values """ + if self._param_macros.has_key(index) and self._param_macros[index][0] and self._param_macros[index][1]: + scaled_value = self.scale_param_value_to_macro(self._param_macros[index][1]) + if scaled_value != self._param_macros[index][0].value: + self._update_param = index + self._tasks.kill() + self._tasks.clear() + self._tasks.add(self.update_param) + + + def param_changed(self, index): + """ Called on param changes to update macros """ + if self._param_macros.has_key(index) and self._param_macros[index][0] and self._param_macros[index][1]: + scaled_value = self.scale_macro_value_to_param(self._param_macros[index][0], self._param_macros[index][1]) + if scaled_value != self._param_macros[index][1].value: + self._update_macro = index + self._tasks.kill() + self._tasks.clear() + self._tasks.add(self.update_macro) + + + def update_param(self, arg=None): + """ Update param to match value of macro. """ + if self._param_macros.has_key(self._update_param): + if self._param_macros[self._update_param][0] and self._param_macros[self._update_param][1]: + self._param_macros[self._update_param][1].value = self.scale_macro_value_to_param(self._param_macros[self._update_param][0], self._param_macros[self._update_param][1]) + self._tasks.kill() + self._tasks.clear() + + + def update_macro(self, arg=None): + """ Update macro to match value of param. """ + if self._param_macros.has_key(self._update_macro): + if self._param_macros[self._update_macro][0] and self._param_macros[self._update_macro][1]: + self._param_macros[self._update_macro][0].value = self.scale_param_value_to_macro(self._param_macros[self._update_macro][1]) + self._tasks.kill() + self._tasks.clear() + + + def get_initial_value(self, arg=None): + """ Get initial values to set macros to. """ + for index in range(1,9): + if self._param_macros.has_key(index): + if self._param_macros[index][0] and self._param_macros[index][1]: + if self._param_macros[index][0].value != self.scale_param_value_to_macro(self._param_macros[index][1]): + self._param_macros[index][0].value = self.scale_param_value_to_macro(self._param_macros[index][1]) + + + def on_off_changed(self): + """ On/off changed, schedule param reset """ + if self._on_off_param and self._on_off_param[0]: + if self._on_off_param[0].value != self._on_off_param[1] and self._on_off_param[0].value == 1.0: + self._parent.schedule_message(1, self.do_reset) + self._on_off_param[1] = self._on_off_param[0].value + + + def do_reset(self): + """ Reset assigned params to default """ + self._update_param = 0 + self._update_macro = 0 + self._tasks.kill() + self._tasks.clear() + for k, v in self._param_macros.items(): + if v[1] and not v[1].is_quantized and v[1].name != 'Chain Selector': + v[1].value = v[1].default_value + v[0].value = self.scale_param_value_to_macro(v[1]) + + + def scale_macro_value_to_param(self, macro, param): + return (((param.max - param.min) / 127.0) * macro.value) + param.min + + + def scale_param_value_to_macro(self, param): + return int(((param.value - param.min) / (param.max - param.min)) * 127.0) + + + def get_drum_rack(self): + """ For use with DR racks, get drum rack to operate on as well as the params of any simplers/samplers in the rack """ + drum_rack = {} + drum_rack['devs_by_index'] = {} + drum_rack['devs_by_name'] = {} + if self._track and self._track.devices: + for d in self._track.devices: + if d.class_name == 'DrumGroupDevice': + drum_rack['rack'] = d + rack_devices_by_index = {} + rack_devices_by_name = {} + for chain_index in range (len(d.chains)): + for device in d.chains[chain_index].devices: + if device.class_name in ('OriginalSimpler', 'MultiSampler'): + current_params = {} + for p in device.parameters: + current_params[str(p.name).upper()] = p + rack_devices_by_index[str(chain_index + 1)] = current_params + rack_devices_by_name[str(device.name)] = current_params + break + drum_rack['devs_by_index'] = rack_devices_by_index + drum_rack['devs_by_name'] = rack_devices_by_name + break + return drum_rack + + + def remove_macro_listeners(self): + for index in range(1,9): + if self._param_macros.has_key(index): + m_listener = lambda index = index:self.macro_changed(index) + p_listener = lambda index = index:self.param_changed(index) + if self._param_macros[index][0] and self._param_macros[index][0].value_has_listener(m_listener): + self._param_macros[index][0].remove_value_listener(m_listener) + if self._param_macros[index][1] and self._param_macros[index][1].value_has_listener(p_listener): + self._param_macros[index][1].remove_value_listener(p_listener) + self._param_macros = {} + if self._on_off_param and self._on_off_param[0] and self._on_off_param[0].value_has_listener(self.on_off_changed): + self._on_off_param[0].remove_value_listener(self.on_off_changed) + self._on_off_param = [] + + +# local variables: +# tab-width: 4 \ No newline at end of file diff --git a/ClyphX/MacrobatParameterRacks.py b/ClyphX/MacrobatParameterRacks.py new file mode 100644 index 0000000..78b7a2f --- /dev/null +++ b/ClyphX/MacrobatParameterRacks.py @@ -0,0 +1,334 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +#---This module contains Learn, Chain Mix, DR, DR Multi, Receiver and Track Racks + +from _Generic.Devices import * +from consts import * +if IS_LIVE_9: + from functools import partial + from MacrobatParameterRackTemplate9 import MacrobatParameterRackTemplate +else: + from MacrobatParameterRackTemplate8 import MacrobatParameterRackTemplate + +LAST_PARAM = {} + +class MacrobatLearnRack(MacrobatParameterRackTemplate): + __module__ = __name__ + __doc__ = ' Macro 1 to learned param ' + + def __init__(self, parent, rack, track): + self._rack = rack + #---delay adding listener to prevent issue with change on set load + if IS_LIVE_9: + parent.schedule_message(8, partial(parent.song().view.add_selected_parameter_listener, self.on_selected_parameter_changed)) + else: + parent.schedule_message(8, parent.song().view.add_selected_parameter_listener, self.on_selected_parameter_changed) + MacrobatParameterRackTemplate.__init__(self, parent, rack, track) + + + def disconnect(self): + if self.song().view.selected_parameter_has_listener(self.on_selected_parameter_changed): + self.song().view.remove_selected_parameter_listener(self.on_selected_parameter_changed) + self._rack = None + MacrobatParameterRackTemplate.disconnect(self) + + + def setup_device(self, rack): + """ Set up macro 1 and learned param """ + MacrobatParameterRackTemplate.setup_device(self, rack) + self._rack = rack + param = self.song().view.selected_parameter + if LAST_PARAM and LAST_PARAM.has_key(0): + param = LAST_PARAM[0] + if self._rack and param: + if self._rack.parameters[1].is_enabled and param.is_enabled: + index = 1 + m_listener = lambda index = index:self.macro_changed(index) + self._rack.parameters[1].add_value_listener(m_listener) + p_listener = lambda index = index:self.param_changed(index) + param.add_value_listener(p_listener) + self._param_macros[index] = (self._rack.parameters[1], param) + if IS_LIVE_9: + self._tasks.add(self.get_initial_value) + else: + self._get_initial_value = True + + + def on_selected_parameter_changed(self): + """ Update rack on new param selected """ + if self.song().view.selected_parameter and self._rack and not self.song().view.selected_parameter.canonical_parent == self._rack: + LAST_PARAM[0] = self.song().view.selected_parameter + self.setup_device(self._rack) + + +class MacrobatChainMixRack(MacrobatParameterRackTemplate): + __module__ = __name__ + __doc__ = ' Macros to params of Rack chains ' + + def __init__(self, parent, rack, track): + self._rack = {} + MacrobatParameterRackTemplate.__init__(self, parent, rack, track) + + + def disconnect(self): + self._rack = None + MacrobatParameterRackTemplate.disconnect(self) + + + def setup_device(self, rack): + """ Set up macros and rack chain params """ + MacrobatParameterRackTemplate.setup_device(self, rack) + self._rack = self.get_rack() + if self._rack: + param_name = self._parent.get_name(rack.name[12:].strip()) + for index in range(1,9): + chain_to_edit = {} + macro = rack.parameters[index] + param = None + if macro.is_enabled: + chain_name = self._parent.get_name(macro.name) + if self._rack.has_key(chain_name): + chain_to_edit = self._rack[chain_name] + if chain_to_edit.has_key(param_name): + param = chain_to_edit[param_name] + if param and param.is_enabled: + m_listener = lambda index = index:self.macro_changed(index) + macro.add_value_listener(m_listener) + p_listener = lambda index = index:self.param_changed(index) + param.add_value_listener(p_listener) + self._param_macros[index] = (macro, param) + if IS_LIVE_9: + self._tasks.add(self.get_initial_value) + else: + self._get_initial_value = True + + + def get_rack(self): + """ Get rack to operate on as well as the mixer params of its chains """ + rack_chains = {} + if self._track and self._track.devices: + for d in self._track.devices: + if d.class_name.endswith('GroupDevice') and not d.class_name.startswith('Midi'): + for chain_index in range (len(d.chains)): + c = d.chains[chain_index] + rack_chains[str(chain_index + 1)] = {'VOL' : c.mixer_device.volume, 'PAN' : c.mixer_device.panning, 'MUTE' : c.mixer_device.chain_activator} + break + return rack_chains + + +class MacrobatDRMultiRack(MacrobatParameterRackTemplate): + __module__ = __name__ + __doc__ = ' Macros to params of multiple Simplers/Samplers ' + + def __init__(self, parent, rack, track): + self._drum_rack = {} + MacrobatParameterRackTemplate.__init__(self, parent, rack, track) + + + def disconnect(self): + self._drum_rack = None + MacrobatParameterRackTemplate.disconnect(self) + + + def setup_device(self, rack): + """ Set up macros and drum rack params """ + MacrobatParameterRackTemplate.setup_device(self, rack) + self._drum_rack = self.get_drum_rack() + if self._drum_rack: + param_name = self._parent.get_name(rack.name[11:].strip()) + for index in range(1,9): + drum_to_edit = {} + macro = rack.parameters[index] + param = None + if macro.is_enabled: + drum_name = macro.name + if self._drum_rack['devs_by_index'].has_key(drum_name): + drum_to_edit = self._drum_rack['devs_by_index'][drum_name] + elif self._drum_rack['devs_by_name'].has_key(drum_name): + drum_to_edit = self._drum_rack['devs_by_name'][drum_name] + if drum_to_edit.has_key(param_name): + param = drum_to_edit[param_name] + if param and param.is_enabled: + m_listener = lambda index = index:self.macro_changed(index) + macro.add_value_listener(m_listener) + p_listener = lambda index = index:self.param_changed(index) + param.add_value_listener(p_listener) + self._param_macros[index] = (macro, param) + if IS_LIVE_9: + self._tasks.add(self.get_initial_value) + else: + self._get_initial_value = True + + +class MacrobatDRRack(MacrobatParameterRackTemplate): + __module__ = __name__ + __doc__ = ' Macros to params of single Simpler/Sampler ' + + def __init__(self, parent, rack, track): + self._drum_rack = {} + MacrobatParameterRackTemplate.__init__(self, parent, rack, track) + + + def disconnect(self): + self._drum_rack = None + MacrobatParameterRackTemplate.disconnect(self) + + + def setup_device(self, rack): + """ Set up macros and drum rack params """ + MacrobatParameterRackTemplate.setup_device(self, rack) + self._drum_rack = self.get_drum_rack() + if self._drum_rack: + drum_name = rack.name[5:].strip() + drum_to_edit = {} + if self._drum_rack['devs_by_index'].has_key(drum_name): + drum_to_edit = self._drum_rack['devs_by_index'][drum_name] + elif self._drum_rack['devs_by_name'].has_key(drum_name): + drum_to_edit = self._drum_rack['devs_by_name'][drum_name] + for index in range(1,9): + macro = rack.parameters[index] + param = None + if macro.is_enabled: + name = self._parent.get_name(macro.name) + if drum_to_edit.has_key(name): + param = drum_to_edit[name] + if param and param.is_enabled: + m_listener = lambda index = index:self.macro_changed(index) + macro.add_value_listener(m_listener) + p_listener = lambda index = index:self.param_changed(index) + param.add_value_listener(p_listener) + self._param_macros[index] = (macro, param) + if IS_LIVE_9: + self._tasks.add(self.get_initial_value) + else: + self._get_initial_value = True + + +class MacrobatReceiverRack(MacrobatParameterRackTemplate): + __module__ = __name__ + __doc__ = ' Macros to macros of other racks ' + + def __init__(self, parent, rack, track): + MacrobatParameterRackTemplate.__init__(self, parent, rack, track) + + + def setup_device(self, rack): + """ Set up receiver and send macros """ + MacrobatParameterRackTemplate.setup_device(self, rack) + receiver_macros = self.get_ident_macros(rack) + if receiver_macros: + self._sender_macros = [] + for t in (tuple(self.song().tracks) + tuple(self.song().return_tracks) + (self.song().master_track,)): + self.get_sender_macros(t.devices) + if self._sender_macros: + for r in receiver_macros: + index = 0 + for s in self._sender_macros: + index += 1 + if r[0] == s[0] and r[1].is_enabled and s[1].is_enabled: + r_listener = lambda index = index:self.macro_changed(index) + r[1].add_value_listener(r_listener) + s_listener = lambda index = index:self.param_changed(index) + s[1].add_value_listener(s_listener) + self._param_macros[index] = (r[1], s[1]) + if IS_LIVE_9: + self._tasks.add(self.get_initial_value) + else: + self._get_initial_value = True + + + def get_sender_macros(self, dev_list): + """ Look through all devices/chains on all tracks for sender macros """ + for d in dev_list: + name = self._parent.get_name(d.name) + if d and d.class_name.endswith('GroupDevice') and not name.startswith('NK RECEIVER'): + self._sender_macros.extend(self.get_ident_macros(d)) + if self._parent._can_have_nested_devices and d.can_have_chains: + for c in d.chains: + self.get_sender_macros(c.devices) + + + def get_ident_macros(self, rack): + """ Get send and receiver macros """ + ident_macros = [] + ident_names = [] + for macro in rack.parameters: + name = self._parent.get_name(macro.name) + if macro.original_name.startswith('Macro') and '(*' in name and '*)' in name and len(name) > 4: + if not name.count('(') > 1 and not name.count(')') > 1: + ident = name[name.index('(') + 2:name.index(')') - 1] + if ident not in ident_names: + ident_macros.append((ident, macro)) + ident_names.append(ident) + return ident_macros + + + def on_off_changed(self): + """ Receiver rack doesn't do reset """ + pass + + +class MacrobatTrackRack(MacrobatParameterRackTemplate): + __module__ = __name__ + __doc__ = ' Macros to track params ' + + def __init__(self, parent, rack, track): + self._rack = rack + MacrobatParameterRackTemplate.__init__(self, parent, rack, track) + + + def disconnect(self): + self._rack = None + MacrobatParameterRackTemplate.disconnect(self) + + + def setup_device(self, rack): + """ Setup macros and track mixer params """ + MacrobatParameterRackTemplate.setup_device(self, rack) + for index in range(1,9): + macro = rack.parameters[index] + param = None + if macro.is_enabled: + name = self._parent.get_name(macro.name) + if name.startswith('SEND') and not self._track == self.song().master_track: + try: param = self._track.mixer_device.sends[ord(name[5]) - 65] + except: param = None + elif name.startswith('VOL'): + param = self._track.mixer_device.volume + elif name.startswith('PAN'): + param = self._track.mixer_device.panning + if param and param.is_enabled: + m_listener = lambda index = index:self.macro_changed(index) + macro.add_value_listener(m_listener) + p_listener = lambda index = index:self.param_changed(index) + param.add_value_listener(p_listener) + self._param_macros[index] = (macro, param) + if IS_LIVE_9: + self._tasks.add(self.get_initial_value) + else: + self._get_initial_value = True + +# local variables: +# tab-width: 4 \ No newline at end of file diff --git a/ClyphX/MacrobatParameterRacks9.py b/ClyphX/MacrobatParameterRacks9.py new file mode 100644 index 0000000..b18e63a --- /dev/null +++ b/ClyphX/MacrobatParameterRacks9.py @@ -0,0 +1,201 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +#---This module contains CS and DR Pad Mix Rack. + +from _Generic.Devices import * +from consts import * +from functools import partial +from MacrobatParameterRackTemplate9 import MacrobatParameterRackTemplate +from _Framework.SubjectSlot import Subject, SlotManager, subject_slot + + +class MacrobatDRPadMixRack(MacrobatParameterRackTemplate): + + __module__ = __name__ + __doc__ = ' Macros to mixer params of selected DR pad ' + + def __init__(self, parent, rack, track): + self._drum_rack = {} + self._rack = None + self._selected_chain = None + MacrobatParameterRackTemplate.__init__(self, parent, rack, track) + + + def disconnect(self): + self._drum_rack = None + self._rack = None + self._selected_chain = None + MacrobatParameterRackTemplate.disconnect(self) + + + def setup_device(self, rack): + """ Set up macros and drum rack params """ + MacrobatParameterRackTemplate.setup_device(self, rack) + self._rack = rack + self._drum_rack = self.get_drum_rack() + self._selected_chain = None + self._on_sends_changed.subject = None + self._on_selected_pad_changed.subject = None + if self._drum_rack: + self._on_selected_pad_changed.subject = self._drum_rack['rack'].view + self._set_selected_chain() + if self._selected_chain: + num_sends = len(self._selected_chain.mixer_device.sends) + for index in range(1,9): + if rack.parameters[index].is_enabled: + param = None + if index == 1: + param = self._selected_chain.mixer_device.volume + elif index == 2: + param = self._selected_chain.mixer_device.panning + else: + s_index = index - 3 + if s_index < num_sends: + param = self._selected_chain.mixer_device.sends[s_index] + if param and param.is_enabled: + macro = rack.parameters[index] + m_listener = lambda index = index:self.macro_changed(index) + macro.add_value_listener(m_listener) + p_listener = lambda index = index:self.param_changed(index) + param.add_value_listener(p_listener) + self._param_macros[index] = (macro, param) + self._tasks.add(self.get_initial_value) + + + @subject_slot('selected_drum_pad') + def _on_selected_pad_changed(self): + self.setup_device(self._rack) + + + @subject_slot('sends') + def _on_sends_changed(self): + self.setup_device(self._rack) + + + def _set_selected_chain(self): + self._selected_chain = None + self._on_sends_changed.subject = None + if self._drum_rack: + sel_pad = self._drum_rack['rack'].view.selected_drum_pad + if sel_pad and sel_pad.chains: + self._selected_chain = sel_pad.chains[0] + self._on_sends_changed.subject = self._selected_chain.mixer_device + + +class CSWrapper(Subject, SlotManager): + """ Wrapper for a chain selector that limits the max value to the number + of chains in the rack. """ + + __subject_events__ = ('value',) + + def __init__(self, cs): + super(CSWrapper, self).__init__() + self._cs = cs + self._max = 0 + self._on_cs_value_changed.subject = self._cs + + @property + def is_enabled(self): + return self._cs.is_enabled + + def _get_value(self): + return min(self._cs.value, self._max) + + def _set_value(self, value): + if value <= self._max: + self._cs.value = value + + value = property(_get_value, _set_value) + + @property + def min(self): + return self._cs.min + + def _get_max(self): + return self._max + + def _set_max(self, value): + self._max = value + + max = property(_get_max, _set_max) + + @subject_slot('value') + def _on_cs_value_changed(self): + if self._cs.value <= self._max: + self.notify_value() + + +class MacrobatChainSelectorRack(MacrobatParameterRackTemplate): + __module__ = __name__ + __doc__ = ' Macro 1 to chain selector' + + def __init__(self, parent, rack, track): + self._rack = rack + self._wrapper = None + MacrobatParameterRackTemplate.__init__(self, parent, rack, track) + + + def disconnect(self): + self._rack = None + self._wrapper = None + MacrobatParameterRackTemplate.disconnect(self) + + + def scale_macro_value_to_param(self, macro, param): + return (((param.max - param.min) / 126.0) * macro.value) + param.min + + + def setup_device(self, rack): + """ Set up macro 1 and chain selector """ + MacrobatParameterRackTemplate.setup_device(self, rack) + self._rack = rack + if self._rack: + macro = self._rack.parameters[1] + cs = self._rack.parameters[9] + if macro.is_enabled and cs.is_enabled: + self._wrapper = CSWrapper(cs) + self._wrapper.max = len(self._rack.chains) + if self._wrapper.max > 1: + self._on_chains_changed.subject = self._rack + index = 1 + m_listener = lambda index = index:self.macro_changed(index) + macro.add_value_listener(m_listener) + p_listener = lambda index = index:self.param_changed(index) + self._wrapper.add_value_listener(p_listener) + self._param_macros[index] = (macro, self._wrapper) + if IS_LIVE_9: + self._tasks.add(self.get_initial_value) + else: + self._get_initial_value = True + + + @subject_slot('chains') + def _on_chains_changed(self): + if self._wrapper and self._rack: + self._wrapper.max = len(self._rack.chains) + self.param_changed(0) + +# local variables: +# tab-width: 4 \ No newline at end of file diff --git a/ClyphX/MacrobatPushRack.py b/ClyphX/MacrobatPushRack.py new file mode 100644 index 0000000..42c6cfb --- /dev/null +++ b/ClyphX/MacrobatPushRack.py @@ -0,0 +1,142 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +from __future__ import with_statement +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent +from consts import NOTE_NAMES + +class MacrobatPushRack(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' Sets up Macros 1 and 2 to control Push root note and scale type respectively. ' + + def __init__(self, parent, rack): + ControlSurfaceComponent.__init__(self) + self._parent = parent + self._rack = rack + self._script = None + self._push_ins = self._connect_to_push() + self.setup_device() + + + def disconnect(self): + self.remove_macro_listeners() + self._rack = None + self._script = None + self._push_ins = None + self._parent = None + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + self._push_ins = self._connect_to_push() + + + def setup_device(self): + """ Rack names needs to start with nK SCL and Push needs to be selected as a control surface. """ + self._push_ins = self._connect_to_push() + self.remove_macro_listeners() + if self._rack: + if self._rack.parameters[1].is_enabled: + self._rack.parameters[1].add_value_listener(self._on_macro_one_value) + if self._rack.parameters[2].is_enabled: + self._rack.parameters[2].add_value_listener(self._on_macro_two_value) + self._parent.schedule_message(1, self._update_rack_name) + + + def _connect_to_push(self): + """ Attempt to connect to Push. """ + if self._parent: + for script in self._parent._control_surfaces(): + script_name = script.__class__.__name__ + if script_name == 'Push': + self._script = script + for c in script.components: + comp_name = c.__class__.__name__ + if comp_name == 'InstrumentComponent': + return c + return None + + + def _on_macro_one_value(self): + """ Set Push root note and update rack name. """ + self._tasks.add(self._handle_root_note_change) + + + def _handle_root_note_change(self, args=None): + if self._push_ins: + new_root = self.scale_macro_value_to_param(self._rack.parameters[1], 12) + if new_root != self._push_ins._note_layout.root_note: + self._push_ins._note_layout.root_note = new_root + self._update_scale_display_and_buttons() + self._parent.schedule_message(1, self._update_rack_name) + + + def _on_macro_two_value(self): + """ Set Push scale type and update rack name. """ + self._tasks.add(self._handle_scale_type_change) + + + def _handle_scale_type_change(self, args=None): + if self._push_ins: + mode_list = self._script._scales_enabler._mode_map['enabled'].mode._component._scale_list.scrollable_list + current_type = self._script._scales_enabler._mode_map['enabled'].mode._component._scale_list.scrollable_list.selected_item_index + new_type = self.scale_macro_value_to_param(self._rack.parameters[2], len(mode_list.items)) + if new_type != current_type: + mode_list._set_selected_item_index(new_type) + self._update_scale_display_and_buttons() + self._parent.schedule_message(1, self._update_rack_name) + + + def _update_scale_display_and_buttons(self): + """ Updates Push's scale display and buttons to indicate current settings. """ + self._script._scales_enabler._mode_map['enabled'].mode._component._update_data_sources() + self._script._scales_enabler._mode_map['enabled'].mode._component.update() + + + def _update_rack_name(self): + """ Update rack name to reflect selected root note and scale type. """ + if self._rack and self._push_ins: + self._rack.name = 'nK SCL - ' + str(NOTE_NAMES[self._push_ins._note_layout.root_note]) + ' - ' + str(self._push_ins._note_layout.scale.name) + + + def scale_macro_value_to_param(self, macro, hi_value): + """ Scale the value of the macro to the Push parameter being controlled. """ + return int((hi_value / 128.0) * macro.value) + + + def remove_macro_listeners(self): + """ Remove listeners """ + if self._rack: + if self._rack.parameters[1].value_has_listener(self._on_macro_one_value): + self._rack.parameters[1].remove_value_listener(self._on_macro_one_value) + if self._rack.parameters[2].value_has_listener(self._on_macro_two_value): + self._rack.parameters[2].remove_value_listener(self._on_macro_two_value) + + +# local variables: +# tab-width: 4 \ No newline at end of file diff --git a/ClyphX/MacrobatRnRRack.py b/ClyphX/MacrobatRnRRack.py new file mode 100644 index 0000000..333a173 --- /dev/null +++ b/ClyphX/MacrobatRnRRack.py @@ -0,0 +1,175 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +import Live +from consts import * +if IS_LIVE_9: + from functools import partial +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent + +class MacrobatRnRRack(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' Rack on/off to Device Randomize/Reset ' + + def __init__(self, parent, rack, name, track): + ControlSurfaceComponent.__init__(self) + self._parent = parent + self._on_off_param = [] + self._devices_to_operate_on = [] + self._track = track + self.setup_device(rack, name) + + + def disconnect(self): + self.remove_on_off_listeners() + self._on_off_param = [] + self._devices_to_operate_on = [] + self._track = None + self._parent = None + if IS_LIVE_9: + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def setup_device(self, rack, name): + """ - Will not reset/randomize any other Macrobat racks except for MIDI Rack + - Allowable rack names are: ['NK RST', 'NK RST ALL', 'NK RND', 'NK RND ALL'] """ + self.remove_on_off_listeners() + if rack: + for p in rack.parameters: + if p.name == 'Device On' and p.is_enabled: + if not p.value_has_listener(self.on_off_changed): + self._on_off_param = [p, name] + if IS_LIVE_9: + self._parent.schedule_message(5, partial(p.add_value_listener, self.on_off_changed))#---use this to get around device on/off switches getting turned on upon set load + else: + self._parent.schedule_message(5, p.add_value_listener, self.on_off_changed)#---use this to get around device on/off switches getting turned on upon set load + break + + + def on_off_changed(self): + """ On/off changed, perform assigned function """ + if self._on_off_param and self._on_off_param[0]: + mess_type = None + is_reset = False + if self._on_off_param[1].startswith('NK RND ALL'): + mess_type = 'all' + elif self._on_off_param[1].startswith('NK RND'): + mess_type = 'next' + elif self._on_off_param[1].startswith('NK RST ALL'): + mess_type = 'all' + is_reset = True + elif self._on_off_param[1].startswith('NK RST'): + mess_type = 'next' + is_reset = True + if mess_type: + if IS_LIVE_9: + if is_reset: + self._parent.schedule_message(1, partial(self.do_device_reset, mess_type)) + else: + self._parent.schedule_message(1, partial(self.do_device_randomize, mess_type)) + else: + if is_reset: + self._parent.schedule_message(1, self.do_device_reset, mess_type) + else: + self._parent.schedule_message(1, self.do_device_randomize, mess_type) + + + def do_device_randomize(self, params): + """ Randomize device params """ + if self._on_off_param and self._on_off_param[0]: + self._devices_to_operate_on = [] + self.get_devices_to_operate_on(self._on_off_param[0].canonical_parent.canonical_parent.devices, params) + if self._devices_to_operate_on: + for d in self._devices_to_operate_on: + for p in d.parameters: + if p.is_enabled and not p.is_quantized and p.name != 'Chain Selector': + p.value = (((p.max - p.min) / 127) * Live.Application.get_random_int(0, 128)) + p.min + + + def do_device_reset(self, params): + """ Reset device params """ + if self._on_off_param and self._on_off_param[0]: + self._devices_to_operate_on = [] + self.get_devices_to_operate_on(self._on_off_param[0].canonical_parent.canonical_parent.devices, params) + if self._devices_to_operate_on: + for d in self._devices_to_operate_on: + for p in d.parameters: + if p and p.is_enabled and not p.is_quantized and p.name != 'Chain Selector': + p.value = p.default_value + + + def get_devices_to_operate_on(self, dev_list, devices_to_get): + """ Get next device on track, all devices on track or all devices on chain """ + if devices_to_get == 'all': + if self._parent._can_have_nested_devices and type(self._on_off_param[0].canonical_parent.canonical_parent) == Live.Chain.Chain: + dev_list = self._on_off_param[0].canonical_parent.canonical_parent.devices + for d in dev_list: + name = self._parent.get_name(d.name) + if d and not name.startswith(('NK RND', 'NK RST', 'NK CHAIN MIX', 'NK DR', 'NK LEARN', 'NK RECEIVER', 'NK TRACK', 'NK SIDECHAIN')): + self._devices_to_operate_on.append(d) + if self._parent._can_have_nested_devices and d.can_have_chains: + for c in d.chains: + self.get_devices_to_operate_on(c.devices, 'all') + else: + self.get_next_device(self._on_off_param[0].canonical_parent, dev_list) + + + def get_next_device(self, rnr_rack, dev_list, store_next = False): + """ Get the next non-RnR device on the track or in the chain """ + for d in dev_list: + if d and not store_next: + if d == rnr_rack: + store_next = True + elif d and store_next: + if not self._devices_to_operate_on or (self._parent._can_have_nested_devices and type(d.canonical_parent) == Live.Chain.Chain): + name = self._parent.get_name(d.name) + if d and not name.startswith(('NK RND', 'NK RST', 'NK CHAIN MIX', 'NK DR', 'NK LEARN', 'NK RECEIVER', 'NK TRACK', 'NK SIDECHAIN')): + self._devices_to_operate_on.append(d) + if self._parent._can_have_nested_devices and type(rnr_rack.canonical_parent) == Live.Chain.Chain: + return + if self._parent._can_have_nested_devices and d.can_have_chains: + for c in d.chains: + self.get_next_device(rnr_rack, c.devices, True) + else: + return + + + def remove_on_off_listeners(self): + """ Remove listeners """ + if self._on_off_param and self._on_off_param[0] and self._on_off_param[0].value_has_listener(self.on_off_changed): + self._on_off_param[0].remove_value_listener(self.on_off_changed) + self._on_off_param = [] + + +# local variables: +# tab-width: 4 + \ No newline at end of file diff --git a/ClyphX/MacrobatSidechainRack.py b/ClyphX/MacrobatSidechainRack.py new file mode 100644 index 0000000..008987e --- /dev/null +++ b/ClyphX/MacrobatSidechainRack.py @@ -0,0 +1,134 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +import Live +from consts import * +if IS_LIVE_9: + from functools import partial +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent + +class MacrobatSidechainRack(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' Macros sidechain to track output ' + + def __init__(self, parent, rack, track): + ControlSurfaceComponent.__init__(self) + self._parent = parent + self._last_meter_left_val = -1 + self._last_meter_right_val = -1 + self._last_midi_meter_val = -1 + self._track = track + self._rack = rack + self.setup_device() + + + def disconnect(self): + if self._track: + if self._track.has_audio_output and self._track.output_meter_left_has_listener(self.audio_left_changed): + self._track.remove_output_meter_left_listener(self.audio_left_changed) + if self._track.has_audio_output and self._track.output_meter_right_has_listener(self.audio_right_changed): + self._track.remove_output_meter_right_listener(self.audio_right_changed) + if self._track.has_midi_output and self._track.output_meter_level_has_listener(self.midi_changed): + self._track.remove_output_meter_level_listener(self.midi_changed) + self._track = None + self._rack = None + self._parent = None + if IS_LIVE_9: + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def setup_device(self): + """ - Rack name needs to start with 'nK Sidechain' + - Macro names needs to start with '[SC]' + - Macro names can be changed dynamically with this rack + - Dev On/Off turns sidechain on/off + - IMPORTANT NOTE: This will hose undo history since each macro movement is undoable """ + if self._track.has_audio_output: + if not self._track.output_meter_left_has_listener(self.audio_left_changed): + self._track.add_output_meter_left_listener(self.audio_left_changed) + if not self._track.output_meter_right_has_listener(self.audio_right_changed): + self._track.add_output_meter_right_listener(self.audio_right_changed) + if self._track.has_midi_output and not self._track.output_meter_level_has_listener(self.midi_changed): + self._track.add_output_meter_level_listener(self.midi_changed) + + + def audio_left_changed(self): + """ Audio left changed, update macro (1 tick delay)""" + val = int(self._track.output_meter_left * 127) + if val != self._last_meter_left_val: + self._last_meter_left_val = val + if IS_LIVE_9: + self._parent.schedule_message(1, partial(self.update_macros, val)) + else: + self._parent.schedule_message(1, self.update_macros, val) + + + def audio_right_changed(self): + """ Audio right changed, update macro (1 tick delay) """ + val = int(self._track.output_meter_right * 127) + if val != self._last_meter_right_val: + self._last_meter_right_val = val + if IS_LIVE_9: + self._parent.schedule_message(1, partial(self.update_macros, val)) + else: + self._parent.schedule_message(1, self.update_macros, val) + + + def midi_changed(self): + """ Midi output changed, update macro (1 tick delay) """ + val = int(self._track.output_meter_level * 127) + if val != self._last_midi_meter_val: + self._last_midi_meter_val = val + if IS_LIVE_9: + self._parent.schedule_message(1, partial(self.update_macros, val)) + else: + self._parent.schedule_message(1, self.update_macros, val) + + + def update_macros(self, val): + """ Update macros based on track output as long as rack is on """ + if self._rack: + for p in self._rack.parameters: + name = self._parent.get_name(p.name) + if name.startswith('DEVICE'): + if p.value == 0.0: + return() + if name.startswith('[SC]') and p.is_enabled: + if (self._track.has_audio_output and self._track.output_meter_right == 0.0 and self._track.output_meter_left == 0.0) or self._track.output_meter_level == 0.0: + val = 0.0 + p.value = val + if self._track.has_midi_output: + self._parent.schedule_message(4, self.midi_changed) + +# local variables: +# tab-width: 4 + \ No newline at end of file diff --git a/ClyphX/MacrobatUserConfig.py b/ClyphX/MacrobatUserConfig.py new file mode 100644 index 0000000..a06ffbd --- /dev/null +++ b/ClyphX/MacrobatUserConfig.py @@ -0,0 +1,83 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +# ***************************** [SETTINGS NOTES] ************************** + +# Please DO NOT change any of the spacing in this file. + +# Please DO NOT change the name of this file or its file extension. When done +# making your changes to the [SETTINGS] below, just save the file. + +# After saving this file, you will need to close/restart Live for your changes +# to take effect. + +# For Windows 7/Vista users, depending on how your privileges are set up, you may +# not be able to save changes you make to this file. You may receive an error +# such as Access Denied when trying to save. If this occurs, you will need to +# drag this file onto your desktop, then make your changes and save. When +# done, drag the file back into the ClyphX folder. + + + +# ******************************* [SETTINGS] ******************************* + +# Below you can define a list of SysEx messages that can be sent from macros. +# The entries in the list below are just examples and can be removed. + +SYSEX_LIST = [#<------Do NOT remove this. + +('Motif Arp I/O', 'F0 43 10 6F 40 70 15 nn F7', 0, 1), +('Motif Arp Type', 'F0 43 10 6F 40 70 14 nn F7', 0, 127), +('Motif Mode', 'F0 43 10 6F 0A 00 01 nn F7', 0, 3), +('Motif EQ Lo', 'F0 43 10 6F 40 70 31 nn F7', 0, 127), +('Motif EQ LoMid', 'F0 43 10 6F 40 70 32 nn F7', 0, 127), +('Motif EQ HiMid', 'F0 43 10 6F 40 70 33 nn F7', 0, 127), +('Motif EQ Hi', 'F0 43 10 6F 40 70 34 nn F7', 0, 127) + +]#<------Do NOT remove this. + +# Entry Format: +# ('Identifier', 'SysEx String', Min Value, Max Value) + +# Identifier: +# A name to identify the SysEx string with. + +# SysEx String: +# The SysEx string (in hexadecimal) to send. +# This must start with F0 and end with F7. +# All other values in the string should be within the range of 00 - 7F. +# nn represents the variable byte in the SysEx string, the one the macro will control. + +# Min Value: +# The lowest value (in decimal) of the variable byte. Should be in the range of 0 - 127. + +# Max Value: +# The highest value (in decimal) of the variable byte. Should be in the range of 0 - 127. + +# Notes: +# The Min and Max Value do not not have quotes around them. +# Except for the last entry in the list, every entry in the list should have a comma following it. + +# local variables: +# tab-width: 4 diff --git a/ClyphX/PushEmuHandler.py b/ClyphX/PushEmuHandler.py new file mode 100644 index 0000000..606a704 --- /dev/null +++ b/ClyphX/PushEmuHandler.py @@ -0,0 +1,61 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +from Push.handshake_component import HandshakeComponent + + +class MockHandshakeTask(object): + """ Mock objects used to replace Push's handshake task. """ + + def kill(self): + pass + + def restart(self): + pass + + def is_running(self): + return False + + +class MockHandshake(HandshakeComponent): + """ Extended HandshakeComponent that overrides methods to allow for + emulation. """ + + def __init__(self, *a, **k): + super(MockHandshake, self).__init__(*a, **k) + self._on_identity_value.subject = None + self._on_dongle_value.subject = None + + def _start_handshake(self): + self._handshake_succeeded = None + self._do_succeed() + + def firmware_version(self): + return 1.16 + + def has_version_requirements(self, x, y): + return True + +# local variables: +# tab-width: 4 \ No newline at end of file diff --git a/ClyphX/Push_APC_Combiner.py b/ClyphX/Push_APC_Combiner.py new file mode 100644 index 0000000..9ed89c8 --- /dev/null +++ b/ClyphX/Push_APC_Combiner.py @@ -0,0 +1,116 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +from _Framework.ControlSurfaceComponent import ControlSurfaceComponent +from _Framework.SessionComponent import SessionComponent +from consts import IS_LIVE_9_5 +SessionRingComponent = None +if IS_LIVE_9_5: + from ableton.v2.control_surface.components.session_ring import SessionRingComponent + +class Push_APC_Combiner(ControlSurfaceComponent): + __module__ = __name__ + __doc__ = ' Class that syncs Push and APC40 session grids for proper emulation. ' + + def __init__(self, parent): + ControlSurfaceComponent.__init__(self) + self._parent = parent + self._push = None + self._push_session = None + self._apc = None + self._apc_session = None + + + def disconnect(self): + self._remove_listeners() + self._push = None + self._push_session = None + self._apc = None + self._apc_session = None + self._parent = None + ControlSurfaceComponent.disconnect(self) + + + def on_enabled_changed(self): + pass + + + def update(self): + pass + + + def set_up_scripts(self, scripts): + """ Remove current listeners, get Push/APC scripts, set up listeners and also set feedback delay on APC+Push encoders. """ + self._remove_listeners() + for script in scripts: + script_name = script.__class__.__name__ + if script_name == 'Push': + self._push = script + self._push_session = self._get_session_component(script) + if self._push_session: + for c in script.controls: + if c.__class__.__name__ == 'TouchEncoderElement': + c.set_feedback_delay(-1) + elif script_name == 'APC40': + self._apc = script + self._apc_session = self._get_session_component(script) + if self._apc_session: + for c in script.controls: + if c.__class__.__name__ == 'RingedEncoderElement': + c.set_feedback_delay(-1) + self._apc_session.add_offset_listener(self._on_apc_offset_changed) + self._on_apc_offset_changed() + + + def _get_session_component(self, script): + """ Get the session component for the given script. """ + comp = None + if script and script._components: + for c in script.components: + if isinstance (c, SessionComponent): + comp = c + break + if comp is None: + if hasattr(script, '_session_ring'): + return script._session_ring + return comp + + + def _on_apc_offset_changed(self): + """ Update Push offset on APC offset changed and suppress its highlight. """ + if self._push_session and self._apc_session: + self._push_session.set_offsets(self._apc_session.track_offset(), self._apc_session.scene_offset()) + if IS_LIVE_9_5: + self._push_session._session_ring.hide_highlight() + else: + self._push._set_session_highlight(-1, -1, -1, -1, False) + + + def _remove_listeners(self): + if self._apc_session and self._apc_session.offset_has_listener(self._on_apc_offset_changed): + self._apc_session.remove_offset_listener(self._on_apc_offset_changed) + + +# local variables: +# tab-width: 4 \ No newline at end of file diff --git a/ClyphX/UserSettings.txt b/ClyphX/UserSettings.txt new file mode 100644 index 0000000..0233def --- /dev/null +++ b/ClyphX/UserSettings.txt @@ -0,0 +1,286 @@ +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray + + + +***************************** [SETTINGS NOTES] ************************** + + +# Please DO NOT change any of the spacing in this file. + +# Please DO NOT change the name of this file or its file extension. When done +# making your changes to the settings below, just save the file. + +# After saving this file, you will need to close/restart Live for your changes +# to take effect. + +# For Windows 7/Vista users, depending on how your privileges are set up, you may +# not be able to save changes you make to this file. You may receive an error +# such as Access Denied when trying to save. If this occurs, you will need to +# drag this file onto your desktop, then make your changes and save. When +# done, drag the file back into the ClyphX folder. + + + +***************************** [SNAPSHOT SETTINGS] ************************** + + +INCLUDE_NESTED_DEVICES_IN_SNAPSHOTS = On +# Setting: +# Off or On + +# Description: +# Determines whether or not nested Devices (Devices inside of Racks) will be +# included in Snapshots. This setting only applies if you're using Live 8.2.2 +# or later. + + + +SNAPSHOT_PARAMETER_LIMIT = 500 +# Setting: +# Any whole number + +# Description: +# Determines the number of parameters that Snapshots will be allowed to store. +# If the limit is exceeded, you'll receive an error message. + +# Note: +# Please use caution when adjusting this setting. Recalling Snapshots that have +# stored 1000 or more parameters can cause delays and momentary freezing of Live's GUI. + + + +***************************** [EXTRA PREFS] ************************** + + +PROCESS_XCLIPS_IF_TRACK_MUTED = True +# Setting: +# True or False + +# Description: +# Determines whether or not X-Clips on a Muted Track will be processed. + + + +STARTUP_ACTIONS = Off +# Setting: +# Off or Action(s) to perform on set load. + +# Description: +# Performs an Action List when a set is loaded. + + + +NAVIGATION_HIGHLIGHT = On +# Setting: +# On or Off + +# Description: +# Displays a highlight around the selected Clip. + + + +EXCLUSIVE_ARM_ON_SELECT = Off +# Setting: +# On or Off + +# Description: +# Upon selecting Tracks, if the selected Track can be armed, it will be armed +# and any other armed Tracks will be disarmed. + +# Note: +# This function may produce undesirable results if Select On Launch is on +# in your Live preferences. + + + +EXCLUSIVE_SHOW_GROUP_ON_SELECT = Off +# Setting: +# On or Off + +# Description: +# Upon selecting Tracks, if the selected Track is a Group Track, it will be +# unfolded and any other Group Tracks will be folded. + +# Note: +# This function may produce undesirable results if Select On Launch is on +# in your Live preferences. + + + +CLIP_RECORD_LENGTH_SET_BY_GLOBAL_QUANTIZATION = Off +# Setting: +# On or Off + +# Description: +# This changes the behavior of launching the selected Clip Slot so that +# (under the Conditions listed below) you can easily record a new Clip with a length +# defined by the Global Quantization value. This will do nothing if the Conditions +# below aren't met. + +# Conditions: +# - Selected Track is armed +# - Selected Clip Slot has no Clip on it +# - Global Quantization is not set to None + + + +DEFAULT_INSERTED_MIDI_CLIP_LENGTH = 0 +# Setting: +# 0 (for Off) or 2 - 16 (for number of bars to use) + +# Description: +# Upon inserting a blank MIDI Clip onto the selected Clip Slot, the Clip's length +# will be set to the length (in bars) specified in the setting above. + +# Note: +# This will not change the default zoom setting of the Clip, so you'll only see the +# Clip's first bar. You'll need to zoom out to see the rest of the Clip. + + + +***************************** [CSLINKER] ************************** + + +# CSLinker allows you to link the grid selectors (colored borders around clips) of +# two Control Surfaces either horizontally or vertically. + +# The Control Surface script names to use are as shown in Live's Control Surface chooser. +# If a Control Surface's name has a space in it (like MXT Live), you should use an underscore +# in place of the space (like MXT_Live). + +# Note, Push and Push2 cannot be used for matched linking. Additionally, horizontal linking +# with Push2 may produce undesirable results due to Push2's inclusion of Chain mixer settings +# along side normal Track mixer settings. + + +CSLINKER_MATCHED_LINK = False +# Setting: +# True for matched link or False for horizonal/vertical link. + +# Description: +# Determines whether the two Control Surfaces should have a matched link meaning that +# they will lay on top of each other. This setting overrides CSLINKER_HORIZONTAL_LINK. +# and CSLINKER_MULTI_AXIS_LINK. + + + +CSLINKER_HORIZONTAL_LINK = True +# Setting: +# True for horizontal link or False for vertical link. + +# Description: +# Determines whether the two Control Surfaces should be horizontal or vertically linked. + + + +CSLINKER_MULTI_AXIS_LINK = False +# Setting: +# True for multi-axis link. + +# Determines whether movement should be sychronized in all directions (vertical and horizontal) or +# purely on a single axis determined by the CS_HORIZONTAL_LINK setting. + + + +CSLINKER_SCRIPT_1_NAME = None +# Setting: +# None (to turn linking off) or the name of a Control Surface. + +# Description: +# The first Control Surface script that should be linked. + + + +CSLINKER_SCRIPT_2_NAME = None +# Setting: +# None (to turn linking off) or the name of a Control Surface. + +# Description: +# The second Control Surface script that should be linked. + + + +******************************* [USER CONTROLS] ******************************* + + +# Below, you can specify a list of MIDI Controls to use as X-Controls. + +# The entry format is: CONTROL_NAME = MSG_TYPE, MIDI_CHANNEL, NOTE_OR_CC_NUM, ON_ACTION_LIST + +# CONTROL_NAME = A unique one-word name (Identifier) for the control. See [IDENTIFIER NOTE] below. +# MSG_TYPE = The word Note or CC. +# MIDI_CHANNEL = The MIDI Channel number in the range of 1 - 16 +# NOTE_OR_CC = The Note or CC number in the range of 0 - 127. +# ON_ACTION_LIST = The Action List to perform when the control sends an on message. + +# Example: MY_BTN1 = NOTE, 1, 10, 1/MUTE ; 2/MUTE + + +# You can optionally specify an Action List to perform when the control sends an off message. +# To do this, place a comma after the On Action List and then specify the Off Action List. + +# Example: MY_BTN2 = CC, 16, 117, 1/MUTE ; 2/MUTE, 3/PLAY > + + +# To perform the same Action List for the On Action List and Off Action List, just specify an asterick +# for the Off Action List. + +# Example: MY_BTN3 = NOTE, 5, 0, 1/MUTE, * + + +# Below is an example list that has been commented out (the # at the beginning of +# a line makes the line a comment). Your list should be formatted in the same way +# except without the # at the beginning of each line. + + +# btn_1 = note, 1, 0, mute , * + +# btn_2 = note, 1, 1, solo + +# btn_3 = cc, 9, 2, arm + +# btn_4 = cc, 9, 3, mon + +#>>>>>>>>DELETE THIS ENTIRE LINE AND START YOUR LIST HERE<<<<<<<<# + + +******************************* [USER VARIABLES] ******************************* + + +# Below, you can specify a list of Variables to use in your Action Lists. + +# The entry format is: VARIABLE_NAME = VALUE + +# VARIABLE_NAME = A unique one-word name (Identifier) for the variable. See [IDENTIFIER NOTE] below. +# VALUE = Any value or word or combination of words. See the User Variables section of the manual for +# more info on this. + +# The Variables listed below are just examples and can be removed. + + +ex_var1 = 10 +ex_var2 = mute + + +******************************* [IDENTIFIER NOTE] ******************************* + + +# Identifiers and Variable names should not contain characters other than letters, numbers and underscores. +# Also, Variable names and their values are not case-sensitive. diff --git a/ClyphX/__init__.py b/ClyphX/__init__.py new file mode 100644 index 0000000..4bad43f --- /dev/null +++ b/ClyphX/__init__.py @@ -0,0 +1,33 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +import Live +from ClyphX import ClyphX + + +def create_instance(c_instance): + return ClyphX(c_instance) + +# local variables: +# tab-width: 4 \ No newline at end of file diff --git a/ClyphX/consts.py b/ClyphX/consts.py new file mode 100644 index 0000000..e76c0a3 --- /dev/null +++ b/ClyphX/consts.py @@ -0,0 +1,235 @@ +""" +# Copyright (C) 2013-2017 Stray +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# For questions regarding this module contact +# Stray +""" + +# emacs-mode: -*- python-*- +# -*- coding: utf-8 -*- + +#--- NOTE: Action names and their corresponding values can't contain a '/' or '-' within the first four chars like this 'EX/ONE', but 'EXMP/ONE' is okay. + +import Live + +app = Live.Application.get_application() +IS_LIVE_10 = app.get_major_version() == 10 +IS_LIVE_9 = app.get_major_version() >= 9 +IS_LIVE_9_1 = IS_LIVE_10 or (IS_LIVE_9 and app.get_minor_version() >= 1) +IS_LIVE_9_5 = IS_LIVE_10 or (IS_LIVE_9 and app.get_minor_version() >= 5) + +GLOBAL_ACTIONS = { + 'ASN' : 'do_variable_assignment', + 'ADDAUDIO' : 'create_audio_track', + 'ADDMIDI' : 'create_midi_track', + 'INSAUDIO' : 'insert_and_configure_audio_track', + 'INSMIDI' : 'insert_and_configure_midi_track', + 'ADDRETURN' : 'create_return_track', + 'ADDSCENE' : 'create_scene', + 'DELSCENE' : 'delete_scene', + 'DUPESCENE' : 'duplicate_scene', + 'LOADDEV' : 'load_device', + 'LOADM4L' : 'load_m4l', + 'SWAP' : 'swap_device_preset', + 'SREC' : 'set_session_record', + 'SRECFIX' : 'trigger_session_record', + 'SATM' : 'set_session_automation_record', + 'B2A' : 'set_back_to_arrange', + 'RPT' : 'set_note_repeat', + 'SWING' : 'adjust_swing', + 'BPM' : 'adjust_tempo', + 'DEVFIRST' : 'move_to_first_device', + 'DEVLAST' : 'move_to_last_device', + 'DEVLEFT' : 'move_to_prev_device', + 'DEVRIGHT' : 'move_to_next_device', + 'FOCBRWSR' : 'focus_browser', + 'FOCDETAIL' : 'focus_detail', + 'FOCMAIN' : 'focus_main', + 'GQ' : 'adjust_global_quantize', + 'GRV' : 'adjust_groove', + 'HZOOM' : 'adjust_horizontal_zoom', + 'VZOOM' : 'adjust_vertical_zoom', + 'UP' : 'move_up', + 'DOWN' : 'move_down', + 'LEFT' : 'move_left', + 'RIGHT' : 'move_right', + 'LOOP' : 'do_loop_action', + 'LOC' : 'do_locator_action', + 'LOCLOOP' : 'do_locator_loop_action', + 'METRO' : 'set_metronome', + 'MIDI': 'send_midi_message', + 'OVER' : 'set_overdub', + 'PIN' : 'set_punch_in', + 'POUT' : 'set_punch_out', + 'REC' : 'set_record', + 'REDO' : 'set_redo', + 'UNDO' : 'set_undo', + 'RESTART' : 'restart_transport', + 'RQ' : 'adjust_record_quantize', + 'RTRIG': 'retrigger_recording_clips', + 'SIG' : 'adjust_time_signature', + 'SCENE' : 'set_scene', + 'SHOWCLIP' : 'show_clip_view', + 'SHOWDEV' : 'show_track_view', + 'SHOWDETAIL' : 'show_detail_view', + 'TGLBRWSR': 'toggle_browser', + 'TGLDETAIL': 'toggle_detail_view', + 'TGLMAIN': 'toggle_main_view', + 'STOPALL' : 'set_stop_all', + 'SETCONT' : 'set_continue_playback', + 'SETLOC' : 'set_locator', + 'SETSTOP' : 'set_stop_transport', + 'SETFOLD' : 'set_fold_all', + 'SETJUMP' : 'set_jump_all', + 'TAPBPM' : 'set_tap_tempo', + 'UNARM' : 'set_unarm_all', + 'UNMUTE' : 'set_unmute_all', + 'UNSOLO' : 'set_unsolo_all', + 'MAKE_DEV_DOC': 'make_instant_mapping_docs'} + +TRACK_ACTIONS = { + 'ARM' : 'set_arm', + 'MUTE' : 'set_mute', + 'SOLO' : 'set_solo', + 'MON' : 'set_monitor', + 'XFADE' : 'set_xfade', + 'SEL' : 'set_selection', + 'ADDCLIP' : 'create_clip', + 'DEL' : 'delete_track', + 'DELDEV' : 'delete_device', + 'DUPE' : 'duplicate_track', + 'FOLD' : 'set_fold', + 'PLAY' : 'set_play', + 'PLAYL' : 'set_play_w_legato', + 'PLAYQ' : 'set_play_w_force_qntz', + 'PLAYLQ' : 'set_play_w_force_qntz_and_legato', + 'STOP' : 'set_stop', + 'JUMP' : 'set_jump', + 'VOL' : 'adjust_volume', + 'PAN' : 'adjust_pan', + 'SEND' : 'adjust_sends', + 'CUE' : 'adjust_preview_volume', + 'XFADER' : 'adjust_crossfader', + 'IN' : 'adjust_input_routing', + 'INSUB' : 'adjust_input_sub_routing', + 'OUT' : 'adjust_output_routing', + 'OUTSUB' : 'adjust_output_sub_routing', + 'NAME' : 'set_name', + 'RENAMEALL' : 'rename_all_clips'} + +CLIP_ACTIONS = { + 'CENT' : 'adjust_detune', + 'SEMI' : 'adjust_transpose', + 'GAIN' : 'adjust_gain', + 'CUE' : 'adjust_cue_point', + 'END' : 'adjust_end', + 'START' : 'adjust_start', + 'GRID' : 'adjust_grid_quantization', + 'TGRID' : 'set_triplet_grid', + 'ENVINS' : 'insert_envelope', + 'ENVCLR' : 'clear_envelope', + 'ENVCAP' : 'capture_to_envelope', + 'ENVSHOW' : 'show_envelope', + 'ENVHIDE' : 'hide_envelopes', + 'QNTZ' : 'quantize', + 'EXTEND' : 'duplicate_clip_content', + 'DEL' : 'delete_clip', + 'DUPE' : 'duplicate_clip', + 'CHOP' : 'chop_clip', + 'SPLIT' : 'split_clip', + 'WARPMODE' : 'adjust_warp_mode', + 'LOOP' : 'do_clip_loop_action', + 'SIG' : 'adjust_time_signature', + 'WARP' : 'set_warp', + 'NAME' : 'set_clip_name'} + +DEVICE_ACTIONS = { + 'CSEL' : 'adjust_selected_chain', + 'CS' : 'adjust_chain_selector', + 'RESET' : 'reset_params', + 'RND' : 'randomize_params', + 'SEL' : 'select_device', + 'SET' : 'set_all_params', + 'P1' : 'adjust_best_of_bank_param', + 'P2' : 'adjust_best_of_bank_param', + 'P3' : 'adjust_best_of_bank_param', + 'P4' : 'adjust_best_of_bank_param', + 'P5' : 'adjust_best_of_bank_param', + 'P6' : 'adjust_best_of_bank_param', + 'P7' : 'adjust_best_of_bank_param', + 'P8' : 'adjust_best_of_bank_param', + 'B1' : 'adjust_banked_param', + 'B2' : 'adjust_banked_param', + 'B3' : 'adjust_banked_param', + 'B4' : 'adjust_banked_param', + 'B5' : 'adjust_banked_param', + 'B6' : 'adjust_banked_param', + 'B7' : 'adjust_banked_param', + 'B8' : 'adjust_banked_param'} + +if IS_LIVE_9: + DR_ACTIONS = { + 'SCROLL' : 'scroll_selector', + 'UNMUTE' : 'unmute_all', + 'UNSOLO' : 'unsolo_all' + } + +LOOPER_ACTIONS = { + 'LOOPER' : 'set_looper_on_off', + 'REV' : 'set_looper_rev', + 'OVER' : 'set_looper_state', + 'PLAY' : 'set_looper_state', + 'REC' : 'set_looper_state', + 'STOP': 'set_looper_state'} + +KEYWORDS = {'ON' : 1, 'OFF' : 0} + +NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] +OCTAVE_NAMES = ['-2', '-1', '0', '1', '2', '3', '4', '5', '6', '7', '8'] + +GQ_STATES = {'NONE' : 0, '8 BARS' : 1, '4 BARS' : 2, '2 BARS' : 3, '1 BAR' : 4, '1/2' : 5, '1/2T' : 6, '1/4' : 7, '1/4T' : 8, '1/8' : 9, '1/8T' : 10, '1/16' : 11, '1/16T' : 12, '1/32' : 13} +RQ_STATES = {'NONE' : 0, '1/4' : 1, '1/8' : 2, '1/8T' : 3, '1/8 + 1/8T' : 4, '1/16' : 5, '1/16T' : 6, '1/16 + 1/16T' : 7, '1/32' : 8} + +XFADE_STATES = {'A': 0, 'OFF' : 1, 'B' : 2} +MON_STATES = {'IN' : 0, 'AUTO' : 1, 'OFF' : 2} + +LOOPER_STATES = {'STOP': 0.0, 'REC' : 1.0, 'PLAY' : 2.0, 'OVER' : 3.0} + +if IS_LIVE_9: + R_QNTZ_STATES = {'1/4' : Live.Song.RecordingQuantization.rec_q_quarter, '1/8' : Live.Song.RecordingQuantization.rec_q_eight, + '1/8T' : Live.Song.RecordingQuantization.rec_q_eight_triplet, '1/8 + 1/8T' : Live.Song.RecordingQuantization.rec_q_eight_eight_triplet, '1/16' : Live.Song.RecordingQuantization.rec_q_sixtenth, + '1/16T' : Live.Song.RecordingQuantization.rec_q_sixtenth_triplet, '1/16 + 1/16T' : Live.Song.RecordingQuantization.rec_q_sixtenth_sixtenth_triplet, + '1/32' : Live.Song.RecordingQuantization.rec_q_thirtysecond} + + CLIP_GRID_STATES = {'OFF' : Live.Clip.GridQuantization.no_grid, '8 BARS' : Live.Clip.GridQuantization.g_8_bars, + '4 BARS' : Live.Clip.GridQuantization.g_4_bars, '2 BARS' : Live.Clip.GridQuantization.g_2_bars, + '1 BAR' : Live.Clip.GridQuantization.g_bar, '1/2' : Live.Clip.GridQuantization.g_half, + '1/4' : Live.Clip.GridQuantization.g_quarter, '1/8' : Live.Clip.GridQuantization.g_eighth, + '1/16' : Live.Clip.GridQuantization.g_sixteenth, '1/32' : Live.Clip.GridQuantization.g_thirtysecond} + + REPEAT_STATES = {'OFF' : 1.0, '1/4' : 1.0, '1/4T' : 0.666666666667, '1/8' : 0.5, '1/8T' : 0.333333333333, '1/16' : 0.25, '1/16T' : 0.166666666667, '1/32' : 0.125, '1/32T' : 0.0833333333333} + + WARP_MODES = {'BEATS': 0, 'TONES': 1, 'TEXTURE': 2, 'RE-PITCH': 3, 'COMPLEX': 4, 'COMPLEX PRO': 6} + + AUDIO_DEVS = {u'SIMPLE DELAY': u'Simple Delay', u'OVERDRIVE': u'Overdrive', u'LOOPER': u'Looper', u'AUTO FILTER': u'Auto Filter', u'EXTERNAL AUDIO EFFECT': u'External Audio Effect', u'SATURATOR': u'Saturator', u'PHASER': u'Phaser', u'VINYL DISTORTION': u'Vinyl Distortion', u'DYNAMIC TUBE': u'Dynamic Tube', u'BEAT REPEAT': u'Beat Repeat', u'MULTIBAND DYNAMICS': u'Multiband Dynamics', u'CABINET': u'Cabinet', u'AUDIO EFFECT RACK': u'Audio Effect Rack', u'FLANGER': u'Flanger', u'GATE': u'Gate', u'REVERB': u'Reverb', u'GRAIN DELAY': u'Grain Delay', u'REDUX': u'Redux', u'PING PONG DELAY': u'Ping Pong Delay', u'SPECTRUM': u'Spectrum', u'COMPRESSOR': u'Compressor', u'VOCODER': u'Vocoder', u'AMP': u'Amp', u'GLUE COMPRESSOR': u'Glue Compressor', u'EROSION': u'Erosion', u'EQ THREE': u'EQ Three', u'EQ EIGHT': u'EQ Eight', u'RESONATORS': u'Resonators', u'FREQUENCY SHIFTER': u'Frequency Shifter', u'AUTO PAN': u'Auto Pan', u'CHORUS': u'Chorus', u'LIMITER': u'Limiter', u'CORPUS': u'Corpus', u'FILTER DELAY': u'Filter Delay', u'UTILITY': u'Utility'} + + INS_DEVS = {u'TENSION': u'Tension', u'EXTERNAL INSTRUMENT': u'External Instrument', u'ELECTRIC': u'Electric', u'INSTRUMENT RACK': u'Instrument Rack', u'DRUM RACK': u'Drum Rack', u'COLLISION': u'Collision', u'IMPULSE': u'Impulse', u'SAMPLER': u'Sampler', u'OPERATOR': u'Operator', u'ANALOG': u'Analog', u'SIMPLER': u'Simpler'} + + MIDI_DEVS = {u'NOTE LENGTH': u'Note Length', u'CHORD': u'Chord', u'RANDOM': u'Random', u'MIDI EFFECT RACK': u'MIDI Effect Rack', u'SCALE': u'Scale', u'PITCH': u'Pitch', u'ARPEGGIATOR': u'Arpeggiator', u'VELOCITY': u'Velocity'} + +# local variables: +# tab-width: 4 diff --git a/Live Instant Mapping Info.html b/Live Instant Mapping Info.html new file mode 100644 index 0000000..33f37d0 --- /dev/null +++ b/Live Instant Mapping Info.html @@ -0,0 +1 @@ +

Live Instant Mapping Info for Live v9.7.0

Brought to you by nativeKONTROL.

The following document covers the parameter banks accessible via Live's Instant Mapping feature for each built in device. This info also applies to controlling device parameters via ClyphX's Device Actions.

NOTE: The order of parameter banks is sometimes changed by Ableton. If you find the information in this document to be incorrect, you can recreate it with ClyphX by triggering an action named MAKE_DEV_DOC. That will create a new version of this file in your user/home directory.

Device Index

Amp
Analog
Arpeggiator
Audio Effect Rack
AutoFilter
AutoPan
BeatRepeat
Cabinet
Chord
Chorus
Collision
Compressor
Corpus
Drum Rack
Dynamic Tube
EQ Eight
EQ Three
Electric
Erosion
FilterDelay
Flanger
FrequencyShifter
Gate
GlueCompressor
GrainDelay
Impulse
Instrument Rack
Looper
MIDI Effect Rack
MultibandDynamics
Note Length
Operator
Overdrive
Phaser
PingPongDelay
Pitch
Random
Redux
Resonator
Reverb
Sampler
Saturator
Scale
Simple Delay
Simpler
Tension
Utility
Velocity
Vinyl Distortion
Vocoder

Amp

Best Of Banks
P1: Amp Type
P2: Bass
P3: Middle
P4: Treble
P5: Presence
P6: Gain
P7: Volume
P8: Dry/Wet

B1: Global
P1: Amp Type
P2: Bass
P3: Middle
P4: Treble
P5: Presence
P6: Gain
P7: Volume
P8: Dry/Wet

B2: Dual Mono
P1: Dual Mono


Back to Device Index

Analog

Best Of Banks
P1: F1 Freq
P2: F1 Resonance
P3: OSC1 Shape
P4: OSC1 Octave
P5: OSC2 Shape
P6: OSC2 Octave
P7: OSC2 Detune
P8: Volume

B1: Oscillators
P1: OSC1 Level
P2: OSC1 Octave
P3: OSC1 Semi
P4: OSC1 Shape
P5: OSC2 Level
P6: OSC2 Octave
P7: OSC2 Semi
P8: OSC2 Shape

B2: Filters
P1: OSC1 Balance
P2: F1 Freq
P3: F1 Resonance
P4: F1 Type
P5: OSC2 Balance
P6: F2 Freq
P7: F2 Resonance
P8: F2 Type

B3: Filter Envelopes
P1: FEG1 Attack
P2: FEG1 Decay
P3: FEG1 Sustain
P4: FEG1 Rel
P5: FEG2 Attack
P6: FEG2 Decay
P7: FEG2 Sustain
P8: FEG2 Rel

B4: Filter Modulation
P1: F1 On/Off
P2: F1 Freq < LFO
P3: F1 Freq < Env
P4: F1 Res < LFO
P5: F2 On/Off
P6: F2 Freq < LFO
P7: F2 Freq < Env
P8: F2 Res < LFO

B5: Volume Envelopes
P1: AEG1 Attack
P2: AEG1 Decay
P3: AEG1 Sustain
P4: AEG1 Rel
P5: AEG2 Attack
P6: AEG2 Decay
P7: AEG2 Sustain
P8: AEG2 Rel

B6: Mix
P1: AMP1 Level
P2: AMP1 Pan
P3: LFO1 Shape
P4: LFO1 Speed
P5: AMP2 Level
P6: AMP2 Pan
P7: LFO2 Shape
P8: LFO2 Speed

B7: Output
P1: Volume
P2: Noise On/Off
P3: Noise Level
P4: Noise Color
P5: Unison On/Off
P6: Unison Detune
P7: Vib On/Off
P8: Vib Amount


Back to Device Index

Arpeggiator

Best Of Banks
P1: Synced Rate
P2: Free Rate
P3: Transp. Steps
P4: Transp. Dist.
P5: Gate
P6: Tranpose Key
P7: Velocity Decay
P8: Velocity Target

B1: Style
P1: Style
P2: Groove
P3: Offset
P4: Synced Rate
P5: Retrigger Mode
P6: Ret. Interval
P7: Repeats
P8: Gate

B2: Pitch/Velocity
P1: Tranpose Mode
P2: Tranpose Key
P3: Transp. Steps
P4: Transp. Dist.
P5: Velocity Decay
P6: Velocity Target
P7: Velocity On
P8: Vel. Retrigger


Back to Device Index

Audio Effect Rack

Best Of Banks
P1: Macro 1
P2: Macro 2
P3: Macro 3
P4: Macro 4
P5: Macro 5
P6: Macro 6
P7: Macro 7
P8: Macro 8


Back to Device Index

AutoFilter

Best Of Banks
P1: Frequency
P2: Resonance
P3: Filter Type
P4: Env. Modulation
P5: LFO Amount
P6: LFO Waveform
P7: LFO Frequency
P8: LFO Phase

B1: Filter
P1: Frequency
P2: Resonance
P3: Env. Attack
P4: Env. Release
P5: Env. Modulation
P6: LFO Amount
P7: LFO Frequency
P8: LFO Phase

B2: Filter Extra
P1: Filter Type
P2: LFO Quantize On
P3: LFO Quantize Rate
P4: LFO Stereo Mode
P5: LFO Spin
P6: LFO Sync
P7: LFO Sync Rate
P8: LFO Offset

B3: Side Chain
P6: Ext. In On
P7: Ext. In Mix
P8: Ext. In Gain


Back to Device Index

AutoPan

Best Of Banks
P1: Frequency
P2: Phase
P3: Shape
P4: Waveform
P5: Sync Rate
P6: Offset
P7: Width (Random)
P8: Amount


Back to Device Index

BeatRepeat

Best Of Banks
P1: Grid
P2: Interval
P3: Offset
P4: Gate
P5: Pitch
P6: Pitch Decay
P7: Variation
P8: Chance

B1: Repeat Rate
P1: Interval
P2: Offset
P3: Grid
P4: Variation
P5: Filter Freq
P6: Filter Width
P7: Volume
P8: Decay

B2: Gate/Pitch
P1: Chance
P2: Gate
P3: Pitch
P4: Pitch Decay
P5: Filter Freq
P6: Filter Width
P7: Volume
P8: Decay


Back to Device Index

Cabinet

Best Of Banks
P1: Cabinet Type
P2: Microphone Position
P3: Microphone Type
P4: Dual Mono
P8: Dry/Wet


Back to Device Index

Chord

Best Of Banks
P1: Shift1
P2: Shift2
P3: Shift3
P4: Shift4
P5: Shift5
P6: Velocity5
P7: Shift6
P8: Velocity6

B1: Shift
P1: Shift1
P2: Shift2
P3: Shift3
P4: Shift4
P5: Shift5
P6: Shift6

B2: Shift %
P1: Velocity1
P2: Velocity2
P3: Velocity3
P4: Velocity4
P5: Velocity5
P6: Velocity6


Back to Device Index

Chorus

Best Of Banks
P1: LFO Amount
P2: LFO Rate
P3: Delay 1 Time
P4: Delay 1 HiPass
P5: Delay 2 Time
P6: Delay 2 Mode
P7: Feedback
P8: Dry/Wet


Back to Device Index

Collision

Best Of Banks
P1: Res 1 Brightness
P2: Res 1 Type
P3: Mallet Stiffness
P4: Mallet Noise Amount
P5: Res 1 Inharmonics
P6: Res 1 Decay
P7: Res 1 Tune
P8: Volume

B1: Mallet
P1: Mallet On/Off
P2: Mallet Volume
P3: Mallet Noise Amount
P4: Mallet Stiffness
P5: Mallet Noise Color

B2: Noise
P1: Noise Volume
P2: Noise Filter Type
P3: Noise Filter Freq
P4: Noise Filter Q
P5: Noise Attack
P6: Noise Decay
P7: Noise Sustain
P8: Noise Release

B3: Resonator 1, Set A
P1: Res 1 Decay
P2: Res 1 Material
P3: Res 1 Type
P4: Res 1 Quality
P5: Res 1 Tune
P6: Res 1 Fine Tune
P7: Res 1 Pitch Env.
P8: Res 1 Pitch Env. Time

B4: Resonator 1, Set B
P1: Res 1 Listening L
P2: Res 1 Listening R
P3: Res 1 Hit
P4: Res 1 Brightness
P5: Res 1 Inharmonics
P6: Res 1 Radius
P7: Res 1 Opening
P8: Res 1 Ratio

B5: Resonator 2, Set A
P1: Res 2 Decay
P2: Res 2 Material
P3: Res 2 Type
P4: Res 2 Quality
P5: Res 2 Tune
P6: Res 2 Fine Tune
P7: Res 2 Pitch Env.
P8: Res 2 Pitch Env. Time

B6: Resonator 2, Set B
P1: Res 2 Listening L
P2: Res 2 Listening R
P3: Res 2 Hit
P4: Res 2 Brightness
P5: Res 2 Inharmonics
P6: Res 2 Radius
P7: Res 2 Opening
P8: Res 2 Ratio


Back to Device Index

Compressor

Best Of Banks
P1: Threshold
P2: Ratio
P3: Attack
P4: Release
P5: Model
P6: Knee
P7: Dry/Wet
P8: Output Gain

B1: Compression
P1: Threshold
P2: Ratio
P3: Attack
P4: Release
P5: Auto Release On/Off
P6: Env Mode
P7: Knee
P8: Model

B2: Output
P1: Threshold
P2: Expansion Ratio
P3: LookAhead
P4: Side Listen
P5: Ext. In Gain
P6: Makeup
P7: Dry/Wet
P8: Output Gain

B3: Side Chain
P1: EQ On
P2: EQ Mode
P3: EQ Freq
P4: EQ Q
P5: EQ Gain
P6: Ext. In On
P7: Ext. In Mix
P8: Ext. In Gain


Back to Device Index

Corpus

Best Of Banks
P1: Brightness
P2: Resonance Type
P3: Material
P4: Inharmonics
P5: Decay
P6: Ratio
P7: Tune
P8: Dry Wet

B1: Amount
P1: Decay
P2: Material
P3: Mid Freq
P4: Width
P5: Bleed
P6: Resonance Type
P7: Gain
P8: Dry Wet

B2: Body
P1: Listening L
P2: Listening R
P3: Hit
P4: Brightness
P5: Inharmonics
P6: Radius
P7: Opening
P8: Ratio

B3: Tune
P1: Resonance Type
P2: Tune
P3: Transpose
P4: Fine
P5: Spread
P6: Resonator Quality
P7: Note Off
P8: Off Decay


Back to Device Index

Drum Rack

Best Of Banks
P1: Macro 1
P2: Macro 2
P3: Macro 3
P4: Macro 4
P5: Macro 5
P6: Macro 6
P7: Macro 7
P8: Macro 8


Back to Device Index

Dynamic Tube

Best Of Banks
P1: Drive
P2: Bias
P3: Envelope
P4: Tone
P5: Attack
P6: Release
P7: Output
P8: Dry/Wet


Back to Device Index

EQ Eight

Best Of Banks
P1: 1 Frequency A
P2: 1 Gain A
P3: 2 Frequency A
P4: 2 Gain A
P5: 3 Frequency A
P6: 3 Gain A
P7: 4 Frequency A
P8: 4 Gain A

B1: Band On/Off
P1: 1 Filter On A
P2: 2 Filter On A
P3: 3 Filter On A
P4: 4 Filter On A
P5: 5 Filter On A
P6: 6 Filter On A
P7: 7 Filter On A
P8: 8 Filter On A

B2: Frequency
P1: 1 Frequency A
P2: 2 Frequency A
P3: 3 Frequency A
P4: 4 Frequency A
P5: 5 Frequency A
P6: 6 Frequency A
P7: 7 Frequency A
P8: 8 Frequency A

B3: Gain
P1: 1 Gain A
P2: 2 Gain A
P3: 3 Gain A
P4: 4 Gain A
P5: 5 Gain A
P6: 6 Gain A
P7: 7 Gain A
P8: 8 Gain A

B4: Resonance
P1: 1 Resonance A
P2: 2 Resonance A
P3: 3 Resonance A
P4: 4 Resonance A
P5: 5 Resonance A
P6: 6 Resonance A
P7: 7 Resonance A
P8: 8 Resonance A

B5: Filter Type
P1: 1 Filter Type A
P2: 2 Filter Type A
P3: 3 Filter Type A
P4: 4 Filter Type A
P5: 5 Filter Type A
P6: 6 Filter Type A
P7: 7 Filter Type A
P8: 8 Filter Type A

B6: Output
P1: Adaptive Q
P7: Scale
P8: Output Gain

B7: EQs 3-5
P1: 3 Gain A
P2: 3 Frequency A
P3: 3 Resonance A
P4: 4 Gain A
P5: 4 Frequency A
P6: 4 Resonance A
P7: 5 Gain A
P8: 5 Frequency A


Back to Device Index

EQ Three

Best Of Banks
P1: GainLo
P2: GainMid
P3: GainHi
P4: FreqLo
P5: LowOn
P6: MidOn
P7: HighOn
P8: FreqHi


Back to Device Index

Electric

Best Of Banks
P1: M Stiffness
P2: M Force
P3: Noise Amount
P4: F Tine Vol
P5: F Tone Vol
P6: F Release
P7: P Symmetry
P8: Volume

B1: Mallet and Tine
P1: M Stiffness
P2: M Force
P3: Noise Pitch
P4: Noise Decay
P5: Noise Amount
P6: F Tine Color
P7: F Tine Decay
P8: F Tine Vol

B2: Tone and Damper
P1: F Tone Decay
P2: F Tone Vol
P3: F Release
P4: Damp Tone
P5: Damp Balance
P6: Damp Amount

B3: Pickup
P1: P Symmetry
P2: P Distance
P3: P Amp In
P4: P Amp Out
P5: Pickup Model

B4: Modulation
P1: M Stiff < Vel
P2: M Stiff < Key
P3: M Force < Vel
P4: M Force < Key
P5: Noise < Key
P6: F Tine < Key
P7: P Amp < Key

B5: Global
P1: Volume
P2: Voices
P3: Semitone
P4: Detune
P5: KB Stretch
P6: PB Range


Back to Device Index

Erosion

Best Of Banks
P1: Frequency
P2: Width
P3: Mode
P4: Amount


Back to Device Index

FilterDelay

Best Of Banks
P1: 2 Filter Freq
P2: 2 Filter Width
P3: 2 Beat Delay
P4: 2 Feedback
P5: 1 Volume
P6: 3 Volume
P7: 2 Volume
P8: Dry

B1: Input L Filter
P1: 1 Filter Freq
P2: 1 Filter Width
P3: 1 Beat Delay
P4: 1 Beat Swing
P5: 1 Feedback
P6: 1 Pan
P7: 1 Volume
P8: Dry

B2: Input L+R Filter
P1: 2 Filter Freq
P2: 2 Filter Width
P3: 2 Beat Delay
P4: 2 Beat Swing
P5: 2 Feedback
P6: 2 Pan
P7: 2 Volume
P8: Dry

B3: Input R Filter
P1: 3 Filter Freq
P2: 3 Filter Width
P3: 3 Beat Delay
P4: 3 Beat Swing
P5: 3 Feedback
P6: 3 Pan
P7: 3 Volume
P8: Dry


Back to Device Index

Flanger

Best Of Banks
P1: Hi Pass
P2: Delay Time
P3: Frequency
P4: Sync Rate
P5: LFO Amount
P6: Env. Modulation
P7: Feedback
P8: Dry/Wet

B1: Frequency Controls
P1: Hi Pass
P2: Dry/Wet
P3: Delay Time
P4: Feedback
P5: Env. Modulation
P6: Env. Attack
P7: Env. Release

B2: LFO / S&H
P1: LFO Amount
P2: Frequency
P3: LFO Phase
P4: Sync
P5: LFO Offset
P6: Sync Rate
P7: LFO Width (Random)
P8: LFO Waveform


Back to Device Index

FrequencyShifter

Best Of Banks
P1: Coarse
P2: Fine
P3: Mode
P4: Ring Mod Frequency
P5: Drive On/Off
P6: Drive
P7: Wide
P8: Dry/Wet


Back to Device Index

Gate

Best Of Banks
P1: Threshold
P2: Return
P3: FlipMode
P4: LookAhead
P5: Attack
P6: Hold
P7: Release
P8: Floor

B1: Gate
P1: Threshold
P2: Return
P3: FlipMode
P4: LookAhead
P5: Attack
P6: Hold
P7: Release
P8: Floor

B2: Side Chain
P1: EQ On
P2: EQ Mode
P3: EQ Freq
P4: EQ Q
P5: EQ Gain
P6: Ext. In On
P7: Ext. In Mix
P8: Ext. In Gain


Back to Device Index

GlueCompressor

Best Of Banks
P1: Threshold
P2: Ratio
P3: Attack
P4: Release
P5: Peak Clip In
P6: Range
P7: Makeup
P8: Dry/Wet

B1: Compression
P1: Threshold
P2: Ratio
P3: Attack
P4: Release
P5: Peak Clip In
P6: Range
P7: Dry/Wet
P8: Makeup

B2: Side Chain
P1: EQ On
P2: EQ Mode
P3: EQ Freq
P4: EQ Q
P5: EQ Gain
P6: Ext. In On
P7: Ext. In Mix
P8: Ext. In Gain


Back to Device Index

GrainDelay

Best Of Banks
P1: Frequency
P2: Pitch
P3: Time Delay
P4: Beat Swing
P5: Random
P6: Spray
P7: Feedback
P8: DryWet


Back to Device Index

Impulse

Best Of Banks
P1: Global Time
P2: Global Transpose
P3: 1 Transpose
P4: 2 Transpose
P5: 3 Transpose
P6: 4 Transpose
P7: 5 Transpose
P8: 6 Transpose

B1: Pad 1
P1: 1 Start
P2: 1 Transpose
P3: 1 Stretch Factor
P4: 1 Saturator Drive
P5: 1 Filter Freq
P6: 1 Filter Res
P7: 1 Pan
P8: 1 Volume

B2: Pad 2
P1: 2 Start
P2: 2 Transpose
P3: 2 Stretch Factor
P4: 2 Saturator Drive
P5: 2 Filter Freq
P6: 2 Filter Res
P7: 2 Pan
P8: 2 Volume

B3: Pad 3
P1: 3 Start
P2: 3 Transpose
P3: 3 Stretch Factor
P4: 3 Saturator Drive
P5: 3 Filter Freq
P6: 3 Filter Res
P7: 3 Pan
P8: 3 Volume

B4: Pad 4
P1: 4 Start
P2: 4 Transpose
P3: 4 Stretch Factor
P4: 4 Saturator Drive
P5: 4 Filter Freq
P6: 4 Filter Res
P7: 4 Pan
P8: 4 Volume

B5: Pad 5
P1: 5 Start
P2: 5 Transpose
P3: 5 Stretch Factor
P4: 5 Saturator Drive
P5: 5 Filter Freq
P6: 5 Filter Res
P7: 5 Pan
P8: 5 Volume

B6: Pad 6
P1: 6 Start
P2: 6 Transpose
P3: 6 Stretch Factor
P4: 6 Saturator Drive
P5: 6 Filter Freq
P6: 6 Filter Res
P7: 6 Pan
P8: 6 Volume

B7: Pad 7
P1: 7 Start
P2: 7 Transpose
P3: 7 Stretch Factor
P4: 7 Saturator Drive
P5: 7 Filter Freq
P6: 7 Filter Res
P7: 7 Pan
P8: 7 Volume

B8: Pad 8
P1: 8 Start
P2: 8 Transpose
P3: 8 Stretch Factor
P4: 8 Saturator Drive
P5: 8 Filter Freq
P6: 8 Filter Res
P7: 8 Pan
P8: 8 Volume


Back to Device Index

Instrument Rack

Best Of Banks
P1: Macro 1
P2: Macro 2
P3: Macro 3
P4: Macro 4
P5: Macro 5
P6: Macro 6
P7: Macro 7
P8: Macro 8


Back to Device Index

Looper

Best Of Banks
P1: State
P2: Speed
P3: Reverse
P4: Quantization
P5: Monitor
P6: Song Control
P7: Tempo Control
P8: Feedback


Back to Device Index

MIDI Effect Rack

Best Of Banks
P1: Macro 1
P2: Macro 2
P3: Macro 3
P4: Macro 4
P5: Macro 5
P6: Macro 6
P7: Macro 7
P8: Macro 8


Back to Device Index

MultibandDynamics

Best Of Banks
P1: Above Threshold (Low)
P2: Above Ratio (Low)
P3: Above Threshold (Mid)
P4: Above Ratio (Mid)
P5: Above Threshold (High)
P6: Above Ratio (High)
P7: Master Output
P8: Amount

B1: Global
P1: Master Output
P2: Amount
P3: Time Scaling
P4: Soft Knee On/Off
P5: Peak/RMS Mode
P6: Band Activator (High)
P7: Band Activator (Mid)
P8: Band Activator (Low)

B2: Low Band
P1: Input Gain (Low)
P2: Below Threshold (Low)
P3: Below Ratio (Low)
P4: Above Threshold (Low)
P5: Above Ratio (Low)
P6: Attack Time (Low)
P7: Release Time (Low)
P8: Output Gain (Low)

B3: Mid Band
P1: Input Gain (Mid)
P2: Below Threshold (Mid)
P3: Below Ratio (Mid)
P4: Above Threshold (Mid)
P5: Above Ratio (Mid)
P6: Attack Time (Mid)
P7: Release Time (Mid)
P8: Output Gain (Mid)

B4: High Band
P1: Input Gain (High)
P2: Below Threshold (High)
P3: Below Ratio (High)
P4: Above Threshold (High)
P5: Above Ratio (High)
P6: Attack Time (High)
P7: Release Time (High)
P8: Output Gain (High)

B5: Split Frequencies
P1: Low-Mid Crossover
P2: Mid-High Crossover

B6: Side Chain
P6: Ext. In On
P7: Ext. In Mix
P8: Ext. In Gain


Back to Device Index

Note Length

Best Of Banks
P1: Sync On
P2: Time Length
P3: Synced Length
P4: Gate
P5: On/Off-Balance
P6: Decay Time
P7: Decay Key Scale


Back to Device Index

Operator

Best Of Banks
P1: Filter Freq
P2: Filter Res
P3: A Coarse
P4: A Fine
P5: B Coarse
P6: B Fine
P7: Osc-B Level
P8: Volume

B1: Oscillator A
P1: Ae Attack
P2: Ae Decay
P3: Ae Sustain
P4: Ae Release
P5: A Coarse
P6: A Fine
P7: Osc-A Lev < Vel
P8: Osc-A Level

B2: Oscillator B
P1: Be Attack
P2: Be Decay
P3: Be Sustain
P4: Be Release
P5: B Coarse
P6: B Fine
P7: Osc-B Lev < Vel
P8: Osc-B Level

B3: Oscillator C
P1: Ce Attack
P2: Ce Decay
P3: Ce Sustain
P4: Ce Release
P5: C Coarse
P6: C Fine
P7: Osc-C Lev < Vel
P8: Osc-C Level

B4: Oscillator D
P1: De Attack
P2: De Decay
P3: De Sustain
P4: De Release
P5: D Coarse
P6: D Fine
P7: Osc-D Lev < Vel
P8: Osc-D Level

B5: LFO
P1: Le Attack
P2: Le Decay
P3: Le Sustain
P4: Le Release
P5: LFO Rate
P6: LFO Amt
P7: LFO Type
P8: LFO R < K

B6: Filter
P1: Fe Attack
P2: Fe Decay
P3: Fe Sustain
P4: Fe Release
P5: Filter Freq
P6: Filter Res
P7: Fe R < Vel
P8: Fe Amount

B7: Pitch Modulation
P1: Pe Attack
P2: Pe Decay
P3: Pe Sustain
P4: Pe Release
P5: Pe Init
P6: Glide Time
P7: Pe Amount
P8: Spread

B8: Routing
P1: Time < Key
P2: Panorama
P3: Pan < Key
P4: Pan < Rnd
P5: Algorithm
P6: Time
P7: Tone
P8: Volume


Back to Device Index

Overdrive

Best Of Banks
P1: Filter Freq
P2: Filter Width
P3: Drive
P4: Tone
P5: Preserve Dynamics
P8: Dry/Wet


Back to Device Index

Phaser

Best Of Banks
P1: Frequency
P2: Feedback
P3: Poles
P4: Env. Modulation
P5: Color
P6: LFO Amount
P7: LFO Frequency
P8: Dry/Wet

B1: Frequency Controls
P1: Poles
P2: Color
P3: Dry/Wet
P4: Frequency
P5: Env. Modulation
P6: Env. Attack
P7: Env. Release
P8: Feedback

B2: LFO / S&H
P1: LFO Amount
P2: LFO Frequency
P3: LFO Phase
P4: LFO Sync
P5: LFO Offset
P6: LFO Sync Rate
P7: LFO Spin
P8: LFO Waveform


Back to Device Index

PingPongDelay

Best Of Banks
P1: Filter Freq
P2: Filter Width
P3: Time Delay
P4: Beat Delay
P5: Beat Swing
P6: Delay Mode
P7: Feedback
P8: Dry/Wet


Back to Device Index

Pitch

Best Of Banks
P1: Pitch
P2: Range
P3: Lowest


Back to Device Index

Random

Best Of Banks
P1: Chance
P2: Choices
P3: Scale
P4: Sign


Back to Device Index

Redux

Best Of Banks
P1: Bit Depth
P2: Sample Mode
P3: Sample Hard
P4: Sample Soft
P5: Bit On


Back to Device Index

Resonator

Best Of Banks
P1: Decay
P2: I Note
P3: II Pitch
P4: III Pitch
P5: IV Pitch
P6: V Pitch
P7: Global Gain
P8: Dry/Wet

B1: General / Mode I
P1: Frequency
P2: Width
P3: Global Gain
P4: Dry/Wet
P5: Decay
P6: I Note
P7: Color
P8: I Gain

B2: Modes II-IV
P1: II Gain
P2: III Gain
P3: IV Gain
P4: V Gain
P5: II Pitch
P6: III Pitch
P7: IV Pitch
P8: V Pitch


Back to Device Index

Reverb

Best Of Banks
P1: DecayTime
P2: Room Size
P3: PreDelay
P4: In Filter Freq
P5: ER Level
P6: Diffuse Level
P7: Stereo Image
P8: Dry/Wet

B1: Reflections
P1: In Filter Freq
P2: In Filter Width
P3: PreDelay
P4: ER Spin On
P5: ER Spin Rate
P6: ER Spin Amount
P7: ER Shape
P8: DecayTime

B2: Diffusion Network
P1: HiShelf Freq
P2: LowShelf Freq
P3: Chorus Rate
P4: Density
P5: HiShelf Gain
P6: LowShelf Gain
P7: Chorus Amount
P8: Scale

B3: Global
P1: DecayTime
P2: Freeze On
P3: Room Size
P4: Stereo Image
P5: ER Level
P6: Diffuse Level
P7: Dry/Wet
P8: Quality


Back to Device Index

Sampler

Best Of Banks
P1: Filter Freq
P2: Filter Res
P3: Fe < Env
P4: Fe Decay
P5: Ve Attack
P6: Ve Release
P7: Transpose
P8: Volume

B1: Volume
P1: Volume
P2: Ve Attack
P3: Ve Decay
P4: Ve Sustain
P5: Ve Release
P6: Vol < Vel
P7: Ve R < Vel
P8: Time

B2: Filter
P1: Filter Type
P2: Filter Morph
P3: Filter Freq
P4: Filter Res
P5: Filt < Vel
P6: Filt < Key
P7: Fe < Env
P8: Shaper Amt

B3: Filter Envelope
P1: Fe Attack
P2: Fe Decay
P3: Fe Sustain
P4: Fe Release
P5: Fe End
P6: Fe Mode
P7: Fe Loop
P8: Fe Retrig

B4: LFO 1
P1: L 1 Wave
P2: L 1 Sync
P3: L 1 Sync Rate
P4: L 1 Rate
P5: Vol < LFO
P6: Filt < LFO
P7: Pan < LFO
P8: Pitch < LFO

B5: LFO 2
P1: L 2 Wave
P2: L 2 Sync
P3: L 2 Sync Rate
P4: L 2 Rate
P5: L 2 R < Key
P6: L 2 St Mode
P7: L 2 Spin
P8: L 2 Phase

B6: LFO 3
P1: L 3 Wave
P2: L 3 Sync
P3: L 3 Sync Rate
P4: L 3 Rate
P5: L 3 R < Key
P6: L 3 St Mode
P7: L 3 Spin
P8: L 3 Phase

B7: Oscillator
P1: O Mode
P2: O Volume
P3: O Coarse
P4: O Fine
P5: Oe Attack
P6: Oe Decay
P7: Oe Sustain
P8: Oe Release

B8: Pitch
P1: Transpose
P2: Spread
P3: Pe < Env
P4: Pe Attack
P5: Pe Peak
P6: Pe Decay
P7: Pe Sustain
P8: Pe Release


Back to Device Index

Saturator

Best Of Banks
P1: Drive
P2: Type
P3: Base
P4: Frequency
P5: Width
P6: Depth
P7: Output
P8: Dry/Wet

B1: General Controls
P1: Drive
P2: Base
P3: Frequency
P4: Width
P5: Depth
P6: Output
P7: Dry/Wet
P8: Type

B2: Waveshaper Controls
P1: WS Drive
P2: WS Lin
P3: WS Curve
P4: WS Damp
P5: WS Depth
P6: WS Period
P7: Dry/Wet


Back to Device Index

Scale

Best Of Banks
P1: Base
P2: Transpose
P3: Range
P4: Lowest


Back to Device Index

Simple Delay

Best Of Banks
P1: L Beat Delay
P2: L Beat Swing
P3: L Time Delay
P4: R Beat Delay
P5: R Beat Swing
P6: R Time Delay
P7: Feedback
P8: Dry/Wet


Back to Device Index

Simpler

Best Of Banks
P1: Filter Freq
P2: Filter Res
P3: S Start
P4: S Length
P5: Ve Attack
P6: Ve Release
P7: Transpose
P8: Volume

B1: Amplitude
P1: Ve Attack
P2: Ve Decay
P3: Ve Sustain
P4: Ve Release
P5: S Start
P6: S Loop Length
P7: S Length
P8: S Loop Fade

B2: Filter
P1: Fe Attack
P2: Fe Decay
P3: Fe Sustain
P4: Fe Release
P5: Filter Freq
P6: Filter Res
P7: Filt < Vel
P8: Fe < Env

B3: LFO
P1: L Attack
P2: L Rate
P3: L R < Key
P4: L Wave
P5: Vol < LFO
P6: Filt < LFO
P7: Pitch < LFO
P8: Pan < LFO

B4: Pitch Modifiers
P1: Pe Attack
P2: Pe Decay
P3: Pe Sustain
P4: Pe Release
P5: Glide Time
P6: Spread
P7: Pan
P8: Volume


Back to Device Index

Tension

Best Of Banks
P1: Filter Freq
P2: Filter Reso
P3: Filter Type
P4: Excitator Type
P5: E Pos
P6: String Decay
P7: Str Damping
P8: Volume

B1: Excitator and String
P1: Excitator Type
P2: String Decay
P3: Str Inharmon
P4: Str Damping
P5: Exc ForceMassProt
P6: Exc FricStiff
P7: Exc Velocity
P8: E Pos

B2: Damper
P1: Damper On
P2: Damper Mass
P3: D Stiffness
P4: D Velocity
P5: Damp Pos
P6: D Damping
P7: D Pos < Vel
P8: D Pos Abs

B3: Termination and Pickup
P1: Term On/Off
P2: Term Mass
P3: Term Fng Stiff
P4: Term Fret Stiff
P5: Pickup On/Off
P6: Pickup Pos
P7: T Mass < Vel
P8: T Mass < Key

B4: Body
P1: Body On/Off
P2: Body Type
P3: Body Size
P4: Body Decay
P5: Body Low-Cut
P6: Body High-Cut
P7: Body Mix
P8: Volume

B5: Vibrato
P1: Vibrato On/Off
P2: Vib Delay
P3: Vib Fade-In
P4: Vib Speed
P5: Vib Amount
P6: Vib < ModWh
P7: Vib Error
P8: Volume

B6: Filter
P1: Filter On/Off
P2: Filter Type
P3: Filter Freq
P4: Filter Reso
P5: Freq < Env
P6: Freq < LFO
P7: Reso < Env
P8: Reso < LFO

B7: Envelope and LFO
P1: FEG On/Off
P2: FEG Attack
P3: FEG Decay
P4: FEG Sustain
P5: FEG Release
P6: LFO On/Off
P7: LFO Shape
P8: LFO Speed

B8: Global
P1: Unison On/Off
P2: Uni Detune
P3: Porta On/Off
P4: Porta Time
P5: Voices
P6: Octave
P7: Semitone
P8: Volume


Back to Device Index

Utility

Best Of Banks
P1: StereoSeparation
P2: BlockDc
P3: PhaseInvertL
P4: PhaseInvertR
P5: Signal Source
P6: Panorama
P7: Mute
P8: Gain


Back to Device Index

Velocity

Best Of Banks
P1: Drive
P2: Compand
P3: Random
P4: Mode
P5: Out Hi
P6: Out Low
P7: Range
P8: Lowest


Back to Device Index

Vinyl Distortion

Best Of Banks
P1: Tracing Freq.
P2: Tracing Width
P3: Tracing Drive
P4: Crackle Density
P5: Pinch Freq.
P6: Pinch Width
P7: Pinch Drive
P8: Crackle Volume


Back to Device Index

Vocoder

Best Of Banks
P1: Formant Shift
P2: Attack Time
P3: Release Time
P4: Unvoiced Level
P5: Gate Threshold
P6: Filter Bandwidth
P7: Envelope Depth
P8: Dry/Wet

B1: Global
P1: Formant Shift
P2: Attack Time
P3: Release Time
P4: Mono/Stereo
P5: Output Level
P6: Gate Threshold
P7: Envelope Depth
P8: Dry/Wet

B2: Filters/Voicing
P1: Filter Bandwidth
P2: Upper Filter Band
P3: Lower Filter Band
P4: Precise/Retro
P5: Unvoiced Level
P6: Unvoiced Sensitivity
P7: Unvoiced Speed
P8: Enhance

B3: Carrier
P1: Noise Rate
P2: Noise Crackle
P3: Upper Pitch Detection
P4: Lower Pitch Detection
P5: Oscillator Pitch
P6: Oscillator Waveform
P7: Ext. In Gain


Back to Device Index
\ No newline at end of file diff --git a/Macrobat Manual.pdf b/Macrobat Manual.pdf new file mode 100644 index 0000000..40a1eaf Binary files /dev/null and b/Macrobat Manual.pdf differ diff --git a/Macrobat Rack Examples.als b/Macrobat Rack Examples.als new file mode 100644 index 0000000..fa50241 Binary files /dev/null and b/Macrobat Rack Examples.als differ